├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 01_BUG_REPORT.md │ ├── 02_FEATURE_REQUEST.md │ ├── 03_CODEBASE_IMPROVEMENT.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── labels.yml └── workflows │ ├── docker-publish.yml │ ├── labels.yml │ ├── lock.yml │ ├── pr-labels.yml │ └── stale.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── compose.yaml ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── SECURITY.md └── images │ ├── logo.png │ ├── logo.svg │ ├── screenshot.png │ ├── scrobblex.png │ └── scrobblex.svg ├── favicon.ico ├── nodemon.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── api.js ├── index.js ├── logger.js ├── requests.js └── utils.js ├── static ├── images │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-128.png │ ├── favicon-16x16.png │ ├── favicon-196x196.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png └── output.css └── views └── pages ├── index.ejs └── input.css /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | .git 6 | data 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TRAKT_ID=YOUR_CLIENT_ID 2 | TRAKT_SECRET=YOUR_CLIENT_SECRET 3 | PORT=3090 4 | LOG_LEVEL=info 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": "latest", 5 | "sourceType": "module" // Allows for the use of imports 6 | }, 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "env": { 9 | "node": true // Enable Node.js global variables 10 | }, 11 | "rules": { 12 | "no-console": "off", 13 | "import/prefer-default-export": "off", 14 | "@typescript-eslint/no-unused-vars": "warn" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ryck 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help scrobblex to improve 4 | title: "bug: " 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | # Bug Report 10 | 11 | **scrobblex version:** 12 | 13 | 14 | 15 | **Current behavior:** 16 | 17 | 18 | 19 | **Expected behavior:** 20 | 21 | 22 | 23 | **Steps to reproduce:** 24 | 25 | 26 | 27 | **Related code:** 28 | 29 | 30 | 31 | ``` 32 | insert short code snippets here 33 | ``` 34 | 35 | **Other information:** 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "feat: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | # Feature Request 10 | 11 | **Describe the Feature Request** 12 | 13 | 14 | 15 | **Describe Preferred Solution** 16 | 17 | 18 | 19 | **Describe Alternatives** 20 | 21 | 22 | 23 | **Related Code** 24 | 25 | 26 | 27 | **Additional Context** 28 | 29 | 30 | 31 | **If the feature request is approved, would you be willing to submit a PR?** 32 | _(Help can be provided if you need assistance submitting a PR)_ 33 | 34 | - [ ] Yes 35 | - [ ] No 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_CODEBASE_IMPROVEMENT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codebase improvement 3 | about: Provide your feedback for the existing codebase. Suggest a better solution for algorithms, development tools, etc. 4 | title: "dev: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: scrobblex Community Support 5 | url: https://github.com/ryck/scrobblex/discussions 6 | about: Please ask and answer questions here. 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Pull Request type 4 | 5 | 6 | 7 | Please check the type of change your PR introduces: 8 | 9 | - [ ] Bugfix 10 | - [ ] Feature 11 | - [ ] Code style update (formatting, renaming) 12 | - [ ] Refactoring (no functional changes, no API changes) 13 | - [ ] Build-related changes 14 | - [ ] Documentation content changes 15 | - [ ] Other (please describe): 16 | 17 | ## What is the current behavior? 18 | 19 | 20 | 21 | Issue Number: N/A 22 | 23 | ## What is the new behavior? 24 | 25 | 26 | 27 | - 28 | - 29 | - 30 | 31 | ## Does this introduce a breaking change? 32 | 33 | - [ ] Yes 34 | - [ ] No 35 | 36 | 37 | 38 | ## Other information 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "breaking-change" 3 | color: ee0701 4 | description: "A change that changes the API or breaks backward compatibility for users." 5 | - name: "bugfix" 6 | color: ee0701 7 | description: "Inconsistencies or issues which will cause a problem for users or implementors." 8 | - name: "documentation" 9 | color: 0052cc 10 | description: "Solely about the documentation of the project." 11 | - name: "enhancement" 12 | color: 1d76db 13 | description: "Enhancement of the code, not introducing new features." 14 | - name: "refactor" 15 | color: 1d76db 16 | description: "Updating the code with simpler, easier to understand or more efficient syntax or methods, but not introducing new features." 17 | - name: "performance" 18 | color: 1d76db 19 | description: "Improving performance of the project, not introducing new features." 20 | - name: "new-feature" 21 | color: 0e8a16 22 | description: "New features or options." 23 | - name: "maintenance" 24 | color: 2af79e 25 | description: "Generic maintenance tasks." 26 | - name: "ci" 27 | color: 1d76db 28 | description: "Work that improves the continuous integration." 29 | - name: "dependencies" 30 | color: 1d76db 31 | description: "Change in project dependencies." 32 | 33 | - name: "in-progress" 34 | color: fbca04 35 | description: "Issue is currently being worked on by a developer." 36 | - name: "stale" 37 | color: fef2c0 38 | description: "No activity for quite some time." 39 | - name: "no-stale" 40 | color: fef2c0 41 | description: "This is exempt from the stale bot." 42 | 43 | - name: "security" 44 | color: ee0701 45 | description: "Addressing a vulnerability or security risk in this project." 46 | - name: "incomplete" 47 | color: fef2c0 48 | description: "Missing information." 49 | - name: "invalid" 50 | color: fef2c0 51 | description: "This is off-topic, spam, or otherwise doesn't apply to this project." 52 | 53 | - name: "beginner-friendly" 54 | color: 0e8a16 55 | description: "Good first issue for people wanting to contribute to this project." 56 | - name: "help-wanted" 57 | color: 0e8a16 58 | description: "We need some extra helping hands or expertise in order to resolve this!" 59 | 60 | - name: "priority-critical" 61 | color: ee0701 62 | description: "Must be addressed as soon as possible." 63 | - name: "priority-high" 64 | color: b60205 65 | description: "After critical issues are fixed, these should be dealt with before any further issues." 66 | - name: "priority-medium" 67 | color: 0e8a16 68 | description: "This issue may be useful, and needs some attention." 69 | - name: "priority-low" 70 | color: e4ea8a 71 | description: "Nice addition, maybe... someday..." 72 | 73 | - name: "major" 74 | color: b60205 75 | description: "This PR causes a major bump in the version number." 76 | - name: "minor" 77 | color: 0e8a16 78 | description: "This PR causes a minor bump in the version number." 79 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | # schedule: 10 | # - cron: '28 6 * * *' 11 | push: 12 | branches: ['main'] 13 | # Publish semver tags as releases. 14 | tags: ['v*.*.*'] 15 | # pull_request: 16 | # branches: ['main'] 17 | 18 | env: 19 | # Use docker.io for Docker Hub if empty 20 | REGISTRY: ghcr.io 21 | # github.repository as / 22 | IMAGE_NAME: ${{ github.repository }} 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | packages: write 30 | # This is used to complete the identity challenge 31 | # with sigstore/fulcio when running outside of PRs. 32 | id-token: write 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | # Set up BuildKit Docker container builder to be able to build 39 | # multi-platform images and export cache 40 | # https://github.com/docker/setup-buildx-action 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3.0.0 43 | 44 | # Login against a Docker registry except on PR 45 | # https://github.com/docker/login-action 46 | - name: Log into GHCR 47 | if: github.event_name != 'pull_request' 48 | uses: docker/login-action@v3.2.0 49 | with: 50 | registry: ${{ env.REGISTRY }} 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Log in to Docker Hub 55 | uses: docker/login-action@v3.2.0 56 | with: 57 | username: ${{ secrets.DOCKER_USERNAME }} 58 | password: ${{ secrets.DOCKER_PASSWORD }} 59 | 60 | # Extract metadata (tags, labels) for Docker 61 | # https://github.com/docker/metadata-action 62 | - name: Extract Docker metadata 63 | id: meta 64 | uses: docker/metadata-action@v5.0.0 65 | with: 66 | images: | 67 | rickgc/scrobblex 68 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 69 | 70 | # Build and push Docker image with Buildx (don't push on PR) 71 | # https://github.com/docker/build-push-action 72 | - name: Build and push Docker image 73 | id: build-and-push 74 | uses: docker/build-push-action@v5.4.0 75 | if: ${{ github.ref != 'refs/heads/main' }} 76 | with: 77 | context: . 78 | platforms: linux/amd64,linux/arm64 79 | push: ${{ github.event_name != 'pull_request' }} 80 | tags: ${{ steps.meta.outputs.tags }} 81 | labels: ${{ steps.meta.outputs.labels }} 82 | cache-from: type=gha 83 | cache-to: type=gha,mode=max 84 | - name: Build and Push Latest Docker Image 85 | id: build-and-push-latest 86 | uses: docker/build-push-action@v5.4.0 87 | if: ${{ github.ref == 'refs/heads/main' }} 88 | with: 89 | context: . 90 | platforms: linux/amd64,linux/arm64 91 | push: ${{ github.event_name != 'pull_request' }} 92 | tags: | 93 | rickgc/scrobblex:latest 94 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 95 | labels: ${{ steps.meta.outputs.labels }} 96 | cache-from: type=gha 97 | cache-to: type=gha,mode=max 98 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sync labels 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - .github/labels.yml 10 | 11 | jobs: 12 | labels: 13 | name: ♻️ Sync labels 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | steps: 19 | - name: ⤵️ Check out code from GitHub 20 | uses: actions/checkout@v4 21 | - name: 🚀 Run Label Syncer 22 | uses: micnncim/action-label-syncer@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lock 3 | 4 | on: 5 | schedule: 6 | - cron: "0 9 * * *" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | lock: 11 | name: 🔒 Lock closed issues and PRs 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | steps: 17 | - uses: dessant/lock-threads@v2.0.3 18 | with: 19 | github-token: ${{ github.token }} 20 | issue-lock-inactive-days: "30" 21 | issue-lock-reason: "" 22 | issue-comment: > 23 | Issue closed and locked due to lack of activity. 24 | 25 | If you encounter this same issue, please open a new issue and refer 26 | to this closed one. 27 | pr-lock-inactive-days: "1" 28 | pr-lock-reason: "" 29 | pr-comment: > 30 | Pull Request closed and locked due to lack of activity. 31 | 32 | If you'd like to build on this closed PR, you can clone it using 33 | this method: https://stackoverflow.com/a/14969986 34 | 35 | Then open a new PR, referencing this closed PR in your message. 36 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR Labels 3 | 4 | on: 5 | pull_request: 6 | types: [opened, labeled, unlabeled, synchronize] 7 | 8 | jobs: 9 | pr_labels: 10 | name: 🏭 Verify 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | steps: 16 | - name: 🏷 Verify PR has a valid label 17 | uses: jesusvasquez333/verify-pr-label-action@v1.4.0 18 | with: 19 | github-token: "${{ secrets.GITHUB_TOKEN }}" 20 | valid-labels: >- 21 | breaking-change, bugfix, documentation, enhancement, 22 | refactor, performance, new-feature, maintenance, ci, dependencies 23 | disable-reviews: true 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Stale 3 | 4 | on: 5 | schedule: 6 | - cron: "0 8 * * *" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | stale: 11 | name: 🧹 Clean up stale issues and PRs 12 | permissions: 13 | contents: read 14 | pull-requests: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 🚀 Run stale 18 | uses: actions/stale@v3 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | days-before-stale: 30 22 | days-before-close: 7 23 | remove-stale-when-updated: true 24 | stale-issue-label: "stale" 25 | exempt-issue-labels: "no-stale,help-wanted" 26 | stale-issue-message: > 27 | There hasn't been any activity on this issue recently, and in order 28 | to prioritize active issues, it will be marked as stale. 29 | 30 | Please make sure to update to the latest version and 31 | check if that solves the issue. Let us know if that works for you 32 | by leaving a 👍 33 | 34 | Because this issue is marked as stale, it will be closed and locked 35 | in 7 days if no further activity occurs. 36 | 37 | Thank you for your contributions! 38 | stale-pr-label: "stale" 39 | exempt-pr-labels: "no-stale" 40 | stale-pr-message: > 41 | There hasn't been any activity on this pull request recently, and in 42 | order to prioritize active work, it has been marked as stale. 43 | 44 | This PR will be closed and locked in 7 days if no further activity 45 | occurs. 46 | 47 | Thank you for your contributions! 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | data 5 | collections -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | LABEL org.opencontainers.image.title="Scrobblex" 4 | LABEL org.opencontainers.image.description="Self-hosted app that enables Plex scrobbling integration with Trakt via webhooks" 5 | LABEL org.opencontainers.image.url="https://github.com/ryck/scrobblex" 6 | LABEL org.opencontainers.image.source="https://github.com/ryck/scrobblex" 7 | LABEL org.opencontainers.image.licenses="MIT" 8 | 9 | ENV NODE_ENV=production 10 | ENV PORT=3090 11 | ENV PORT=$PORT 12 | ENV LOG_LEVEL=info 13 | ENV LOG_LEVEL=$LOG_LEVEL 14 | 15 | RUN apk --no-cache add curl 16 | 17 | HEALTHCHECK CMD curl --fail http://localhost:${PORT}/healthcheck || exit 1 18 | 19 | WORKDIR /app 20 | 21 | COPY package*.json ./ 22 | 23 | RUN npm ci --omit=dev 24 | 25 | COPY . . 26 | 27 | EXPOSE ${PORT} 28 | 29 | CMD ["npm", "start"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025, Ricardo Gonzalez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | scrobblex 5 | 6 |

