diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6dd22fb..d7f2d70 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,13 +7,17 @@ jobs:
docker:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ secrets.DOCKER_PASSWORD }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
diff --git a/.gitignore b/.gitignore
index a2f3de4..69ef532 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,78 +1,105 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
# Logs
+
logs
-*.log
-npm-debug.log*
+_.log
+npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
+# Caches
+
+.cache
+
# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
+
pids
-*.pid
-*.seed
+_.pid
+_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
+
lib-cov
# Coverage directory used by tools like istanbul
+
coverage
*.lcov
# nyc test coverage
+
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
.grunt
# Bower dependency directory (https://bower.io/)
+
bower_components
# node-waf configuration
+
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
+
build/Release
# Dependency directories
+
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
+
web_modules/
# TypeScript cache
+
*.tsbuildinfo
# Optional npm cache directory
+
.npm
# Optional eslint cache
+
.eslintcache
# Optional stylelint cache
+
.stylelintcache
# Microbundle cache
+
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
+
.node_repl_history
# Output of 'npm pack'
+
*.tgz
# Yarn Integrity file
+
.yarn-integrity
# dotenv environment variable files
+
.env
.env.development.local
.env.test.local
@@ -80,55 +107,71 @@ web_modules/
.env.local
# parcel-bundler cache (https://parceljs.org/)
-.cache
+
.parcel-cache
# Next.js build output
+
.next
out
# Nuxt.js build / generate output
+
.nuxt
dist
# Gatsby files
-.cache/
+
# Comment in the public line in if your project uses Gatsby and not Next.js
+
# https://nextjs.org/blog/next-9-1#public-directory-support
+
# public
# vuepress build output
+
.vuepress/dist
# vuepress v2.x temp and cache directory
+
.temp
-.cache
# Docusaurus cache and generated files
+
.docusaurus
# Serverless directories
+
.serverless/
# FuseBox cache
+
.fusebox/
# DynamoDB Local files
+
.dynamodb/
# TernJS port file
+
.tern-port
# Stores VSCode versions used for testing VSCode extensions
+
.vscode-test
# yarn v2
+
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
-s3.json
-yt-dlp
-.DS_Store
\ No newline at end of file
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
+
+s3.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 3f42341..dddd5cc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
-FROM node:alpine
+FROM oven/bun:1 AS base
RUN mkdir -p /usr/src/preservetube/backend
WORKDIR /usr/src/preservetube/backend
COPY . /usr/src/preservetube/backend
-RUN yarn
+RUN bun install
-CMD ["node", "index.js"]
\ No newline at end of file
+CMD ["bun", "run", "src/index.ts"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 29ebfa5..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,661 +0,0 @@
- GNU AFFERO GENERAL PUBLIC LICENSE
- Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
- A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate. Many developers of free software are heartened and
-encouraged by the resulting cooperation. However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
- The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community. It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server. Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
- An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals. This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU Affero General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Remote Network Interaction; Use with the GNU General Public License.
-
- Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software. This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time. Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source. For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code. There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-.
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..f959ebb
Binary files /dev/null and b/bun.lockb differ
diff --git a/controller/latest.js b/controller/latest.js
deleted file mode 100644
index 56a1592..0000000
--- a/controller/latest.js
+++ /dev/null
@@ -1,102 +0,0 @@
-const { PrismaClient } = require('@prisma/client')
-const redis = require('../utils/redis.js')
-const prisma = new PrismaClient()
-
-function createSitemapXML(urls) {
- const xml = urls.map(url => `
-
- ${url}
- never
- 0.7
-
- `).join('');
-
- return `
-
- ${xml}
- `;
-}
-
-function createSitemapIndexXML(sitemaps) {
- const xml = sitemaps.map((sitemap, index) => `
-
- https://api.preservetube.com/${sitemap}
- ${new Date().toISOString()}
-
- `).join('');
-
- return `
-
- ${xml}
- `;
-}
-
-exports.getLatest = async (req, res) => {
- let json
- const cached = await redis.get('latest')
-
- if (cached) {
- json = JSON.parse(cached)
- } else {
- json = await prisma.videos.findMany({
- take: 90,
- orderBy: [
- {
- archived: 'desc'
- }
- ],
- select: {
- id: true,
- title: true,
- thumbnail: true,
- published: true,
- archived: true,
- channel: true,
- channelId: true,
- channelAvatar: true,
- channelVerified: true
- }
- })
- await redis.set('latest', JSON.stringify(json), 'EX', 3600)
- }
-
- res.json(json)
-}
-
-exports.getSitemap = async (req, res) => {
- const cachedSitemapIndex = await redis.get('sitemap-index');
- if (cachedSitemapIndex) {
- res.header('Content-Type', 'application/xml');
- return res.send(cachedSitemapIndex);
- }
-
- const dbVideos = await prisma.videos.findMany({
- select: {
- id: true,
- },
- });
-
- const urls = dbVideos.map((video) => `https://preservetube.com/watch?v=${video.id}`);
- const sitemaps = [];
- for (let i = 0; i < urls.length; i += 50000) {
- const batch = urls.slice(i, i + 50000);
- await redis.set(`sitemap-${sitemaps.length}`, createSitemapXML(batch), 'EX', 86400);
- sitemaps.push(`sitemap-${sitemaps.length}.xml`);
- }
-
- const sitemapIndexXML = createSitemapIndexXML(sitemaps);
- await redis.set('sitemap-index', sitemapIndexXML, 'EX', 86400);
-
- res.header('Content-Type', 'application/xml');
- res.send(sitemapIndexXML);
-};
-
-exports.getSubSitemap = async (req, res) => {
- const cachedSitemap = await redis.get(`sitemap-${req.params.index}`);
- if (cachedSitemap) {
- res.header('Content-Type', 'application/xml');
- return res.send(cachedSitemap);
- }
-
- res.status(404).send('');
-};
\ No newline at end of file
diff --git a/controller/search.js b/controller/search.js
deleted file mode 100644
index 6b5e18a..0000000
--- a/controller/search.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const crypto = require('node:crypto')
-const validate = require('../utils/validate.js')
-const redis = require('../utils/redis.js')
-const { RedisRateLimiter } = require('rolling-rate-limiter')
-const { PrismaClient } = require('@prisma/client')
-const prisma = new PrismaClient()
-
-const limiter = new RedisRateLimiter({
- client: redis,
- namespace: 'search:',
- interval: 5 * 60 * 1000,
- maxInInterval: 5
-})
-
-exports.searchVideo = async (req, res) => {
- const ipHash = crypto.createHash('sha256').update(req.headers['x-userip'] || '0.0.0.0').digest('hex')
- const isLimited = await limiter.limit(ipHash)
- if (isLimited) return res.status(429).send('error-You have been ratelimited.')
-
- const id = await validate.validateVideoInput(req.query.search)
- if (id.fail) {
- const videos = await prisma.videos.findMany({
- where: {
- title: {
- contains: req.query.search,
- mode: 'insensitive'
- }
- }
- })
- res.json(videos)
- } else {
- res.send(`redirect-${process.env.FRONTEND}/watch?v=${id}`)
- }
-}
-
-exports.searchPlaylist = async (req, res) => {
- const id = await validate.validatePlaylistInput(req.query.url)
- if (id.fail) {
- res.status(500).send(id.message)
- } else {
- res.redirect(`${process.env.FRONTEND}/playlist?list=${id}`)
- }
-}
-
-exports.searchChannel = async (req, res) => {
- const id = await validate.validateChannelInput(req.query.url)
- if (id.fail) {
- res.status(500).send(id.message)
- } else {
- res.redirect(`${process.env.FRONTEND}/channel/${id}`)
- }
-}
\ No newline at end of file
diff --git a/controller/transparency.js b/controller/transparency.js
deleted file mode 100644
index 2b581fa..0000000
--- a/controller/transparency.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const { PrismaClient } = require('@prisma/client')
-const redis = require('../utils/redis.js')
-const prisma = new PrismaClient()
-
-exports.getReports = async (req, res) => {
- let json
- const cached = await redis.get('transparency')
-
- if (cached) {
- json = JSON.parse(cached)
- } else {
- json = (await prisma.reports.findMany()).map(r => {
- return {
- ...r,
- details: (r.details).split('<').join('<').split('>').join('>'),
- date: (r.date).toISOString().slice(0,10)
- }
- })
- await redis.set('transparency', JSON.stringify(json), 'EX', 3600)
- }
-
- res.json(json)
-}
-
-exports.getReports = async (req, res) => {
- const reports = await prisma.reports.findMany({
- where: {
- target: req.params.id
- }
- })
- res.json(reports)
-}
\ No newline at end of file
diff --git a/controller/video.js b/controller/video.js
deleted file mode 100644
index d1985c3..0000000
--- a/controller/video.js
+++ /dev/null
@@ -1,200 +0,0 @@
-const { PrismaClient } = require('@prisma/client')
-const prisma = new PrismaClient()
-
-const DOMPurify = require('isomorphic-dompurify')
-const rtm = require('readable-to-ms')
-const metadata = require('../utils/metadata.js')
-const redis = require('../utils/redis.js')
-
-exports.getVideo = async (req, res) => {
- let info
- const cached = await redis.get(`video:${req.params.id}`)
-
- if (cached) {
- info = JSON.parse(cached)
- } else {
- info = await prisma.videos.findFirst({
- where: {
- id: req.params.id
- },
- select: {
- title: true,
- description: true,
- thumbnail: true,
- source: true,
- published: true,
- archived: true,
- channel: true,
- channelId: true,
- channelAvatar: true,
- channelVerified: true,
- disabled: true,
- hasBeenReported: true
- }
- })
-
- if (!info) return res.json({ error: '404' })
- await redis.set(`video:${req.params.id}`, JSON.stringify(info), 'EX', 3600)
- }
-
- res.json({
- ...info,
- description: DOMPurify.sanitize(info.description),
- })
-}
-
-exports.getChannel = async (req, res) => {
- const cached = await redis.get(`channel:${req.params.id}`)
- if (cached) return res.json(JSON.parse(cached))
-
- const [videos, channel] = await Promise.all([
- metadata.getChannelVideos(req.params.id),
- metadata.getChannel(req.params.id)
- ])
-
- if (!videos || !channel || videos.error || channel.error) {
- return res.json({ error: '404' });
- }
-
- const archived = await prisma.videos.findMany({
- where: {
- channelId: req.params.id
- },
- select: {
- id: true,
- title: true,
- thumbnail: true,
- published: true,
- archived: true
- }
- })
-
- const processedVideos = videos.map(video => {
- const date = !isNaN(new Date(video.published.text).getTime()) ? new Date(video.published.text) : new Date((new Date()).getTime() - rtm(video.published.text).ms); // life is great.
- return {
- id: video.id,
- title: video.title.text,
- thumbnail: video.thumbnails[0].url,
- published: new Date(date).toISOString().slice(0, 10)
- }
- });
-
- archived.forEach(v => {
- const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id);
- if (existingVideoIndex !== -1) {
- processedVideos[existingVideoIndex] = v;
- } else {
- processedVideos.push({ ...v, deleted: undefined });
- }
- });
-
- processedVideos.sort((a, b) => new Date(b.published) - new Date(a.published));
-
- const json = {
- name: channel.metadata.title,
- avatar: channel.metadata.avatar[0].url,
- verified: channel.header.author?.is_verified,
- videos: processedVideos
- }
- await redis.set(`channel:${req.params.id}`, JSON.stringify(json), 'EX', 3600)
- res.json(json)
-}
-
-exports.getOnlyChannelVideos = async (req, res) => {
- const cached = await redis.get(`channelVideos:${req.params.id}`)
- if (cached) return res.json(JSON.parse(cached))
-
- const archived = await prisma.videos.findMany({
- where: {
- channelId: req.params.id
- },
- select: {
- id: true,
- title: true,
- thumbnail: true,
- published: true,
- archived: true
- },
- orderBy: {
- published: 'desc'
- }
- })
-
- const json = {
- videos: archived
- }
- await redis.set(`channelVideos:${req.params.id}`, JSON.stringify(json), 'EX', 3600)
- res.json(json)
-}
-
-exports.getPlaylist = async (req, res) => {
- const cached = await redis.get(`playlist:${req.params.id}`)
- if (cached) return res.json(JSON.parse(cached))
-
- const playlist = await metadata.getPlaylistVideos(req.params.id)
- if (!playlist || playlist.error) return res.json({ error: '404' })
-
- const playlistArchived = await prisma.videos.findMany({
- where: {
- playlist: req.params.id
- },
- select: {
- id: true,
- title: true,
- thumbnail: true,
- published: true,
- archived: true
- }
- })
-
- const allVideos = playlist.relatedStreams.map(video => ({
- id: video.url.replace('/watch?v=', ''),
- published: new Date(video.uploaded).toISOString().slice(0, 10),
- ...video
- }));
-
- await Promise.all(playlistArchived.map(async (v) => {
- const allVideo = allVideos.find(o => o.id == v.id);
- if (allVideo) {
- const index = allVideos.findIndex(o => o.id == v.id);
- allVideos[index] = v;
- } else {
- const live = await metadata.getVideoMetadata(v.id);
- allVideos.push({
- ...v,
- deleted: live.error ? true : false
- });
- }
- }));
-
- await Promise.all(allVideos.filter(v => !v.archived).map(async (v) => {
- const video = await prisma.videos.findFirst({
- where: {
- id: v.id
- },
- select: {
- id: true,
- title: true,
- thumbnail: true,
- published: true,
- archived: true
- }
- });
- if (video) {
- const index = allVideos.findIndex(o => o.id == v.id);
- allVideos[index] = video;
- }
- }));
-
- allVideos.sort((a, b) => new Date(b.published) - new Date(a.published));
-
- const json = {
- name: playlist.name,
- channel: playlist.uploader,
- url: playlist.uploaderUrl,
- avatar: playlist.uploaderAvatar,
- videos: allVideos
- }
- await redis.set(`playlist:${req.params.id}`, JSON.stringify(json), 'EX', 3600)
- res.json(json)
-}
\ No newline at end of file
diff --git a/controller/websocket.js b/controller/websocket.js
deleted file mode 100644
index 4674c4c..0000000
--- a/controller/websocket.js
+++ /dev/null
@@ -1,335 +0,0 @@
-const fs = require('node:fs')
-const crypto = require('node:crypto')
-const { RedisRateLimiter } = require('rolling-rate-limiter')
-const rtm = require('readable-to-ms')
-
-const upload = require('../utils/upload.js')
-const ytdlp = require('../utils/ytdlp.js')
-const redis = require('../utils/redis.js')
-
-const validate = require('../utils/validate.js')
-const metadata = require('../utils/metadata.js')
-const websocket = require('../utils/websocket.js')
-const captcha = require("../utils/captcha.js")
-const logger = require("../utils/logger.js")
-
-const { PrismaClient } = require('@prisma/client')
-const prisma = new PrismaClient()
-
-const limiter = new RedisRateLimiter({
- client: redis,
- namespace: 'autodownload:',
- interval: 24 * 60 * 60 * 1000,
- maxInInterval: 5
-})
-
-exports.save = async (ws, req) => {
- logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` })
-
- const id = await validate.validateVideoInput(req.query.url)
- if (id.fail) {
- ws.send(`ERROR - ${id.message}`)
- return ws.close()
- }
-
- if (await redis.get(id)) {
- ws.send('DATA - Someone is already downloading this video...')
- return ws.close()
- }
-
- if (await redis.get(`blacklist:${id}`)) {
- ws.send('DATA - You can\'t download that. The video is blacklisted.')
- return ws.close()
- }
-
- const already = await prisma.videos.findFirst({
- where: {
- id: id
- }
- })
-
- if (already) return ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${id}`)
-
- ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
- ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
-
- ws.on('message', async function(msg) {
- if (msg == 'alive') return
-
- if (await redis.get(id) != 'downloading') {
- await redis.set(id, 'downloading', 'EX', 300)
- const confirm = await captcha.checkCaptcha(msg)
-
- if (confirm) startDownloading()
- else {
- await redis.del(id)
- ws.send('DATA - You little goofy goober tried to mess with the captcha...')
- ws.close()
- }
- } else {
- ws.send('DATA - You already sent captcha reply...')
- }
- })
-
- async function startDownloading() {
- const download = await ytdlp.downloadVideo(`https://www.youtube.com/watch?v=${id}`, ws, id)
- if (download.fail) {
- await redis.del(id)
- ws.send(`DATA - ${download.message}`)
- ws.close()
- } else {
- const file = fs.readdirSync("videos").find(f => f.includes(id))
- if (file) {
- ws.send('DATA - Uploading file...')
- const videoUrl = await upload.uploadVideo(`./videos/${id}.mp4`)
- fs.unlinkSync(`./videos/${id}.mp4`)
-
- const uploaded = await websocket.createDatabaseVideo(id, videoUrl)
- if (uploaded != 'success') {
- ws.send(`DATA - Error while uploading - ${JSON.stringify(uploaded)}`)
- } else {
- ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${id}`)
- }
- }
-
- await redis.del(id)
- ws.close();
- }
- }
-}
-
-exports.playlist = async (ws, req) => {
- logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` })
-
- const playlistId = await validate.validatePlaylistInput(req.query.url)
- if (playlistId.fail) {
- ws.send(`ERROR - ${playlistId.message}`)
- return ws.close()
- }
-
- let status = 'captcha'
- ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
- ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
-
- ws.on('message', async function(msg) {
- if (msg == 'alive') return
-
- if (status == 'captcha') {
- status = 'downloading'
- const confirm = await captcha.checkCaptcha(msg)
-
- if (confirm) startDownloading()
- else {
- await redis.del(id)
- ws.send('DATA - You little goofy goober tried to mess with the captcha...')
- ws.close()
- }
- } else {
- ws.send('DATA - You already sent captcha reply...')
- }
- })
-
- async function startDownloading() {
- const playlist = await metadata.getPlaylistVideos(playlistId)
- for (video of playlist.relatedStreams.slice(0, 5)) {
- if (ws.readyState !== ws.OPEN) {
- return logger.info({ message: `Stopped downloading ${playlistId}, websocket is closed` })
- }
-
- const id = video.url.match(/[?&]v=([^&]+)/)[1]
-
- const already = await prisma.videos.findFirst({
- where: {
- id: id
- }
- })
-
- if (already) {
- ws.send(`DATA - Already downloaded ${video.title}`)
- await prisma.videos.updateMany({
- where: {
- id: id
- },
- data: {
- playlist: playlistId
- }
- })
- continue
- }
-
- if (await redis.get(id)) {
- ws.send(`DATA - Someone is already downloading ${video.title}, skipping.`)
- continue
- }
-
- if (await redis.get(`blacklist:${id}`)) {
- ws.send(`DATA - ${video.title} is blacklisted from downloading, skipping`)
- continue
- }
-
- ws.send(`INFO - Downloading ${video.title}
`)
- await redis.set(id, 'downloading', 'EX', 300)
-
- const download = await ytdlp.downloadVideo('https://www.youtube.com' + video.url, ws, id)
- if (download.fail) {
- ws.send(`DATA - ${download.message}`)
- await redis.del(id)
- continue
- } else {
- const file = fs.readdirSync("./videos").find(f => f.includes(id))
- if (file) {
- try {
- ws.send(`DATA - Downloaded ${video.title}`)
- ws.send(`DATA - Uploading ${video.title}`)
-
- const videoUrl = await upload.uploadVideo(`./videos/${id}.mp4`)
- ws.send(`DATA - Uploaded ${video.title}`)
- fs.unlinkSync(`./videos/${id}.mp4`)
-
- await websocket.createDatabaseVideo(id, videoUrl, playlistId)
- ws.send(`DATA - Created video page for ${video.title}`)
- } catch (e) {
- ws.send(`DATA - Failed downloading video ${video.title}. Going to next video`)
- logger.error(e)
- }
- } else {
- ws.send(`DATA - Failed to find file for ${video.title}. Going to next video in the playlist`)
- }
-
- await redis.del(id)
- }
- }
-
- ws.send(`DONE - ${process.env.FRONTEND}/playlist?list=${playlistId}`)
- }
-}
-
-exports.channel = async (ws, req) => {
- logger.info({ message: `${req.path} ${JSON.stringify(req.query)}` })
-
- const channelId = await validate.validateChannelInput(req.query.url)
- if (channelId.fail) {
- ws.send(`ERROR - ${channelId.message}`)
- return ws.close()
- }
-
- let status = 'captcha'
- ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
- ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
-
- ws.on('message', async function(msg) {
- if (msg == 'alive') return
-
- if (status == 'captcha') {
- status = 'downloading'
- const confirm = await captcha.checkCaptcha(msg)
-
- if (confirm) startDownloading()
- else {
- ws.send('DATA - You little goofy goober tried to mess with the captcha...')
- ws.close()
- }
- } else {
- ws.send('DATA - You already sent captcha reply...')
- }
- })
-
- async function startDownloading() {
- const videos = await metadata.getChannelVideos(channelId)
-
- for (const video of videos.slice(0, 5)) {
- if (ws.readyState !== ws.OPEN) {
- return logger.info({ message: `Stopped downloading ${channelId}, websocket is closed` })
- }
-
- const already = await prisma.videos.findFirst({
- where: {
- id: video.id
- }
- })
-
- if (already) {
- ws.send(`DATA - Already downloaded ${video.title.text}`)
- continue
- }
-
- if (await redis.get(video.id)) {
- ws.send(`DATA - Someone is already downloading ${video.title.text}, skipping.`)
- continue
- }
-
- if (await redis.get(`blacklist:${video.id}`)) {
- ws.send(`DATA - ${video.title.text} is blacklisted from downloading, skipping`)
- continue
- }
-
- ws.send(`INFO - Downloading ${video.title.text}
`)
- await redis.set(video.id, 'downloading', 'EX', 300)
-
- const download = await ytdlp.downloadVideo(`https://www.youtube.com/watch?v=${video.id}`, ws, video.id)
- if (download.fail) {
- ws.send(`DATA - ${download.message}`)
- await redis.del(video.id)
- continue
- } else {
- const file = fs.readdirSync("./videos").find(f => f.includes(video.id))
- if (file) {
- try {
- ws.send(`DATA - Downloaded ${video.title.text}`)
- ws.send(`DATA - Uploading ${video.title.text}`)
-
- const videoUrl = await upload.uploadVideo(`./videos/${video.id}.mp4`)
- ws.send(`DATA - Uploaded ${video.title.text}`)
- fs.unlinkSync(`./videos/${video.id}.mp4`)
-
- await websocket.createDatabaseVideo(video.id, videoUrl)
- ws.send(`DATA - Created video page for ${video.title.text}`)
- } catch (e) {
- ws.send(`DATA - Failed downloading video ${video.title.text}. Going to next video`)
- logger.error(e)
- }
- } else {
- ws.send(`DATA - Failed to find file for ${video.title.text}. Going to next video`)
- }
-
- await redis.del(video.id)
- }
- }
-
- ws.send(`DONE - ${process.env.FRONTEND}/channel/${channelId}`)
- }
-}
-
-exports.addAutodownload = async (req, res) => {
- const confirm = await captcha.checkCaptcha(req.query.captcha)
- if (!confirm) return res.status(500).send('You little goofy goober tried to mess with the captcha...')
-
- const channelId = await validate.validateChannelInput(req.query.url)
- if (channelId.fail) {
- return res.status(500).send(channelId.message)
- }
-
- const already = await prisma.autodownload.findFirst({
- where: {
- channel: channelId
- }
- })
-
- if (already) {
- res.status(500).send(`This channel is already being automatically downloaded...`)
- } else {
- const ipHash = crypto.createHash('sha256').update(req.headers['x-forwarded-for'] || req.connection.remoteAddress).digest('hex')
- const isLimited = await limiter.limit(ipHash)
-
- if (isLimited) return res.status(420).send(`Hey! You have reached the limit of 5 queued auto-download channels per day. Sadly, hard drives don't grow on trees, so rate limits are necessary. The "Save Channel" feature has no limits, so feel free to use that.
-
-Are you planning something awesome? Feel free to email me at admin[@]preservetube.com.`)
-
- await prisma.autodownload.create({
- data: {
- channel: channelId
- }
- })
- res.send('Perfect! Each time this channel uploads their videos will be downloaded')
- }
-}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 3d5ca4f..a940e87 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,17 +6,13 @@ services:
image: ghcr.io/preservetube/backend
networks:
- public
- - db
restart: on-failure
volumes:
- - ./logs:/usr/src/preservetube/backend/logs
+ - /mnt/hdd/preservetube-videos:/usr/src/preservetube/backend/videos
- ./.env:/usr/src/preservetube/backend/.env
- ./s3.json:/usr/src/preservetube/backend/s3.json
networks:
public:
external: true
- name: public
- db:
- external: true
- name: db
\ No newline at end of file
+ name: public
\ No newline at end of file
diff --git a/index.js b/index.js
deleted file mode 100644
index f1514e3..0000000
--- a/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-require('dotenv').config()
-
-const express = require('express')
-const cors = require('cors')
-
-const logger = require('./utils/logger.js')
-
-const latestController = require('./controller/latest.js')
-const videoController = require('./controller/video.js')
-const searchController = require('./controller/search.js')
-const websocketController = require('./controller/websocket.js')
-const transparencyController = require('./controller/transparency.js')
-
-const app = express()
-
-require('express-ws')(app)
-app.use(cors())
-
-app.get('/latest', latestController.getLatest)
-app.get('/sitemap-index.xml', latestController.getSitemap)
-app.get('/sitemap-:index.xml', latestController.getSubSitemap)
-
-app.get('/video/:id', videoController.getVideo)
-app.get('/channel/:id', videoController.getChannel)
-app.get('/channel/:id/videos', videoController.getOnlyChannelVideos)
-app.get('/playlist/:id', videoController.getPlaylist)
-
-app.get('/search/video', searchController.searchVideo)
-app.get('/search/playlist', searchController.searchPlaylist)
-app.get('/search/channel', searchController.searchChannel)
-
-app.get('/transparency/list', transparencyController.getReports)
-app.get('/transparency/:id', transparencyController.getReports)
-
-app.ws('/save', websocketController.save)
-app.ws('/saveplaylist', websocketController.playlist)
-app.ws('/savechannel', websocketController.channel)
-app.get('/autodownload', websocketController.addAutodownload)
-
-process.on('uncaughtException', err => {
- logger.error(err)
- console.log(err)
-})
-
-app.listen(1337, () => {
- logger.info({ message: 'Server listening on port 1337!' })
-})
\ No newline at end of file
diff --git a/package.json b/package.json
index 61b280e..ba3ad49 100644
--- a/package.json
+++ b/package.json
@@ -1,26 +1,27 @@
{
- "name": "preservetube",
- "version": "1.0.0",
- "main": "index.js",
- "license": "AGPL-3.0",
- "dependencies": {
- "@logtail/node": "^0.4.0",
- "@logtail/winston": "^0.4.1",
- "@prisma/client": "4.9.0",
- "aws-sdk": "2.1128.0",
- "cors": "^2.8.5",
- "dotenv": "^16.0.3",
- "express": "^4.18.2",
- "express-ws": "^5.0.2",
- "ioredis": "^5.3.1",
- "isomorphic-dompurify": "^1.0.0",
- "node-fetch": "2",
- "readable-to-ms": "^1.0.3",
- "rolling-rate-limiter": "^0.4.2",
- "winston": "^3.8.2",
- "ws": "^8.17.1"
+ "name": "preservetube-backend",
+ "module": "src/index.ts",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "bun run --watch src/index.ts"
},
"devDependencies": {
- "prisma": "4.9.0"
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@types/pg": "^8.11.10",
+ "date-fns": "^4.1.0",
+ "elysia": "^1.1.25",
+ "ioredis": "^5.4.1",
+ "isomorphic-dompurify": "^2.18.0",
+ "kysely": "^0.27.4",
+ "pg": "^8.13.1",
+ "readable-to-ms": "^1.0.3",
+ "rolling-rate-limiter": "^0.4.2",
+ "ultralight-s3": "^0.0.7"
}
-}
+}
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
deleted file mode 100644
index 3276758..0000000
--- a/prisma/schema.prisma
+++ /dev/null
@@ -1,44 +0,0 @@
-// This is your Prisma schema file,
-// learn more about it in the docs: https://pris.ly/d/prisma-schema
-
-generator client {
- provider = "prisma-client-js"
-}
-
-datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
-}
-
-model videos {
- uuid String @id @default(uuid())
- id String @unique
- title String
- description String
- thumbnail String
- source String
- published String
- archived String
- channel String
- channelId String
- channelVerified Boolean
- channelAvatar String
- playlist String?
- disabled Boolean @default(false)
- hasBeenReported Boolean @default(false)
-
- @@index([title], name: "idx_title")
-}
-
-model reports {
- uuid String @id @default(uuid())
- target String
- title String
- details String
- date DateTime @default(now())
-}
-
-model autodownload {
- uuid String @id @default(uuid())
- channel String
-}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..dc96d78
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,23 @@
+import { Elysia } from 'elysia';
+
+import latest from '@/router/latest'
+import search from '@/router/search'
+import transparency from '@/router/transparency'
+import video from '@/router/video'
+import websocket from '@/router/websocket'
+
+const app = new Elysia()
+app.use(latest)
+app.use(search)
+app.use(transparency)
+app.use(video)
+app.use(websocket)
+
+process.on('uncaughtException', err => {
+ console.log(err)
+})
+
+app.listen(1337);
+console.log(
+ `api is running at ${app.server?.hostname}:${app.server?.port}`
+);
\ No newline at end of file
diff --git a/src/router/latest.ts b/src/router/latest.ts
new file mode 100644
index 0000000..d1ba98a
--- /dev/null
+++ b/src/router/latest.ts
@@ -0,0 +1,66 @@
+import { Elysia } from 'elysia';
+import { Redis } from 'ioredis'
+
+import { db } from '@/utils/database'
+import { createSitemapXML, createSitemapIndexXML } from '@/utils/sitemap'
+
+const app = new Elysia()
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ password: process.env.REDIS_PASS,
+});
+
+app.get('/latest', async () => {
+ const cached = await redis.get('latest')
+ if (cached) return JSON.parse(cached)
+
+ const json = await db.selectFrom('videos')
+ .select(['id', 'title', 'thumbnail', 'published', 'archived', 'channel', 'channelId', 'channelAvatar', 'channelVerified'])
+ .orderBy('archived desc')
+ .limit(50)
+ .execute()
+
+ await redis.set('latest', JSON.stringify(json), 'EX', 3600)
+
+ return json
+})
+
+app.get('/sitemap-index.xml', async ({ set }) => {
+ const cachedSitemapIndex = await redis.get('sitemap-index');
+ if (cachedSitemapIndex) {
+ set.headers['Content-Type'] = 'application/xml'
+ return cachedSitemapIndex
+ }
+
+ const videos = await db.selectFrom('videos')
+ .select('id')
+ .execute()
+
+ const urls = videos.map((video) => `https://preservetube.com/watch?v=${video.id}`);
+ const sitemaps = [];
+
+ for (let i = 0; i < urls.length; i += 50000) {
+ const batch = urls.slice(i, i + 50000);
+ await redis.set(`sitemap-${sitemaps.length}`, createSitemapXML(batch), 'EX', 86400);
+ sitemaps.push(`sitemap-${sitemaps.length}.xml`);
+ }
+
+ const sitemapIndexXML = createSitemapIndexXML(sitemaps);
+ await redis.set('sitemap-index', sitemapIndexXML, 'EX', 86400);
+
+ set.headers['Content-Type'] = 'application/xml'
+ return sitemapIndexXML
+})
+
+app.get('/sitemap-:index.xml', async ({ set, params: { index }, error, path }) => {
+ const indexNum = path.replace('/sitemap-', '').replace('.xml', '')
+ const cachedSitemap = await redis.get(`sitemap-${indexNum}`);
+ if (cachedSitemap) {
+ set.headers['Content-Type'] = 'application/xml'
+ return cachedSitemap
+ }
+
+ return error(404)
+})
+
+export default app
\ No newline at end of file
diff --git a/src/router/search.ts b/src/router/search.ts
new file mode 100644
index 0000000..f3f954a
--- /dev/null
+++ b/src/router/search.ts
@@ -0,0 +1,52 @@
+import { Elysia, t } from 'elysia';
+import { Redis } from 'ioredis'
+import { RedisRateLimiter } from 'rolling-rate-limiter'
+
+import { db } from '@/utils/database'
+import { validateVideo, validatePlaylist, validateChannel } from '@/utils/regex'
+
+const app = new Elysia()
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ password: process.env.REDIS_PASS,
+});
+
+const limiter = new RedisRateLimiter({
+ client: redis,
+ namespace: 'search:',
+ interval: 5 * 60 * 1000,
+ maxInInterval: 15
+})
+
+app.get('/search/video', async ({ headers, query: { search }, error }) => {
+ const hash = Bun.hash(headers['x-userip'] || headers['cf-connecting-ip'] || '0.0.0.0')
+ const isLimited = await limiter.limit(hash.toString())
+ if (isLimited) return error(429, 'error-You have been ratelimited.')
+
+ const videoId = validateVideo(search)
+ if (videoId) return `redirect-${process.env.FRONTEND}/watch?v=${videoId}`
+
+ const videos = await db.selectFrom('videos')
+ .selectAll()
+ .where('title', 'ilike', `%${search}%`)
+ .execute()
+
+ return videos
+}, {
+ query: t.Object({
+ search: t.String()
+ })
+})
+
+app.get('/search/channel', async ({ query: { url }, error, redirect }) => {
+ const channelId = await validateChannel(url)
+ if (!channelId) return error(400, 'Whoops! What is that? That is not a Youtube Channel.')
+
+ return redirect(`${process.env.FRONTEND}/channel/${channelId}`)
+}, {
+ query: t.Object({
+ url: t.String()
+ })
+})
+
+export default app
\ No newline at end of file
diff --git a/src/router/transparency.ts b/src/router/transparency.ts
new file mode 100644
index 0000000..0deb6c4
--- /dev/null
+++ b/src/router/transparency.ts
@@ -0,0 +1,45 @@
+import { Elysia } from 'elysia';
+import { Redis } from 'ioredis'
+
+import { db } from '@/utils/database'
+
+const app = new Elysia()
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ password: process.env.REDIS_PASS,
+});
+
+app.get('/transparency/list', async () => {
+ const cached = await redis.get('transparency')
+ if (cached) return JSON.parse(cached)
+
+ const reports = await db.selectFrom('reports')
+ .selectAll()
+ .execute()
+
+ const json = reports.map(r => {
+ return {
+ ...r,
+ details: (r.details).split('<').join('<').split('>').join('>'),
+ date: (r.date).toISOString().slice(0, 10)
+ }
+ })
+
+ await redis.set('transparency', JSON.stringify(json), 'EX', 3600)
+ return json
+})
+
+app.get('/transparency/:id', async ({ params: { id } }) => {
+ const cached = await redis.get(`transparency:${id}`)
+ if (cached) return JSON.parse(cached)
+
+ const json = await db.selectFrom('reports')
+ .selectAll()
+ .where('target', '=', id)
+ .execute()
+
+ await redis.set(`transparency:${id}`, JSON.stringify(json), 'EX', 3600)
+ return json
+})
+
+export default app
\ No newline at end of file
diff --git a/src/router/video.ts b/src/router/video.ts
new file mode 100644
index 0000000..be277b5
--- /dev/null
+++ b/src/router/video.ts
@@ -0,0 +1,103 @@
+import { Elysia } from 'elysia';
+import { Redis } from 'ioredis'
+import DOMPurify from 'isomorphic-dompurify'
+
+import { db } from '@/utils/database'
+import { getChannel, getChannelVideos } from '@/utils/metadata';
+import { convertRelativeToDate } from '@/utils/common';
+
+const app = new Elysia()
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ password: process.env.REDIS_PASS,
+});
+
+interface processedVideo {
+ id: string;
+ title: string;
+ thumbnail: string;
+ published: string;
+ deleted?: undefined;
+}
+
+app.get('/video/:id', async ({ params: { id }, error }) => {
+ const cached = await redis.get(`video:${id}`)
+ if (cached) return JSON.parse(cached)
+
+ const json = await db.selectFrom('videos')
+ .selectAll()
+ .where('id', '=', id)
+ .executeTakeFirst()
+
+ if (!json) return error(404, { error: '404' })
+ await redis.set(`video:${id}`, JSON.stringify(json), 'EX', 3600)
+
+ return {
+ ...json,
+ description: DOMPurify.sanitize(json.description),
+ }
+})
+
+app.get('/channel/:id', async ({ params: { id }, error }) => {
+ const cached = await redis.get(`channel:${id}`)
+ if (cached) return JSON.parse(cached)
+
+ const [videos, channel] = await Promise.all([
+ getChannelVideos(id),
+ getChannel(id)
+ ])
+
+ if (!videos || !channel || videos.error || channel.error) return error(404, { error: '404' })
+
+ const archived = await db.selectFrom('videos')
+ .select(['id', 'title', 'thumbnail', 'published', 'archived'])
+ .where('channelId', '=', id)
+ .execute()
+
+ const processedVideos: processedVideo[] = videos.map((video: any) => ({ // it would be impossible to set types for youtube output... they change it every day.
+ id: video.id,
+ title: video.title.text,
+ thumbnail: video.thumbnails[0].url,
+ published: (video.published.text.endsWith('ago') ? convertRelativeToDate(video.published.text) : new Date(video.published.text)).toISOString().slice(0, 10)
+ }))
+
+ archived.forEach(v => {
+ const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id);
+ if (existingVideoIndex !== -1) {
+ processedVideos[existingVideoIndex] = v;
+ } else {
+ processedVideos.push({ ...v, deleted: undefined });
+ }
+ });
+
+ processedVideos.sort((a: any, b: any) => new Date(b.published).getTime() - new Date(a.published).getTime());
+
+ const json = {
+ name: channel.metadata.title,
+ avatar: channel.metadata.avatar[0].url,
+ verified: channel.header.author?.is_verified,
+ videos: processedVideos
+ }
+
+ await redis.set(`channel:${id}`, JSON.stringify(json), 'EX', 3600)
+ return json
+})
+
+app.get('/channel/:id/videos', async ({ params: { id } }) => {
+ const cached = await redis.get(`channelVideos:${id}`)
+ if (cached) return JSON.parse(cached)
+
+ const archived = await db.selectFrom('videos')
+ .select(['id', 'title', 'thumbnail', 'published', 'archived'])
+ .where('channelId', '=', id)
+ .orderBy('published desc')
+ .execute()
+
+ const json = {
+ videos: archived
+ }
+ await redis.set(`channelVideos:${id}`, JSON.stringify(json), 'EX', 3600)
+ return json
+})
+
+export default app
\ No newline at end of file
diff --git a/src/router/websocket.ts b/src/router/websocket.ts
new file mode 100644
index 0000000..ce502f6
--- /dev/null
+++ b/src/router/websocket.ts
@@ -0,0 +1,179 @@
+import { Elysia, t } from 'elysia';
+import { Redis } from 'ioredis'
+import * as fs from 'node:fs'
+
+import { db } from '@/utils/database'
+import { validateVideo, validateChannel } from '@/utils/regex'
+import { checkCaptcha, createDatabaseVideo } from '@/utils/common';
+import { downloadVideo } from '@/utils/download';
+import { uploadVideo } from '@/utils/upload';
+import { getChannelVideos } from '@/utils/metadata';
+
+const app = new Elysia()
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ password: process.env.REDIS_PASS,
+});
+const videoIds: Record = {}
+
+const sendError = (ws: any, message: string) => {
+ ws.send(`ERROR - ${message}`);
+ ws.close();
+};
+
+const cleanup = async (ws: any, videoId: string) => {
+ delete videoIds[ws.id];
+ if (videoId) await redis.del(videoId);
+ await redis.del(ws.id);
+};
+
+const handleUpload = async (ws: any, videoId: string, isChannel: boolean = false) => {
+ const filePath = `./videos/${videoId}.mp4`;
+ if (!fs.existsSync(filePath)) {
+ ws.send(`DATA - Video file for ${videoId} not found. Skipping.`);
+ return false;
+ }
+
+ try {
+ ws.send('DATA - Uploading file...');
+ const videoUrl = await uploadVideo(filePath);
+ fs.unlinkSync(filePath);
+
+ const uploaded = await createDatabaseVideo(videoId, videoUrl);
+ if (uploaded !== 'success') {
+ ws.send(`DATA - Error while uploading - ${JSON.stringify(uploaded)}`);
+ return false;
+ }
+
+ if (!isChannel) ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`);
+ return true;
+ } catch (error: any) {
+ ws.send(`ERROR - Upload failed for ${videoId}: ${error.message}`);
+ fs.unlinkSync(filePath);
+ return false;
+ }
+};
+
+
+app.ws('/save', {
+ query: t.Object({
+ url: t.String()
+ }),
+ body: t.String(),
+ open: async (ws) => {
+ console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
+
+ const videoId = validateVideo(ws.data.query.url)
+ if (!videoId) return sendError(ws, 'Invalid video URL.');
+ if (await redis.get(videoId)) return sendError(ws, 'Someone is already downloading this video...');
+ if (await redis.get(`blacklist:${videoId}`)) return sendError(ws, 'This video is blacklisted.');
+
+ const already = await db.selectFrom('videos')
+ .select('id')
+ .where('id', '=', videoId)
+ .executeTakeFirst()
+
+ if (already) {
+ ws.send(`DONE - ${process.env.FRONTEND}/watch?v=${videoId}`)
+ ws.close()
+ } else {
+ ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
+ ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
+ videoIds[ws.id] = videoId
+ }
+ },
+ message: async (ws, message) => {
+ if (message == 'alive') return
+
+ const videoId = videoIds[ws.id];
+ if (!videoId) return sendError(ws, 'No video ID associated with this session.');
+
+ if (await redis.get(videoId) !== 'downloading') {
+ await redis.set(videoId, 'downloading', 'EX', 300)
+
+ if (!(await checkCaptcha(message))) {
+ await cleanup(ws, videoId);
+ return sendError(ws, 'Captcha validation failed.');
+ }
+
+ const downloadResult = await downloadVideo(ws, videoId);
+ if (downloadResult.fail) {
+ await cleanup(ws, videoId);
+ return sendError(ws, downloadResult.message);
+ }
+
+ const uploadSuccess = await handleUpload(ws, videoId);
+ if (!uploadSuccess) await redis.del(videoId);
+
+ await cleanup(ws, videoId);
+ ws.close();
+ } else {
+ ws.send('DATA - Captcha already submitted.');
+ }
+ },
+ close: async (ws) => {
+ await cleanup(ws, videoIds[ws.id]);
+ console.log(`closed - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
+ }
+})
+
+app.ws('/savechannel', {
+ query: t.Object({
+ url: t.String()
+ }),
+ body: t.String(),
+ open: async (ws) => {
+ console.log(`${ws.id} - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
+
+ const channelId = await validateChannel(ws.data.query.url);
+ if (!channelId) return sendError(ws, 'Invalid channel URL.');
+
+ ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
+ ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')
+ videoIds[ws.id] = `captcha-${channelId}`;
+ },
+ message: async (ws, message) => {
+ if (message == 'alive') return
+
+ const status = videoIds[ws.id];
+ if (!status || !status.startsWith('captcha-')) return sendError(ws, 'No channel associated with this session.');
+
+ const channelId = status.replace('captcha-', '');
+ if (!(await checkCaptcha(message))) {
+ await cleanup(ws, channelId);
+ return sendError(ws, 'Captcha validation failed.');
+ }
+
+ videoIds[ws.id] = `downloading-${channelId}`;
+ const videos = await getChannelVideos(channelId);
+
+ for (const video of videos.slice(0, 5)) {
+ if (!video || (await redis.get(video.id)) || (await redis.get(`blacklist:${video.id}`))) continue;
+
+ const already = await db.selectFrom('videos')
+ .select('id')
+ .where('id', '=', video.id)
+ .executeTakeFirst()
+ if (already) continue
+
+ ws.send(`DATA - Processing video: ${video.title.text}`);
+ await redis.set(video.id, 'downloading', 'EX', 300);
+
+ const downloadResult = await downloadVideo(ws, video.id);
+ if (!downloadResult.fail) await handleUpload(ws, video.id, true);
+
+ await redis.del(video.id);
+ ws.send(`DATA - Created video page for ${video.title.text}`)
+ }
+
+ await cleanup(ws, channelId);
+ ws.send(`DONE - ${process.env.FRONTEND}/channel/${channelId}`)
+ ws.close();
+ },
+ close: async (ws) => {
+ await cleanup(ws, videoIds[ws.id]);
+ console.log(`closed - ${ws.data.path} - ${JSON.stringify(ws.data.query)}`)
+ }
+})
+
+export default app
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..580ae0a
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,45 @@
+import type {
+ Generated,
+ Insertable,
+ Selectable,
+ Updateable,
+} from 'kysely'
+
+export interface Database {
+ videos: VideosTable
+ reports: ReportsTable
+}
+
+export interface VideosTable {
+ uuid: Generated
+ id: string
+ title: string
+ description: string
+ thumbnail: string
+ source: string
+ published: string
+ archived: string
+ channel: string
+ channelId: string
+ channelVerified: boolean
+ channelAvatar: string
+ playlist?: string | null
+ disabled: boolean
+ hasBeenReported: boolean
+}
+
+export type Video = Selectable
+export type NewVideo = Insertable
+export type UpdateVideo = Updateable
+
+export interface ReportsTable {
+ uuid: Generated
+ target: string
+ title: string
+ details: string
+ date: Date
+}
+
+export type Report = Selectable
+export type NewReport = Insertable
+export type UpdateReport = Updateable
\ No newline at end of file
diff --git a/src/utils/common.ts b/src/utils/common.ts
new file mode 100644
index 0000000..5a0a963
--- /dev/null
+++ b/src/utils/common.ts
@@ -0,0 +1,86 @@
+import { getVideo, getChannel } from "@/utils/metadata";
+import { uploadImage } from "@/utils/upload";
+import { db } from '@/utils/database'
+import crypto from 'node:crypto';
+
+function convertRelativeToDate(relativeTime: string) {
+ const parts = relativeTime.split(' ');
+ const amount = parseInt(parts[0]);
+ const unit = parts[1];
+
+ const currentDate = new Date();
+
+ switch (unit) {
+ case 'hour':
+ case 'hours':
+ currentDate.setHours(currentDate.getHours() - amount);
+ break;
+ case 'minute':
+ case 'minutes':
+ currentDate.setMinutes(currentDate.getMinutes() - amount);
+ break;
+ case 'day':
+ case 'days':
+ currentDate.setDate(currentDate.getDate() - amount);
+ break;
+ case 'month':
+ case 'months':
+ currentDate.setMonth(currentDate.getMonth() - amount);
+ break;
+ case 'year':
+ case 'years':
+ currentDate.setFullYear(currentDate.getFullYear() - amount);
+ break;
+ }
+
+ return currentDate;
+}
+
+async function checkCaptcha(response: string) {
+ const confirm = await (await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
+ method: 'POST',
+ body: JSON.stringify({
+ 'response': response,
+ 'secret': process.env.CAPTCHA_SECRET
+ }),
+ headers: {
+ 'content-type': 'application/json'
+ }
+ })).json()
+
+ return confirm.success
+}
+
+async function createDatabaseVideo(id: string, videoUrl: string) {
+ const data = await getVideo(id)
+ const channelData = await getChannel(data.basic_info.channel_id)
+
+ if (data.error) return data
+ if (channelData.error) return channelData
+
+ const uploaderAvatar = await uploadImage(data.basic_info.channel_id, channelData.metadata.thumbnail[0].url)
+ const thumbnailUrl = await uploadImage(id, data.basic_info.thumbnail[0].url)
+
+ await db.insertInto('videos')
+ .values({
+ uuid: crypto.randomUUID(),
+ id: id,
+ title: data.basic_info.title,
+ description: (data.basic_info.short_description).replaceAll('\n', '
'),
+ thumbnail: thumbnailUrl,
+ source: videoUrl,
+ published: (data.primary_info.published.text.endsWith('ago') ? convertRelativeToDate(data.primary_info.published.text) : new Date(data.primary_info.published.text)).toISOString().slice(0, 10),
+ archived: (new Date()).toISOString().slice(0, 10),
+ channel: channelData.metadata.title,
+ channelId: channelData.metadata.external_id,
+ channelVerified: channelData.header.author?.is_verified || false,
+ channelAvatar: uploaderAvatar,
+ disabled: false,
+ hasBeenReported: false
+ })
+ .execute()
+
+ return 'success'
+}
+
+export { convertRelativeToDate, checkCaptcha, createDatabaseVideo }
\ No newline at end of file
diff --git a/src/utils/database.ts b/src/utils/database.ts
new file mode 100644
index 0000000..30de953
--- /dev/null
+++ b/src/utils/database.ts
@@ -0,0 +1,14 @@
+import type { Database } from '@/types.ts'
+import { Pool } from 'pg'
+import { Kysely, PostgresDialect } from 'kysely'
+
+const dialect = new PostgresDialect({
+ pool: new Pool({
+ connectionString: process.env.DATABASE_URL,
+ max: 10,
+ })
+})
+
+export const db = new Kysely({
+ dialect,
+})
\ No newline at end of file
diff --git a/src/utils/download.ts b/src/utils/download.ts
new file mode 100644
index 0000000..b04fe4d
--- /dev/null
+++ b/src/utils/download.ts
@@ -0,0 +1,62 @@
+import WebSocket from 'ws';
+import { getVideo } from '@/utils/metadata';
+
+async function downloadVideo(ws: any, id: string): Promise<{ fail: boolean, message: string }> {
+ return new Promise(async (resolve, reject) => {
+ let quality = '480p'
+ const video = await getVideo(id)
+ if (video.error) {
+ return resolve({
+ message: `Failed to request Youtube with error ${video.error}. Please retry...`,
+ fail: true
+ })
+ }
+ if (video.basic_info.duration >= 900) quality = '360p' // 15 minutes
+
+ quality = await getVideoQuality(video, quality)
+
+ let isDownloading = true
+ const downloader = new WebSocket(`ws://${(process.env.METADATA!).replace('http://', '')}/download/${id}/${quality}`)
+
+ downloader.on('message', async function message(data: any) {
+ const text = data.toString()
+ if (text == 'done') {
+ isDownloading = false
+ return resolve({
+ fail: false,
+ message: ''
+ })
+ } else {
+ ws.send(`DATA - ${text}`)
+ }
+ })
+
+ downloader.on('close', function close() {
+ if (!isDownloading) return
+
+ return resolve({
+ fail: true,
+ message: 'The metadata server unexpectedly closed the websocket. Please try again.'
+ })
+ })
+ })
+}
+
+async function getVideoQuality(json: any, quality: string) {
+ const adaptiveFormats = json['streaming_data']['adaptive_formats'];
+ let video = adaptiveFormats.find((f: any) => f.quality_label === quality && !f.has_audio);
+
+ // If the specified quality isn't available, find the lowest quality video
+ if (!video) { // @ts-ignore
+ video = adaptiveFormats.filter((f: any) => !f.has_audio).reduce((prev, current) => {
+ if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) {
+ return current;
+ }
+ return prev;
+ }, null);
+ }
+
+ return video ? video.quality_label : null;
+}
+
+export { downloadVideo }
\ No newline at end of file
diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts
new file mode 100644
index 0000000..45c7e2c
--- /dev/null
+++ b/src/utils/metadata.ts
@@ -0,0 +1,15 @@
+// metadata either returns innertube or { error: string }
+
+async function getVideo(id: string) {
+ return await (await fetch(`${process.env.METADATA}/video/${id}`)).json()
+}
+
+async function getChannel(id: string) {
+ return await (await fetch(`${process.env.METADATA}/channel/${id}`)).json()
+}
+
+async function getChannelVideos(id: string) {
+ return await (await fetch(`${process.env.METADATA}/videos/${id}`)).json()
+}
+
+export { getVideo, getChannel, getChannelVideos }
\ No newline at end of file
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
new file mode 100644
index 0000000..4c78b9f
--- /dev/null
+++ b/src/utils/regex.ts
@@ -0,0 +1,92 @@
+function validateVideo(input: string): string | false {
+ try {
+ const url = new URL(input);
+ const hostnames = [
+ 'youtube.com',
+ 'www.youtube.com',
+ 'm.youtube.com'
+ ];
+
+ // basic hostname check
+ if (hostnames.includes(url.hostname)) {
+ // basic url
+ if (url.pathname === '/watch') {
+ const videoId = url.searchParams.get('v');
+ return videoId || false;
+ }
+
+ // embed url
+ const embedMatch = url.pathname.match(/^\/embed\/([a-zA-Z0-9_-]+)/);
+ if (embedMatch) {
+ return embedMatch[1];
+ }
+
+ return false;
+ }
+
+ // short urls
+ if (url.hostname === 'youtu.be') {
+ const videoId = url.pathname.replace(/^\//, '');
+ return videoId || false;
+ }
+
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+function validatePlaylist(input: string): string | false {
+ try {
+ const url = new URL(input);
+ const hostnames = [
+ 'youtube.com',
+ 'www.youtube.com',
+ 'm.youtube.com'
+ ];
+
+ if (hostnames.includes(url.hostname)) {
+ // all urls are the same, thank god
+ if (url.pathname === '/playlist' || url.pathname === '/watch') {
+ const playlistId = url.searchParams.get('list');
+ return playlistId || false;
+ }
+ }
+
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+async function validateChannel(input: string): Promise {
+ try {
+ const url = new URL(input);
+ const hostnames = [
+ 'youtube.com',
+ 'www.youtube.com',
+ 'm.youtube.com'
+ ];
+
+ if (hostnames.includes(url.hostname)) {
+ // @ urls
+ const atMatch = url.pathname.match(/^\/@([a-zA-Z0-9.-]+)/);
+ if (atMatch) {
+ const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${atMatch[1]}`)).json()
+ return channelId['items'][0]['id']
+ }
+
+ // /channel/ and /c/
+ const channelMatch = url.pathname.match(/^\/(channel|c)\/([a-zA-Z0-9_-]+)/);
+ if (channelMatch) {
+ return channelMatch[2];
+ }
+ }
+
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+export { validateVideo, validatePlaylist, validateChannel }
\ No newline at end of file
diff --git a/src/utils/sitemap.ts b/src/utils/sitemap.ts
new file mode 100644
index 0000000..16851e8
--- /dev/null
+++ b/src/utils/sitemap.ts
@@ -0,0 +1,32 @@
+import { format } from 'date-fns';
+
+function createSitemapXML(urls: string[]) {
+ const xml = urls.map(url => `
+
+ ${encodeURI(url)}
+ never
+ 0.7
+
+ `).join('');
+
+ return `
+
+ ${xml}
+`;
+}
+
+function createSitemapIndexXML(sitemaps: string[]) {
+ const xml = sitemaps.map(sitemap => `
+
+ ${encodeURI(sitemap)}
+ ${format(new Date(), 'yyyy-MM-dd')}
+
+ `).join('');
+
+ return `
+
+ ${xml}
+`;
+}
+
+export { createSitemapXML, createSitemapIndexXML }
\ No newline at end of file
diff --git a/src/utils/upload.ts b/src/utils/upload.ts
new file mode 100644
index 0000000..5f27395
--- /dev/null
+++ b/src/utils/upload.ts
@@ -0,0 +1,38 @@
+import { S3 } from 'ultralight-s3';
+import * as fs from 'node:fs'
+
+const keys = JSON.parse(fs.readFileSync('s3.json', 'utf-8'))
+const videos3 = new S3({
+ endpoint: keys.endpoint,
+ accessKeyId: keys.videos[0].access,
+ secretAccessKey: keys.videos[0].secret,
+ bucketName: keys.videos[0].bucket,
+ region: 'auto'
+})
+
+const images3 = new S3({
+ endpoint: keys.endpoint,
+ accessKeyId: keys.images[0].access,
+ secretAccessKey: keys.images[0].secret,
+ bucketName: keys.images[0].bucket,
+ region: 'auto'
+});
+
+async function uploadVideo(video: string) {
+ const videoFile = fs.readFileSync(video)
+ const uploaded = await videos3.put(video.split('/')[2], videoFile)
+ return uploaded.url.replace(keys.endpoint, 'https://s2.archive.party')
+}
+
+async function uploadImage(id: string, url: string) {
+ const exists = await images3.fileExists(`${id}.webp`)
+ if (exists) return `${keys.images[0].url}${id}.webp`
+
+ const response = await fetch(url)
+ const buffer = Buffer.from(await response.arrayBuffer())
+
+ const uploaded = await images3.put(`${id}.webp`, buffer)
+ return uploaded.url.replace(keys.endpoint, 'https://s2.archive.party')
+}
+
+export { uploadVideo, uploadImage }
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2c4f033
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/utils/captcha.js b/utils/captcha.js
deleted file mode 100644
index f5c4498..0000000
--- a/utils/captcha.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const fetch = require('node-fetch')
-
-async function checkCaptcha(response) {
- const confirm = await (await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
- method: 'POST',
- body: JSON.stringify({
- 'response': response,
- 'secret': process.env.CAPTCHA_SECRET
- }),
- headers: {
- 'content-type': 'application/json'
- }
- })).json()
-
- return confirm.success
-}
-
-module.exports = { checkCaptcha }
\ No newline at end of file
diff --git a/utils/logger.js b/utils/logger.js
deleted file mode 100644
index d1e5eb8..0000000
--- a/utils/logger.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const winston = require('winston')
-const { Logtail } = require("@logtail/node")
-const { LogtailTransport } = require("@logtail/winston")
-
-const logtail = new Logtail(process.env.LOGTAIL)
-
-const logger = winston.createLogger({
- format: winston.format.json(),
- transports: [
- new winston.transports.Console({
- format:winston.format.combine(
- winston.format.timestamp({format: 'MMM-DD-YYYY HH:mm:ss'}),
- winston.format.printf(info => `${[info.timestamp]}: ${info.message}`),
- )}),
- new winston.transports.File({
- filename: 'logs/client.log',
- format:winston.format.combine(
- winston.format.timestamp({format: 'MMM-DD-YYYY HH:mm:ss'}),
- winston.format.printf(info => `${[info.timestamp]}: ${info.message}`),
- )}),
- new LogtailTransport(logtail, {
- level: 'error'
- })
- ],
-});
-
-module.exports = logger
\ No newline at end of file
diff --git a/utils/metadata.js b/utils/metadata.js
deleted file mode 100644
index cccd6ab..0000000
--- a/utils/metadata.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const fetch = require('node-fetch')
-
-async function getPipedInstance() {
- const instances = await (await fetch('https://piped-instances.kavin.rocks/', {
- headers: {
- 'User-Agent': 'Mozilla/5.0 (compatible; PreserveTube/0.0; +https://preservetube.com)'
- }
- })).json()
- return (instances[Math.floor(Math.random() * instances.length)]).api_url
-}
-
-async function getVideoMetadata(id) {
- return await (await fetch(`${process.env.METADATA}/video/${id}`)).json()
-}
-
-async function getChannel(id) {
- return await (await fetch(`${process.env.METADATA}/channel/${id}`)).json()
-}
-
-async function getChannelVideos(id) {
- return await (await fetch(`${process.env.METADATA}/videos/${id}`)).json()
-}
-
-async function getPlaylistVideos(id) {
- const instance = await getPipedInstance()
- const json = await (await fetch(`${instance}/playlists/${id}`, {
- headers: {
- 'User-Agent': 'Mozilla/5.0 (compatible; PreserveTube/0.0; +https://preservetube.com)'
- }
- })).json()
- return json
-}
-
-async function getVideoDownload(json, quality) {
- const adaptiveFormats = json['streaming_data']['adaptive_formats'];
- let video = adaptiveFormats.find(f => f.quality_label === `${quality}p` && !f.has_audio);
- if (!video) { // stupid bullshit. basically finding the lowest quality if the quality specified isnt there
- video = adaptiveFormats.filter(f => !f.has_audio).reduce((prev, current) => {
- if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) {
- return current;
- }
- return prev;
- }, null);
- }
-
- const audio = adaptiveFormats.find(f => f.has_audio);
-
- return {
- url: [
- video.url,
- audio.url
- ]
- };
-}
-
-
-module.exports = { getVideoMetadata, getChannel, getChannelVideos, getPlaylistVideos, getVideoDownload }
\ No newline at end of file
diff --git a/utils/redis.js b/utils/redis.js
deleted file mode 100644
index 14694fe..0000000
--- a/utils/redis.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const Redis = require('ioredis')
-const fs = require('node:fs')
-
-const logger = require("./logger.js")
-
-const redis = new Redis({
- host: process.env.REDIS_HOST,
- port: process.env.REDIS_PORT,
- password: process.env.REDIS_PASS,
-});
-
-redis.on('ready', async function () {
- logger.info({ message: 'Connected to redis!' })
-
- const keys = await redis.keys('*')
- const filteredKeys = keys.filter(key => !key.startsWith('blacklist:'))
- if (filteredKeys.length) await redis.del(filteredKeys)
-
- setInterval(async () => {
- const files = fs.readdirSync('videos')
- const webmFiles = files.filter((file) => file.endsWith('.mp4'))
- webmFiles.forEach(async (f) => {
- const videoId = f.replace('.mp4', '')
- const isActive = await redis.get(videoId)
- if (!isActive) {
- fs.unlinkSync(`./videos/${f}`)
- logger.info({ message: `deleted file ${f} because there is no active download of it` })
- }
- })
- }, 5*60000)
-})
-
-module.exports = redis
\ No newline at end of file
diff --git a/utils/upload.js b/utils/upload.js
deleted file mode 100644
index a2fa76d..0000000
--- a/utils/upload.js
+++ /dev/null
@@ -1,68 +0,0 @@
-const AWS = require('aws-sdk')
-const fs = require('node:fs')
-
-const keys = require('../s3.json')
-
-async function uploadVideo(video) {
- const key = keys.videos[0]
-
- const s3 = new AWS.S3({
- accessKeyId: key.access,
- secretAccessKey: key.secret,
- endpoint: keys.endpoint,
- s3ForcePathStyle: true
- })
-
- const videoFile = fs.createReadStream(video)
- const uploaded = await s3.upload({
- Bucket: key.bucket,
- Key: video.split('/')[2],
- Body: videoFile,
- ContentType: 'video/mp4',
- }).promise()
-
- return (uploaded.Location).replace(keys.endpoint, 'https://s2.archive.party')
-}
-
-async function uploadImage(id, url) {
- const key = keys.images[0]
-
- const s3 = new AWS.S3({
- accessKeyId: key.access,
- secretAccessKey: key.secret,
- endpoint: keys.endpoint,
- s3ForcePathStyle: true
- })
-
- const exists = await checkIfFileExists({
- Bucket: key.bucket,
- Key: `${id}.webp`
- }, s3)
-
- if (exists) {
- return `${key.url}${id}.webp`
- } else {
- const response = await fetch(url)
- const buffer = Buffer.from(await response.arrayBuffer())
-
- const uploaded = await s3.upload({
- Bucket: key.bucket,
- Key: `${id}.webp`,
- Body: buffer,
- ContentType: 'video/webp',
- }).promise()
-
- return (uploaded.Location).replace(keys.endpoint, 'https://s2.archive.party')
- }
-}
-
-async function checkIfFileExists(params, s3) {
- try {
- await s3.headObject(params).promise()
- return true
- } catch (err) {
- return false
- }
-}
-
-module.exports = { uploadVideo, uploadImage }
\ No newline at end of file
diff --git a/utils/validate.js b/utils/validate.js
deleted file mode 100644
index f8edd8e..0000000
--- a/utils/validate.js
+++ /dev/null
@@ -1,53 +0,0 @@
-const fetch = require('node-fetch')
-
-async function validateVideoInput(input) {
- if (!input) return {
- fail: true,
- message: 'Missing URL'
- }
-
- const id = (input.trim()).match(/^https:\/\/(?:(?:www\.)?youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)([A-Za-z0-9_-]{11})$/m)?.[1]
- if (!id) return {
- fail: true,
- message: 'Whoops! What is that? That is not a Youtube url.'
- }
-
- return id
-}
-
-async function validatePlaylistInput(input) {
- if (!input) return {
- fail: true,
- message: 'Missing URL'
- }
-
- const id = (input.trim()).match(/^(?:https?:\/\/)?(?:www\.)?youtu(?:(?:\.be)|(?:be\.com))\/playlist\?list=([\w_-]{34})$/m)?.[1]
- if (!id) return {
- fail: true,
- message: 'Whoops! What is that? That is not a Youtube Playlist.'
- }
-
- return id
-}
-
-async function validateChannelInput(input) {
- if (!input) return {
- fail: true,
- message: 'Missing URL'
- }
-
- const id = input.match(/^(?:https?:\/\/)?(?:www\.)?youtu(?:(?:\.be)|(?:be\.com))\/(?:channel\/|@)([\w-]+)/m)?.[1]
- if (!id) return {
- fail: true,
- message: 'Whoops! What is that? That is not a Youtube Channel.'
- }
-
- if (input.includes('@')) {
- const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${id}`)).json()
- return channelId['items'][0]['id']
- } else {
- return id
- }
-}
-
-module.exports = { validateVideoInput, validatePlaylistInput, validateChannelInput }
\ No newline at end of file
diff --git a/utils/websocket.js b/utils/websocket.js
deleted file mode 100644
index 81a4728..0000000
--- a/utils/websocket.js
+++ /dev/null
@@ -1,62 +0,0 @@
-const { PrismaClient } = require('@prisma/client')
-const prisma = new PrismaClient()
-
-const metadata = require('./metadata.js')
-const upload = require('./upload.js')
-
-function convertRelativeToDate(relativeTime) {
- const parts = relativeTime.split(' ');
- const amount = parseInt(parts[0]);
- const unit = parts[1];
-
- const currentDate = new Date();
-
- switch (unit) {
- case 'hour':
- case 'hours':
- currentDate.setHours(currentDate.getHours() - amount);
- break;
- case 'minute':
- case 'minutes':
- currentDate.setMinutes(currentDate.getMinutes() - amount);
- break;
- case 'day':
- case 'days':
- currentDate.setDate(currentDate.getDate() - amount);
- break;
- }
-
- return currentDate;
-}
-
-async function createDatabaseVideo(id, videoUrl, playlistId) {
- const data = await metadata.getVideoMetadata(id)
- const channelData = await metadata.getChannel(data.basic_info.channel_id)
-
- if (data.error) return data
- if (channelData.error) return channelData
-
- const uploaderAvatar = await upload.uploadImage(data.basic_info.channel_id, channelData.metadata.thumbnail[0].url)
- const thumbnailUrl = await upload.uploadImage(id, data.basic_info.thumbnail[0].url)
-
- await prisma.videos.create({
- data: {
- id: id,
- title: data.basic_info.title,
- description: (data.basic_info.short_description).replaceAll('\n', '
'),
- thumbnail: thumbnailUrl,
- source: videoUrl,
- published: (data.primary_info.published.text.endsWith('ago') ? convertRelativeToDate(data.primary_info.published.text) : new Date(data.primary_info.published.text)).toISOString().slice(0, 10),
- archived: (new Date()).toISOString().slice(0, 10),
- channel: channelData.metadata.title,
- channelId: channelData.metadata.external_id,
- channelAvatar: uploaderAvatar,
- channelVerified: channelData.header.author?.is_verified || false,
- playlist: playlistId
- }
- })
-
- return 'success'
-}
-
-module.exports = { createDatabaseVideo }
\ No newline at end of file
diff --git a/utils/ytdlp.js b/utils/ytdlp.js
deleted file mode 100644
index a8c6a56..0000000
--- a/utils/ytdlp.js
+++ /dev/null
@@ -1,60 +0,0 @@
-const WebSocket = require('ws')
-const metadata = require('./metadata.js')
-
-async function downloadVideo(url, ws, id) {
- return new Promise(async (resolve, reject) => {
- let quality = '480p'
- const video = await metadata.getVideoMetadata(id)
- if (video.error) {
- return resolve({
- message: `Failed to request Youtube with error ${video.error}. Please retry...`,
- fail: true
- })
- }
- if (video.basic_info.duration >= 900) quality = '360p' // 15 minutes
-
- quality = await getVideoQuality(video, quality)
-
- let isDownloading = true
- const downloader = new WebSocket(`ws://${process.env.METADATA.replace('http://', '')}/download/${id}/${quality}`)
- downloader.on('message', async function message(data) {
- const text = data.toString()
- if (text == 'done') {
- isDownloading = false
- return resolve({
- fail: false
- })
- } else {
- ws.send(`DATA - ${text}`)
- }
- })
-
- downloader.on('close', function close(code, reason) {
- if (!isDownloading) return
-
- return resolve({
- fail: true,
- message: 'The metadata server unexpectedly closed the websocket. Please try again.'
- })
- })
- })
-}
-
-async function getVideoQuality(json, quality) {
- const adaptiveFormats = json['streaming_data']['adaptive_formats'];
- let video = adaptiveFormats.find(f => f.quality_label === quality && !f.has_audio);
-
- // If the specified quality isn't available, find the lowest quality video
- if (!video) {
- video = adaptiveFormats.filter(f => !f.has_audio).reduce((prev, current) => {
- if (!prev || parseInt(current.quality_label) < parseInt(prev.quality_label)) {
- return current;
- }
- return prev;
- }, null);
- }
-
- return video ? video.quality_label : null;
-}
-
-module.exports = { downloadVideo }
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
deleted file mode 100644
index c4f08e3..0000000
--- a/yarn.lock
+++ /dev/null
@@ -1,1392 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@colors/colors@1.5.0":
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
- integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
-
-"@dabh/diagnostics@^2.0.2":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
- integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
- dependencies:
- colorspace "1.1.x"
- enabled "2.0.x"
- kuler "^2.0.0"
-
-"@ioredis/commands@^1.1.1":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
- integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
-
-"@logtail/core@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@logtail/core/-/core-0.4.0.tgz#a8e510756bf9fd1b2cc6c901225eb03685919ca8"
- integrity sha512-xItEU5vY2LvcJyAVkxKKZ3UvG3CsK2pMvhuLDntlC+US1LlNcfQmVzvInN5j5iXl0kjNO6n+0EY2YyhhOgjWCQ==
- dependencies:
- "@logtail/tools" "^0.4.0"
- "@logtail/types" "^0.4.0"
-
-"@logtail/node@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@logtail/node/-/node-0.4.0.tgz#71488911a6dc4bf218871af80e3788cd727f8474"
- integrity sha512-0/1to4HLf95FrDlZoFLz3A0Fz/B55G82syY22Yl9N4mjC2wW6t5Lo9d4q/u/VmHBHW3PBbytbmFEoHx0H5Sarg==
- dependencies:
- "@logtail/core" "^0.4.0"
- "@logtail/types" "^0.4.0"
- "@msgpack/msgpack" "^2.5.1"
- "@types/stack-trace" "^0.0.29"
- cross-fetch "^3.0.4"
- minimatch "^3.0.4"
- stack-trace "^0.0.10"
-
-"@logtail/tools@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@logtail/tools/-/tools-0.4.0.tgz#ff1bed193a005b12007df510f6ce294b8f518207"
- integrity sha512-7lSKdJTq7NMUvriMhkf6cTxY9QlXi+YLLuHxbSOKI56HpJN8qJGPVcmOdzlbk8pJofe9RnME8hBMLTHAD3bmsw==
- dependencies:
- "@logtail/types" "^0.4.0"
-
-"@logtail/types@^0.4.0":
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/@logtail/types/-/types-0.4.0.tgz#39c28e08f805657c70107071dc166866153563a8"
- integrity sha512-2CR3w7Xf5rKWbUlgiaBKeVrqWuWnVBz0Ymycw/gYwupVI0fb6Ameid9fHfeM5LI/gbIO3ZTIMADlA0FvFbuXMQ==
- dependencies:
- js "^0.1.0"
-
-"@logtail/winston@^0.4.1":
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/@logtail/winston/-/winston-0.4.1.tgz#bbdb60db0ae1e013a1e9678ba25160eb2554fecf"
- integrity sha512-YsMXzhcpGHDd3ijyqnDD+2qraJAiar/mVjR7HP8NYQsQMk9dQTIHbQzXkBUwAfVOsKSzNbfzeWTnft8sQBYxYQ==
- dependencies:
- "@logtail/node" "^0.4.0"
- "@logtail/types" "^0.4.0"
- winston-transport "^4.3.0"
-
-"@msgpack/msgpack@^2.5.1":
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.8.0.tgz#4210deb771ee3912964f14a15ddfb5ff877e70b9"
- integrity sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==
-
-"@prisma/client@4.9.0":
- version "4.9.0"
- resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.9.0.tgz#4a4068f3540732ea5723c008d49ed684d20f9340"
- integrity sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==
- dependencies:
- "@prisma/engines-version" "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5"
-
-"@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5":
- version "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5"
- resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz#9d817a5779fc05b107eb02f63d197ad296d60b3c"
- integrity sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==
-
-"@prisma/engines@4.9.0":
- version "4.9.0"
- resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.9.0.tgz#05a1411964e047c1bc43f777c7a1c69f86a2a26c"
- integrity sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==
-
-"@tootallnate/once@2":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
- integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
-
-"@types/dompurify@^2.4.0":
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9"
- integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==
- dependencies:
- "@types/trusted-types" "*"
-
-"@types/stack-trace@^0.0.29":
- version "0.0.29"
- resolved "https://registry.yarnpkg.com/@types/stack-trace/-/stack-trace-0.0.29.tgz#eb7a7c60098edb35630ed900742a5ecb20cfcb4d"
- integrity sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==
-
-"@types/triple-beam@^1.3.2":
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"
- integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==
-
-"@types/trusted-types@*":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
- integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
-
-abab@^2.0.6:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
- integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
-
-accepts@~1.3.8:
- version "1.3.8"
- resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
- integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
- dependencies:
- mime-types "~2.1.34"
- negotiator "0.6.3"
-
-acorn-globals@^7.0.0:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3"
- integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==
- dependencies:
- acorn "^8.1.0"
- acorn-walk "^8.0.2"
-
-acorn-walk@^8.0.2:
- version "8.2.0"
- resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
- integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
-
-acorn@^8.1.0, acorn@^8.8.1:
- version "8.8.2"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
- integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
-
-agent-base@6:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
- integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
- dependencies:
- debug "4"
-
-array-flatten@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
- integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
-
-async@^3.2.3:
- version "3.2.4"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
- integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
-
-asynckit@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
- integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
-
-aws-sdk@2.1128.0:
- version "2.1128.0"
- resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1128.0.tgz#1d8762d8124a3ca1d5ee01c3da063fa6cf72381e"
- integrity sha512-A3YjMQHDtV4mXf35m8zlM+nsVq5KfyKeiXrEMG7Bdlucuxl7aPdKEmYrPQLw9ZyhYY/x4o08pPuTr/stJaVGeg==
- dependencies:
- buffer "4.9.2"
- events "1.1.1"
- ieee754 "1.1.13"
- jmespath "0.16.0"
- querystring "0.2.0"
- sax "1.2.1"
- url "0.10.3"
- uuid "3.3.2"
- xml2js "0.4.19"
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.0.2:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-body-parser@1.20.1:
- version "1.20.1"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
- integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
- dependencies:
- bytes "3.1.2"
- content-type "~1.0.4"
- debug "2.6.9"
- depd "2.0.0"
- destroy "1.2.0"
- http-errors "2.0.0"
- iconv-lite "0.4.24"
- on-finished "2.4.1"
- qs "6.11.0"
- raw-body "2.5.1"
- type-is "~1.6.18"
- unpipe "1.0.0"
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-buffer@4.9.2:
- version "4.9.2"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
- integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
- dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
- isarray "^1.0.0"
-
-bytes@3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
- integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
-
-call-bind@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
- dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
-
-cluster-key-slot@^1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
- integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
-
-color-convert@^1.9.3:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
- integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
- dependencies:
- color-name "1.1.3"
-
-color-name@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
- integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
-color-name@^1.0.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-color-string@^1.6.0:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
- integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
- dependencies:
- color-name "^1.0.0"
- simple-swizzle "^0.2.2"
-
-color@^3.1.3:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
- integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
- dependencies:
- color-convert "^1.9.3"
- color-string "^1.6.0"
-
-colorspace@1.1.x:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
- integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
- dependencies:
- color "^3.1.3"
- text-hex "1.0.x"
-
-combined-stream@^1.0.8:
- version "1.0.8"
- resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
- integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
- dependencies:
- delayed-stream "~1.0.0"
-
-commander@~1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/commander/-/commander-1.1.1.tgz#50d1651868ae60eccff0a2d9f34595376bc6b041"
- integrity sha512-71Rod2AhcH3JhkBikVpNd0pA+fWsmAaVoti6OR38T76chA7vE3pSerS0Jor4wDw+tOueD2zLVvFOw5H0Rcj7rA==
- dependencies:
- keypress "0.1.x"
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-content-disposition@0.5.4:
- version "0.5.4"
- resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
- integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
- dependencies:
- safe-buffer "5.2.1"
-
-content-type@~1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
- integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-
-cookie-signature@1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
- integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-
-cookie@0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
- integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
-
-cors@^2.8.5:
- version "2.8.5"
- resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
- integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
- dependencies:
- object-assign "^4"
- vary "^1"
-
-cross-fetch@^3.0.4:
- version "3.1.8"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
- integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
- dependencies:
- node-fetch "^2.6.12"
-
-cssom@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
- integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
-
-cssom@~0.3.6:
- version "0.3.8"
- resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
- integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
-
-cssstyle@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
- integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
- dependencies:
- cssom "~0.3.6"
-
-data-urls@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
- integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
- dependencies:
- abab "^2.0.6"
- whatwg-mimetype "^3.0.0"
- whatwg-url "^11.0.0"
-
-debug@2.6.9:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
- integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
- dependencies:
- ms "2.0.0"
-
-debug@4, debug@^4.3.4:
- version "4.3.4"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
- integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
- dependencies:
- ms "2.1.2"
-
-decimal.js@^10.4.2:
- version "10.4.3"
- resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
- integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
-
-deep-is@~0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
- integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
-
-delayed-stream@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
- integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
-
-denque@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
- integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
-
-depd@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
- integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-
-destroy@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
- integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
-
-domexception@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
- integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
- dependencies:
- webidl-conversions "^7.0.0"
-
-dompurify@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.0.tgz#6adc6f918376d93419ed1ee35811850680027cba"
- integrity sha512-0g/yr2IJn4nTbxwL785YxS7/AvvgGFJw6LLWP+BzWzB1+BYOqPUT9Hy0rXrZh5HLdHnxH72aDdzvC9SdTjsuaA==
-
-dotenv@^16.0.3:
- version "16.0.3"
- resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
- integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
-
-ee-first@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
- integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
-
-enabled@2.0.x:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
- integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
-
-encodeurl@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
- integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
-
-entities@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
- integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
-
-escape-html@~1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
- integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
-
-escodegen@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
- integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==
- dependencies:
- esprima "^4.0.1"
- estraverse "^5.2.0"
- esutils "^2.0.2"
- optionator "^0.8.1"
- optionalDependencies:
- source-map "~0.6.1"
-
-esprima@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
- integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-
-estraverse@^5.2.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
- integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
-
-esutils@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
- integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-
-etag@~1.8.1:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
- integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
-
-events@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
- integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==
-
-express-ws@^5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb"
- integrity sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==
- dependencies:
- ws "^7.4.6"
-
-express@^4.18.2:
- version "4.18.2"
- resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
- integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
- dependencies:
- accepts "~1.3.8"
- array-flatten "1.1.1"
- body-parser "1.20.1"
- content-disposition "0.5.4"
- content-type "~1.0.4"
- cookie "0.5.0"
- cookie-signature "1.0.6"
- debug "2.6.9"
- depd "2.0.0"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- etag "~1.8.1"
- finalhandler "1.2.0"
- fresh "0.5.2"
- http-errors "2.0.0"
- merge-descriptors "1.0.1"
- methods "~1.1.2"
- on-finished "2.4.1"
- parseurl "~1.3.3"
- path-to-regexp "0.1.7"
- proxy-addr "~2.0.7"
- qs "6.11.0"
- range-parser "~1.2.1"
- safe-buffer "5.2.1"
- send "0.18.0"
- serve-static "1.15.0"
- setprototypeof "1.2.0"
- statuses "2.0.1"
- type-is "~1.6.18"
- utils-merge "1.0.1"
- vary "~1.1.2"
-
-fast-levenshtein@~2.0.6:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
- integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
-
-fecha@^4.2.0:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
- integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
-
-finalhandler@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
- integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
- dependencies:
- debug "2.6.9"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- on-finished "2.4.1"
- parseurl "~1.3.3"
- statuses "2.0.1"
- unpipe "~1.0.0"
-
-fn.name@1.x.x:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
- integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
-
-form-data@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
- integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
- dependencies:
- asynckit "^0.4.0"
- combined-stream "^1.0.8"
- mime-types "^2.1.12"
-
-forwarded@0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
- integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
-
-fresh@0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
- integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-intrinsic@^1.0.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
- integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
- dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
- has-symbols "^1.0.3"
-
-has-symbols@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
- integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-html-encoding-sniffer@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
- integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
- dependencies:
- whatwg-encoding "^2.0.0"
-
-http-errors@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
- integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
- dependencies:
- depd "2.0.0"
- inherits "2.0.4"
- setprototypeof "1.2.0"
- statuses "2.0.1"
- toidentifier "1.0.1"
-
-http-proxy-agent@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
- integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
- dependencies:
- "@tootallnate/once" "2"
- agent-base "6"
- debug "4"
-
-https-proxy-agent@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
- integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
- dependencies:
- agent-base "6"
- debug "4"
-
-iconv-lite@0.4.24:
- version "0.4.24"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
- integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
- dependencies:
- safer-buffer ">= 2.1.2 < 3"
-
-iconv-lite@0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
- dependencies:
- safer-buffer ">= 2.1.2 < 3.0.0"
-
-ieee754@1.1.13:
- version "1.1.13"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
- integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
-
-ieee754@^1.1.4:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
- integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-inherits@2.0.4, inherits@^2.0.3:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-ioredis@^5.3.1:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.1.tgz#55d394a51258cee3af9e96c21c863b1a97bf951f"
- integrity sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg==
- dependencies:
- "@ioredis/commands" "^1.1.1"
- cluster-key-slot "^1.1.0"
- debug "^4.3.4"
- denque "^2.1.0"
- lodash.defaults "^4.2.0"
- lodash.isarguments "^3.1.0"
- redis-errors "^1.2.0"
- redis-parser "^3.0.0"
- standard-as-callback "^2.1.0"
-
-ipaddr.js@1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
- integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
-
-is-arrayish@^0.3.1:
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
- integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
-
-is-potential-custom-element-name@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
- integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
-
-is-stream@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
- integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
-
-isarray@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
- integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
-
-isomorphic-dompurify@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-1.0.0.tgz#33a55b8edac845fb16cf06dfff00541eb455e13e"
- integrity sha512-rxkJ2b2rwsgN/uvtaW+Z2JGfD9CYcixYMj0eTIa4V2hG47VDRMiehtUypi4lkpuwW41UrnOR65Dy0POiH3Lbjg==
- dependencies:
- "@types/dompurify" "^2.4.0"
- dompurify "^3.0.0"
- jsdom "^21.1.0"
-
-jmespath@0.16.0:
- version "0.16.0"
- resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
- integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
-
-js@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/js/-/js-0.1.0.tgz#e1d0afd55ea39c2b28da304e8143eaf2c133f366"
- integrity sha512-ZBbGYOpact8QAH9RprFWL4RAESYwbDodxiuDjOnzwzzk9pBzKycoifGuUrHHcDixE/eLMKPHRaXenTgu1qXBqA==
- dependencies:
- commander "~1.1.1"
-
-jsdom@^21.1.0:
- version "21.1.0"
- resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.0.tgz#d56ba4a84ed478260d83bd53dc181775f2d8e6ef"
- integrity sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==
- dependencies:
- abab "^2.0.6"
- acorn "^8.8.1"
- acorn-globals "^7.0.0"
- cssom "^0.5.0"
- cssstyle "^2.3.0"
- data-urls "^3.0.2"
- decimal.js "^10.4.2"
- domexception "^4.0.0"
- escodegen "^2.0.0"
- form-data "^4.0.0"
- html-encoding-sniffer "^3.0.0"
- http-proxy-agent "^5.0.0"
- https-proxy-agent "^5.0.1"
- is-potential-custom-element-name "^1.0.1"
- nwsapi "^2.2.2"
- parse5 "^7.1.1"
- saxes "^6.0.0"
- symbol-tree "^3.2.4"
- tough-cookie "^4.1.2"
- w3c-xmlserializer "^4.0.0"
- webidl-conversions "^7.0.0"
- whatwg-encoding "^2.0.0"
- whatwg-mimetype "^3.0.0"
- whatwg-url "^11.0.0"
- ws "^8.11.0"
- xml-name-validator "^4.0.0"
-
-keypress@0.1.x:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.1.0.tgz#4a3188d4291b66b4f65edb99f806aa9ae293592a"
- integrity sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA==
-
-kuler@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
- integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
-
-levn@~0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
- integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==
- dependencies:
- prelude-ls "~1.1.2"
- type-check "~0.3.2"
-
-lodash.defaults@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
- integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==
-
-lodash.isarguments@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
- integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==
-
-logform@^2.3.2, logform@^2.4.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b"
- integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==
- dependencies:
- "@colors/colors" "1.5.0"
- "@types/triple-beam" "^1.3.2"
- fecha "^4.2.0"
- ms "^2.1.1"
- safe-stable-stringify "^2.3.1"
- triple-beam "^1.3.0"
-
-media-typer@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
- integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
-
-merge-descriptors@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
- integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
-
-methods@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
- integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
-
-microtime@^3.0.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.1.1.tgz#f3ce12f4091c55fb2982c87205e2e7698afdcd0c"
- integrity sha512-to1r7o24cDsud9IhN6/8wGmMx5R2kT0w2Xwm5okbYI3d1dk6Xv0m+Z+jg2vS9pt+ocgQHTCtgs/YuyJhySzxNg==
- dependencies:
- node-addon-api "^5.0.0"
- node-gyp-build "^4.4.0"
-
-mime-db@1.52.0:
- version "1.52.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
- integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-
-mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
- version "2.1.35"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
- integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
- dependencies:
- mime-db "1.52.0"
-
-mime@1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
- integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
-minimatch@^3.0.4:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
- integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
- dependencies:
- brace-expansion "^1.1.7"
-
-ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
- integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
-
-ms@2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
- integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@2.1.3, ms@^2.1.1:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
- integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-negotiator@0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
- integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
-
-node-addon-api@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
- integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
-
-node-fetch@2:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
- integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
- dependencies:
- whatwg-url "^5.0.0"
-
-node-fetch@^2.6.12:
- version "2.6.12"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
- integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==
- dependencies:
- whatwg-url "^5.0.0"
-
-node-gyp-build@^4.4.0:
- version "4.6.1"
- resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e"
- integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==
-
-nwsapi@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
- integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
-
-object-assign@^4:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
- integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
-
-object-inspect@^1.9.0:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
- integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
-
-on-finished@2.4.1:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
- integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
- dependencies:
- ee-first "1.1.1"
-
-one-time@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
- integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
- dependencies:
- fn.name "1.x.x"
-
-optionator@^0.8.1:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
- integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
- dependencies:
- deep-is "~0.1.3"
- fast-levenshtein "~2.0.6"
- levn "~0.3.0"
- prelude-ls "~1.1.2"
- type-check "~0.3.2"
- word-wrap "~1.2.3"
-
-parse5@^7.1.1:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
- integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
- dependencies:
- entities "^4.4.0"
-
-parseurl@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
- integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
-path-to-regexp@0.1.7:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
- integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
-
-prelude-ls@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
- integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
-
-prisma@4.9.0:
- version "4.9.0"
- resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.9.0.tgz#295954b2a89cd35a0e6bcf66b2b036dbf80c75ee"
- integrity sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==
- dependencies:
- "@prisma/engines" "4.9.0"
-
-proxy-addr@~2.0.7:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
- integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
- dependencies:
- forwarded "0.2.0"
- ipaddr.js "1.9.1"
-
-psl@^1.1.33:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"
- integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==
-
-punycode@1.3.2:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
- integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==
-
-punycode@^2.1.1:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
-
-qs@6.11.0:
- version "6.11.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
- integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
- dependencies:
- side-channel "^1.0.4"
-
-querystring@0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
- integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==
-
-querystringify@^2.1.1:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
- integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
-
-range-parser@~1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
- integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
-raw-body@2.5.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
- integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
- dependencies:
- bytes "3.1.2"
- http-errors "2.0.0"
- iconv-lite "0.4.24"
- unpipe "1.0.0"
-
-readable-stream@^3.4.0, readable-stream@^3.6.0:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62"
- integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==
- dependencies:
- inherits "^2.0.3"
- string_decoder "^1.1.1"
- util-deprecate "^1.0.1"
-
-readable-to-ms@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/readable-to-ms/-/readable-to-ms-1.0.3.tgz#7a1bc1cdf15a13e0808d488c160ea723fd04e60f"
- integrity sha512-xxxnoflc1CT6st5azNw5u8m739XkrqrkYgLGTe9noe2OG5ohxz3viFQoOSXN2qdckf79da353uBPHQrsjPuYdQ==
-
-redis-errors@^1.0.0, redis-errors@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
- integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
-
-redis-parser@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
- integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
- dependencies:
- redis-errors "^1.0.0"
-
-requires-port@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
- integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
-
-rolling-rate-limiter@^0.4.2:
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/rolling-rate-limiter/-/rolling-rate-limiter-0.4.2.tgz#19204ceabf4e4e421681bfe876a779d5b0d2282b"
- integrity sha512-4lKCwwuINmfPvyfLtvAHn+SiwXisH8fmmEvTSHYAenAfKF0p6IErkUEIS+9dvWAslU+97WtX000ne26RAToo2w==
- dependencies:
- microtime "^3.0.0"
- uuid "^9.0.0"
-
-safe-buffer@5.2.1, safe-buffer@~5.2.0:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-safe-stable-stringify@^2.3.1:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa"
- integrity sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==
-
-"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
- integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-sax@1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
- integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==
-
-sax@>=0.6.0:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
- integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
-
-saxes@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
- integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
- dependencies:
- xmlchars "^2.2.0"
-
-send@0.18.0:
- version "0.18.0"
- resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
- integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
- dependencies:
- debug "2.6.9"
- depd "2.0.0"
- destroy "1.2.0"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- etag "~1.8.1"
- fresh "0.5.2"
- http-errors "2.0.0"
- mime "1.6.0"
- ms "2.1.3"
- on-finished "2.4.1"
- range-parser "~1.2.1"
- statuses "2.0.1"
-
-serve-static@1.15.0:
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
- integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
- dependencies:
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- parseurl "~1.3.3"
- send "0.18.0"
-
-setprototypeof@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
- integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
-
-side-channel@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
- integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
- dependencies:
- call-bind "^1.0.0"
- get-intrinsic "^1.0.2"
- object-inspect "^1.9.0"
-
-simple-swizzle@^0.2.2:
- version "0.2.2"
- resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
- integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
- dependencies:
- is-arrayish "^0.3.1"
-
-source-map@~0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
- integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-stack-trace@0.0.x, stack-trace@^0.0.10:
- version "0.0.10"
- resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
- integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
-
-standard-as-callback@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
- integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
-
-statuses@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
- integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
-
-string_decoder@^1.1.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
- integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
- dependencies:
- safe-buffer "~5.2.0"
-
-symbol-tree@^3.2.4:
- version "3.2.4"
- resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
- integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
-
-text-hex@1.0.x:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
- integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
-
-toidentifier@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
- integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
-
-tough-cookie@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874"
- integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==
- dependencies:
- psl "^1.1.33"
- punycode "^2.1.1"
- universalify "^0.2.0"
- url-parse "^1.5.3"
-
-tr46@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
- integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
- dependencies:
- punycode "^2.1.1"
-
-tr46@~0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
- integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
-
-triple-beam@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
- integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
-
-type-check@~0.3.2:
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
- integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==
- dependencies:
- prelude-ls "~1.1.2"
-
-type-is@~1.6.18:
- version "1.6.18"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
- integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
- dependencies:
- media-typer "0.3.0"
- mime-types "~2.1.24"
-
-universalify@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
- integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
-
-unpipe@1.0.0, unpipe@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
- integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
-
-url-parse@^1.5.3:
- version "1.5.10"
- resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
- integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
- dependencies:
- querystringify "^2.1.1"
- requires-port "^1.0.0"
-
-url@0.10.3:
- version "0.10.3"
- resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
- integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==
- dependencies:
- punycode "1.3.2"
- querystring "0.2.0"
-
-util-deprecate@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
-utils-merge@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
- integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
-
-uuid@3.3.2:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
- integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
-
-uuid@^9.0.0:
- version "9.0.1"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
- integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
-
-vary@^1, vary@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
- integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
-
-w3c-xmlserializer@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"
- integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==
- dependencies:
- xml-name-validator "^4.0.0"
-
-webidl-conversions@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
- integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
-
-webidl-conversions@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
- integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
-
-whatwg-encoding@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
- integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
- dependencies:
- iconv-lite "0.6.3"
-
-whatwg-mimetype@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
- integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
-
-whatwg-url@^11.0.0:
- version "11.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
- integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
- dependencies:
- tr46 "^3.0.0"
- webidl-conversions "^7.0.0"
-
-whatwg-url@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
- integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
- dependencies:
- tr46 "~0.0.3"
- webidl-conversions "^3.0.0"
-
-winston-transport@^4.3.0, winston-transport@^4.5.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
- integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
- dependencies:
- logform "^2.3.2"
- readable-stream "^3.6.0"
- triple-beam "^1.3.0"
-
-winston@^3.8.2:
- version "3.8.2"
- resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50"
- integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==
- dependencies:
- "@colors/colors" "1.5.0"
- "@dabh/diagnostics" "^2.0.2"
- async "^3.2.3"
- is-stream "^2.0.0"
- logform "^2.4.0"
- one-time "^1.0.0"
- readable-stream "^3.4.0"
- safe-stable-stringify "^2.3.1"
- stack-trace "0.0.x"
- triple-beam "^1.3.0"
- winston-transport "^4.5.0"
-
-word-wrap@~1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
- integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-
-ws@^7.4.6:
- version "7.5.9"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
- integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
-
-ws@^8.11.0:
- version "8.12.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f"
- integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==
-
-ws@^8.17.1:
- version "8.17.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
- integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
-
-xml-name-validator@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
- integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
-
-xml2js@0.4.19:
- version "0.4.19"
- resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
- integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
- dependencies:
- sax ">=0.6.0"
- xmlbuilder "~9.0.1"
-
-xmlbuilder@~9.0.1:
- version "9.0.7"
- resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
- integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==
-
-xmlchars@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
- integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==