diff --git a/LICENSE b/LICENSE
index 0ad25db4..8e9af564 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,661 +1,19 @@
- 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
-.
+Copyright (c) 2024 ArabCoders
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index 209a39eb..e531b13a 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,20 @@

-Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel support. Allows you to download videos from YouTube and [dozens of other sites](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md).
+Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel support.
-YTPTube started as a fork of [meTube](https://github.com/alexta69/metube) project by alexta69. Since then it went under heavy changes, and it supports many new features.
+YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since then it went under heavy changes, and it supports many new features.
-# YTPTube Features compared to meTube.
+# YTPTube Features.
* A built in video player that can play any video file regardless of the format.
* New `/add_batch` endpoint that allow multiple links to be sent.
-* Re-Imagined the frontend and re-wrote the code in VueJS.
+* Completely redesigned the frontend UI.
* Switched out of binary file storage in favor of SQLite.
* Handle live streams.
-* Support per link, `yt-dlp config` and `cookies`. and `output format`
+* Support per link, `yt-dlp config` and `cookies`. and `output format`.
* Tasks Runner. It allow you to queue channels for downloading using simple `json` file.
* Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file.
+* **Experimental** Multi-downloads support.
### Tips
Your `yt-dlp` config should include the following options for optimal working conditions.
@@ -73,6 +74,7 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __YTP_HOST__: Which ip address to bind to. Defaults to `0.0.0.0`.
* __YTP_PORT__: Which port to bind to. Defaults to `8081`.
* __YTP_LOGGING_LEVEL__: Logging level. Defaults to `info`.
+* __YTP_MAX_WORKERS__: How many works to use for downloads. Defaults to `1`.
## Running behind a reverse proxy
diff --git a/app/AsyncPool.py b/app/AsyncPool.py
index 58d7bb54..62f6aff2 100644
--- a/app/AsyncPool.py
+++ b/app/AsyncPool.py
@@ -49,6 +49,7 @@ class AsyncPool:
self._total_queued = 0
self._log_every_n = log_every_n
self._expected_total = expected_total
+ self._active_workers = 0
async def _worker_loop(self):
while True:
@@ -62,6 +63,7 @@ class AsyncPool:
if item.__class__ is Terminator:
break
+ self._active_workers += 1
future, args, kwargs = item
# the wait_for will cancel the task (task sees CancelledError) and raises a TimeoutError from here
# so be wary of catching TimeoutErrors in this loop
@@ -83,6 +85,7 @@ class AsyncPool:
else:
self._logger.exception('Worker call failed')
finally:
+ self._active_workers -= 1
if got_obj:
self._queue.task_done()
@@ -98,7 +101,13 @@ class AsyncPool:
"""
:return: True if there are open workers.
"""
- return self._queue.qsize() < self._num_workers
+ return self._active_workers < self._num_workers
+
+ def get_avalialbe_workers(self) -> int:
+ """
+ :return: number of available workers.
+ """
+ return self._num_workers - self._active_workers
async def __aenter__(self):
self.start()
@@ -143,7 +152,7 @@ class AsyncPool:
if not self._workers:
return
- self._logger.debug('Joining {}'.format(self._name))
+ self._logger.info('Joining {}'.format(self._name))
# The Terminators will kick each worker from being blocked against the _queue.get() and allow
# each one to exit
for _ in range(self._num_workers):
@@ -156,7 +165,7 @@ class AsyncPool:
self._logger.exception('Exception joining {}'.format(self._name))
raise
finally:
- self._logger.debug('Completed {}'.format(self._name))
+ self._logger.info('Completed {}'.format(self._name))
if self._exceptions and self._raise_on_join:
raise Exception("Exception occurred in pool {}".format(self._name))
diff --git a/app/Config.py b/app/Config.py
index 078cec00..1bf0ba77 100644
--- a/app/Config.py
+++ b/app/Config.py
@@ -37,7 +37,7 @@ class Config:
allow_manifestless: bool = False
- max_workers: int = 0
+ max_workers: int = 1
version: str = APP_VERSION
diff --git a/app/DataStore.py b/app/DataStore.py
index 5e7422da..2e02a9f0 100644
--- a/app/DataStore.py
+++ b/app/DataStore.py
@@ -28,7 +28,7 @@ class DataStore:
def load(self) -> None:
for id, item in self.saved_items():
- self.dict[id] = Download(
+ self.dict.update({id: Download(
info=item,
download_dir=calcDownloadPath(
basePath=self.config.download_path,
@@ -36,8 +36,8 @@ class DataStore:
),
temp_dir=self.config.temp_path,
output_template_chapter=self.config.output_template_chapter,
- default_ytdl_opts=self.config.ytdl_options,
- )
+ default_ytdl_opts=self.config.ytdl_options)
+ })
def exists(self, key: str = None, url: str = None) -> bool:
if not key and not url:
@@ -54,13 +54,13 @@ class DataStore:
def get(self, key: str, url: str = None) -> Download:
if not key and not url:
- raise KeyError('key or url must be provided')
+ raise KeyError('key or url must be provided.')
for i in self.dict:
if (key and self.dict[i].info._id == key) or (url and self.dict[i].info.url == url):
return self.dict[i]
- raise KeyError('key or url not found')
+ raise KeyError(f'{key=} or {url=} not found.')
def items(self) -> list[tuple[str, Download]]:
return self.dict.items()
@@ -92,16 +92,13 @@ class DataStore:
# value.info._id = key
# return
- self.dict[value.info._id] = value
+ self.dict.update({value.info._id: value})
self._updateStoreItem(self.type, value.info)
return self.dict[value.info._id]
def delete(self, key: str) -> None:
- if not key in self.dict:
- return
-
- del self.dict[key]
+ self.dict.pop(key, None)
self._deleteStoreItem(key)
def next(self) -> tuple[str, Download]:
diff --git a/app/Download.py b/app/Download.py
index 323fc2c9..9d2fd40a 100644
--- a/app/Download.py
+++ b/app/Download.py
@@ -3,7 +3,6 @@ import json
import logging
import multiprocessing
import os
-import queue
import re
import shutil
import yt_dlp
@@ -75,30 +74,32 @@ class Download:
self.max_workers = int(config.max_workers)
self.tempKeep = bool(config.temp_keep)
+ def _progress_hook(self, data: dict):
+ dicto = {k: v for k, v in data.items() if k in self._ytdlp_fields}
+ self.status_queue.put({'id': self.id, **dicto})
+
+ def _postprocessor_hook(self, data: dict):
+ if data.get('postprocessor') != 'MoveFiles' or data.get('status') != 'finished':
+ return
+
+ if '__finaldir' in data['info_dict']:
+ filename = os.path.join(
+ data['info_dict']['__finaldir'],
+ os.path.basename(data['info_dict']['filepath'])
+ )
+ else:
+ filename = data['info_dict']['filepath']
+
+ self.status_queue.put({
+ 'id': self.id,
+ 'status': 'finished',
+ 'filename': filename
+ })
+
def _download(self):
try:
- def put_status(st):
- dicto = {k: v for k, v in st.items() if k in self._ytdlp_fields}
- self.status_queue.put({'id': self.id, **dicto})
-
- def put_status_postprocessor(d):
- if d['postprocessor'] == 'MoveFiles' and d['status'] == 'finished':
- if '__finaldir' in d['info_dict']:
- filename = os.path.join(
- d['info_dict']['__finaldir'],
- os.path.basename(d['info_dict']['filepath'])
- )
- else:
- filename = d['info_dict']['filepath']
-
- self.status_queue.put({
- 'id': self.id,
- 'status': 'finished',
- 'filename': filename
- })
-
params: dict = {
- 'no_color': True,
+ 'color': 'no_color',
'format': self.format,
'paths': {
'home': self.download_dir,
@@ -108,11 +109,11 @@ class Download:
'default': self.output_template,
'chapter': self.output_template_chapter
},
- 'socket_timeout': 60,
- 'break_per_url': True,
+ 'noprogress': True,
+ 'break_on_existing': True,
'ignoreerrors': False,
- 'progress_hooks': [put_status],
- 'postprocessor_hooks': [put_status_postprocessor],
+ 'progress_hooks': [self._progress_hook],
+ 'postprocessor_hooks': [self._postprocessor_hook],
**mergeConfig(self.default_ytdl_opts, self.ytdl_opts),
}
@@ -120,6 +121,7 @@ class Download:
if self.debug:
params['verbose'] = True
+ params['noprogress'] = False
if self.info.ytdlp_cookies:
try:
@@ -136,7 +138,7 @@ class Download:
f'Invalid cookies: was provided for {self.info.title} - {str(e)}')
log.info(
- f'Downloading id="{self.info.id}" title="{self.info.title}".')
+ f'Downloading {os.getpid()=} id="{self.info.id}" title="{self.info.title}".')
ret = yt_dlp.YoutubeDL(params=params).download([self.info.url])
@@ -152,6 +154,9 @@ class Download:
'error': str(exc)
})
+ log.info(
+ f'Finished {os.getpid()=} id="{self.info.id}" title="{self.info.title}".')
+
async def start(self, notifier: Notifier):
if self.manager is None:
self.manager = multiprocessing.Manager()
diff --git a/app/DownloadQueue.py b/app/DownloadQueue.py
index b1362428..6a65bc8f 100644
--- a/app/DownloadQueue.py
+++ b/app/DownloadQueue.py
@@ -11,7 +11,6 @@ from ItemDTO import ItemDTO
from DataStore import DataStore
from Utils import Notifier, ObjectSerializer, calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig
from AsyncPool import AsyncPool
-from concurrent.futures import ProcessPoolExecutor
log = logging.getLogger('DownloadQueue')
dl_queue = asyncio.Queue()
@@ -38,14 +37,10 @@ class DownloadQueue:
async def initialize(self):
self.event = asyncio.Event()
- if self.config.max_workers > 0:
- log.info(
- f'Using {self.config.max_workers} workers for downloading.')
- else:
- log.info('Using single threaded download.')
+ log.info(f'Using {self.config.max_workers} workers for downloading.')
asyncio.create_task(
- self.__download_pool() if self.config.max_workers > 0 else self.__download()
+ self.__download_pool() if self.config.max_workers > 1 else self.__download()
)
async def __add_entry(
@@ -257,30 +252,37 @@ class DownloadQueue:
async def cancel(self, ids):
for id in ids:
- if self.queue.exists(key=id) is False:
- log.warn(f'Requested cancel for non-existent download {id=}')
+ try:
+ item = self.queue.get(key=id)
+ except KeyError as e:
+ log.warn(
+ f'Requested cancel for non-existent download {id=}. {str(e)}')
continue
- item = self.queue.get(key=id)
-
if item.started() is True:
log.info(f'Canceling {id=} {item.info.title=}')
item.cancel()
else:
- self.queue.delete(id)
item.close()
- log.info(f'Deleting {id=} {item.info.title=}')
+ log.info(f'Deleting from queue {id=} {item.info.title=}')
+ self.queue.delete(id)
await self.notifier.canceled(id)
+ self.done.put(item)
+ await self.notifier.completed(item)
return {'status': 'ok'}
async def clear(self, ids):
for id in ids:
- if not self.done.exists(key=id):
- log.warn(f'Requested delete for non-existent download {id}')
+
+ try:
+ item = self.done.get(key=id)
+ except KeyError as e:
+ log.warn(
+ f'Requested delete for non-existent download {id=}. {str(e)}')
continue
- item = self.done.get(key=id)
- log.info(f'Deleting {id=} {item.info.title=}')
+
+ log.info(f'Deleting completed download {id=} {item.info.title=}')
self.done.delete(id)
await self.notifier.cleared(id)
@@ -311,22 +313,21 @@ class DownloadQueue:
num_workers=self.config.max_workers,
worker_co=self.__downloadFile,
name='WorkerPool',
- logger=logging.getLogger('WorkerPool')
+ logger=log,
) as executor:
while True:
- while not self.queue.hasDownloads():
- log.info('waiting for item to download.')
- await self.event.wait()
- self.event.clear()
-
while True:
if executor.has_open_workers() is False:
- log.info(
- f'Waiting for workers to finish. {executor.total_queued} items in queue.')
await asyncio.sleep(1)
else:
break
+ while not self.queue.hasDownloads():
+ log.info(
+ f'Waiting for item to download. [{executor.get_avalialbe_workers()}] Workers available.')
+ await self.event.wait()
+ self.event.clear()
+
entry = self.queue.getNextDownload()
await asyncio.sleep(0.2)
@@ -334,6 +335,16 @@ class DownloadQueue:
await executor.push(id=entry.info._id, entry=entry)
await asyncio.sleep(1)
+ async def __download(self):
+ while True:
+ while self.queue.empty():
+ log.info('waiting for item to download.')
+ await self.event.wait()
+ self.event.clear()
+
+ id, entry = self.queue.next()
+ await self.__downloadFile(id, entry)
+
async def __downloadFile(self, id: str, entry: Download):
log.info(
f'Queuing {id=} - {entry.info.title=} - {entry.info.url=} - {entry.info.folder=}.')
@@ -352,22 +363,16 @@ class DownloadQueue:
entry.close()
if self.queue.exists(key=id):
+
self.queue.delete(key=id)
+
if entry.is_canceled() is True:
await self.notifier.canceled(id)
- else:
- self.done.put(value=entry)
- await self.notifier.completed(entry.info)
+ entry.info.status = 'canceled'
+ entry.info.error = 'Canceled by user.'
- async def __download(self):
- while True:
- while self.queue.empty():
- log.info('waiting for item to download.')
- await self.event.wait()
- self.event.clear()
-
- id, entry = self.queue.next()
- await self.__downloadFile(id, entry)
+ self.done.put(value=entry)
+ await self.notifier.completed(entry.info)
def isDownloaded(self, info: dict) -> bool:
return self.config.keep_archive and isDownloaded(self.config.ytdl_options.get('download_archive', None), info)
diff --git a/app/Utils.py b/app/Utils.py
index f2436c2f..f5737e2b 100644
--- a/app/Utils.py
+++ b/app/Utils.py
@@ -117,7 +117,7 @@ def get_opts(format: str, quality: str, ytdl_opts: dict) -> dict:
def getVideoInfo(url: str, ytdlp_opts: dict = None) -> (Any | dict[str, Any] | None):
params: dict = {
'quiet': True,
- 'no_color': True,
+ 'color': 'no_color',
'extract_flat': True,
}
@@ -143,7 +143,7 @@ def getAttributes(vclass: str | type) -> dict:
def calcDownloadPath(basePath: str, folder: str = None, createPath: bool = True) -> str:
- """Calculates download path And prevents directory traversal attacks.
+ """Calculates download path and prevents directory traversal.
Returns:
Dir with base dir factored in.
@@ -166,7 +166,7 @@ def calcDownloadPath(basePath: str, folder: str = None, createPath: bool = True)
def ExtractInfo(config: dict, url: str, debug: bool = False) -> dict:
params: dict = {
- 'no_color': True,
+ 'color': 'no_color',
'extract_flat': True,
'skip_download': True,
'ignoreerrors': True,
diff --git a/app/main.py b/app/main.py
index 6e7c4418..2f8fdfe0 100644
--- a/app/main.py
+++ b/app/main.py
@@ -107,6 +107,7 @@ class Main:
port=self.config.port,
reuse_port=True,
loop=self.loop,
+ access_log=None,
print=lambda _: print(
f'YTPTube v{self.config.version} - listening on http://{self.config.host}:{self.config.port}'),
)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7e26e5e7..4f70eacf 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@vueuse/core": "^10.6.1",
"core-js": "^3.8.3",
"cron-parser": "^4.9.0",
+ "floating-vue": "^5.0.2",
"hls.js": "^1.4.12",
"moment": "^2.29.4",
"plyr": "^3.7.8",
@@ -25,7 +26,6 @@
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
- "@creativebulma/bulma-tooltip": "^1.2.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "^5.0.8",
@@ -1825,12 +1825,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/@creativebulma/bulma-tooltip": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@creativebulma/bulma-tooltip/-/bulma-tooltip-1.2.0.tgz",
- "integrity": "sha512-ooImbeXEBxf77cttbzA7X5rC5aAWm9UsXIGViFOnsqB+6M944GkB28S5R4UWRqjFd2iW4zGEkEifAU+q43pt2w==",
- "dev": true
- },
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -1887,6 +1881,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz",
+ "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.0"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
+ "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
+ "dependencies": {
+ "@floating-ui/core": "^1.1.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
+ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ },
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
@@ -6389,6 +6404,24 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true
},
+ "node_modules/floating-vue": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.0.2.tgz",
+ "integrity": "sha512-CY6emiecyIIvdoK4xYN8mDSNRWutetR+rM10mu6VGrTCqxSdvHyF6TX/zslIgXDC2eIjEkcrpAMg3Mxh724t9w==",
+ "dependencies": {
+ "@floating-ui/dom": "~1.1.1",
+ "vue-resize": "^2.0.0-alpha.1"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": "^3.2.0",
+ "vue": "^3.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ }
+ }
+ },
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
@@ -11237,6 +11270,14 @@
"node": ">=8"
}
},
+ "node_modules/vue-resize": {
+ "version": "2.0.0-alpha.1",
+ "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
+ "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 68f9700c..c56cbdb3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,6 +16,7 @@
"@vueuse/core": "^10.6.1",
"core-js": "^3.8.3",
"cron-parser": "^4.9.0",
+ "floating-vue": "^5.0.2",
"hls.js": "^1.4.12",
"moment": "^2.29.4",
"plyr": "^3.7.8",
@@ -26,7 +27,6 @@
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
- "@creativebulma/bulma-tooltip": "^1.2.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "^5.0.8",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 81d309d7..041cf5f5 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -92,7 +92,7 @@ onMounted(() => {
return
}
- toast.info('Download canceled: ' + completed[id]?.title);
+ toast.info('Download canceled: ' + downloading[id]?.title);
delete downloading[id];
});
diff --git a/frontend/src/assets/css/style.css b/frontend/src/assets/css/style.css
index 5d6bb3cb..dccca85b 100644
--- a/frontend/src/assets/css/style.css
+++ b/frontend/src/assets/css/style.css
@@ -95,3 +95,7 @@
white-space: nowrap;
}
}
+
+.is-borderless {
+ border: none;
+}
diff --git a/frontend/src/components/Form-Add.vue b/frontend/src/components/Form-Add.vue
index bc919cf3..7168e270 100644
--- a/frontend/src/components/Form-Add.vue
+++ b/frontend/src/components/Form-Add.vue
@@ -44,7 +44,7 @@