7 | 8 |
9 | scrobblex 10 |
11 |
12 | Report a Bug 13 | · 14 | Request a Feature 15 | · 16 | Ask a Question 17 |
18 | 19 |
20 |
21 | 22 | [![Project license](https://img.shields.io/github/license/ryck/scrobblex.svg?style=flat-square)](LICENSE) 23 | 24 | [![Pull Requests welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg?style=flat-square)](https://github.com/ryck/scrobblex/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) 25 | [![code with love by ryck](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-ryck-ff1414.svg?style=flat-square)](https://github.com/ryck) 26 | 27 | 28 | ![Plex Pass](https://img.shields.io/badge/plex-pass-orange?style=flat-square&logo=plex&label=%20&labelColor=gray) 29 | [![Docker](https://github.com/ryck/scrobblex/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/ryck/scrobblex/actions/workflows/docker-publish.yml) 30 | [![latest version](https://img.shields.io/github/tag/ryck/scrobblex.svg)](https://github.com/ryck/scrobblex/releases) 31 | [![MIT License](https://img.shields.io/github/license/ryck/scrobblex.svg)](https://www.apache.org/licenses/LICENSE-2.0) 32 | [![Pulls from DockerHub](https://img.shields.io/docker/pulls/rickgc/scrobblex.svg)](https://hub.docker.com/r/rickgc/scrobblex) 33 | [![GitHub release date](https://img.shields.io/github/release-date/ryck/scrobblex)](#) 34 | [![GitHub last commit](https://img.shields.io/github/last-commit/ryck/scrobblex)](#) 35 | 36 |
37 | 38 |
39 | Table of Contents 40 | 41 | - [💡 About](#-about) 42 | - [🚀 Features](#-features) 43 | - [🐥 Getting Started](#-getting-started) 44 | - [Usage](#usage) 45 | - [Docker](#docker) 46 | - [Using docker run](#using-docker-run) 47 | - [Using compose.yml](#using-composeyml) 48 | - [📄 Environment Variables](#-environment-variables) 49 | - [🚧 Roadmap](#-roadmap) 50 | - [🛟 Support](#-support) 51 | - [FAQ](#faq) 52 | - [How to get your Plex user](#how-to-get-your-plex-user) 53 | - [🤝🏻 Contributing](#-contributing) 54 | - [👥 Authors \& contributors](#-authors--contributors) 55 | - [🛡️ Security](#️-security) 56 | - [🪪 License](#-license) 57 | - [❤️ Acknowledgements](#️-acknowledgements) 58 | 59 |
60 | 61 | --- 62 | 63 | ## 💡 About 64 | 65 | Scrobblex is a self-hosted nodejs app that enables Plex scrobbling integration with Trakt via webhooks. It also allows you to push your ratings to Trakt. 66 | 67 | Plex provides webhook integration for all Plex Pass subscribers, and users of their servers. A webhook is a request that the Plex application sends to third party services when a user takes an action, such as watching a movie or episode. 68 | 69 | You can ask Plex to send these webhooks to this app, which will then log those plays in your Trakt account. 70 | 71 | This tool is not affiliated with, endorsed by, or associated with Plex Inc. 72 | 73 |
74 | Screenshots 75 |
76 | 77 | This is basically a command line app, so there are no screenshots really, BUT I wanted to have at least some pretty logs: 78 | 79 | ![Screenshot](https://github.com/ryck/scrobblex/blob/main/docs/images/screenshot.png?raw=true) 80 | 81 |
82 | 83 | ## 🚀 Features 84 | 85 | - Scrobble Plex plays to Trakt 86 | - Push Plex ratings to Trakt 87 | - Self-hosted 88 | - No Trakt VIP account required 89 | 90 | 91 | ## 🐥 Getting Started 92 | 93 | You don't need a Trakt VIP account to use this app (scrobblex will take care of that), BUT you need a Plex Pass subscription in order to have access to webhooks. 94 | 95 | ### Usage 96 | 97 | ```bash 98 | git clone https://github.com/ryck/scrobblex.git && cd scrobblex 99 | npm install 100 | npm run start 101 | ``` 102 | 103 | Once Scrobblex is running, just go to http://$YOUR_IP:$PORT/ (ie: http://10.20.30.40:3090/) and a web page will guide you to get your token. 104 | 105 | ### Docker 106 | 107 | Scrobblex is designed to be run in Docker. You can host it right on your Plex server! 108 | 109 | To run it yourself, first create an API application through Trakt [here](https://trakt.tv/oauth/applications). 110 | 111 | Set the `Redirect URI` (previously know as `Allowed Hostnames`) to be the URI you will hit to access Scrobblex, plus /authorize. 112 | 113 | So if you're exposing your server at http://10.20.30.40:3090, you'll set it to http://10.20.30.40:3090/authorize. 114 | 115 | Bare IP addresses and ports are totally fine, but keep in mind your Scrobblex instance _must_ be accessible to _all_ the Plex servers you intend to play media from. 116 | 117 | You can also have multiple URIs, one per line. 118 | 119 | Screenshot 2025-03-14 at 09 52 55 120 | 121 | 122 | Again, once Scrobblex is running, just go to http://$YOUR_IP:$PORT/ (ie: http://127.0.0.1:3090/) and a web page will guide you to get your token. 123 | 124 | #### Using docker run 125 | 126 | ```bash 127 | docker run \ 128 | --name=scrobbler \ 129 | --restart always \ 130 | -v :/app/data \ 131 | -e TRAKT_ID= \ 132 | -e TRAKT_SECRET= \ 133 | -p 3090:3090 \ 134 | rickgc/scrobblex:latest 135 | ``` 136 | 137 | #### Using compose.yml 138 | 139 | ```yaml 140 | services: 141 | scrobbled: 142 | image: rickgc/scrobblex:latest 143 | container_name: scrobblex 144 | restart: always 145 | ports: 146 | - 3090:3090 147 | environment: 148 | - TRAKT_ID=YOUR_TRAKT_ID 149 | - TRAKT_SECRET=YOUR_TRAKT_SECRET 150 | volumes: 151 | - ./scrobblex:/app/data 152 | ``` 153 | 154 | 155 | ## 📄 Environment Variables 156 | 157 | | Variable | Default | Required | Description | 158 | | ------------ | --------- | -------- | ------------------------------------------------------------------------ | 159 | | TRAKT_ID | undefined | Yes | Trakt application ID | 160 | | TRAKT_SECRET | undefined | Yes | Trakt application secret | 161 | | PLEX_USER | undefined | No | Plex username (comma separated list if you want to allow multiple users) | 162 | | PORT | 3090 | No | Exposed express port | 163 | | LOG_LEVEL | info | No | winston log level: ie: info, debug | 164 | 165 | 166 | 167 | ## 🚧 Roadmap 168 | 169 | See the [open issues](https://github.com/ryck/scrobblex/issues) for a list of proposed features (and known issues). 170 | 171 | - [Top Feature Requests](https://github.com/ryck/scrobblex/issues?q=label%3Aenhancement+is%3Aopen+sort%3Areactions-%2B1-desc) (Add your votes using the 👍 reaction) 172 | - [Top Bugs](https://github.com/ryck/scrobblex/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) (Add your votes using the 👍 reaction) 173 | - [Newest Bugs](https://github.com/ryck/scrobblex/issues?q=is%3Aopen+is%3Aissue+label%3Abug) 174 | 175 | ## 🛟 Support 176 | 177 | Reach out to the maintainer at one of the following places: 178 | 179 | - [GitHub Discussions](https://github.com/ryck/scrobblex/discussions) 180 | - Contact options listed on [this GitHub profile](https://github.com/ryck) 181 | 182 | ### FAQ 183 | #### How to get your Plex user 184 | 185 | You can find your plex user (don't confuse it with your Plex ID) by going to the [Plex website](https://www.plex.tv) and logging in. Your username will be in the top right corner. 186 | 187 | Alternatively, you can find it by going to your [account settings](https://app.plex.tv/desktop/#!/settings/account). Your username will be below your profile picture. 188 | 189 | 190 | ## 🤝🏻 Contributing 191 | 192 | First off, thanks for taking the time to contribute! Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are **greatly appreciated**. 193 | 194 | 195 | Please read [our contribution guidelines](docs/CONTRIBUTING.md), and thank you for being involved! 196 | 197 | ## 👥 Authors & contributors 198 | 199 | The original setup of this repository is by [Ricardo Gonzalez](https://github.com/ryck). 200 | 201 | For a full list of all authors and contributors, see [the contributors page](https://github.com/ryck/scrobblex/contributors). 202 | 203 | ## 🛡️ Security 204 | 205 | scrobblex follows good practices of security, but 100% security cannot be assured. 206 | scrobblex is provided **"as is"** without any **warranty**. Use at your own risk. 207 | 208 | _For more information and to report security issues, please refer to our [security documentation](docs/SECURITY.md)._ 209 | 210 | ## 🪪 License 211 | 212 | This project is licensed under the **MIT license**. 213 | 214 | See [LICENSE](LICENSE) for more information. 215 | 216 | ## ❤️ Acknowledgements 217 | 218 | [XanderStrike](https://github.com/XanderStrike) for his [goplaxt](https://github.com/XanderStrike/goplaxt) project (now sadly dicontinued). It was a great inspiration for this project. Scrobblex is basically the same thing, but in NodeJS, so kudos to him! 219 | 220 | If you want something more powerful, (or something that doesn't need a plex pass), check [PlexTraktSync](https://github.com/Taxel/PlexTraktSync), it's an awesome project that will allow you to sync your watched media, your ratings, your lists, etc. from Plex to Trakt. -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | scrobblex: 3 | image: ghcr.io/ryck/scrobblex:latest 4 | container_name: scrobblex 5 | restart: always 6 | ports: 7 | - $PORT:$PORT 8 | environment: 9 | - TRAKT_ID=$TRAKT_ID 10 | - TRAKT_SECRET=$TRAKT_SECRET 11 | - LOG_LEVEL=$LOG_LEVEL 12 | volumes: 13 | - ./scrobblex:/app/data 14 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer using any of the [private contact addresses](https://github.com/ryck/scrobblex#support). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at 44 | 45 | For answers to common questions about this code of conduct, see 46 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 5 | 6 | ## Development environment setup 7 | 8 | To set up a development environment, please follow these steps: 9 | 10 | 1. Clone the repo 11 | 12 | ```sh 13 | git clone https://github.com/ryck/scrobblex 14 | ``` 15 | 16 | 2. Set your `LOG_LEVEL` to `debug` 17 | 18 | ``` 19 | LOG_LEVEL=debug 20 | ``` 21 | 22 | 3. Install dependencies 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 4. Start the development server 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | ## Issues and feature requests 34 | 35 | You've found a bug in the source code, a mistake in the documentation or maybe you'd like a new feature?Take a look at [GitHub Discussions](https://github.com/ryck/scrobblex/discussions) to see if it's already being discussed. You can help us by [submitting an issue on GitHub](https://github.com/ryck/scrobblex/issues). Before you create an issue, make sure to search the issue archive -- your issue may have already been addressed! 36 | 37 | Please try to create bug reports that are: 38 | 39 | - _Reproducible._ Include steps to reproduce the problem. 40 | - _Specific._ Include as much detail as possible: which version, what environment, etc. 41 | - _Unique._ Do not duplicate existing opened issues. 42 | - _Scoped to a Single Bug._ One bug per report. 43 | 44 | **Even better: Submit a pull request with a fix or new feature!** 45 | 46 | ### How to submit a Pull Request 47 | 48 | 1. Search our repository for open or closed 49 | [Pull Requests](https://github.com/ryck/scrobblex/pulls) 50 | that relate to your submission. You don't want to duplicate effort. 51 | 2. Fork the project 52 | 3. Create your feature branch (`git checkout -b feat/amazing_feature`) 53 | 4. Commit your changes (`git commit -m 'feat: add amazing_feature'`) scrobblex uses [conventional commits](https://www.conventionalcommits.org), so please follow the specification in your commit messages. 54 | 5. Push to the branch (`git push origin feat/amazing_feature`) 55 | 6. [Open a Pull Request](https://github.com/ryck/scrobblex/compare?expand=1) 56 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If there are any vulnerabilities in **scrobblex**, don't hesitate to _report them_. 6 | 7 | 1. Use any of the [private contact addresses](https://github.com/ryck/scrobblex#support). 8 | 2. Describe the vulnerability. 9 | 10 | If you have a fix, that is most welcome -- please attach or summarize it in your message! 11 | 12 | 3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report. 13 | 14 | Please **do not disclose the vulnerability publicly** until a fix is released! 15 | 16 | 4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it. 17 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/images/scrobblex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/docs/images/scrobblex.png -------------------------------------------------------------------------------- /docs/images/scrobblex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/favicon.ico -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "watch": ["src/**", ".env"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrobblex", 3 | "description": "Self-hosted app that enables Plex scrobbling integration with Trakt via webhooks", 4 | "version": "1.4.3", 5 | "homepage": "https://github.com/ryck/scrobblex", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ryck/scrobblex.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/ryck/scrobblex/issues" 12 | }, 13 | "engines": { 14 | "node": ">=18.0.0" 15 | }, 16 | "main": "src/index.js", 17 | "type": "module", 18 | "license": "MIT", 19 | "author": "Ricardo Gonzalez ", 20 | "keywords": [ 21 | "plex", 22 | "trakt", 23 | "webhook", 24 | "scrobbler", 25 | "express" 26 | ], 27 | "scripts": { 28 | "start": "node src/index.js", 29 | "dev": "nodemon src/index.js", 30 | "css": "npx @tailwindcss/cli -i ./views/pages/input.css -o ./static/output.css --watch", 31 | "lint": "eslint src/**/*.js", 32 | "format": "eslint src/**/*.js --fix", 33 | "pretty": "prettier --write \"src/**/*.js\"" 34 | }, 35 | "devDependencies": { 36 | "@types/body-parser": "^1.19.5", 37 | "@types/cors": "^2.8.17", 38 | "@types/express": "^5.0.1", 39 | "@types/multer": "^1.4.12", 40 | "@types/node": "^22.13.13", 41 | "eslint": "^9.23.0", 42 | "nodemon": "^3.1.9", 43 | "prettier": "^3.5.3" 44 | }, 45 | "dependencies": { 46 | "@tailwindcss/cli": "^4.0.15", 47 | "axios": "^1.8.4", 48 | "axios-cache-interceptor": "^1.7.0", 49 | "chalk": "^5.4.1", 50 | "cors": "^2.8.5", 51 | "date-fns": "^4.1.0", 52 | "dotenv": "^16.4.7", 53 | "ejs": "^3.1.10", 54 | "express": "^4.21.2", 55 | "express-async-errors": "^3.1.1", 56 | "express-rate-limit": "^7.5.0", 57 | "lodash": "^4.17.21", 58 | "multer": "^1.4.5-lts.2", 59 | "node-localstorage": "^3.0.5", 60 | "tailwindcss": "^4.0.15", 61 | "winston": "^3.17.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@tailwindcss/cli': 12 | specifier: ^4.0.15 13 | version: 4.0.15 14 | axios: 15 | specifier: ^1.8.4 16 | version: 1.8.4 17 | axios-cache-interceptor: 18 | specifier: ^1.7.0 19 | version: 1.7.0(axios@1.8.4) 20 | chalk: 21 | specifier: ^5.4.1 22 | version: 5.4.1 23 | cors: 24 | specifier: ^2.8.5 25 | version: 2.8.5 26 | date-fns: 27 | specifier: ^4.1.0 28 | version: 4.1.0 29 | dotenv: 30 | specifier: ^16.4.7 31 | version: 16.4.7 32 | ejs: 33 | specifier: ^3.1.10 34 | version: 3.1.10 35 | express: 36 | specifier: ^4.21.2 37 | version: 4.21.2 38 | express-async-errors: 39 | specifier: ^3.1.1 40 | version: 3.1.1(express@4.21.2) 41 | express-rate-limit: 42 | specifier: ^7.5.0 43 | version: 7.5.0(express@4.21.2) 44 | lodash: 45 | specifier: ^4.17.21 46 | version: 4.17.21 47 | multer: 48 | specifier: ^1.4.5-lts.2 49 | version: 1.4.5-lts.2 50 | node-localstorage: 51 | specifier: ^3.0.5 52 | version: 3.0.5 53 | tailwindcss: 54 | specifier: ^4.0.15 55 | version: 4.0.15 56 | winston: 57 | specifier: ^3.17.0 58 | version: 3.17.0 59 | devDependencies: 60 | '@types/body-parser': 61 | specifier: ^1.19.5 62 | version: 1.19.5 63 | '@types/cors': 64 | specifier: ^2.8.17 65 | version: 2.8.17 66 | '@types/express': 67 | specifier: ^5.0.1 68 | version: 5.0.1 69 | '@types/multer': 70 | specifier: ^1.4.12 71 | version: 1.4.12 72 | '@types/node': 73 | specifier: ^22.13.13 74 | version: 22.13.13 75 | eslint: 76 | specifier: ^9.23.0 77 | version: 9.23.0(jiti@2.4.2) 78 | nodemon: 79 | specifier: ^3.1.9 80 | version: 3.1.9 81 | prettier: 82 | specifier: ^3.5.3 83 | version: 3.5.3 84 | 85 | packages: 86 | 87 | '@colors/colors@1.6.0': 88 | resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} 89 | engines: {node: '>=0.1.90'} 90 | 91 | '@dabh/diagnostics@2.0.3': 92 | resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} 93 | 94 | '@eslint-community/eslint-utils@4.5.1': 95 | resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} 96 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 97 | peerDependencies: 98 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 99 | 100 | '@eslint-community/regexpp@4.12.1': 101 | resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 102 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 103 | 104 | '@eslint/config-array@0.19.2': 105 | resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} 106 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 107 | 108 | '@eslint/config-helpers@0.2.0': 109 | resolution: {integrity: sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==} 110 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 111 | 112 | '@eslint/core@0.12.0': 113 | resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} 114 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 115 | 116 | '@eslint/eslintrc@3.3.1': 117 | resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} 118 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 119 | 120 | '@eslint/js@9.23.0': 121 | resolution: {integrity: sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==} 122 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 123 | 124 | '@eslint/object-schema@2.1.6': 125 | resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} 126 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 127 | 128 | '@eslint/plugin-kit@0.2.7': 129 | resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} 130 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 131 | 132 | '@humanfs/core@0.19.1': 133 | resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 134 | engines: {node: '>=18.18.0'} 135 | 136 | '@humanfs/node@0.16.6': 137 | resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} 138 | engines: {node: '>=18.18.0'} 139 | 140 | '@humanwhocodes/module-importer@1.0.1': 141 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 142 | engines: {node: '>=12.22'} 143 | 144 | '@humanwhocodes/retry@0.3.1': 145 | resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} 146 | engines: {node: '>=18.18'} 147 | 148 | '@humanwhocodes/retry@0.4.2': 149 | resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} 150 | engines: {node: '>=18.18'} 151 | 152 | '@parcel/watcher-android-arm64@2.5.1': 153 | resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} 154 | engines: {node: '>= 10.0.0'} 155 | cpu: [arm64] 156 | os: [android] 157 | 158 | '@parcel/watcher-darwin-arm64@2.5.1': 159 | resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} 160 | engines: {node: '>= 10.0.0'} 161 | cpu: [arm64] 162 | os: [darwin] 163 | 164 | '@parcel/watcher-darwin-x64@2.5.1': 165 | resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} 166 | engines: {node: '>= 10.0.0'} 167 | cpu: [x64] 168 | os: [darwin] 169 | 170 | '@parcel/watcher-freebsd-x64@2.5.1': 171 | resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} 172 | engines: {node: '>= 10.0.0'} 173 | cpu: [x64] 174 | os: [freebsd] 175 | 176 | '@parcel/watcher-linux-arm-glibc@2.5.1': 177 | resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} 178 | engines: {node: '>= 10.0.0'} 179 | cpu: [arm] 180 | os: [linux] 181 | 182 | '@parcel/watcher-linux-arm-musl@2.5.1': 183 | resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} 184 | engines: {node: '>= 10.0.0'} 185 | cpu: [arm] 186 | os: [linux] 187 | 188 | '@parcel/watcher-linux-arm64-glibc@2.5.1': 189 | resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} 190 | engines: {node: '>= 10.0.0'} 191 | cpu: [arm64] 192 | os: [linux] 193 | 194 | '@parcel/watcher-linux-arm64-musl@2.5.1': 195 | resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} 196 | engines: {node: '>= 10.0.0'} 197 | cpu: [arm64] 198 | os: [linux] 199 | 200 | '@parcel/watcher-linux-x64-glibc@2.5.1': 201 | resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} 202 | engines: {node: '>= 10.0.0'} 203 | cpu: [x64] 204 | os: [linux] 205 | 206 | '@parcel/watcher-linux-x64-musl@2.5.1': 207 | resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} 208 | engines: {node: '>= 10.0.0'} 209 | cpu: [x64] 210 | os: [linux] 211 | 212 | '@parcel/watcher-win32-arm64@2.5.1': 213 | resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} 214 | engines: {node: '>= 10.0.0'} 215 | cpu: [arm64] 216 | os: [win32] 217 | 218 | '@parcel/watcher-win32-ia32@2.5.1': 219 | resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} 220 | engines: {node: '>= 10.0.0'} 221 | cpu: [ia32] 222 | os: [win32] 223 | 224 | '@parcel/watcher-win32-x64@2.5.1': 225 | resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} 226 | engines: {node: '>= 10.0.0'} 227 | cpu: [x64] 228 | os: [win32] 229 | 230 | '@parcel/watcher@2.5.1': 231 | resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} 232 | engines: {node: '>= 10.0.0'} 233 | 234 | '@tailwindcss/cli@4.0.15': 235 | resolution: {integrity: sha512-52RdNZCpij4O8+25N9sfWZPG124e6ahmIS1uMHcJrdw10UdpPUFgSJtyMwf7COVOnkx0nkXfmp8CcYomPCrQ1Q==} 236 | hasBin: true 237 | 238 | '@tailwindcss/node@4.0.15': 239 | resolution: {integrity: sha512-IODaJjNmiasfZX3IoS+4Em3iu0fD2HS0/tgrnkYfW4hyUor01Smnr5eY3jc4rRgaTDrJlDmBTHbFO0ETTDaxWA==} 240 | 241 | '@tailwindcss/oxide-android-arm64@4.0.15': 242 | resolution: {integrity: sha512-EBuyfSKkom7N+CB3A+7c0m4+qzKuiN0WCvzPvj5ZoRu4NlQadg/mthc1tl5k9b5ffRGsbDvP4k21azU4VwVk3Q==} 243 | engines: {node: '>= 10'} 244 | cpu: [arm64] 245 | os: [android] 246 | 247 | '@tailwindcss/oxide-darwin-arm64@4.0.15': 248 | resolution: {integrity: sha512-ObVAnEpLepMhV9VoO0JSit66jiN5C4YCqW3TflsE9boo2Z7FIjV80RFbgeL2opBhtxbaNEDa6D0/hq/EP03kgQ==} 249 | engines: {node: '>= 10'} 250 | cpu: [arm64] 251 | os: [darwin] 252 | 253 | '@tailwindcss/oxide-darwin-x64@4.0.15': 254 | resolution: {integrity: sha512-IElwoFhUinOr9MyKmGTPNi1Rwdh68JReFgYWibPWTGuevkHkLWKEflZc2jtI5lWZ5U9JjUnUfnY43I4fEXrc4g==} 255 | engines: {node: '>= 10'} 256 | cpu: [x64] 257 | os: [darwin] 258 | 259 | '@tailwindcss/oxide-freebsd-x64@4.0.15': 260 | resolution: {integrity: sha512-6BLLqyx7SIYRBOnTZ8wgfXANLJV5TQd3PevRJZp0vn42eO58A2LykRKdvL1qyPfdpmEVtF+uVOEZ4QTMqDRAWA==} 261 | engines: {node: '>= 10'} 262 | cpu: [x64] 263 | os: [freebsd] 264 | 265 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15': 266 | resolution: {integrity: sha512-Zy63EVqO9241Pfg6G0IlRIWyY5vNcWrL5dd2WAKVJZRQVeolXEf1KfjkyeAAlErDj72cnyXObEZjMoPEKHpdNw==} 267 | engines: {node: '>= 10'} 268 | cpu: [arm] 269 | os: [linux] 270 | 271 | '@tailwindcss/oxide-linux-arm64-gnu@4.0.15': 272 | resolution: {integrity: sha512-2NemGQeaTbtIp1Z2wyerbVEJZTkAWhMDOhhR5z/zJ75yMNf8yLnE+sAlyf6yGDNr+1RqvWrRhhCFt7i0CIxe4Q==} 273 | engines: {node: '>= 10'} 274 | cpu: [arm64] 275 | os: [linux] 276 | 277 | '@tailwindcss/oxide-linux-arm64-musl@4.0.15': 278 | resolution: {integrity: sha512-342GVnhH/6PkVgKtEzvNVuQ4D+Q7B7qplvuH20Cfz9qEtydG6IQczTZ5IT4JPlh931MG1NUCVxg+CIorr1WJyw==} 279 | engines: {node: '>= 10'} 280 | cpu: [arm64] 281 | os: [linux] 282 | 283 | '@tailwindcss/oxide-linux-x64-gnu@4.0.15': 284 | resolution: {integrity: sha512-g76GxlKH124RuGqacCEFc2nbzRl7bBrlC8qDQMiUABkiifDRHOIUjgKbLNG4RuR9hQAD/MKsqZ7A8L08zsoBrw==} 285 | engines: {node: '>= 10'} 286 | cpu: [x64] 287 | os: [linux] 288 | 289 | '@tailwindcss/oxide-linux-x64-musl@4.0.15': 290 | resolution: {integrity: sha512-Gg/Y1XrKEvKpq6WeNt2h8rMIKOBj/W3mNa5NMvkQgMC7iO0+UNLrYmt6zgZufht66HozNpn+tJMbbkZ5a3LczA==} 291 | engines: {node: '>= 10'} 292 | cpu: [x64] 293 | os: [linux] 294 | 295 | '@tailwindcss/oxide-win32-arm64-msvc@4.0.15': 296 | resolution: {integrity: sha512-7QtSSJwYZ7ZK1phVgcNZpuf7c7gaCj8Wb0xjliligT5qCGCp79OV2n3SJummVZdw4fbTNKUOYMO7m1GinppZyA==} 297 | engines: {node: '>= 10'} 298 | cpu: [arm64] 299 | os: [win32] 300 | 301 | '@tailwindcss/oxide-win32-x64-msvc@4.0.15': 302 | resolution: {integrity: sha512-JQ5H+5MLhOjpgNp6KomouE0ZuKmk3hO5h7/ClMNAQ8gZI2zkli3IH8ZqLbd2DVfXDbdxN2xvooIEeIlkIoSCqw==} 303 | engines: {node: '>= 10'} 304 | cpu: [x64] 305 | os: [win32] 306 | 307 | '@tailwindcss/oxide@4.0.15': 308 | resolution: {integrity: sha512-e0uHrKfPu7JJGMfjwVNyt5M0u+OP8kUmhACwIRlM+JNBuReDVQ63yAD1NWe5DwJtdaHjugNBil76j+ks3zlk6g==} 309 | engines: {node: '>= 10'} 310 | 311 | '@types/body-parser@1.19.5': 312 | resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} 313 | 314 | '@types/connect@3.4.38': 315 | resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 316 | 317 | '@types/cors@2.8.17': 318 | resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} 319 | 320 | '@types/estree@1.0.7': 321 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} 322 | 323 | '@types/express-serve-static-core@5.0.6': 324 | resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} 325 | 326 | '@types/express@5.0.1': 327 | resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} 328 | 329 | '@types/http-errors@2.0.4': 330 | resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} 331 | 332 | '@types/json-schema@7.0.15': 333 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 334 | 335 | '@types/mime@1.3.5': 336 | resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 337 | 338 | '@types/multer@1.4.12': 339 | resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==} 340 | 341 | '@types/node@22.13.13': 342 | resolution: {integrity: sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==} 343 | 344 | '@types/qs@6.9.18': 345 | resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} 346 | 347 | '@types/range-parser@1.2.7': 348 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 349 | 350 | '@types/send@0.17.4': 351 | resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} 352 | 353 | '@types/serve-static@1.15.7': 354 | resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} 355 | 356 | '@types/triple-beam@1.3.5': 357 | resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} 358 | 359 | accepts@1.3.8: 360 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 361 | engines: {node: '>= 0.6'} 362 | 363 | acorn-jsx@5.3.2: 364 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 365 | peerDependencies: 366 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 367 | 368 | acorn@8.14.1: 369 | resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} 370 | engines: {node: '>=0.4.0'} 371 | hasBin: true 372 | 373 | ajv@6.12.6: 374 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 375 | 376 | ansi-styles@4.3.0: 377 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 378 | engines: {node: '>=8'} 379 | 380 | anymatch@3.1.3: 381 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 382 | engines: {node: '>= 8'} 383 | 384 | append-field@1.0.0: 385 | resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} 386 | 387 | argparse@2.0.1: 388 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 389 | 390 | array-flatten@1.1.1: 391 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 392 | 393 | async@3.2.6: 394 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 395 | 396 | asynckit@0.4.0: 397 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 398 | 399 | axios-cache-interceptor@1.7.0: 400 | resolution: {integrity: sha512-SP/QpCApakyGncF7ttyGsKt0CK5XG+tsdGAGTumHTzO1acnn9v7bd1XK1LiIcyXp46mlKS8j5iwvhNyFFWUKZQ==} 401 | engines: {node: '>=12'} 402 | peerDependencies: 403 | axios: ^1 404 | 405 | axios@1.8.4: 406 | resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} 407 | 408 | balanced-match@1.0.2: 409 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 410 | 411 | binary-extensions@2.3.0: 412 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 413 | engines: {node: '>=8'} 414 | 415 | body-parser@1.20.3: 416 | resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} 417 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 418 | 419 | brace-expansion@1.1.11: 420 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 421 | 422 | brace-expansion@2.0.1: 423 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 424 | 425 | braces@3.0.3: 426 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 427 | engines: {node: '>=8'} 428 | 429 | buffer-from@1.1.2: 430 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 431 | 432 | busboy@1.6.0: 433 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 434 | engines: {node: '>=10.16.0'} 435 | 436 | bytes@3.1.2: 437 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 438 | engines: {node: '>= 0.8'} 439 | 440 | cache-parser@1.2.5: 441 | resolution: {integrity: sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==} 442 | 443 | call-bind-apply-helpers@1.0.2: 444 | resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 445 | engines: {node: '>= 0.4'} 446 | 447 | call-bound@1.0.4: 448 | resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 449 | engines: {node: '>= 0.4'} 450 | 451 | callsites@3.1.0: 452 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 453 | engines: {node: '>=6'} 454 | 455 | chalk@4.1.2: 456 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 457 | engines: {node: '>=10'} 458 | 459 | chalk@5.4.1: 460 | resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} 461 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 462 | 463 | chokidar@3.6.0: 464 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 465 | engines: {node: '>= 8.10.0'} 466 | 467 | color-convert@1.9.3: 468 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 469 | 470 | color-convert@2.0.1: 471 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 472 | engines: {node: '>=7.0.0'} 473 | 474 | color-name@1.1.3: 475 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 476 | 477 | color-name@1.1.4: 478 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 479 | 480 | color-string@1.9.1: 481 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 482 | 483 | color@3.2.1: 484 | resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} 485 | 486 | colorspace@1.1.4: 487 | resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} 488 | 489 | combined-stream@1.0.8: 490 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 491 | engines: {node: '>= 0.8'} 492 | 493 | concat-map@0.0.1: 494 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 495 | 496 | concat-stream@1.6.2: 497 | resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} 498 | engines: {'0': node >= 0.8} 499 | 500 | content-disposition@0.5.4: 501 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 502 | engines: {node: '>= 0.6'} 503 | 504 | content-type@1.0.5: 505 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 506 | engines: {node: '>= 0.6'} 507 | 508 | cookie-signature@1.0.6: 509 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 510 | 511 | cookie@0.7.1: 512 | resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} 513 | engines: {node: '>= 0.6'} 514 | 515 | core-util-is@1.0.3: 516 | resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 517 | 518 | cors@2.8.5: 519 | resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 520 | engines: {node: '>= 0.10'} 521 | 522 | cross-spawn@7.0.6: 523 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 524 | engines: {node: '>= 8'} 525 | 526 | date-fns@4.1.0: 527 | resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} 528 | 529 | debug@2.6.9: 530 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 531 | peerDependencies: 532 | supports-color: '*' 533 | peerDependenciesMeta: 534 | supports-color: 535 | optional: true 536 | 537 | debug@4.4.0: 538 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 539 | engines: {node: '>=6.0'} 540 | peerDependencies: 541 | supports-color: '*' 542 | peerDependenciesMeta: 543 | supports-color: 544 | optional: true 545 | 546 | deep-is@0.1.4: 547 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 548 | 549 | delayed-stream@1.0.0: 550 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 551 | engines: {node: '>=0.4.0'} 552 | 553 | depd@2.0.0: 554 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 555 | engines: {node: '>= 0.8'} 556 | 557 | destroy@1.2.0: 558 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 559 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 560 | 561 | detect-libc@1.0.3: 562 | resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} 563 | engines: {node: '>=0.10'} 564 | hasBin: true 565 | 566 | detect-libc@2.0.3: 567 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 568 | engines: {node: '>=8'} 569 | 570 | dotenv@16.4.7: 571 | resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} 572 | engines: {node: '>=12'} 573 | 574 | dunder-proto@1.0.1: 575 | resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 576 | engines: {node: '>= 0.4'} 577 | 578 | ee-first@1.1.1: 579 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 580 | 581 | ejs@3.1.10: 582 | resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} 583 | engines: {node: '>=0.10.0'} 584 | hasBin: true 585 | 586 | enabled@2.0.0: 587 | resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} 588 | 589 | encodeurl@1.0.2: 590 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 591 | engines: {node: '>= 0.8'} 592 | 593 | encodeurl@2.0.0: 594 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 595 | engines: {node: '>= 0.8'} 596 | 597 | enhanced-resolve@5.18.1: 598 | resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} 599 | engines: {node: '>=10.13.0'} 600 | 601 | es-define-property@1.0.1: 602 | resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 603 | engines: {node: '>= 0.4'} 604 | 605 | es-errors@1.3.0: 606 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 607 | engines: {node: '>= 0.4'} 608 | 609 | es-object-atoms@1.1.1: 610 | resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 611 | engines: {node: '>= 0.4'} 612 | 613 | es-set-tostringtag@2.1.0: 614 | resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 615 | engines: {node: '>= 0.4'} 616 | 617 | escape-html@1.0.3: 618 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 619 | 620 | escape-string-regexp@4.0.0: 621 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 622 | engines: {node: '>=10'} 623 | 624 | eslint-scope@8.3.0: 625 | resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} 626 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 627 | 628 | eslint-visitor-keys@3.4.3: 629 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 630 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 631 | 632 | eslint-visitor-keys@4.2.0: 633 | resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} 634 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 635 | 636 | eslint@9.23.0: 637 | resolution: {integrity: sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==} 638 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 639 | hasBin: true 640 | peerDependencies: 641 | jiti: '*' 642 | peerDependenciesMeta: 643 | jiti: 644 | optional: true 645 | 646 | espree@10.3.0: 647 | resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} 648 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 649 | 650 | esquery@1.6.0: 651 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 652 | engines: {node: '>=0.10'} 653 | 654 | esrecurse@4.3.0: 655 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 656 | engines: {node: '>=4.0'} 657 | 658 | estraverse@5.3.0: 659 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 660 | engines: {node: '>=4.0'} 661 | 662 | esutils@2.0.3: 663 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 664 | engines: {node: '>=0.10.0'} 665 | 666 | etag@1.8.1: 667 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 668 | engines: {node: '>= 0.6'} 669 | 670 | express-async-errors@3.1.1: 671 | resolution: {integrity: sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==} 672 | peerDependencies: 673 | express: ^4.16.2 674 | 675 | express-rate-limit@7.5.0: 676 | resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} 677 | engines: {node: '>= 16'} 678 | peerDependencies: 679 | express: ^4.11 || 5 || ^5.0.0-beta.1 680 | 681 | express@4.21.2: 682 | resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} 683 | engines: {node: '>= 0.10.0'} 684 | 685 | fast-deep-equal@3.1.3: 686 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 687 | 688 | fast-defer@1.1.8: 689 | resolution: {integrity: sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==} 690 | 691 | fast-json-stable-stringify@2.1.0: 692 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 693 | 694 | fast-levenshtein@2.0.6: 695 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 696 | 697 | fecha@4.2.3: 698 | resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} 699 | 700 | file-entry-cache@8.0.0: 701 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 702 | engines: {node: '>=16.0.0'} 703 | 704 | filelist@1.0.4: 705 | resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} 706 | 707 | fill-range@7.1.1: 708 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 709 | engines: {node: '>=8'} 710 | 711 | finalhandler@1.3.1: 712 | resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} 713 | engines: {node: '>= 0.8'} 714 | 715 | find-up@5.0.0: 716 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 717 | engines: {node: '>=10'} 718 | 719 | flat-cache@4.0.1: 720 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 721 | engines: {node: '>=16'} 722 | 723 | flatted@3.3.3: 724 | resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 725 | 726 | fn.name@1.1.0: 727 | resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} 728 | 729 | follow-redirects@1.15.9: 730 | resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 731 | engines: {node: '>=4.0'} 732 | peerDependencies: 733 | debug: '*' 734 | peerDependenciesMeta: 735 | debug: 736 | optional: true 737 | 738 | form-data@4.0.2: 739 | resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} 740 | engines: {node: '>= 6'} 741 | 742 | forwarded@0.2.0: 743 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 744 | engines: {node: '>= 0.6'} 745 | 746 | fresh@0.5.2: 747 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 748 | engines: {node: '>= 0.6'} 749 | 750 | fsevents@2.3.3: 751 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 752 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 753 | os: [darwin] 754 | 755 | function-bind@1.1.2: 756 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 757 | 758 | get-intrinsic@1.3.0: 759 | resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 760 | engines: {node: '>= 0.4'} 761 | 762 | get-proto@1.0.1: 763 | resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 764 | engines: {node: '>= 0.4'} 765 | 766 | glob-parent@5.1.2: 767 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 768 | engines: {node: '>= 6'} 769 | 770 | glob-parent@6.0.2: 771 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 772 | engines: {node: '>=10.13.0'} 773 | 774 | globals@14.0.0: 775 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 776 | engines: {node: '>=18'} 777 | 778 | gopd@1.2.0: 779 | resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 780 | engines: {node: '>= 0.4'} 781 | 782 | graceful-fs@4.2.11: 783 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 784 | 785 | has-flag@3.0.0: 786 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 787 | engines: {node: '>=4'} 788 | 789 | has-flag@4.0.0: 790 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 791 | engines: {node: '>=8'} 792 | 793 | has-symbols@1.1.0: 794 | resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 795 | engines: {node: '>= 0.4'} 796 | 797 | has-tostringtag@1.0.2: 798 | resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 799 | engines: {node: '>= 0.4'} 800 | 801 | hasown@2.0.2: 802 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 803 | engines: {node: '>= 0.4'} 804 | 805 | http-errors@2.0.0: 806 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 807 | engines: {node: '>= 0.8'} 808 | 809 | iconv-lite@0.4.24: 810 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 811 | engines: {node: '>=0.10.0'} 812 | 813 | ignore-by-default@1.0.1: 814 | resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} 815 | 816 | ignore@5.3.2: 817 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 818 | engines: {node: '>= 4'} 819 | 820 | import-fresh@3.3.1: 821 | resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 822 | engines: {node: '>=6'} 823 | 824 | imurmurhash@0.1.4: 825 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 826 | engines: {node: '>=0.8.19'} 827 | 828 | inherits@2.0.4: 829 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 830 | 831 | ipaddr.js@1.9.1: 832 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 833 | engines: {node: '>= 0.10'} 834 | 835 | is-arrayish@0.3.2: 836 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 837 | 838 | is-binary-path@2.1.0: 839 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 840 | engines: {node: '>=8'} 841 | 842 | is-extglob@2.1.1: 843 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 844 | engines: {node: '>=0.10.0'} 845 | 846 | is-glob@4.0.3: 847 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 848 | engines: {node: '>=0.10.0'} 849 | 850 | is-number@7.0.0: 851 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 852 | engines: {node: '>=0.12.0'} 853 | 854 | is-stream@2.0.1: 855 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 856 | engines: {node: '>=8'} 857 | 858 | isarray@1.0.0: 859 | resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} 860 | 861 | isexe@2.0.0: 862 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 863 | 864 | jake@10.9.2: 865 | resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} 866 | engines: {node: '>=10'} 867 | hasBin: true 868 | 869 | jiti@2.4.2: 870 | resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} 871 | hasBin: true 872 | 873 | js-yaml@4.1.0: 874 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 875 | hasBin: true 876 | 877 | json-buffer@3.0.1: 878 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 879 | 880 | json-schema-traverse@0.4.1: 881 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 882 | 883 | json-stable-stringify-without-jsonify@1.0.1: 884 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 885 | 886 | keyv@4.5.4: 887 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 888 | 889 | kuler@2.0.0: 890 | resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} 891 | 892 | levn@0.4.1: 893 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 894 | engines: {node: '>= 0.8.0'} 895 | 896 | lightningcss-darwin-arm64@1.29.2: 897 | resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} 898 | engines: {node: '>= 12.0.0'} 899 | cpu: [arm64] 900 | os: [darwin] 901 | 902 | lightningcss-darwin-x64@1.29.2: 903 | resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} 904 | engines: {node: '>= 12.0.0'} 905 | cpu: [x64] 906 | os: [darwin] 907 | 908 | lightningcss-freebsd-x64@1.29.2: 909 | resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} 910 | engines: {node: '>= 12.0.0'} 911 | cpu: [x64] 912 | os: [freebsd] 913 | 914 | lightningcss-linux-arm-gnueabihf@1.29.2: 915 | resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} 916 | engines: {node: '>= 12.0.0'} 917 | cpu: [arm] 918 | os: [linux] 919 | 920 | lightningcss-linux-arm64-gnu@1.29.2: 921 | resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} 922 | engines: {node: '>= 12.0.0'} 923 | cpu: [arm64] 924 | os: [linux] 925 | 926 | lightningcss-linux-arm64-musl@1.29.2: 927 | resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} 928 | engines: {node: '>= 12.0.0'} 929 | cpu: [arm64] 930 | os: [linux] 931 | 932 | lightningcss-linux-x64-gnu@1.29.2: 933 | resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} 934 | engines: {node: '>= 12.0.0'} 935 | cpu: [x64] 936 | os: [linux] 937 | 938 | lightningcss-linux-x64-musl@1.29.2: 939 | resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} 940 | engines: {node: '>= 12.0.0'} 941 | cpu: [x64] 942 | os: [linux] 943 | 944 | lightningcss-win32-arm64-msvc@1.29.2: 945 | resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} 946 | engines: {node: '>= 12.0.0'} 947 | cpu: [arm64] 948 | os: [win32] 949 | 950 | lightningcss-win32-x64-msvc@1.29.2: 951 | resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} 952 | engines: {node: '>= 12.0.0'} 953 | cpu: [x64] 954 | os: [win32] 955 | 956 | lightningcss@1.29.2: 957 | resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} 958 | engines: {node: '>= 12.0.0'} 959 | 960 | locate-path@6.0.0: 961 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 962 | engines: {node: '>=10'} 963 | 964 | lodash.merge@4.6.2: 965 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 966 | 967 | lodash@4.17.21: 968 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 969 | 970 | logform@2.7.0: 971 | resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} 972 | engines: {node: '>= 12.0.0'} 973 | 974 | math-intrinsics@1.1.0: 975 | resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 976 | engines: {node: '>= 0.4'} 977 | 978 | media-typer@0.3.0: 979 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 980 | engines: {node: '>= 0.6'} 981 | 982 | merge-descriptors@1.0.3: 983 | resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} 984 | 985 | methods@1.1.2: 986 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 987 | engines: {node: '>= 0.6'} 988 | 989 | micromatch@4.0.8: 990 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 991 | engines: {node: '>=8.6'} 992 | 993 | mime-db@1.52.0: 994 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 995 | engines: {node: '>= 0.6'} 996 | 997 | mime-types@2.1.35: 998 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 999 | engines: {node: '>= 0.6'} 1000 | 1001 | mime@1.6.0: 1002 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 1003 | engines: {node: '>=4'} 1004 | hasBin: true 1005 | 1006 | minimatch@3.1.2: 1007 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1008 | 1009 | minimatch@5.1.6: 1010 | resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} 1011 | engines: {node: '>=10'} 1012 | 1013 | minimist@1.2.8: 1014 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1015 | 1016 | mkdirp@0.5.6: 1017 | resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} 1018 | hasBin: true 1019 | 1020 | mri@1.2.0: 1021 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 1022 | engines: {node: '>=4'} 1023 | 1024 | ms@2.0.0: 1025 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 1026 | 1027 | ms@2.1.3: 1028 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1029 | 1030 | multer@1.4.5-lts.2: 1031 | resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} 1032 | engines: {node: '>= 6.0.0'} 1033 | 1034 | natural-compare@1.4.0: 1035 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1036 | 1037 | negotiator@0.6.3: 1038 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 1039 | engines: {node: '>= 0.6'} 1040 | 1041 | node-addon-api@7.1.1: 1042 | resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} 1043 | 1044 | node-localstorage@3.0.5: 1045 | resolution: {integrity: sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg==} 1046 | engines: {node: '>=0.12'} 1047 | 1048 | nodemon@3.1.9: 1049 | resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==} 1050 | engines: {node: '>=10'} 1051 | hasBin: true 1052 | 1053 | normalize-path@3.0.0: 1054 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1055 | engines: {node: '>=0.10.0'} 1056 | 1057 | object-assign@4.1.1: 1058 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1059 | engines: {node: '>=0.10.0'} 1060 | 1061 | object-code@1.3.3: 1062 | resolution: {integrity: sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==} 1063 | 1064 | object-inspect@1.13.4: 1065 | resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 1066 | engines: {node: '>= 0.4'} 1067 | 1068 | on-finished@2.4.1: 1069 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 1070 | engines: {node: '>= 0.8'} 1071 | 1072 | one-time@1.0.0: 1073 | resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} 1074 | 1075 | optionator@0.9.4: 1076 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1077 | engines: {node: '>= 0.8.0'} 1078 | 1079 | p-limit@3.1.0: 1080 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1081 | engines: {node: '>=10'} 1082 | 1083 | p-locate@5.0.0: 1084 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1085 | engines: {node: '>=10'} 1086 | 1087 | parent-module@1.0.1: 1088 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1089 | engines: {node: '>=6'} 1090 | 1091 | parseurl@1.3.3: 1092 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 1093 | engines: {node: '>= 0.8'} 1094 | 1095 | path-exists@4.0.0: 1096 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1097 | engines: {node: '>=8'} 1098 | 1099 | path-key@3.1.1: 1100 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1101 | engines: {node: '>=8'} 1102 | 1103 | path-to-regexp@0.1.12: 1104 | resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} 1105 | 1106 | picocolors@1.1.1: 1107 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1108 | 1109 | picomatch@2.3.1: 1110 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1111 | engines: {node: '>=8.6'} 1112 | 1113 | prelude-ls@1.2.1: 1114 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1115 | engines: {node: '>= 0.8.0'} 1116 | 1117 | prettier@3.5.3: 1118 | resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} 1119 | engines: {node: '>=14'} 1120 | hasBin: true 1121 | 1122 | process-nextick-args@2.0.1: 1123 | resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} 1124 | 1125 | proxy-addr@2.0.7: 1126 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 1127 | engines: {node: '>= 0.10'} 1128 | 1129 | proxy-from-env@1.1.0: 1130 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1131 | 1132 | pstree.remy@1.1.8: 1133 | resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} 1134 | 1135 | punycode@2.3.1: 1136 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1137 | engines: {node: '>=6'} 1138 | 1139 | qs@6.13.0: 1140 | resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} 1141 | engines: {node: '>=0.6'} 1142 | 1143 | range-parser@1.2.1: 1144 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 1145 | engines: {node: '>= 0.6'} 1146 | 1147 | raw-body@2.5.2: 1148 | resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} 1149 | engines: {node: '>= 0.8'} 1150 | 1151 | readable-stream@2.3.8: 1152 | resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} 1153 | 1154 | readable-stream@3.6.2: 1155 | resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 1156 | engines: {node: '>= 6'} 1157 | 1158 | readdirp@3.6.0: 1159 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1160 | engines: {node: '>=8.10.0'} 1161 | 1162 | resolve-from@4.0.0: 1163 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1164 | engines: {node: '>=4'} 1165 | 1166 | safe-buffer@5.1.2: 1167 | resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 1168 | 1169 | safe-buffer@5.2.1: 1170 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1171 | 1172 | safe-stable-stringify@2.5.0: 1173 | resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 1174 | engines: {node: '>=10'} 1175 | 1176 | safer-buffer@2.1.2: 1177 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1178 | 1179 | semver@7.7.1: 1180 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 1181 | engines: {node: '>=10'} 1182 | hasBin: true 1183 | 1184 | send@0.19.0: 1185 | resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} 1186 | engines: {node: '>= 0.8.0'} 1187 | 1188 | serve-static@1.16.2: 1189 | resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} 1190 | engines: {node: '>= 0.8.0'} 1191 | 1192 | setprototypeof@1.2.0: 1193 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 1194 | 1195 | shebang-command@2.0.0: 1196 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1197 | engines: {node: '>=8'} 1198 | 1199 | shebang-regex@3.0.0: 1200 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1201 | engines: {node: '>=8'} 1202 | 1203 | side-channel-list@1.0.0: 1204 | resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 1205 | engines: {node: '>= 0.4'} 1206 | 1207 | side-channel-map@1.0.1: 1208 | resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 1209 | engines: {node: '>= 0.4'} 1210 | 1211 | side-channel-weakmap@1.0.2: 1212 | resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 1213 | engines: {node: '>= 0.4'} 1214 | 1215 | side-channel@1.1.0: 1216 | resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 1217 | engines: {node: '>= 0.4'} 1218 | 1219 | signal-exit@4.1.0: 1220 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1221 | engines: {node: '>=14'} 1222 | 1223 | simple-swizzle@0.2.2: 1224 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 1225 | 1226 | simple-update-notifier@2.0.0: 1227 | resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} 1228 | engines: {node: '>=10'} 1229 | 1230 | stack-trace@0.0.10: 1231 | resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} 1232 | 1233 | statuses@2.0.1: 1234 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 1235 | engines: {node: '>= 0.8'} 1236 | 1237 | streamsearch@1.1.0: 1238 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 1239 | engines: {node: '>=10.0.0'} 1240 | 1241 | string_decoder@1.1.1: 1242 | resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} 1243 | 1244 | string_decoder@1.3.0: 1245 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1246 | 1247 | strip-json-comments@3.1.1: 1248 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1249 | engines: {node: '>=8'} 1250 | 1251 | supports-color@5.5.0: 1252 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 1253 | engines: {node: '>=4'} 1254 | 1255 | supports-color@7.2.0: 1256 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1257 | engines: {node: '>=8'} 1258 | 1259 | tailwindcss@4.0.15: 1260 | resolution: {integrity: sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==} 1261 | 1262 | tapable@2.2.1: 1263 | resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} 1264 | engines: {node: '>=6'} 1265 | 1266 | text-hex@1.0.0: 1267 | resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} 1268 | 1269 | to-regex-range@5.0.1: 1270 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1271 | engines: {node: '>=8.0'} 1272 | 1273 | toidentifier@1.0.1: 1274 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 1275 | engines: {node: '>=0.6'} 1276 | 1277 | touch@3.1.1: 1278 | resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} 1279 | hasBin: true 1280 | 1281 | triple-beam@1.4.1: 1282 | resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} 1283 | engines: {node: '>= 14.0.0'} 1284 | 1285 | type-check@0.4.0: 1286 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1287 | engines: {node: '>= 0.8.0'} 1288 | 1289 | type-is@1.6.18: 1290 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 1291 | engines: {node: '>= 0.6'} 1292 | 1293 | typedarray@0.0.6: 1294 | resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} 1295 | 1296 | undefsafe@2.0.5: 1297 | resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} 1298 | 1299 | undici-types@6.20.0: 1300 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 1301 | 1302 | unpipe@1.0.0: 1303 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 1304 | engines: {node: '>= 0.8'} 1305 | 1306 | uri-js@4.4.1: 1307 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1308 | 1309 | util-deprecate@1.0.2: 1310 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1311 | 1312 | utils-merge@1.0.1: 1313 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 1314 | engines: {node: '>= 0.4.0'} 1315 | 1316 | vary@1.1.2: 1317 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 1318 | engines: {node: '>= 0.8'} 1319 | 1320 | which@2.0.2: 1321 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1322 | engines: {node: '>= 8'} 1323 | hasBin: true 1324 | 1325 | winston-transport@4.9.0: 1326 | resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} 1327 | engines: {node: '>= 12.0.0'} 1328 | 1329 | winston@3.17.0: 1330 | resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} 1331 | engines: {node: '>= 12.0.0'} 1332 | 1333 | word-wrap@1.2.5: 1334 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1335 | engines: {node: '>=0.10.0'} 1336 | 1337 | write-file-atomic@5.0.1: 1338 | resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} 1339 | engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 1340 | 1341 | xtend@4.0.2: 1342 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 1343 | engines: {node: '>=0.4'} 1344 | 1345 | yocto-queue@0.1.0: 1346 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1347 | engines: {node: '>=10'} 1348 | 1349 | snapshots: 1350 | 1351 | '@colors/colors@1.6.0': {} 1352 | 1353 | '@dabh/diagnostics@2.0.3': 1354 | dependencies: 1355 | colorspace: 1.1.4 1356 | enabled: 2.0.0 1357 | kuler: 2.0.0 1358 | 1359 | '@eslint-community/eslint-utils@4.5.1(eslint@9.23.0(jiti@2.4.2))': 1360 | dependencies: 1361 | eslint: 9.23.0(jiti@2.4.2) 1362 | eslint-visitor-keys: 3.4.3 1363 | 1364 | '@eslint-community/regexpp@4.12.1': {} 1365 | 1366 | '@eslint/config-array@0.19.2': 1367 | dependencies: 1368 | '@eslint/object-schema': 2.1.6 1369 | debug: 4.4.0(supports-color@5.5.0) 1370 | minimatch: 3.1.2 1371 | transitivePeerDependencies: 1372 | - supports-color 1373 | 1374 | '@eslint/config-helpers@0.2.0': {} 1375 | 1376 | '@eslint/core@0.12.0': 1377 | dependencies: 1378 | '@types/json-schema': 7.0.15 1379 | 1380 | '@eslint/eslintrc@3.3.1': 1381 | dependencies: 1382 | ajv: 6.12.6 1383 | debug: 4.4.0(supports-color@5.5.0) 1384 | espree: 10.3.0 1385 | globals: 14.0.0 1386 | ignore: 5.3.2 1387 | import-fresh: 3.3.1 1388 | js-yaml: 4.1.0 1389 | minimatch: 3.1.2 1390 | strip-json-comments: 3.1.1 1391 | transitivePeerDependencies: 1392 | - supports-color 1393 | 1394 | '@eslint/js@9.23.0': {} 1395 | 1396 | '@eslint/object-schema@2.1.6': {} 1397 | 1398 | '@eslint/plugin-kit@0.2.7': 1399 | dependencies: 1400 | '@eslint/core': 0.12.0 1401 | levn: 0.4.1 1402 | 1403 | '@humanfs/core@0.19.1': {} 1404 | 1405 | '@humanfs/node@0.16.6': 1406 | dependencies: 1407 | '@humanfs/core': 0.19.1 1408 | '@humanwhocodes/retry': 0.3.1 1409 | 1410 | '@humanwhocodes/module-importer@1.0.1': {} 1411 | 1412 | '@humanwhocodes/retry@0.3.1': {} 1413 | 1414 | '@humanwhocodes/retry@0.4.2': {} 1415 | 1416 | '@parcel/watcher-android-arm64@2.5.1': 1417 | optional: true 1418 | 1419 | '@parcel/watcher-darwin-arm64@2.5.1': 1420 | optional: true 1421 | 1422 | '@parcel/watcher-darwin-x64@2.5.1': 1423 | optional: true 1424 | 1425 | '@parcel/watcher-freebsd-x64@2.5.1': 1426 | optional: true 1427 | 1428 | '@parcel/watcher-linux-arm-glibc@2.5.1': 1429 | optional: true 1430 | 1431 | '@parcel/watcher-linux-arm-musl@2.5.1': 1432 | optional: true 1433 | 1434 | '@parcel/watcher-linux-arm64-glibc@2.5.1': 1435 | optional: true 1436 | 1437 | '@parcel/watcher-linux-arm64-musl@2.5.1': 1438 | optional: true 1439 | 1440 | '@parcel/watcher-linux-x64-glibc@2.5.1': 1441 | optional: true 1442 | 1443 | '@parcel/watcher-linux-x64-musl@2.5.1': 1444 | optional: true 1445 | 1446 | '@parcel/watcher-win32-arm64@2.5.1': 1447 | optional: true 1448 | 1449 | '@parcel/watcher-win32-ia32@2.5.1': 1450 | optional: true 1451 | 1452 | '@parcel/watcher-win32-x64@2.5.1': 1453 | optional: true 1454 | 1455 | '@parcel/watcher@2.5.1': 1456 | dependencies: 1457 | detect-libc: 1.0.3 1458 | is-glob: 4.0.3 1459 | micromatch: 4.0.8 1460 | node-addon-api: 7.1.1 1461 | optionalDependencies: 1462 | '@parcel/watcher-android-arm64': 2.5.1 1463 | '@parcel/watcher-darwin-arm64': 2.5.1 1464 | '@parcel/watcher-darwin-x64': 2.5.1 1465 | '@parcel/watcher-freebsd-x64': 2.5.1 1466 | '@parcel/watcher-linux-arm-glibc': 2.5.1 1467 | '@parcel/watcher-linux-arm-musl': 2.5.1 1468 | '@parcel/watcher-linux-arm64-glibc': 2.5.1 1469 | '@parcel/watcher-linux-arm64-musl': 2.5.1 1470 | '@parcel/watcher-linux-x64-glibc': 2.5.1 1471 | '@parcel/watcher-linux-x64-musl': 2.5.1 1472 | '@parcel/watcher-win32-arm64': 2.5.1 1473 | '@parcel/watcher-win32-ia32': 2.5.1 1474 | '@parcel/watcher-win32-x64': 2.5.1 1475 | 1476 | '@tailwindcss/cli@4.0.15': 1477 | dependencies: 1478 | '@parcel/watcher': 2.5.1 1479 | '@tailwindcss/node': 4.0.15 1480 | '@tailwindcss/oxide': 4.0.15 1481 | enhanced-resolve: 5.18.1 1482 | lightningcss: 1.29.2 1483 | mri: 1.2.0 1484 | picocolors: 1.1.1 1485 | tailwindcss: 4.0.15 1486 | 1487 | '@tailwindcss/node@4.0.15': 1488 | dependencies: 1489 | enhanced-resolve: 5.18.1 1490 | jiti: 2.4.2 1491 | tailwindcss: 4.0.15 1492 | 1493 | '@tailwindcss/oxide-android-arm64@4.0.15': 1494 | optional: true 1495 | 1496 | '@tailwindcss/oxide-darwin-arm64@4.0.15': 1497 | optional: true 1498 | 1499 | '@tailwindcss/oxide-darwin-x64@4.0.15': 1500 | optional: true 1501 | 1502 | '@tailwindcss/oxide-freebsd-x64@4.0.15': 1503 | optional: true 1504 | 1505 | '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.15': 1506 | optional: true 1507 | 1508 | '@tailwindcss/oxide-linux-arm64-gnu@4.0.15': 1509 | optional: true 1510 | 1511 | '@tailwindcss/oxide-linux-arm64-musl@4.0.15': 1512 | optional: true 1513 | 1514 | '@tailwindcss/oxide-linux-x64-gnu@4.0.15': 1515 | optional: true 1516 | 1517 | '@tailwindcss/oxide-linux-x64-musl@4.0.15': 1518 | optional: true 1519 | 1520 | '@tailwindcss/oxide-win32-arm64-msvc@4.0.15': 1521 | optional: true 1522 | 1523 | '@tailwindcss/oxide-win32-x64-msvc@4.0.15': 1524 | optional: true 1525 | 1526 | '@tailwindcss/oxide@4.0.15': 1527 | optionalDependencies: 1528 | '@tailwindcss/oxide-android-arm64': 4.0.15 1529 | '@tailwindcss/oxide-darwin-arm64': 4.0.15 1530 | '@tailwindcss/oxide-darwin-x64': 4.0.15 1531 | '@tailwindcss/oxide-freebsd-x64': 4.0.15 1532 | '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.15 1533 | '@tailwindcss/oxide-linux-arm64-gnu': 4.0.15 1534 | '@tailwindcss/oxide-linux-arm64-musl': 4.0.15 1535 | '@tailwindcss/oxide-linux-x64-gnu': 4.0.15 1536 | '@tailwindcss/oxide-linux-x64-musl': 4.0.15 1537 | '@tailwindcss/oxide-win32-arm64-msvc': 4.0.15 1538 | '@tailwindcss/oxide-win32-x64-msvc': 4.0.15 1539 | 1540 | '@types/body-parser@1.19.5': 1541 | dependencies: 1542 | '@types/connect': 3.4.38 1543 | '@types/node': 22.13.13 1544 | 1545 | '@types/connect@3.4.38': 1546 | dependencies: 1547 | '@types/node': 22.13.13 1548 | 1549 | '@types/cors@2.8.17': 1550 | dependencies: 1551 | '@types/node': 22.13.13 1552 | 1553 | '@types/estree@1.0.7': {} 1554 | 1555 | '@types/express-serve-static-core@5.0.6': 1556 | dependencies: 1557 | '@types/node': 22.13.13 1558 | '@types/qs': 6.9.18 1559 | '@types/range-parser': 1.2.7 1560 | '@types/send': 0.17.4 1561 | 1562 | '@types/express@5.0.1': 1563 | dependencies: 1564 | '@types/body-parser': 1.19.5 1565 | '@types/express-serve-static-core': 5.0.6 1566 | '@types/serve-static': 1.15.7 1567 | 1568 | '@types/http-errors@2.0.4': {} 1569 | 1570 | '@types/json-schema@7.0.15': {} 1571 | 1572 | '@types/mime@1.3.5': {} 1573 | 1574 | '@types/multer@1.4.12': 1575 | dependencies: 1576 | '@types/express': 5.0.1 1577 | 1578 | '@types/node@22.13.13': 1579 | dependencies: 1580 | undici-types: 6.20.0 1581 | 1582 | '@types/qs@6.9.18': {} 1583 | 1584 | '@types/range-parser@1.2.7': {} 1585 | 1586 | '@types/send@0.17.4': 1587 | dependencies: 1588 | '@types/mime': 1.3.5 1589 | '@types/node': 22.13.13 1590 | 1591 | '@types/serve-static@1.15.7': 1592 | dependencies: 1593 | '@types/http-errors': 2.0.4 1594 | '@types/node': 22.13.13 1595 | '@types/send': 0.17.4 1596 | 1597 | '@types/triple-beam@1.3.5': {} 1598 | 1599 | accepts@1.3.8: 1600 | dependencies: 1601 | mime-types: 2.1.35 1602 | negotiator: 0.6.3 1603 | 1604 | acorn-jsx@5.3.2(acorn@8.14.1): 1605 | dependencies: 1606 | acorn: 8.14.1 1607 | 1608 | acorn@8.14.1: {} 1609 | 1610 | ajv@6.12.6: 1611 | dependencies: 1612 | fast-deep-equal: 3.1.3 1613 | fast-json-stable-stringify: 2.1.0 1614 | json-schema-traverse: 0.4.1 1615 | uri-js: 4.4.1 1616 | 1617 | ansi-styles@4.3.0: 1618 | dependencies: 1619 | color-convert: 2.0.1 1620 | 1621 | anymatch@3.1.3: 1622 | dependencies: 1623 | normalize-path: 3.0.0 1624 | picomatch: 2.3.1 1625 | 1626 | append-field@1.0.0: {} 1627 | 1628 | argparse@2.0.1: {} 1629 | 1630 | array-flatten@1.1.1: {} 1631 | 1632 | async@3.2.6: {} 1633 | 1634 | asynckit@0.4.0: {} 1635 | 1636 | axios-cache-interceptor@1.7.0(axios@1.8.4): 1637 | dependencies: 1638 | axios: 1.8.4 1639 | cache-parser: 1.2.5 1640 | fast-defer: 1.1.8 1641 | object-code: 1.3.3 1642 | 1643 | axios@1.8.4: 1644 | dependencies: 1645 | follow-redirects: 1.15.9 1646 | form-data: 4.0.2 1647 | proxy-from-env: 1.1.0 1648 | transitivePeerDependencies: 1649 | - debug 1650 | 1651 | balanced-match@1.0.2: {} 1652 | 1653 | binary-extensions@2.3.0: {} 1654 | 1655 | body-parser@1.20.3: 1656 | dependencies: 1657 | bytes: 3.1.2 1658 | content-type: 1.0.5 1659 | debug: 2.6.9 1660 | depd: 2.0.0 1661 | destroy: 1.2.0 1662 | http-errors: 2.0.0 1663 | iconv-lite: 0.4.24 1664 | on-finished: 2.4.1 1665 | qs: 6.13.0 1666 | raw-body: 2.5.2 1667 | type-is: 1.6.18 1668 | unpipe: 1.0.0 1669 | transitivePeerDependencies: 1670 | - supports-color 1671 | 1672 | brace-expansion@1.1.11: 1673 | dependencies: 1674 | balanced-match: 1.0.2 1675 | concat-map: 0.0.1 1676 | 1677 | brace-expansion@2.0.1: 1678 | dependencies: 1679 | balanced-match: 1.0.2 1680 | 1681 | braces@3.0.3: 1682 | dependencies: 1683 | fill-range: 7.1.1 1684 | 1685 | buffer-from@1.1.2: {} 1686 | 1687 | busboy@1.6.0: 1688 | dependencies: 1689 | streamsearch: 1.1.0 1690 | 1691 | bytes@3.1.2: {} 1692 | 1693 | cache-parser@1.2.5: {} 1694 | 1695 | call-bind-apply-helpers@1.0.2: 1696 | dependencies: 1697 | es-errors: 1.3.0 1698 | function-bind: 1.1.2 1699 | 1700 | call-bound@1.0.4: 1701 | dependencies: 1702 | call-bind-apply-helpers: 1.0.2 1703 | get-intrinsic: 1.3.0 1704 | 1705 | callsites@3.1.0: {} 1706 | 1707 | chalk@4.1.2: 1708 | dependencies: 1709 | ansi-styles: 4.3.0 1710 | supports-color: 7.2.0 1711 | 1712 | chalk@5.4.1: {} 1713 | 1714 | chokidar@3.6.0: 1715 | dependencies: 1716 | anymatch: 3.1.3 1717 | braces: 3.0.3 1718 | glob-parent: 5.1.2 1719 | is-binary-path: 2.1.0 1720 | is-glob: 4.0.3 1721 | normalize-path: 3.0.0 1722 | readdirp: 3.6.0 1723 | optionalDependencies: 1724 | fsevents: 2.3.3 1725 | 1726 | color-convert@1.9.3: 1727 | dependencies: 1728 | color-name: 1.1.3 1729 | 1730 | color-convert@2.0.1: 1731 | dependencies: 1732 | color-name: 1.1.4 1733 | 1734 | color-name@1.1.3: {} 1735 | 1736 | color-name@1.1.4: {} 1737 | 1738 | color-string@1.9.1: 1739 | dependencies: 1740 | color-name: 1.1.4 1741 | simple-swizzle: 0.2.2 1742 | 1743 | color@3.2.1: 1744 | dependencies: 1745 | color-convert: 1.9.3 1746 | color-string: 1.9.1 1747 | 1748 | colorspace@1.1.4: 1749 | dependencies: 1750 | color: 3.2.1 1751 | text-hex: 1.0.0 1752 | 1753 | combined-stream@1.0.8: 1754 | dependencies: 1755 | delayed-stream: 1.0.0 1756 | 1757 | concat-map@0.0.1: {} 1758 | 1759 | concat-stream@1.6.2: 1760 | dependencies: 1761 | buffer-from: 1.1.2 1762 | inherits: 2.0.4 1763 | readable-stream: 2.3.8 1764 | typedarray: 0.0.6 1765 | 1766 | content-disposition@0.5.4: 1767 | dependencies: 1768 | safe-buffer: 5.2.1 1769 | 1770 | content-type@1.0.5: {} 1771 | 1772 | cookie-signature@1.0.6: {} 1773 | 1774 | cookie@0.7.1: {} 1775 | 1776 | core-util-is@1.0.3: {} 1777 | 1778 | cors@2.8.5: 1779 | dependencies: 1780 | object-assign: 4.1.1 1781 | vary: 1.1.2 1782 | 1783 | cross-spawn@7.0.6: 1784 | dependencies: 1785 | path-key: 3.1.1 1786 | shebang-command: 2.0.0 1787 | which: 2.0.2 1788 | 1789 | date-fns@4.1.0: {} 1790 | 1791 | debug@2.6.9: 1792 | dependencies: 1793 | ms: 2.0.0 1794 | 1795 | debug@4.4.0(supports-color@5.5.0): 1796 | dependencies: 1797 | ms: 2.1.3 1798 | optionalDependencies: 1799 | supports-color: 5.5.0 1800 | 1801 | deep-is@0.1.4: {} 1802 | 1803 | delayed-stream@1.0.0: {} 1804 | 1805 | depd@2.0.0: {} 1806 | 1807 | destroy@1.2.0: {} 1808 | 1809 | detect-libc@1.0.3: {} 1810 | 1811 | detect-libc@2.0.3: {} 1812 | 1813 | dotenv@16.4.7: {} 1814 | 1815 | dunder-proto@1.0.1: 1816 | dependencies: 1817 | call-bind-apply-helpers: 1.0.2 1818 | es-errors: 1.3.0 1819 | gopd: 1.2.0 1820 | 1821 | ee-first@1.1.1: {} 1822 | 1823 | ejs@3.1.10: 1824 | dependencies: 1825 | jake: 10.9.2 1826 | 1827 | enabled@2.0.0: {} 1828 | 1829 | encodeurl@1.0.2: {} 1830 | 1831 | encodeurl@2.0.0: {} 1832 | 1833 | enhanced-resolve@5.18.1: 1834 | dependencies: 1835 | graceful-fs: 4.2.11 1836 | tapable: 2.2.1 1837 | 1838 | es-define-property@1.0.1: {} 1839 | 1840 | es-errors@1.3.0: {} 1841 | 1842 | es-object-atoms@1.1.1: 1843 | dependencies: 1844 | es-errors: 1.3.0 1845 | 1846 | es-set-tostringtag@2.1.0: 1847 | dependencies: 1848 | es-errors: 1.3.0 1849 | get-intrinsic: 1.3.0 1850 | has-tostringtag: 1.0.2 1851 | hasown: 2.0.2 1852 | 1853 | escape-html@1.0.3: {} 1854 | 1855 | escape-string-regexp@4.0.0: {} 1856 | 1857 | eslint-scope@8.3.0: 1858 | dependencies: 1859 | esrecurse: 4.3.0 1860 | estraverse: 5.3.0 1861 | 1862 | eslint-visitor-keys@3.4.3: {} 1863 | 1864 | eslint-visitor-keys@4.2.0: {} 1865 | 1866 | eslint@9.23.0(jiti@2.4.2): 1867 | dependencies: 1868 | '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0(jiti@2.4.2)) 1869 | '@eslint-community/regexpp': 4.12.1 1870 | '@eslint/config-array': 0.19.2 1871 | '@eslint/config-helpers': 0.2.0 1872 | '@eslint/core': 0.12.0 1873 | '@eslint/eslintrc': 3.3.1 1874 | '@eslint/js': 9.23.0 1875 | '@eslint/plugin-kit': 0.2.7 1876 | '@humanfs/node': 0.16.6 1877 | '@humanwhocodes/module-importer': 1.0.1 1878 | '@humanwhocodes/retry': 0.4.2 1879 | '@types/estree': 1.0.7 1880 | '@types/json-schema': 7.0.15 1881 | ajv: 6.12.6 1882 | chalk: 4.1.2 1883 | cross-spawn: 7.0.6 1884 | debug: 4.4.0(supports-color@5.5.0) 1885 | escape-string-regexp: 4.0.0 1886 | eslint-scope: 8.3.0 1887 | eslint-visitor-keys: 4.2.0 1888 | espree: 10.3.0 1889 | esquery: 1.6.0 1890 | esutils: 2.0.3 1891 | fast-deep-equal: 3.1.3 1892 | file-entry-cache: 8.0.0 1893 | find-up: 5.0.0 1894 | glob-parent: 6.0.2 1895 | ignore: 5.3.2 1896 | imurmurhash: 0.1.4 1897 | is-glob: 4.0.3 1898 | json-stable-stringify-without-jsonify: 1.0.1 1899 | lodash.merge: 4.6.2 1900 | minimatch: 3.1.2 1901 | natural-compare: 1.4.0 1902 | optionator: 0.9.4 1903 | optionalDependencies: 1904 | jiti: 2.4.2 1905 | transitivePeerDependencies: 1906 | - supports-color 1907 | 1908 | espree@10.3.0: 1909 | dependencies: 1910 | acorn: 8.14.1 1911 | acorn-jsx: 5.3.2(acorn@8.14.1) 1912 | eslint-visitor-keys: 4.2.0 1913 | 1914 | esquery@1.6.0: 1915 | dependencies: 1916 | estraverse: 5.3.0 1917 | 1918 | esrecurse@4.3.0: 1919 | dependencies: 1920 | estraverse: 5.3.0 1921 | 1922 | estraverse@5.3.0: {} 1923 | 1924 | esutils@2.0.3: {} 1925 | 1926 | etag@1.8.1: {} 1927 | 1928 | express-async-errors@3.1.1(express@4.21.2): 1929 | dependencies: 1930 | express: 4.21.2 1931 | 1932 | express-rate-limit@7.5.0(express@4.21.2): 1933 | dependencies: 1934 | express: 4.21.2 1935 | 1936 | express@4.21.2: 1937 | dependencies: 1938 | accepts: 1.3.8 1939 | array-flatten: 1.1.1 1940 | body-parser: 1.20.3 1941 | content-disposition: 0.5.4 1942 | content-type: 1.0.5 1943 | cookie: 0.7.1 1944 | cookie-signature: 1.0.6 1945 | debug: 2.6.9 1946 | depd: 2.0.0 1947 | encodeurl: 2.0.0 1948 | escape-html: 1.0.3 1949 | etag: 1.8.1 1950 | finalhandler: 1.3.1 1951 | fresh: 0.5.2 1952 | http-errors: 2.0.0 1953 | merge-descriptors: 1.0.3 1954 | methods: 1.1.2 1955 | on-finished: 2.4.1 1956 | parseurl: 1.3.3 1957 | path-to-regexp: 0.1.12 1958 | proxy-addr: 2.0.7 1959 | qs: 6.13.0 1960 | range-parser: 1.2.1 1961 | safe-buffer: 5.2.1 1962 | send: 0.19.0 1963 | serve-static: 1.16.2 1964 | setprototypeof: 1.2.0 1965 | statuses: 2.0.1 1966 | type-is: 1.6.18 1967 | utils-merge: 1.0.1 1968 | vary: 1.1.2 1969 | transitivePeerDependencies: 1970 | - supports-color 1971 | 1972 | fast-deep-equal@3.1.3: {} 1973 | 1974 | fast-defer@1.1.8: {} 1975 | 1976 | fast-json-stable-stringify@2.1.0: {} 1977 | 1978 | fast-levenshtein@2.0.6: {} 1979 | 1980 | fecha@4.2.3: {} 1981 | 1982 | file-entry-cache@8.0.0: 1983 | dependencies: 1984 | flat-cache: 4.0.1 1985 | 1986 | filelist@1.0.4: 1987 | dependencies: 1988 | minimatch: 5.1.6 1989 | 1990 | fill-range@7.1.1: 1991 | dependencies: 1992 | to-regex-range: 5.0.1 1993 | 1994 | finalhandler@1.3.1: 1995 | dependencies: 1996 | debug: 2.6.9 1997 | encodeurl: 2.0.0 1998 | escape-html: 1.0.3 1999 | on-finished: 2.4.1 2000 | parseurl: 1.3.3 2001 | statuses: 2.0.1 2002 | unpipe: 1.0.0 2003 | transitivePeerDependencies: 2004 | - supports-color 2005 | 2006 | find-up@5.0.0: 2007 | dependencies: 2008 | locate-path: 6.0.0 2009 | path-exists: 4.0.0 2010 | 2011 | flat-cache@4.0.1: 2012 | dependencies: 2013 | flatted: 3.3.3 2014 | keyv: 4.5.4 2015 | 2016 | flatted@3.3.3: {} 2017 | 2018 | fn.name@1.1.0: {} 2019 | 2020 | follow-redirects@1.15.9: {} 2021 | 2022 | form-data@4.0.2: 2023 | dependencies: 2024 | asynckit: 0.4.0 2025 | combined-stream: 1.0.8 2026 | es-set-tostringtag: 2.1.0 2027 | mime-types: 2.1.35 2028 | 2029 | forwarded@0.2.0: {} 2030 | 2031 | fresh@0.5.2: {} 2032 | 2033 | fsevents@2.3.3: 2034 | optional: true 2035 | 2036 | function-bind@1.1.2: {} 2037 | 2038 | get-intrinsic@1.3.0: 2039 | dependencies: 2040 | call-bind-apply-helpers: 1.0.2 2041 | es-define-property: 1.0.1 2042 | es-errors: 1.3.0 2043 | es-object-atoms: 1.1.1 2044 | function-bind: 1.1.2 2045 | get-proto: 1.0.1 2046 | gopd: 1.2.0 2047 | has-symbols: 1.1.0 2048 | hasown: 2.0.2 2049 | math-intrinsics: 1.1.0 2050 | 2051 | get-proto@1.0.1: 2052 | dependencies: 2053 | dunder-proto: 1.0.1 2054 | es-object-atoms: 1.1.1 2055 | 2056 | glob-parent@5.1.2: 2057 | dependencies: 2058 | is-glob: 4.0.3 2059 | 2060 | glob-parent@6.0.2: 2061 | dependencies: 2062 | is-glob: 4.0.3 2063 | 2064 | globals@14.0.0: {} 2065 | 2066 | gopd@1.2.0: {} 2067 | 2068 | graceful-fs@4.2.11: {} 2069 | 2070 | has-flag@3.0.0: {} 2071 | 2072 | has-flag@4.0.0: {} 2073 | 2074 | has-symbols@1.1.0: {} 2075 | 2076 | has-tostringtag@1.0.2: 2077 | dependencies: 2078 | has-symbols: 1.1.0 2079 | 2080 | hasown@2.0.2: 2081 | dependencies: 2082 | function-bind: 1.1.2 2083 | 2084 | http-errors@2.0.0: 2085 | dependencies: 2086 | depd: 2.0.0 2087 | inherits: 2.0.4 2088 | setprototypeof: 1.2.0 2089 | statuses: 2.0.1 2090 | toidentifier: 1.0.1 2091 | 2092 | iconv-lite@0.4.24: 2093 | dependencies: 2094 | safer-buffer: 2.1.2 2095 | 2096 | ignore-by-default@1.0.1: {} 2097 | 2098 | ignore@5.3.2: {} 2099 | 2100 | import-fresh@3.3.1: 2101 | dependencies: 2102 | parent-module: 1.0.1 2103 | resolve-from: 4.0.0 2104 | 2105 | imurmurhash@0.1.4: {} 2106 | 2107 | inherits@2.0.4: {} 2108 | 2109 | ipaddr.js@1.9.1: {} 2110 | 2111 | is-arrayish@0.3.2: {} 2112 | 2113 | is-binary-path@2.1.0: 2114 | dependencies: 2115 | binary-extensions: 2.3.0 2116 | 2117 | is-extglob@2.1.1: {} 2118 | 2119 | is-glob@4.0.3: 2120 | dependencies: 2121 | is-extglob: 2.1.1 2122 | 2123 | is-number@7.0.0: {} 2124 | 2125 | is-stream@2.0.1: {} 2126 | 2127 | isarray@1.0.0: {} 2128 | 2129 | isexe@2.0.0: {} 2130 | 2131 | jake@10.9.2: 2132 | dependencies: 2133 | async: 3.2.6 2134 | chalk: 4.1.2 2135 | filelist: 1.0.4 2136 | minimatch: 3.1.2 2137 | 2138 | jiti@2.4.2: {} 2139 | 2140 | js-yaml@4.1.0: 2141 | dependencies: 2142 | argparse: 2.0.1 2143 | 2144 | json-buffer@3.0.1: {} 2145 | 2146 | json-schema-traverse@0.4.1: {} 2147 | 2148 | json-stable-stringify-without-jsonify@1.0.1: {} 2149 | 2150 | keyv@4.5.4: 2151 | dependencies: 2152 | json-buffer: 3.0.1 2153 | 2154 | kuler@2.0.0: {} 2155 | 2156 | levn@0.4.1: 2157 | dependencies: 2158 | prelude-ls: 1.2.1 2159 | type-check: 0.4.0 2160 | 2161 | lightningcss-darwin-arm64@1.29.2: 2162 | optional: true 2163 | 2164 | lightningcss-darwin-x64@1.29.2: 2165 | optional: true 2166 | 2167 | lightningcss-freebsd-x64@1.29.2: 2168 | optional: true 2169 | 2170 | lightningcss-linux-arm-gnueabihf@1.29.2: 2171 | optional: true 2172 | 2173 | lightningcss-linux-arm64-gnu@1.29.2: 2174 | optional: true 2175 | 2176 | lightningcss-linux-arm64-musl@1.29.2: 2177 | optional: true 2178 | 2179 | lightningcss-linux-x64-gnu@1.29.2: 2180 | optional: true 2181 | 2182 | lightningcss-linux-x64-musl@1.29.2: 2183 | optional: true 2184 | 2185 | lightningcss-win32-arm64-msvc@1.29.2: 2186 | optional: true 2187 | 2188 | lightningcss-win32-x64-msvc@1.29.2: 2189 | optional: true 2190 | 2191 | lightningcss@1.29.2: 2192 | dependencies: 2193 | detect-libc: 2.0.3 2194 | optionalDependencies: 2195 | lightningcss-darwin-arm64: 1.29.2 2196 | lightningcss-darwin-x64: 1.29.2 2197 | lightningcss-freebsd-x64: 1.29.2 2198 | lightningcss-linux-arm-gnueabihf: 1.29.2 2199 | lightningcss-linux-arm64-gnu: 1.29.2 2200 | lightningcss-linux-arm64-musl: 1.29.2 2201 | lightningcss-linux-x64-gnu: 1.29.2 2202 | lightningcss-linux-x64-musl: 1.29.2 2203 | lightningcss-win32-arm64-msvc: 1.29.2 2204 | lightningcss-win32-x64-msvc: 1.29.2 2205 | 2206 | locate-path@6.0.0: 2207 | dependencies: 2208 | p-locate: 5.0.0 2209 | 2210 | lodash.merge@4.6.2: {} 2211 | 2212 | lodash@4.17.21: {} 2213 | 2214 | logform@2.7.0: 2215 | dependencies: 2216 | '@colors/colors': 1.6.0 2217 | '@types/triple-beam': 1.3.5 2218 | fecha: 4.2.3 2219 | ms: 2.1.3 2220 | safe-stable-stringify: 2.5.0 2221 | triple-beam: 1.4.1 2222 | 2223 | math-intrinsics@1.1.0: {} 2224 | 2225 | media-typer@0.3.0: {} 2226 | 2227 | merge-descriptors@1.0.3: {} 2228 | 2229 | methods@1.1.2: {} 2230 | 2231 | micromatch@4.0.8: 2232 | dependencies: 2233 | braces: 3.0.3 2234 | picomatch: 2.3.1 2235 | 2236 | mime-db@1.52.0: {} 2237 | 2238 | mime-types@2.1.35: 2239 | dependencies: 2240 | mime-db: 1.52.0 2241 | 2242 | mime@1.6.0: {} 2243 | 2244 | minimatch@3.1.2: 2245 | dependencies: 2246 | brace-expansion: 1.1.11 2247 | 2248 | minimatch@5.1.6: 2249 | dependencies: 2250 | brace-expansion: 2.0.1 2251 | 2252 | minimist@1.2.8: {} 2253 | 2254 | mkdirp@0.5.6: 2255 | dependencies: 2256 | minimist: 1.2.8 2257 | 2258 | mri@1.2.0: {} 2259 | 2260 | ms@2.0.0: {} 2261 | 2262 | ms@2.1.3: {} 2263 | 2264 | multer@1.4.5-lts.2: 2265 | dependencies: 2266 | append-field: 1.0.0 2267 | busboy: 1.6.0 2268 | concat-stream: 1.6.2 2269 | mkdirp: 0.5.6 2270 | object-assign: 4.1.1 2271 | type-is: 1.6.18 2272 | xtend: 4.0.2 2273 | 2274 | natural-compare@1.4.0: {} 2275 | 2276 | negotiator@0.6.3: {} 2277 | 2278 | node-addon-api@7.1.1: {} 2279 | 2280 | node-localstorage@3.0.5: 2281 | dependencies: 2282 | write-file-atomic: 5.0.1 2283 | 2284 | nodemon@3.1.9: 2285 | dependencies: 2286 | chokidar: 3.6.0 2287 | debug: 4.4.0(supports-color@5.5.0) 2288 | ignore-by-default: 1.0.1 2289 | minimatch: 3.1.2 2290 | pstree.remy: 1.1.8 2291 | semver: 7.7.1 2292 | simple-update-notifier: 2.0.0 2293 | supports-color: 5.5.0 2294 | touch: 3.1.1 2295 | undefsafe: 2.0.5 2296 | 2297 | normalize-path@3.0.0: {} 2298 | 2299 | object-assign@4.1.1: {} 2300 | 2301 | object-code@1.3.3: {} 2302 | 2303 | object-inspect@1.13.4: {} 2304 | 2305 | on-finished@2.4.1: 2306 | dependencies: 2307 | ee-first: 1.1.1 2308 | 2309 | one-time@1.0.0: 2310 | dependencies: 2311 | fn.name: 1.1.0 2312 | 2313 | optionator@0.9.4: 2314 | dependencies: 2315 | deep-is: 0.1.4 2316 | fast-levenshtein: 2.0.6 2317 | levn: 0.4.1 2318 | prelude-ls: 1.2.1 2319 | type-check: 0.4.0 2320 | word-wrap: 1.2.5 2321 | 2322 | p-limit@3.1.0: 2323 | dependencies: 2324 | yocto-queue: 0.1.0 2325 | 2326 | p-locate@5.0.0: 2327 | dependencies: 2328 | p-limit: 3.1.0 2329 | 2330 | parent-module@1.0.1: 2331 | dependencies: 2332 | callsites: 3.1.0 2333 | 2334 | parseurl@1.3.3: {} 2335 | 2336 | path-exists@4.0.0: {} 2337 | 2338 | path-key@3.1.1: {} 2339 | 2340 | path-to-regexp@0.1.12: {} 2341 | 2342 | picocolors@1.1.1: {} 2343 | 2344 | picomatch@2.3.1: {} 2345 | 2346 | prelude-ls@1.2.1: {} 2347 | 2348 | prettier@3.5.3: {} 2349 | 2350 | process-nextick-args@2.0.1: {} 2351 | 2352 | proxy-addr@2.0.7: 2353 | dependencies: 2354 | forwarded: 0.2.0 2355 | ipaddr.js: 1.9.1 2356 | 2357 | proxy-from-env@1.1.0: {} 2358 | 2359 | pstree.remy@1.1.8: {} 2360 | 2361 | punycode@2.3.1: {} 2362 | 2363 | qs@6.13.0: 2364 | dependencies: 2365 | side-channel: 1.1.0 2366 | 2367 | range-parser@1.2.1: {} 2368 | 2369 | raw-body@2.5.2: 2370 | dependencies: 2371 | bytes: 3.1.2 2372 | http-errors: 2.0.0 2373 | iconv-lite: 0.4.24 2374 | unpipe: 1.0.0 2375 | 2376 | readable-stream@2.3.8: 2377 | dependencies: 2378 | core-util-is: 1.0.3 2379 | inherits: 2.0.4 2380 | isarray: 1.0.0 2381 | process-nextick-args: 2.0.1 2382 | safe-buffer: 5.1.2 2383 | string_decoder: 1.1.1 2384 | util-deprecate: 1.0.2 2385 | 2386 | readable-stream@3.6.2: 2387 | dependencies: 2388 | inherits: 2.0.4 2389 | string_decoder: 1.3.0 2390 | util-deprecate: 1.0.2 2391 | 2392 | readdirp@3.6.0: 2393 | dependencies: 2394 | picomatch: 2.3.1 2395 | 2396 | resolve-from@4.0.0: {} 2397 | 2398 | safe-buffer@5.1.2: {} 2399 | 2400 | safe-buffer@5.2.1: {} 2401 | 2402 | safe-stable-stringify@2.5.0: {} 2403 | 2404 | safer-buffer@2.1.2: {} 2405 | 2406 | semver@7.7.1: {} 2407 | 2408 | send@0.19.0: 2409 | dependencies: 2410 | debug: 2.6.9 2411 | depd: 2.0.0 2412 | destroy: 1.2.0 2413 | encodeurl: 1.0.2 2414 | escape-html: 1.0.3 2415 | etag: 1.8.1 2416 | fresh: 0.5.2 2417 | http-errors: 2.0.0 2418 | mime: 1.6.0 2419 | ms: 2.1.3 2420 | on-finished: 2.4.1 2421 | range-parser: 1.2.1 2422 | statuses: 2.0.1 2423 | transitivePeerDependencies: 2424 | - supports-color 2425 | 2426 | serve-static@1.16.2: 2427 | dependencies: 2428 | encodeurl: 2.0.0 2429 | escape-html: 1.0.3 2430 | parseurl: 1.3.3 2431 | send: 0.19.0 2432 | transitivePeerDependencies: 2433 | - supports-color 2434 | 2435 | setprototypeof@1.2.0: {} 2436 | 2437 | shebang-command@2.0.0: 2438 | dependencies: 2439 | shebang-regex: 3.0.0 2440 | 2441 | shebang-regex@3.0.0: {} 2442 | 2443 | side-channel-list@1.0.0: 2444 | dependencies: 2445 | es-errors: 1.3.0 2446 | object-inspect: 1.13.4 2447 | 2448 | side-channel-map@1.0.1: 2449 | dependencies: 2450 | call-bound: 1.0.4 2451 | es-errors: 1.3.0 2452 | get-intrinsic: 1.3.0 2453 | object-inspect: 1.13.4 2454 | 2455 | side-channel-weakmap@1.0.2: 2456 | dependencies: 2457 | call-bound: 1.0.4 2458 | es-errors: 1.3.0 2459 | get-intrinsic: 1.3.0 2460 | object-inspect: 1.13.4 2461 | side-channel-map: 1.0.1 2462 | 2463 | side-channel@1.1.0: 2464 | dependencies: 2465 | es-errors: 1.3.0 2466 | object-inspect: 1.13.4 2467 | side-channel-list: 1.0.0 2468 | side-channel-map: 1.0.1 2469 | side-channel-weakmap: 1.0.2 2470 | 2471 | signal-exit@4.1.0: {} 2472 | 2473 | simple-swizzle@0.2.2: 2474 | dependencies: 2475 | is-arrayish: 0.3.2 2476 | 2477 | simple-update-notifier@2.0.0: 2478 | dependencies: 2479 | semver: 7.7.1 2480 | 2481 | stack-trace@0.0.10: {} 2482 | 2483 | statuses@2.0.1: {} 2484 | 2485 | streamsearch@1.1.0: {} 2486 | 2487 | string_decoder@1.1.1: 2488 | dependencies: 2489 | safe-buffer: 5.1.2 2490 | 2491 | string_decoder@1.3.0: 2492 | dependencies: 2493 | safe-buffer: 5.2.1 2494 | 2495 | strip-json-comments@3.1.1: {} 2496 | 2497 | supports-color@5.5.0: 2498 | dependencies: 2499 | has-flag: 3.0.0 2500 | 2501 | supports-color@7.2.0: 2502 | dependencies: 2503 | has-flag: 4.0.0 2504 | 2505 | tailwindcss@4.0.15: {} 2506 | 2507 | tapable@2.2.1: {} 2508 | 2509 | text-hex@1.0.0: {} 2510 | 2511 | to-regex-range@5.0.1: 2512 | dependencies: 2513 | is-number: 7.0.0 2514 | 2515 | toidentifier@1.0.1: {} 2516 | 2517 | touch@3.1.1: {} 2518 | 2519 | triple-beam@1.4.1: {} 2520 | 2521 | type-check@0.4.0: 2522 | dependencies: 2523 | prelude-ls: 1.2.1 2524 | 2525 | type-is@1.6.18: 2526 | dependencies: 2527 | media-typer: 0.3.0 2528 | mime-types: 2.1.35 2529 | 2530 | typedarray@0.0.6: {} 2531 | 2532 | undefsafe@2.0.5: {} 2533 | 2534 | undici-types@6.20.0: {} 2535 | 2536 | unpipe@1.0.0: {} 2537 | 2538 | uri-js@4.4.1: 2539 | dependencies: 2540 | punycode: 2.3.1 2541 | 2542 | util-deprecate@1.0.2: {} 2543 | 2544 | utils-merge@1.0.1: {} 2545 | 2546 | vary@1.1.2: {} 2547 | 2548 | which@2.0.2: 2549 | dependencies: 2550 | isexe: 2.0.0 2551 | 2552 | winston-transport@4.9.0: 2553 | dependencies: 2554 | logform: 2.7.0 2555 | readable-stream: 3.6.2 2556 | triple-beam: 1.4.1 2557 | 2558 | winston@3.17.0: 2559 | dependencies: 2560 | '@colors/colors': 1.6.0 2561 | '@dabh/diagnostics': 2.0.3 2562 | async: 3.2.6 2563 | is-stream: 2.0.1 2564 | logform: 2.7.0 2565 | one-time: 1.0.0 2566 | readable-stream: 3.6.2 2567 | safe-stable-stringify: 2.5.0 2568 | stack-trace: 0.0.10 2569 | triple-beam: 1.4.1 2570 | winston-transport: 4.9.0 2571 | 2572 | word-wrap@1.2.5: {} 2573 | 2574 | write-file-atomic@5.0.1: 2575 | dependencies: 2576 | imurmurhash: 0.1.4 2577 | signal-exit: 4.1.0 2578 | 2579 | xtend@4.0.2: {} 2580 | 2581 | yocto-queue@0.1.0: {} 2582 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { LocalStorage } from 'node-localstorage'; 3 | import { fromUnixTime, differenceInHours } from 'date-fns'; 4 | import { setupCache } from 'axios-cache-interceptor'; 5 | import chalk from 'chalk'; 6 | 7 | import { logger } from './logger.js'; 8 | 9 | const localStorage = new LocalStorage('./data'); 10 | 11 | export const authorizeRequest = async ({ code, redirect_uri, refresh_token, grant_type }) => { 12 | logger.info(`🔑 Getting token...`); 13 | const client_id = process.env.TRAKT_ID; 14 | const client_secret = process.env.TRAKT_SECRET; 15 | 16 | const body = { 17 | code, 18 | refresh_token, 19 | client_id, 20 | client_secret, 21 | redirect_uri, 22 | grant_type, 23 | }; 24 | 25 | try { 26 | const response = await axios.post(`https://api.trakt.tv/oauth/token`, JSON.stringify(body), { 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | }, { cache: false }); 31 | const tokens = response.data; 32 | logger.debug(JSON.stringify(tokens)); 33 | logger.info(`🔐 Token adquired`); 34 | return tokens; 35 | } catch (err) { 36 | logger.error(`❌ ${chalk.red(`Auth API error: ${err.message}`)}`); 37 | } 38 | }; 39 | 40 | export const getAccessToken = async () => { 41 | const tokens = localStorage.getItem('tokens'); 42 | 43 | if (!tokens || tokens == 'undefined') { 44 | logger.error(`❌ ${chalk.red(`Error getting token.`)}`); 45 | logger.info( 46 | `ℹ️ Have you authorized the application? Go to http://localhost:${process.env.PORT} to do it if needed.`, 47 | ); 48 | return 49 | } 50 | let { access_token, refresh_token, created_at } = JSON.parse(tokens); 51 | 52 | if (!access_token || !refresh_token) { 53 | logger.error(`❌ ${chalk.red(`No access / refresh token found! Please authorize the application again...`)}`); 54 | return 55 | } 56 | 57 | const tokenAge = differenceInHours(new Date(), new Date(fromUnixTime(created_at))); 58 | if (tokenAge > 23) { 59 | // tokens expire after 24 hours, so we refresh after 23 60 | logger.info(`🔐 Token expired, refreshing...`); 61 | const redirect_uri = `http://localhost:${process.env.PORT}/authorize`; 62 | 63 | const tokens = await authorizeRequest({ grant_type: 'refresh_token', redirect_uri, refresh_token }); 64 | 65 | if (tokens) { 66 | const data = JSON.stringify(tokens); 67 | localStorage.setItem('tokens', data); 68 | access_token = tokens.access_token; 69 | } else { 70 | logger.error(`❌ ${chalk.red(`No tokens found!`)}`); 71 | logger.info( 72 | `ℹ️ Have you authorized the application? Go to http://localhost:${process.env.PORT} to do it if needed.`, 73 | ); 74 | return 75 | } 76 | } 77 | 78 | return access_token 79 | } 80 | 81 | export const instance = axios.create({ 82 | baseURL: 'https://api.trakt.tv', 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | 'trakt-api-key': process.env.TRAKT_ID, 86 | 'trakt-api-version': '2', 87 | }, 88 | }); 89 | 90 | instance.interceptors.request.use( 91 | async (config) => { 92 | const token = await getAccessToken(); 93 | if (token) config.headers.Authorization = `Bearer ${token}`; 94 | return config; 95 | }, 96 | (error) => { 97 | return Promise.reject(error); 98 | } 99 | ); 100 | 101 | export const api = setupCache(instance); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // https://support.plex.tv/hc/en-us/articles/115002267687-Webhooks 2 | 3 | import 'dotenv/config'; 4 | import express from 'express'; 5 | import cors from 'cors'; 6 | import multer from 'multer'; 7 | import chalk from 'chalk'; 8 | import { LocalStorage } from 'node-localstorage'; 9 | import _ from 'lodash'; 10 | import "express-async-errors"; 11 | import RateLimit from 'express-rate-limit'; 12 | import os from 'os'; 13 | 14 | import { logger } from './logger.js'; 15 | import { handle } from './utils.js'; 16 | import { authorizeRequest } from './api.js'; 17 | 18 | const app = express(); 19 | const PORT = process.env.PORT || 3090; 20 | 21 | const upload = multer({ storage: multer.memoryStorage() }); 22 | 23 | const localStorage = new LocalStorage('./data'); 24 | 25 | const authorizeLimiter = RateLimit({ 26 | windowMs: 15 * 60 * 1000, // 15 minutes 27 | max: 100, // max 100 requests per windowMs 28 | }); 29 | 30 | function errorHandler(err, req, res, next) { 31 | console.error(err.stack); 32 | logger.error(`❌ ${chalk.red(err.stack)}`); 33 | res.status(500).json({ error: 'Internal Server Error' }); 34 | } 35 | 36 | app.use(express.json()); 37 | app.use(express.urlencoded({ extended: true })); 38 | app.use(cors()); 39 | app.use(errorHandler); 40 | app.use(express.static('static')); 41 | app.use('/favicon.ico', express.static('favicon.ico')); 42 | app.set('view engine', 'ejs'); 43 | 44 | const orange = chalk.rgb(235, 175, 0); 45 | 46 | const getLocalIpAddress = () => { 47 | const interfaces = os.networkInterfaces(); 48 | for (const name of Object.keys(interfaces)) { 49 | for (const iface of interfaces[name]) { 50 | if (iface.family === 'IPv4' && !iface.internal) { 51 | return iface.address; 52 | } 53 | } 54 | } 55 | return 'localhost'; 56 | }; 57 | 58 | app.post('/plex', upload.single('thumb'), async (req, res) => { 59 | if (!req.body.payload) { 60 | logger.error(`❌ ${chalk.red(`Missing payload.`)}`); 61 | return res.status(400).json({ error: 'Missing payload' }); 62 | } 63 | 64 | const payload = JSON.parse(req.body.payload); 65 | 66 | const event = payload?.event; 67 | const type = payload?.Metadata?.type; 68 | const title = payload?.Metadata?.title; 69 | const id = payload?.Account?.id; 70 | const name = payload?.Account?.title; 71 | 72 | if (!event || !type || !title) { 73 | logger.debug(`Event: ${event} Type: ${type} Title: ${title} ID: ${id} Name: ${name}`); 74 | logger.error(`❌ ${chalk.red(`Missing required data.`)}`); 75 | return res.status(400).json({ error: 'Missing required data' }); 76 | } 77 | 78 | logger.debug(`🔥 Event: ${event} 🏷️ Type: ${type} 🔖 Title: ${title} 👤 ${name} (${id})`); 79 | 80 | if (process.env.PLEX_USER) { 81 | if (!process.env.PLEX_USER.trim().toLowerCase().split(",").includes(name.trim().toLowerCase())) { 82 | logger.error(`❌ ${chalk.red(`User ${name} (${id}) is not in the list of allowed users: ${process.env.PLEX_USER}`)}`); 83 | return res.status(403).json({ error: 'User not allowed' }); 84 | } 85 | } 86 | 87 | try { 88 | await handle({ payload }); 89 | return res.status(200).json({ message: 'Success' }); 90 | } catch (error) { 91 | logger.error(`❌ ${chalk.red(error.message)}`); 92 | return res.status(500).json({ error: 'Internal Server Error' }); 93 | } 94 | }); 95 | 96 | app.get('/healthcheck', async (_req, res) => { 97 | const healthcheck = { 98 | uptime: process.uptime(), 99 | message: 'OK', 100 | timestamp: Date.now(), 101 | }; 102 | try { 103 | res.status(200).send(healthcheck); 104 | } catch (e) { 105 | healthcheck.message = e; 106 | res.status(503).send(healthcheck); 107 | } 108 | }); 109 | 110 | app.get('/', async (req, res) => { 111 | res.render('pages/index', { 112 | self_url: `${req.protocol}://${req.get('host')}`, 113 | client_id: process.env.TRAKT_ID, 114 | authorized: false, 115 | }); 116 | }); 117 | 118 | app.get('/authorize', authorizeLimiter, async (req, res) => { 119 | const code = req.query.code; 120 | const redirect_uri = `${req.protocol}://${req.get('host')}/authorize`; 121 | try { 122 | const tokens = await authorizeRequest({ code, grant_type: 'authorization_code', redirect_uri }); 123 | if (tokens) { 124 | const data = JSON.stringify(tokens); 125 | localStorage.setItem('tokens', data); 126 | } else { 127 | logger.error(`❌ ${chalk.red(`No tokens found!`)}`); 128 | logger.info(`ℹ️ Have you authorized the application? Go to ${req.protocol}://${req.get('host')} to do it if needed.`); 129 | return res.status(401).json({ error: 'Authorization required' }); 130 | } 131 | 132 | res.render('pages/index', { 133 | self_url: `${req.protocol}://${req.get('host')}`, 134 | client_id: process.env.TRAKT_ID, 135 | code: req.query.code, 136 | authorized: true, 137 | }); 138 | } catch (error) { 139 | logger.error(`❌ ${chalk.red(error.message)}`); 140 | res.status(500).json({ error: 'Internal Server Error' }); 141 | } 142 | }); 143 | 144 | app.listen(PORT, (error) => { 145 | if (!error) { 146 | const localIp = getLocalIpAddress(); 147 | 148 | logger.info(`🤖 Scrobb${orange('lex')} v${process.env.npm_package_version}`); 149 | logger.info(`🚀 Connected successfully on http://${localIp}:${PORT}`); 150 | 151 | const tokens = localStorage.getItem('tokens'); 152 | if (!tokens || tokens == 'undefined') { 153 | logger.error(`❌ ${chalk.red(`Error getting token.`)}`); 154 | logger.warn(`🛟 You need to authorize the app. Please go to http://${localIp}:${PORT} and follow the instructions.`); 155 | } 156 | } else { 157 | logger.error(`❌ ${chalk.red(`Error occurred: ${error.message}`)}`); 158 | } 159 | }); 160 | 161 | ['SIGHUP', 'SIGINT', 'SIGTERM'].forEach((signal) => process.on(signal, () => process.exit())); -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | const { combine, timestamp, printf } = format; 3 | 4 | const colorizer = format.colorize(); 5 | 6 | export const logger = createLogger({ 7 | level: process.env.LOG_LEVEL || 'info', 8 | format: format.combine( 9 | format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 10 | format.simple(), 11 | format.padLevels(), 12 | format.printf(({ level, message, timestamp }) => 13 | colorizer.colorize(level, `[${timestamp}] ${level}: ${message}`) 14 | ), 15 | format.colorize({ all: true }), 16 | ), 17 | transports: [new transports.Console()], 18 | }); -------------------------------------------------------------------------------- /src/requests.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { formatDistanceToNow } from 'date-fns'; 3 | 4 | import { logger } from './logger.js'; 5 | import { api } from './api.js'; 6 | import { GetGuids } from './utils.js'; 7 | 8 | export const scrobbleRequest = async ({ action, body, title }) => { 9 | try { 10 | const response = await api.post(`/scrobble/${action}`, JSON.stringify(body), { cache: false }); 11 | logger.info(`📡 Scrobbling ${title} [${action}]`); 12 | logger.debug(JSON.stringify(body, null, 2)) 13 | logger.debug(JSON.stringify(response.data, null, 2)) 14 | 15 | } catch (err) { 16 | if (err.response?.status == '409') { 17 | logger.error( 18 | `❌ ${chalk.red(`${title} has been scrobbled ${formatDistanceToNow(new Date(err.response.data.watched_at))} ago. Try again in ${formatDistanceToNow(new Date(err.response.data.expires_at))}.`)}`, 19 | ); 20 | } 21 | logger.error(`❌ ${chalk.red(`Scrobble API error: ${err.message}`)}`); 22 | } 23 | }; 24 | 25 | export const rateRequest = async ({ body, title, rating }) => { 26 | if (!rating || rating < 1 || rating > 10) { 27 | logger.error(`❌ ${chalk.red(`Invalid rating, aborting`)}`); 28 | return 29 | } 30 | logger.debug(JSON.stringify(body, null, 2)) 31 | try { 32 | const response = await api.post(`/sync/ratings`, JSON.stringify(body), { cache: false }); 33 | logger.info(`❤️ Rating ${title} with (${rating}) ${'⭐'.repeat(rating)}`); 34 | logger.debug(JSON.stringify(response.data, null, 2)) 35 | } catch (err) { 36 | logger.info(JSON.stringify(err, null, 2)) 37 | logger.error(`❌ ${chalk.red(`Rate API error: ${err.message}`)}`); 38 | } 39 | }; 40 | 41 | 42 | export const findMovieRequest = async (payload) => { 43 | const guids = GetGuids({ payload }); 44 | 45 | for (const { service, id } of guids) { 46 | if (service && id) { 47 | logger.info(`🔍 Finding movie info for ${payload.Metadata.title} (${payload.Metadata.year}) using ${service}://${id}`); 48 | 49 | try { 50 | const response = await api.get(`https://api.trakt.tv/search/${service}/${id}?type=movie`, { ttl: 1000 * 60 * 180 }); 51 | logger.debug(JSON.stringify(response.data, null, 2)); 52 | if (response.data.length) { 53 | const movie = response.data[0].movie; 54 | const { title, year } = movie; 55 | logger.info(`🎬 Movie found: ${title} (${year})`); 56 | return movie; 57 | } else { 58 | logger.error(`❌ ${chalk.red(`Response from ${service.toUpperCase()} was empty!`)}`); 59 | } 60 | } catch (err) { 61 | logger.error(`❌ ${chalk.red(`Search movie API error: ${err.message}`)}`); 62 | } 63 | } else { 64 | logger.error(`❌ ${chalk.red(`No GUID available`)}`); 65 | } 66 | } 67 | 68 | logger.error(`❌ ${chalk.red(`No movie found for any GUIDs`)}`); 69 | return null; 70 | }; 71 | 72 | export const findEpisodeRequest = async (payload) => { 73 | const guids = GetGuids({ payload }); 74 | 75 | for (const { service, id } of guids) { 76 | if (service && id) { 77 | logger.info( 78 | `🔍 Finding episode info for ${payload.Metadata.grandparentTitle} - S${String(payload.Metadata.parentIndex).padStart(2, '0')}E${String(payload.Metadata.index).padStart(2, '0')} - ${payload.Metadata.title} using ${service}://${id}`, 79 | ) 80 | try { 81 | const response = await api.get(`https://api.trakt.tv/search/${service}/${id}?type=episode`, { ttl: 1000 * 60 * 180 }); 82 | logger.debug(JSON.stringify(response.data, null, 2)); 83 | if (response.data.length) { 84 | const { episode, show } = response.data[0]; 85 | const { title, season, number } = episode; 86 | const { title: showTitle, year } = show; 87 | logger.info(`📺 Episode found: ${showTitle} (${year}) - S${String(season).padStart(2, '0')}E${String(number).padStart(2, '0')} - ${title}`); 88 | return episode; 89 | } else { 90 | logger.error(`❌ ${chalk.red(`Response from ${service.toUpperCase()} was empty!`)}`); 91 | } 92 | } catch (err) { 93 | logger.error(`❌ ${chalk.red(`Search episode API error: ${err.message}`)}`); 94 | } 95 | } else { 96 | logger.error(`❌ ${chalk.red(`No GUID available`)}`); 97 | } 98 | } 99 | 100 | logger.error(`❌ ${chalk.red(`No episode found for any GUIDs`)}`); 101 | return null; 102 | }; 103 | 104 | 105 | export const findShowRequest = async (payload) => { 106 | const guids = GetGuids({ payload }); 107 | 108 | for (const { service, id } of guids) { 109 | if (service && id) { 110 | logger.info(`🔍 Finding show info for ${payload.Metadata.title} (${payload.Metadata.year}) using ${service}://${id}`); 111 | 112 | try { 113 | const response = await api.get(`https://api.trakt.tv/search/${service}/${id}?type=show`, { ttl: 1000 * 60 * 180 }); 114 | logger.debug(JSON.stringify(response.data, null, 2)); 115 | if (response.data.length) { 116 | const show = response.data[0].show; 117 | const { title, year } = show; 118 | logger.info(`📺 Show found: ${title} (${year})`); 119 | return show; 120 | } else { 121 | logger.error(`❌ ${chalk.red(`Response from ${service.toUpperCase()} was empty!`)}`); 122 | } 123 | } catch (err) { 124 | logger.error(`❌ ${chalk.red(`Search show API error: ${err.message}`)}`); 125 | } 126 | } else { 127 | logger.error(`❌ ${chalk.red(`No GUID available`)}`); 128 | } 129 | } 130 | 131 | logger.error(`❌ ${chalk.red(`No show found for any GUIDs`)}`); 132 | return null; 133 | }; 134 | 135 | export const findSeasonRequest = async (payload) => { 136 | logger.info( 137 | `🔍 Finding season info for ${payload.Metadata.parentTitle} (${payload.Metadata.parentYear}) - ${payload.Metadata.title} using ?query=${payload.Metadata.parentTitle} (${payload.Metadata.parentYear})`, 138 | ); 139 | 140 | try { 141 | const response = await api.get(`/search/show?query=${payload.Metadata.parentTitle} (${payload.Metadata.parentYear})`, { ttl: 1000 * 60 * 180 }); 142 | logger.debug(JSON.stringify(response.data, null, 2)); 143 | if (!response.data.length) { 144 | logger.error(`❌ ${chalk.red(`Response was empty!`)}`); 145 | return 146 | } 147 | const { show } = response.data[0]; 148 | logger.info( 149 | `📺 Season found: ${show.title} (${show.year})`, 150 | ); 151 | return show; 152 | } catch (err) { 153 | logger.error(`❌ ${chalk.red(`Search show API error: ${err.message}`)}`); 154 | } 155 | }; 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { logger } from './logger.js'; 3 | import { scrobbleRequest, rateRequest, findEpisodeRequest, findMovieRequest, findShowRequest, findSeasonRequest } from './requests.js'; 4 | 5 | export const getAction = ({ event, viewOffset, duration }) => { 6 | logger.debug(`${viewOffset} / ${duration}`); 7 | let progress = null; 8 | if (viewOffset && duration) { 9 | progress = ((viewOffset / duration) * 100).toFixed(2); 10 | } 11 | let res = { 12 | action: 'start', // start, pause, stop, scrobble 13 | progress: 0, // 0, 90 14 | }; 15 | switch (event) { 16 | case 'media.play': 17 | res.action = 'start'; 18 | res.progress = progress ? progress : 0; 19 | break; 20 | case 'media.pause': 21 | res.action = 'pause'; 22 | res.progress = progress ? progress : 0; 23 | break; 24 | case 'media.resume': 25 | res.action = 'start'; 26 | res.progress = progress ? progress : 0; 27 | break; 28 | case 'media.stop': 29 | res.action = 'stop'; 30 | res.progress = progress ? progress : 0; 31 | break; 32 | case 'media.scrobble': 33 | res.action = 'stop'; 34 | res.progress = progress && progress < 80 ? progress : 90; 35 | break; 36 | } 37 | return res; 38 | }; 39 | 40 | export const handle = async ({ payload }) => { 41 | const scrobblingEvents = process.env.LOG_LEVEL === 'debug' 42 | ? ['media.play', 'media.pause', 'media.resume', 'media.scrobble'] 43 | : ['media.scrobble']; 44 | const ratingEvents = ['media.rate']; 45 | const { type } = payload.Metadata; 46 | 47 | if (![...scrobblingEvents, ...ratingEvents].includes(payload.event)) { 48 | logger.debug(`❌ ${chalk.red(`Event ${payload.event} is not supported`)}`); 49 | return; 50 | } 51 | 52 | logger.debug(JSON.stringify(payload, null, 2)); 53 | 54 | try { 55 | if (scrobblingEvents.includes(payload.event)) { 56 | if (['show', 'season', 'episode'].includes(type)) { 57 | await handlePlayingShow({ payload }); 58 | } else if (['movie'].includes(type)) { 59 | await handlePlayingMovie({ payload }); 60 | } 61 | } else if (ratingEvents.includes(payload.event)) { 62 | await handleRating({ payload, type }); 63 | } 64 | } catch (error) { 65 | logger.error(`❌ ${chalk.red(error.message)}`); 66 | } 67 | }; 68 | 69 | const handleRating = async ({ payload, type }) => { 70 | switch (type) { 71 | case 'show': 72 | await handleRatingShow({ payload }); 73 | break; 74 | case 'season': 75 | await handleRatingSeason({ payload }); 76 | break; 77 | case 'episode': 78 | await handleRatingEpisode({ payload }); 79 | break; 80 | case 'movie': 81 | await handleRatingMovie({ payload }); 82 | break; 83 | default: 84 | throw new Error(`Type ${payload.Metadata.type} is not supported`); 85 | } 86 | }; 87 | 88 | export const handlePlayingMovie = async ({ payload }) => { 89 | const { event } = payload; 90 | const { viewOffset, duration } = payload.Metadata; 91 | const { action, progress } = getAction({ event, viewOffset, duration }); 92 | const movie = await findMovieRequest(payload); 93 | if (!movie) { 94 | logger.error(`❌ ${chalk.red(`Couldn't find movie info`)}`); 95 | return; 96 | } 97 | const body = { movie, progress }; 98 | const title = `🎬 ${payload.Metadata.title} (${payload.Metadata.year})`; 99 | await scrobbleRequest({ action, body, title }); 100 | }; 101 | 102 | export const handlePlayingShow = async ({ payload }) => { 103 | const { event } = payload; 104 | const { viewOffset, duration } = payload.Metadata; 105 | const { action, progress } = getAction({ event, viewOffset, duration }); 106 | const episode = await findEpisodeRequest(payload); 107 | if (!episode) { 108 | logger.error(`❌ ${chalk.red(`Couldn't find episode info`)}`); 109 | return; 110 | } 111 | const body = { episode, progress }; 112 | const title = `📺 ${payload.Metadata.grandparentTitle} (${payload.Metadata.year}) - S${String(payload.Metadata.parentIndex).padStart(2, '0')}E${String(payload.Metadata.index).padStart(2, '0')} - ${payload.Metadata.title}`; 113 | await scrobbleRequest({ action, body, title }); 114 | }; 115 | 116 | export const handleRatingShow = async ({ payload }) => { 117 | logger.debug(JSON.stringify(payload, null, 2)) 118 | 119 | const { rating } = payload; 120 | const show = await findShowRequest(payload); 121 | if (!show) { 122 | logger.error(`❌ ${chalk.red(`Couldn't find show info`)}`); 123 | return; 124 | } 125 | const body = { shows: [{ rating, ...show }] }; 126 | const title = `📺 ${payload.Metadata.title} (${payload.Metadata.year})`; 127 | await rateRequest({ body, title, rating }); 128 | }; 129 | 130 | export const handleRatingSeason = async ({ payload }) => { 131 | logger.debug(JSON.stringify(payload, null, 2)) 132 | 133 | const { rating } = payload; 134 | const number = payload.Metadata.index; 135 | const show = await findSeasonRequest(payload); 136 | if (!show) { 137 | logger.error(`❌ ${chalk.red(`Couldn't find season info`)}`); 138 | return; 139 | } 140 | const body = { 141 | shows: [ 142 | { 143 | ...show, 144 | seasons: [{ rating, number }], 145 | }, 146 | ], 147 | }; 148 | const title = `📺 ${payload.Metadata.parentTitle} (${payload.Metadata.parentYear}) - ${payload.Metadata.title}`; 149 | await rateRequest({ body, title, rating }); 150 | }; 151 | 152 | export const handleRatingEpisode = async ({ payload }) => { 153 | const { rating } = payload; 154 | const episode = await findEpisodeRequest(payload); 155 | if (!episode) { 156 | logger.error(`❌ ${chalk.red(`Couldn't find episode info`)}`); 157 | return; 158 | } 159 | const body = { episodes: [{ rating, ...episode }] }; 160 | const title = `📺 ${payload.Metadata.grandparentTitle} - S${String(payload.Metadata.parentIndex).padStart(2, '0')}E${String(payload.Metadata.index).padStart(2, '0')} - ${payload.Metadata.title}`; 161 | await rateRequest({ body, title, rating }); 162 | }; 163 | 164 | export const handleRatingMovie = async ({ payload }) => { 165 | const { rating } = payload; 166 | const movie = await findMovieRequest(payload); 167 | if (!movie) { 168 | logger.error(`❌ ${chalk.red(`Couldn't find movie info`)}`); 169 | return; 170 | } 171 | const body = { movies: [{ rating, ...movie }] }; 172 | const title = `🎬 ${payload.Metadata.title} (${payload.Metadata.year})`; 173 | await rateRequest({ body, title, rating }); 174 | }; 175 | 176 | export const GetGuids = ({ payload }) => { 177 | const Guid = payload?.Metadata?.Guid; 178 | if (!Guid) { 179 | logger.error(`❌ ${chalk.red(`Couldn't find Guid`)}`); 180 | return; 181 | } 182 | const guids = Guid.map((guid) => { 183 | const service = guid?.id?.substring(0, 4); 184 | const id = guid?.id?.substring(7); 185 | return { service, id }; 186 | }); 187 | logger.debug(`services and ids: ${JSON.stringify(guids)}`); 188 | return guids; 189 | }; -------------------------------------------------------------------------------- /static/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/apple-touch-icon.png -------------------------------------------------------------------------------- /static/images/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon-128.png -------------------------------------------------------------------------------- /static/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon-16x16.png -------------------------------------------------------------------------------- /static/images/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon-196x196.png -------------------------------------------------------------------------------- /static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /static/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon-96x96.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/mstile-144x144.png -------------------------------------------------------------------------------- /static/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/mstile-150x150.png -------------------------------------------------------------------------------- /static/images/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/mstile-310x150.png -------------------------------------------------------------------------------- /static/images/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/mstile-310x310.png -------------------------------------------------------------------------------- /static/images/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/mstile-70x70.png -------------------------------------------------------------------------------- /static/images/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /static/images/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryck/scrobblex/f0661e006f7791f7964b63fe48201220f4c59f39/static/images/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /static/output.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v4.0.8 | MIT License | https://tailwindcss.com */ 2 | @layer theme, base, components, utilities; 3 | @layer theme { 4 | :root, :host { 5 | --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", 6 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 7 | --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", 8 | "Courier New", monospace; 9 | --color-orange-300: oklch(0.837 0.128 66.29); 10 | --color-orange-600: oklch(0.646 0.222 41.116); 11 | --color-orange-700: oklch(0.553 0.195 38.402); 12 | --color-orange-800: oklch(0.47 0.157 37.304); 13 | --color-green-400: oklch(0.792 0.209 151.711); 14 | --color-green-500: oklch(0.723 0.219 149.579); 15 | --color-gray-50: oklch(0.985 0.002 247.839); 16 | --color-gray-200: oklch(0.928 0.006 264.531); 17 | --color-gray-400: oklch(0.707 0.022 261.325); 18 | --color-gray-500: oklch(0.551 0.027 264.364); 19 | --color-gray-700: oklch(0.373 0.034 259.733); 20 | --color-gray-800: oklch(0.278 0.033 256.848); 21 | --color-gray-900: oklch(0.21 0.034 264.665); 22 | --color-neutral-700: oklch(0.371 0 0); 23 | --color-neutral-800: oklch(0.269 0 0); 24 | --color-neutral-900: oklch(0.205 0 0); 25 | --color-white: #fff; 26 | --spacing: 0.25rem; 27 | --breakpoint-lg: 64rem; 28 | --breakpoint-xl: 80rem; 29 | --container-md: 28rem; 30 | --text-sm: 0.875rem; 31 | --text-sm--line-height: calc(1.25 / 0.875); 32 | --text-lg: 1.125rem; 33 | --text-lg--line-height: calc(1.75 / 1.125); 34 | --text-3xl: 1.875rem; 35 | --text-3xl--line-height: calc(2.25 / 1.875); 36 | --text-4xl: 2.25rem; 37 | --text-4xl--line-height: calc(2.5 / 2.25); 38 | --font-weight-light: 300; 39 | --font-weight-medium: 500; 40 | --font-weight-bold: 700; 41 | --tracking-tight: -0.025em; 42 | --radius-lg: 0.5rem; 43 | --default-transition-duration: 150ms; 44 | --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 45 | --default-font-family: var(--font-sans); 46 | --default-font-feature-settings: var(--font-sans--font-feature-settings); 47 | --default-font-variation-settings: var( 48 | --font-sans--font-variation-settings 49 | ); 50 | --default-mono-font-family: var(--font-mono); 51 | --default-mono-font-feature-settings: var( 52 | --font-mono--font-feature-settings 53 | ); 54 | --default-mono-font-variation-settings: var( 55 | --font-mono--font-variation-settings 56 | ); 57 | } 58 | } 59 | @layer base { 60 | *, ::after, ::before, ::backdrop, ::file-selector-button { 61 | box-sizing: border-box; 62 | margin: 0; 63 | padding: 0; 64 | border: 0 solid; 65 | } 66 | html, :host { 67 | line-height: 1.5; 68 | -webkit-text-size-adjust: 100%; 69 | tab-size: 4; 70 | font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ); 71 | font-feature-settings: var(--default-font-feature-settings, normal); 72 | font-variation-settings: var( --default-font-variation-settings, normal ); 73 | -webkit-tap-highlight-color: transparent; 74 | } 75 | body { 76 | line-height: inherit; 77 | } 78 | hr { 79 | height: 0; 80 | color: inherit; 81 | border-top-width: 1px; 82 | } 83 | abbr:where([title]) { 84 | -webkit-text-decoration: underline dotted; 85 | text-decoration: underline dotted; 86 | } 87 | h1, h2, h3, h4, h5, h6 { 88 | font-size: inherit; 89 | font-weight: inherit; 90 | } 91 | a { 92 | color: inherit; 93 | -webkit-text-decoration: inherit; 94 | text-decoration: inherit; 95 | } 96 | b, strong { 97 | font-weight: bolder; 98 | } 99 | code, kbd, samp, pre { 100 | font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace ); 101 | font-feature-settings: var( --default-mono-font-feature-settings, normal ); 102 | font-variation-settings: var( --default-mono-font-variation-settings, normal ); 103 | font-size: 1em; 104 | } 105 | small { 106 | font-size: 80%; 107 | } 108 | sub, sup { 109 | font-size: 75%; 110 | line-height: 0; 111 | position: relative; 112 | vertical-align: baseline; 113 | } 114 | sub { 115 | bottom: -0.25em; 116 | } 117 | sup { 118 | top: -0.5em; 119 | } 120 | table { 121 | text-indent: 0; 122 | border-color: inherit; 123 | border-collapse: collapse; 124 | } 125 | :-moz-focusring { 126 | outline: auto; 127 | } 128 | progress { 129 | vertical-align: baseline; 130 | } 131 | summary { 132 | display: list-item; 133 | } 134 | ol, ul, menu { 135 | list-style: none; 136 | } 137 | img, svg, video, canvas, audio, iframe, embed, object { 138 | display: block; 139 | vertical-align: middle; 140 | } 141 | img, video { 142 | max-width: 100%; 143 | height: auto; 144 | } 145 | button, input, select, optgroup, textarea, ::file-selector-button { 146 | font: inherit; 147 | font-feature-settings: inherit; 148 | font-variation-settings: inherit; 149 | letter-spacing: inherit; 150 | color: inherit; 151 | border-radius: 0; 152 | background-color: transparent; 153 | opacity: 1; 154 | } 155 | :where(select:is([multiple], [size])) optgroup { 156 | font-weight: bolder; 157 | } 158 | :where(select:is([multiple], [size])) optgroup option { 159 | padding-inline-start: 20px; 160 | } 161 | ::file-selector-button { 162 | margin-inline-end: 4px; 163 | } 164 | ::placeholder { 165 | opacity: 1; 166 | color: color-mix(in oklab, currentColor 50%, transparent); 167 | } 168 | textarea { 169 | resize: vertical; 170 | } 171 | ::-webkit-search-decoration { 172 | -webkit-appearance: none; 173 | } 174 | ::-webkit-date-and-time-value { 175 | min-height: 1lh; 176 | text-align: inherit; 177 | } 178 | ::-webkit-datetime-edit { 179 | display: inline-flex; 180 | } 181 | ::-webkit-datetime-edit-fields-wrapper { 182 | padding: 0; 183 | } 184 | ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { 185 | padding-block: 0; 186 | } 187 | :-moz-ui-invalid { 188 | box-shadow: none; 189 | } 190 | button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { 191 | appearance: button; 192 | } 193 | ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { 194 | height: auto; 195 | } 196 | [hidden]:where(:not([hidden="until-found"])) { 197 | display: none !important; 198 | } 199 | } 200 | @layer utilities { 201 | .collapse { 202 | visibility: collapse; 203 | } 204 | .invisible { 205 | visibility: hidden; 206 | } 207 | .visible { 208 | visibility: visible; 209 | } 210 | .fixed { 211 | position: fixed; 212 | } 213 | .relative { 214 | position: relative; 215 | } 216 | .static { 217 | position: static; 218 | } 219 | .container { 220 | width: 100%; 221 | @media (width >= 40rem) { 222 | max-width: 40rem; 223 | } 224 | @media (width >= 48rem) { 225 | max-width: 48rem; 226 | } 227 | @media (width >= 64rem) { 228 | max-width: 64rem; 229 | } 230 | @media (width >= 80rem) { 231 | max-width: 80rem; 232 | } 233 | @media (width >= 96rem) { 234 | max-width: 96rem; 235 | } 236 | } 237 | .mx-auto { 238 | margin-inline: auto; 239 | } 240 | .me-2 { 241 | margin-inline-end: calc(var(--spacing) * 2); 242 | } 243 | .mt-12 { 244 | margin-top: calc(var(--spacing) * 12); 245 | } 246 | .mr-2 { 247 | margin-right: calc(var(--spacing) * 2); 248 | } 249 | .mb-1 { 250 | margin-bottom: calc(var(--spacing) * 1); 251 | } 252 | .mb-2 { 253 | margin-bottom: calc(var(--spacing) * 2); 254 | } 255 | .mb-4 { 256 | margin-bottom: calc(var(--spacing) * 4); 257 | } 258 | .block { 259 | display: block; 260 | } 261 | .contents { 262 | display: contents; 263 | } 264 | .flex { 265 | display: flex; 266 | } 267 | .hidden { 268 | display: none; 269 | } 270 | .inline-flex { 271 | display: inline-flex; 272 | } 273 | .list-item { 274 | display: list-item; 275 | } 276 | .table { 277 | display: table; 278 | } 279 | .size-4 { 280 | width: calc(var(--spacing) * 4); 281 | height: calc(var(--spacing) * 4); 282 | } 283 | .h-3 { 284 | height: calc(var(--spacing) * 3); 285 | } 286 | .h-3\.5 { 287 | height: calc(var(--spacing) * 3.5); 288 | } 289 | .w-3 { 290 | width: calc(var(--spacing) * 3); 291 | } 292 | .w-3\.5 { 293 | width: calc(var(--spacing) * 3.5); 294 | } 295 | .max-w-md { 296 | max-width: var(--container-md); 297 | } 298 | .max-w-screen-lg { 299 | max-width: var(--breakpoint-lg); 300 | } 301 | .max-w-screen-xl { 302 | max-width: var(--breakpoint-xl); 303 | } 304 | .flex-shrink { 305 | flex-shrink: 1; 306 | } 307 | .flex-shrink-0 { 308 | flex-shrink: 0; 309 | } 310 | .border-collapse { 311 | border-collapse: collapse; 312 | } 313 | .transform { 314 | transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y); 315 | } 316 | .resize { 317 | resize: both; 318 | } 319 | .list-inside { 320 | list-style-position: inside; 321 | } 322 | .flex-col { 323 | flex-direction: column; 324 | } 325 | .items-center { 326 | align-items: center; 327 | } 328 | .space-y-1 { 329 | :where(& > :not(:last-child)) { 330 | --tw-space-y-reverse: 0; 331 | margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); 332 | margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); 333 | } 334 | } 335 | .gap-x-2 { 336 | column-gap: calc(var(--spacing) * 2); 337 | } 338 | .gap-x-3 { 339 | column-gap: calc(var(--spacing) * 3); 340 | } 341 | .divide-y { 342 | :where(& > :not(:last-child)) { 343 | --tw-divide-y-reverse: 0; 344 | border-bottom-style: var(--tw-border-style); 345 | border-top-style: var(--tw-border-style); 346 | border-top-width: calc(1px * var(--tw-divide-y-reverse)); 347 | border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); 348 | } 349 | } 350 | .divide-gray-200 { 351 | :where(& > :not(:last-child)) { 352 | border-color: var(--color-gray-200); 353 | } 354 | } 355 | .rounded-lg { 356 | border-radius: var(--radius-lg); 357 | } 358 | .border { 359 | border-style: var(--tw-border-style); 360 | border-width: 1px; 361 | } 362 | .border-gray-200 { 363 | border-color: var(--color-gray-200); 364 | } 365 | .bg-orange-700 { 366 | background-color: var(--color-orange-700); 367 | } 368 | .bg-white { 369 | background-color: var(--color-white); 370 | } 371 | .p-1 { 372 | padding: calc(var(--spacing) * 1); 373 | } 374 | .px-4 { 375 | padding-inline: calc(var(--spacing) * 4); 376 | } 377 | .px-5 { 378 | padding-inline: calc(var(--spacing) * 5); 379 | } 380 | .py-2 { 381 | padding-block: calc(var(--spacing) * 2); 382 | } 383 | .py-2\.5 { 384 | padding-block: calc(var(--spacing) * 2.5); 385 | } 386 | .py-3 { 387 | padding-block: calc(var(--spacing) * 3); 388 | } 389 | .py-8 { 390 | padding-block: calc(var(--spacing) * 8); 391 | } 392 | .pt-3 { 393 | padding-top: calc(var(--spacing) * 3); 394 | } 395 | .pb-3 { 396 | padding-bottom: calc(var(--spacing) * 3); 397 | } 398 | .text-3xl { 399 | font-size: var(--text-3xl); 400 | line-height: var(--tw-leading, var(--text-3xl--line-height)); 401 | } 402 | .text-4xl { 403 | font-size: var(--text-4xl); 404 | line-height: var(--tw-leading, var(--text-4xl--line-height)); 405 | } 406 | .text-sm { 407 | font-size: var(--text-sm); 408 | line-height: var(--tw-leading, var(--text-sm--line-height)); 409 | } 410 | .font-bold { 411 | --tw-font-weight: var(--font-weight-bold); 412 | font-weight: var(--font-weight-bold); 413 | } 414 | .font-light { 415 | --tw-font-weight: var(--font-weight-light); 416 | font-weight: var(--font-weight-light); 417 | } 418 | .font-medium { 419 | --tw-font-weight: var(--font-weight-medium); 420 | font-weight: var(--font-weight-medium); 421 | } 422 | .tracking-tight { 423 | --tw-tracking: var(--tracking-tight); 424 | letter-spacing: var(--tracking-tight); 425 | } 426 | .text-\[\#9F42C7\] { 427 | color: #9F42C7; 428 | } 429 | .text-\[\#ebaf00\] { 430 | color: #ebaf00; 431 | } 432 | .text-gray-500 { 433 | color: var(--color-gray-500); 434 | } 435 | .text-gray-800 { 436 | color: var(--color-gray-800); 437 | } 438 | .text-gray-900 { 439 | color: var(--color-gray-900); 440 | } 441 | .text-green-500 { 442 | color: var(--color-green-500); 443 | } 444 | .text-white { 445 | color: var(--color-white); 446 | } 447 | .italic { 448 | font-style: italic; 449 | } 450 | .underline { 451 | text-decoration-line: underline; 452 | } 453 | .shadow-sm { 454 | --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); 455 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); 456 | } 457 | .outline { 458 | outline-style: var(--tw-outline-style); 459 | outline-width: 1px; 460 | } 461 | .filter { 462 | filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); 463 | } 464 | .backdrop-filter { 465 | -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); 466 | backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); 467 | } 468 | .transition { 469 | transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; 470 | transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); 471 | transition-duration: var(--tw-duration, var(--default-transition-duration)); 472 | } 473 | .group-hover\:rotate-6 { 474 | &:is(:where(.group):hover *) { 475 | @media (hover: hover) { 476 | rotate: 6deg; 477 | } 478 | } 479 | } 480 | .hover\:bg-gray-50 { 481 | &:hover { 482 | @media (hover: hover) { 483 | background-color: var(--color-gray-50); 484 | } 485 | } 486 | } 487 | .hover\:bg-orange-800 { 488 | &:hover { 489 | @media (hover: hover) { 490 | background-color: var(--color-orange-800); 491 | } 492 | } 493 | } 494 | .focus\:ring-4 { 495 | &:focus { 496 | --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor); 497 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); 498 | } 499 | } 500 | .focus\:ring-orange-300 { 501 | &:focus { 502 | --tw-ring-color: var(--color-orange-300); 503 | } 504 | } 505 | .focus\:outline-none { 506 | &:focus { 507 | --tw-outline-style: none; 508 | outline-style: none; 509 | } 510 | } 511 | .disabled\:pointer-events-none { 512 | &:disabled { 513 | pointer-events: none; 514 | } 515 | } 516 | .disabled\:opacity-50 { 517 | &:disabled { 518 | opacity: 50%; 519 | } 520 | } 521 | .sm\:text-lg { 522 | @media (width >= 40rem) { 523 | font-size: var(--text-lg); 524 | line-height: var(--tw-leading, var(--text-lg--line-height)); 525 | } 526 | } 527 | .lg\:px-6 { 528 | @media (width >= 64rem) { 529 | padding-inline: calc(var(--spacing) * 6); 530 | } 531 | } 532 | .lg\:py-16 { 533 | @media (width >= 64rem) { 534 | padding-block: calc(var(--spacing) * 16); 535 | } 536 | } 537 | .dark\:divide-gray-700 { 538 | @media (prefers-color-scheme: dark) { 539 | :where(& > :not(:last-child)) { 540 | border-color: var(--color-gray-700); 541 | } 542 | } 543 | } 544 | .dark\:border-neutral-700 { 545 | @media (prefers-color-scheme: dark) { 546 | border-color: var(--color-neutral-700); 547 | } 548 | } 549 | .dark\:bg-gray-900 { 550 | @media (prefers-color-scheme: dark) { 551 | background-color: var(--color-gray-900); 552 | } 553 | } 554 | .dark\:bg-neutral-900 { 555 | @media (prefers-color-scheme: dark) { 556 | background-color: var(--color-neutral-900); 557 | } 558 | } 559 | .dark\:bg-orange-600 { 560 | @media (prefers-color-scheme: dark) { 561 | background-color: var(--color-orange-600); 562 | } 563 | } 564 | .dark\:text-gray-400 { 565 | @media (prefers-color-scheme: dark) { 566 | color: var(--color-gray-400); 567 | } 568 | } 569 | .dark\:text-green-400 { 570 | @media (prefers-color-scheme: dark) { 571 | color: var(--color-green-400); 572 | } 573 | } 574 | .dark\:text-white { 575 | @media (prefers-color-scheme: dark) { 576 | color: var(--color-white); 577 | } 578 | } 579 | .dark\:hover\:bg-neutral-800 { 580 | @media (prefers-color-scheme: dark) { 581 | &:hover { 582 | @media (hover: hover) { 583 | background-color: var(--color-neutral-800); 584 | } 585 | } 586 | } 587 | } 588 | .dark\:hover\:bg-orange-700 { 589 | @media (prefers-color-scheme: dark) { 590 | &:hover { 591 | @media (hover: hover) { 592 | background-color: var(--color-orange-700); 593 | } 594 | } 595 | } 596 | } 597 | .dark\:focus\:ring-orange-800 { 598 | @media (prefers-color-scheme: dark) { 599 | &:focus { 600 | --tw-ring-color: var(--color-orange-800); 601 | } 602 | } 603 | } 604 | } 605 | @property --tw-rotate-x { 606 | syntax: "*"; 607 | inherits: false; 608 | initial-value: rotateX(0); 609 | } 610 | @property --tw-rotate-y { 611 | syntax: "*"; 612 | inherits: false; 613 | initial-value: rotateY(0); 614 | } 615 | @property --tw-rotate-z { 616 | syntax: "*"; 617 | inherits: false; 618 | initial-value: rotateZ(0); 619 | } 620 | @property --tw-skew-x { 621 | syntax: "*"; 622 | inherits: false; 623 | initial-value: skewX(0); 624 | } 625 | @property --tw-skew-y { 626 | syntax: "*"; 627 | inherits: false; 628 | initial-value: skewY(0); 629 | } 630 | @property --tw-space-y-reverse { 631 | syntax: "*"; 632 | inherits: false; 633 | initial-value: 0; 634 | } 635 | @property --tw-divide-y-reverse { 636 | syntax: "*"; 637 | inherits: false; 638 | initial-value: 0; 639 | } 640 | @property --tw-border-style { 641 | syntax: "*"; 642 | inherits: false; 643 | initial-value: solid; 644 | } 645 | @property --tw-font-weight { 646 | syntax: "*"; 647 | inherits: false; 648 | } 649 | @property --tw-tracking { 650 | syntax: "*"; 651 | inherits: false; 652 | } 653 | @property --tw-shadow { 654 | syntax: "*"; 655 | inherits: false; 656 | initial-value: 0 0 #0000; 657 | } 658 | @property --tw-shadow-color { 659 | syntax: "*"; 660 | inherits: false; 661 | } 662 | @property --tw-inset-shadow { 663 | syntax: "*"; 664 | inherits: false; 665 | initial-value: 0 0 #0000; 666 | } 667 | @property --tw-inset-shadow-color { 668 | syntax: "*"; 669 | inherits: false; 670 | } 671 | @property --tw-ring-color { 672 | syntax: "*"; 673 | inherits: false; 674 | } 675 | @property --tw-ring-shadow { 676 | syntax: "*"; 677 | inherits: false; 678 | initial-value: 0 0 #0000; 679 | } 680 | @property --tw-inset-ring-color { 681 | syntax: "*"; 682 | inherits: false; 683 | } 684 | @property --tw-inset-ring-shadow { 685 | syntax: "*"; 686 | inherits: false; 687 | initial-value: 0 0 #0000; 688 | } 689 | @property --tw-ring-inset { 690 | syntax: "*"; 691 | inherits: false; 692 | } 693 | @property --tw-ring-offset-width { 694 | syntax: ""; 695 | inherits: false; 696 | initial-value: 0px; 697 | } 698 | @property --tw-ring-offset-color { 699 | syntax: "*"; 700 | inherits: false; 701 | initial-value: #fff; 702 | } 703 | @property --tw-ring-offset-shadow { 704 | syntax: "*"; 705 | inherits: false; 706 | initial-value: 0 0 #0000; 707 | } 708 | @property --tw-outline-style { 709 | syntax: "*"; 710 | inherits: false; 711 | initial-value: solid; 712 | } 713 | @property --tw-blur { 714 | syntax: "*"; 715 | inherits: false; 716 | } 717 | @property --tw-brightness { 718 | syntax: "*"; 719 | inherits: false; 720 | } 721 | @property --tw-contrast { 722 | syntax: "*"; 723 | inherits: false; 724 | } 725 | @property --tw-grayscale { 726 | syntax: "*"; 727 | inherits: false; 728 | } 729 | @property --tw-hue-rotate { 730 | syntax: "*"; 731 | inherits: false; 732 | } 733 | @property --tw-invert { 734 | syntax: "*"; 735 | inherits: false; 736 | } 737 | @property --tw-opacity { 738 | syntax: "*"; 739 | inherits: false; 740 | } 741 | @property --tw-saturate { 742 | syntax: "*"; 743 | inherits: false; 744 | } 745 | @property --tw-sepia { 746 | syntax: "*"; 747 | inherits: false; 748 | } 749 | @property --tw-drop-shadow { 750 | syntax: "*"; 751 | inherits: false; 752 | } 753 | @property --tw-backdrop-blur { 754 | syntax: "*"; 755 | inherits: false; 756 | } 757 | @property --tw-backdrop-brightness { 758 | syntax: "*"; 759 | inherits: false; 760 | } 761 | @property --tw-backdrop-contrast { 762 | syntax: "*"; 763 | inherits: false; 764 | } 765 | @property --tw-backdrop-grayscale { 766 | syntax: "*"; 767 | inherits: false; 768 | } 769 | @property --tw-backdrop-hue-rotate { 770 | syntax: "*"; 771 | inherits: false; 772 | } 773 | @property --tw-backdrop-invert { 774 | syntax: "*"; 775 | inherits: false; 776 | } 777 | @property --tw-backdrop-opacity { 778 | syntax: "*"; 779 | inherits: false; 780 | } 781 | @property --tw-backdrop-saturate { 782 | syntax: "*"; 783 | inherits: false; 784 | } 785 | @property --tw-backdrop-sepia { 786 | syntax: "*"; 787 | inherits: false; 788 | } 789 | -------------------------------------------------------------------------------- /views/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 103 | Scrobblex 104 | 105 | 106 | 107 |
108 |
109 |
110 |

111 | Scrobblex 112 |

113 | 114 |

115 | Plex provides 116 | webhook integration 117 | for all Plex Pass subscribers, and users of their servers. A webhook is 118 | a request that the Plex application sends to third party services when 119 | a user takes an action, such as watching a movie or episode. 120 |

121 |

122 | You can ask Plex to send these webhooks to this tool, which will then 123 | log those plays in your Trakt account. 124 |

125 | 126 |

127 | Step 1: Setup a Trakt application 128 |

129 |

130 | Go to your 131 | Trakt 132 | account and click on Your API Apps and 133 | create a new application. 134 |

135 |

You need to use the following information:

136 |
137 |
138 |
Name
139 |
Scrobblex (or anything you like, really)
140 |
141 |
142 |
Redirect uri & Javascript (cors) origins
143 |
144 |
145 |
146 | <%= self_url %>/authorize 147 |
148 | 149 | 166 |
167 |
168 |
169 |
170 |
Permissions
171 |
172 |
    173 |
  • 174 | 179 | /scrobble 180 |
  • 181 |
  • 182 | 187 | /checkin (optional) 188 |
  • 189 |
190 |
191 |
192 |
193 |

194 | This wil give you the Client ID and Client Secret you need to provide to Scrobblex. 196 |

197 | 198 | <% if (authorized) { %> 199 |

200 | Step 2: Authorize with Trakt 201 |

202 |

Nice!

203 | 204 | <% } else { %> 205 |

206 | Step 2: Authorize with Trakt 207 |

208 |

209 | This will take you to Trakt, then they'll send you back here. 210 | (Hopefully!) 211 |

212 | Authorize 214 | <% } %> 215 | 216 |

217 | Step 3: Configure Plex 218 |

219 |

220 | In Plex settings, find the Webhooks section. Add a 222 | webhook with the 223 | following 224 | link: 225 |

226 |
227 |
228 | <%= self_url %>/plex 229 |
230 | 231 | 247 |
248 | 249 |

Step 4: Enjoy

250 |

You're done!

251 | 252 |

More info

253 |

Please check the github repo 255 |

256 |
257 |
258 |
259 | 260 | 261 | -------------------------------------------------------------------------------- /views/pages/input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; --------------------------------------------------------------------------------