diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0d95f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +dist/ +node_modules/ +app/node_modules/ +app/dist/ +#app/client/dist/ +app/client/node_modules/ +electron_dist/ +translations.zip +package-lock.json +app/package-lock.json +app/client/package-lock.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..92a2a4c --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Saturn +## Freezer Reborn +### Your go-to **ToS Compliant** Custom Deezer Client +### ⚠️ A premium account is required in order to use this client + +# Featuring: +- FLAC & MP3 320 support +- BYO Last.fm Integration (Safer solution!) +- Discord Listen Together & RPC +- Fixed homepage +- Minor updates to make things work with newer API +- Redundant importer removed +- (aaand don't forget everything the older app had) + +### You can download Saturn right away although it is highly advised to build it yourself, customized to your own liking. + +## Building + +Requirements: NodeJS 12+ + +You can build binary using npm script: +``` +npm i +npm run build +``` + +Or manually: + +``` +npm i +cd app +npm i +``` + +Frontend: + +``` +cd client +npm i +npm run build +cd ../.. +``` + +Then you can run server-only using, default port: `10069`: + +``` +cd app +node main.js +``` + +You can build binaries using: + +``` +npm run dist +``` + +# Links +- website: https://saturnclient.dev +- discord: https://saturnclient.dev/discord +- telegram: https://t.me/SaturnReleases + +# Download from Releases +https://github.com/SaturnMusic/PC/releases + +# Mobile Version +https://github.com/SaturnMusic/mobile diff --git a/app/.eslintrc.js b/app/.eslintrc.js new file mode 100644 index 0000000..8e97371 --- /dev/null +++ b/app/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + "env": { + "browser": true, + "commonjs": true, + "node": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": { + "allowEmptyCatch": 0 + } +}; diff --git a/app/assets/icon-taskbar-black.png b/app/assets/icon-taskbar-black.png new file mode 100644 index 0000000..011d8b6 Binary files /dev/null and b/app/assets/icon-taskbar-black.png differ diff --git a/app/assets/icon-taskbar-white.png b/app/assets/icon-taskbar-white.png new file mode 100644 index 0000000..25076ff Binary files /dev/null and b/app/assets/icon-taskbar-white.png differ diff --git a/app/assets/icon-taskbar.png b/app/assets/icon-taskbar.png new file mode 100644 index 0000000..26163ef Binary files /dev/null and b/app/assets/icon-taskbar.png differ diff --git a/app/assets/icon.png b/app/assets/icon.png new file mode 100644 index 0000000..8bd35c5 Binary files /dev/null and b/app/assets/icon.png differ diff --git a/app/assets/pause.png b/app/assets/pause.png new file mode 100644 index 0000000..5329883 Binary files /dev/null and b/app/assets/pause.png differ diff --git a/app/assets/play.png b/app/assets/play.png new file mode 100644 index 0000000..48abc15 Binary files /dev/null and b/app/assets/play.png differ diff --git a/app/assets/skip-next.png b/app/assets/skip-next.png new file mode 100644 index 0000000..41aa1ba Binary files /dev/null and b/app/assets/skip-next.png differ diff --git a/app/assets/skip-previous.png b/app/assets/skip-previous.png new file mode 100644 index 0000000..6cad0cc Binary files /dev/null and b/app/assets/skip-previous.png differ diff --git a/app/background.js b/app/background.js new file mode 100644 index 0000000..0d94ef6 --- /dev/null +++ b/app/background.js @@ -0,0 +1,369 @@ +const {app, BrowserWindow, ipcMain, Tray, Menu, session, dialog, shell, nativeTheme} = require('electron'); +const {createServer} = require('./src/server'); +const path = require('path'); +const arg = require('arg'); +const { exit, platform } = require('process'); +const packageJson = require('./package.json'); +const chalk = require('chalk'); +const {Settings} = require('./src/settings'); +const fs = require('fs'); + +let win; +let tray; +let settings; + +let shouldExit = false; +let playing = false; + +//Arguments +const args = arg({ + '--server': Boolean, + '--host': String, + '--port': Number, + '--help': Boolean, + '--settings': Boolean, + '--reset-settings': Boolean, + '--reset-downloads': Boolean, + '--log': Boolean, + + '-S': '--server', + '-H': '--host', + '-h': '--help', + '-p': '--port' +}, {argv: process.argv.slice(1)}); + +executeCli(); + +//Get path to asset +function assetPath(a) { + return path.join(__dirname, 'assets', a); +} + +//Execute actions by parameters +function executeCli() { + if (args['--help']) { + console.log(` +${chalk.bold.blue('Saturn')} ${chalk.bold(`v${packageJson.version}`)} Release + +${chalk.bold('USAGE:')} +--help, -h Prints this and exits +--server, -S Starts in server mode +--host, -H Override host (default: 127.0.0.1) +--port, -p Override port (default: 10069) + +${chalk.bold('TOOLS:')} +--settings Prints current settings and exits +--log Prints server log and exits +--reset-settings Reset settings to default +--reset-downloads Delete downloads cache and database + `); + exit(0); + } + //Print settings and exit + if (args["--settings"]) { + let settings = new Settings(); + settings.load(); + console.log(JSON.stringify(settings, null, 2)); + exit(0); + } + if (args["--reset-settings"]) { + fs.unlinkSync(Settings.getPath()); + exit(0); + } + //Delete downloads db and temp + if (args['--reset-downloads']) { + fs.unlinkSync(Settings.getDownloadsDB()); + fs.rmdirSync(Settings.getTempDownloads(), {recursive: true}); + exit(0); + } + //Show log + if (args['--log']) { + let p = path.join(Settings.getDir(), "saturn-server.log"); + console.log(fs.readFileSync(p, {encoding: 'utf-8'}).toString()); + exit(0); + } +} + +async function startServer() { + + //Override settings + let override = {}; + if (args["--host"]) + override['host'] = args["--host"]; + if (args["--port"]) + override['port'] = args["--port"]; + + settings = await createServer(true, () => { + //Server error + shouldExit = true; + if (win) win.close(); + + dialog.showMessageBoxSync({ + type: 'error', + title: 'Server error', + message: 'Server error occured, Saturn is probably already running!', + buttons: ['Close'] + }); + }, override); +} + +async function createWindow() { + //Create window + win = new BrowserWindow({ + width: settings.width, + darkTheme: true, + height: settings.height, + minWidth: 620, + minHeight: 600, + resizable: true, + autoHideMenuBar: true, + frame: settings.nativeTopBar, + icon: assetPath("icon.png"), + title: 'Saturn', + webPreferences: { + enableRemoteModule: true, + nodeIntegration: true, + devTools: true, + contextIsolation: false + } + }); + + win.loadURL(`http://localhost:${settings.port}`); + + //Minimize to tray + win.on('minimize', (event) => { + if (settings.minimizeToTray) { + event.preventDefault(); + win.hide(); + } + }); + + //On close + win.on('close', async (event) => { + if (shouldExit) { + win = null; + tray = null; + app.quit(); + return true; + } + + //Normal exit + if (!settings || !settings.arl || settings.arl == '' || settings.closeOnExit) { + win.webContents.send('onExit'); + shouldExit = true; + } + event.preventDefault(); + win.hide(); + return false; + }); + + //Thumbnail Toolbars + setThumbarButtons(); +} + +//Single instance +const singleInstanceLock = app.requestSingleInstanceLock(); +if (!singleInstanceLock) { + app.quit(); +} else { + app.on('second-instance', () => { + if (win) { + if (!win.visible) win.show(); + } + }); +} + +//Create window +app.on('ready', async () => { + //No mac + if (platform == 'darwin') + process.exit(-1); + + await startServer(); + //Server mode + if (args['--server']) return; + createWindow(); + + //Create Tray + if (settings.forceWhiteTrayIcon || nativeTheme.shouldUseDarkColors) + tray = new Tray(assetPath("icon-taskbar-white.png")); + else + tray = new Tray(assetPath("icon-taskbar-black.png")); + tray.on('double-click', () => restoreWindow()); + tray.on('click', () => restoreWindow()); + + setTray(); +}); + +//Restore or create new window +function restoreWindow() { + if (win) { + win.show(); + setThumbarButtons(); + return; + } + createWindow(); +} + +//Update tray context menu +function setTray() { + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Restore', + type: 'normal', + click: () => restoreWindow() + }, + playing ? + { + label: 'Pause', + type: 'normal', + click: () => win.webContents.send('togglePlayback') + } + : { + label: 'Play', + type: 'normal', + click: () => win.webContents.send('togglePlayback') + }, + { + label: 'Next', + type: 'normal', + click: () => win.webContents.send('skipNext') + }, + { + label: 'Previous', + type: 'normal', + click: () => win.webContents.send('skipPrev') + }, + { + label: 'Exit', + type: 'normal', + click: () => { + shouldExit = true; + if (!win) return app.quit(); + win.close(); + } + } + ]); + tray.setContextMenu(contextMenu); +} + +//Update Thumbnail Toolbars (Windows) +function setThumbarButtons() { + win.setThumbarButtons([ + { + tooltip: 'Skip Previous', + icon: assetPath('skip-previous.png'), + click: () => win.webContents.send('skipPrev') + }, + //Play/Pause + playing ? + { + tooltip: 'Pause', + icon: assetPath('pause.png'), + click: () => win.webContents.send('togglePlayback') + } : + { + tooltip: 'Play', + icon: assetPath('play.png'), + click: () => win.webContents.send('togglePlayback') + }, + //Skip next + { + tooltip: 'Skip Next', + icon: assetPath('skip-next.png'), + click: () => win.webContents.send('skipNext') + }, + ]); +} + + +//[] button +ipcMain.on('maximize', () => { + win.isMaximized() ? win.unmaximize() : win.maximize(); +}); + +//_ button in ui +ipcMain.on('minimize', () => { + win.minimize(); +}); + +//X button in ui +ipcMain.on('close', () => { + win.close(); +}); + +ipcMain.on('openUrl', (event, args) => { + shell.openExternal(args); +}); + +//Playing state change from UI +ipcMain.on('playing', (event, args) => { + playing = args; + setThumbarButtons(); + setTray(); +}); + +//Update settings from ui +ipcMain.on('updateSettings', (event, args) => { + Object.assign(settings, args); +}); + +//onExit callback +ipcMain.on('onExit', () => { + shouldExit = true; + win.close(); +}); + +//Open downloads directory +ipcMain.on('openDownloadsDir', async () => { + if ((await shell.openPath(settings.downloadsPath)) == "") return; + shell.showItemInFolder(settings.downloadsPath); +}); + +//Download path picker +ipcMain.on('selectDownloadPath', async (event) => { + let res = await dialog.showOpenDialog({ + title: 'Downloads folder', + properties: ['openDirectory', 'promptToCreate'], + }); + if (!res.canceled && res.filePaths.length > 0) { + event.reply('selectDownloadPath', res.filePaths[0]); + } +}); + +//Login using browser +ipcMain.on('browserLogin', async (event) => { + //Initial clean + session.defaultSession.clearStorageData(); + + let lwin = new BrowserWindow({ + width: 800, + height: 600, + icon: assetPath('icon.png'), + title: "Deezer Login", + resizable: true, + autoHideMenuBar: true, + webPreferences: { + nodeIntegration: false + } + }); + lwin.loadURL('https://deezer.com/login'); + + let arl = await new Promise((res) => { + lwin.webContents.on('did-navigate', async () => { + let arlCookie = await session.defaultSession.cookies.get({ + name: "arl" + }); + if (arlCookie.length > 0) { + res(arlCookie[0].value); + } + }); + }); + + lwin.close(); + lwin = null; + //Delete deezer junk + session.defaultSession.clearStorageData(); + + event.reply('browserLogin', arl); +}); \ No newline at end of file diff --git a/app/client/.env b/app/client/.env new file mode 100644 index 0000000..f256c63 --- /dev/null +++ b/app/client/.env @@ -0,0 +1,2 @@ +VUE_APP_I18N_LOCALE=en +VUE_APP_I18N_FALLBACK_LOCALE=en diff --git a/app/client/.gitignore b/app/client/.gitignore new file mode 100644 index 0000000..7df6d18 --- /dev/null +++ b/app/client/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/client/package.json b/app/client/package.json new file mode 100644 index 0000000..98dcdcb --- /dev/null +++ b/app/client/package.json @@ -0,0 +1,57 @@ +{ + "name": "client", + "version": "1.0.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint", + "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'", + "watch": "vue-cli-service build --watch" + }, + "dependencies": { + "@mdi/font": "^5.9.55", + "axios": "^0.21.1", + "roboto-fontface": "*", + "socket.io-client": "^4.1.2", + "vue": "^2.6.12", + "vue-esc": "^3.0.1", + "vue-i18n": "^8.22.4", + "vue-router": "^3.4.9", + "vuedraggable": "^2.24.3", + "vuetify": "^2.5.1" + }, + "devDependencies": { + "@intlify/vue-i18n-loader": "^1.0.0", + "@vue/cli-plugin-eslint": "^4.5.10", + "@vue/cli-plugin-router": "^4.5.10", + "@vue/cli-service": "^4.5.10", + "eslint": "^6.7.2", + "eslint-plugin-vue": "^6.2.2", + "sass": "^1.32.5", + "sass-loader": "^8.0.0", + "vue-cli-plugin-i18n": "~1.0.1", + "vue-cli-plugin-vuetify": "^2.0.9", + "vue-template-compiler": "^2.6.12", + "vuetify-loader": "^1.7.2" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/essential", + "eslint:recommended" + ], + "parserOptions": { + "ecmaVersion": 2020 + }, + "rules": {} + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ] +} diff --git a/app/client/public/banner.png b/app/client/public/banner.png new file mode 100644 index 0000000..625723b Binary files /dev/null and b/app/client/public/banner.png differ diff --git a/app/client/public/favicon.ico b/app/client/public/favicon.ico new file mode 100644 index 0000000..d091f4d Binary files /dev/null and b/app/client/public/favicon.ico differ diff --git a/app/client/public/index.html b/app/client/public/index.html new file mode 100644 index 0000000..02bf7fe --- /dev/null +++ b/app/client/public/index.html @@ -0,0 +1,39 @@ + + + + + + + + Saturn + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/app/client/public/lastfm.svg b/app/client/public/lastfm.svg new file mode 100644 index 0000000..d98cf3c --- /dev/null +++ b/app/client/public/lastfm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/App.vue b/app/client/src/App.vue new file mode 100644 index 0000000..408ba6a --- /dev/null +++ b/app/client/src/App.vue @@ -0,0 +1,604 @@ + + + + + + \ No newline at end of file diff --git a/app/client/src/components/AlbumContext.vue b/app/client/src/components/AlbumContext.vue new file mode 100644 index 0000000..1d83f8f --- /dev/null +++ b/app/client/src/components/AlbumContext.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/AlbumTile.vue b/app/client/src/components/AlbumTile.vue new file mode 100644 index 0000000..223124f --- /dev/null +++ b/app/client/src/components/AlbumTile.vue @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/ArtistTile.vue b/app/client/src/components/ArtistTile.vue new file mode 100644 index 0000000..68f1547 --- /dev/null +++ b/app/client/src/components/ArtistTile.vue @@ -0,0 +1,123 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/DeezerChannel.vue b/app/client/src/components/DeezerChannel.vue new file mode 100644 index 0000000..c78233e --- /dev/null +++ b/app/client/src/components/DeezerChannel.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/DownloadDialog.vue b/app/client/src/components/DownloadDialog.vue new file mode 100644 index 0000000..5db08dc --- /dev/null +++ b/app/client/src/components/DownloadDialog.vue @@ -0,0 +1,132 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/LibraryAlbums.vue b/app/client/src/components/LibraryAlbums.vue new file mode 100644 index 0000000..8a612f5 --- /dev/null +++ b/app/client/src/components/LibraryAlbums.vue @@ -0,0 +1,122 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/LibraryArtists.vue b/app/client/src/components/LibraryArtists.vue new file mode 100644 index 0000000..9e4b24f --- /dev/null +++ b/app/client/src/components/LibraryArtists.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/LibraryHistory.vue b/app/client/src/components/LibraryHistory.vue new file mode 100644 index 0000000..1e8fbb4 --- /dev/null +++ b/app/client/src/components/LibraryHistory.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/LibraryPlaylists.vue b/app/client/src/components/LibraryPlaylists.vue new file mode 100644 index 0000000..3bd09bf --- /dev/null +++ b/app/client/src/components/LibraryPlaylists.vue @@ -0,0 +1,142 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/LibraryTracks.vue b/app/client/src/components/LibraryTracks.vue new file mode 100644 index 0000000..8719836 --- /dev/null +++ b/app/client/src/components/LibraryTracks.vue @@ -0,0 +1,182 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/Lyrics.vue b/app/client/src/components/Lyrics.vue new file mode 100644 index 0000000..4552e09 --- /dev/null +++ b/app/client/src/components/Lyrics.vue @@ -0,0 +1,126 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/PlaylistPopup.vue b/app/client/src/components/PlaylistPopup.vue new file mode 100644 index 0000000..56278e5 --- /dev/null +++ b/app/client/src/components/PlaylistPopup.vue @@ -0,0 +1,133 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/PlaylistTile.vue b/app/client/src/components/PlaylistTile.vue new file mode 100644 index 0000000..4f6f987 --- /dev/null +++ b/app/client/src/components/PlaylistTile.vue @@ -0,0 +1,213 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/SmartTrackList.vue b/app/client/src/components/SmartTrackList.vue new file mode 100644 index 0000000..e6ad5a0 --- /dev/null +++ b/app/client/src/components/SmartTrackList.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/app/client/src/components/TrackTile.vue b/app/client/src/components/TrackTile.vue new file mode 100644 index 0000000..03afe80 --- /dev/null +++ b/app/client/src/components/TrackTile.vue @@ -0,0 +1,273 @@ + + + \ No newline at end of file diff --git a/app/client/src/js/i18n.js b/app/client/src/js/i18n.js new file mode 100644 index 0000000..c65e32b --- /dev/null +++ b/app/client/src/js/i18n.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import VueI18n from 'vue-i18n' + +Vue.use(VueI18n); + +function loadLocaleMessages () { + const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) + const messages = {} + locales.keys().forEach(key => { + const matched = key.match(/([A-Za-z0-9-_]+)\./i) + if (matched && matched.length > 1) { + const locale = matched[1] + messages[locale] = locales(key) + } + }) + return messages +} + +export default new VueI18n({ + locale: process.env.VUE_APP_I18N_LOCALE || 'en', + fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en', + messages: loadLocaleMessages() +}) diff --git a/app/client/src/js/router.js b/app/client/src/js/router.js new file mode 100644 index 0000000..a611383 --- /dev/null +++ b/app/client/src/js/router.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; + +import Login from '@/views/Login.vue'; +import HomeScreen from '@/views/HomeScreen.vue'; +import Search from '@/views/Search.vue'; +import Library from '@/views/Library.vue'; +import AlbumPage from '@/views/AlbumPage.vue'; +import PlaylistPage from '@/views/PlaylistPage.vue'; +import ArtistPage from '@/views/ArtistPage.vue'; +import Settings from '@/views/Settings.vue'; +import DeezerPage from '@/views/DeezerPage.vue'; +import DownloadsPage from '@/views/DownloadsPage.vue'; +import About from '@/views/About.vue'; + +Vue.use(VueRouter); + +const routes = [ + { + path: '/home', + component: HomeScreen + }, + { + path: '/login', + component: Login + }, + { + path: '/search', + component: Search, + props: (route) => { + return {query: route.query.q} + } + }, + { + path: '/library', + component: Library, + }, + //Library short links + {path: '/library/tracks', component: Library, props: () => {return {routeTab: 'tracks'}}}, + {path: '/library/albums', component: Library, props: () => {return {routeTab: 'albums'}}}, + {path: '/library/artists', component: Library, props: () => {return {routeTab: 'artists'}}}, + {path: '/library/playlists', component: Library, props: () => {return {routeTab: 'playlists'}}}, + { + path: '/album', + component: AlbumPage, + props: (route) => { + return {albumData: JSON.parse(route.query.album)} + } + }, + { + path: '/playlist', + component: PlaylistPage, + props: (route) => { + return {playlistData: JSON.parse(route.query.playlist)} + } + }, + { + path: '/artist', + component: ArtistPage, + props: (route) => { + return {artistData: JSON.parse(route.query.artist)} + } + }, + { + path: '/settings', + component: Settings + }, + { + path: '/page', + component: DeezerPage, + props: (route) => { + return {target: route.query.target} + } + }, + { + path: '/downloads', + component: DownloadsPage, + }, + { + path: '/about', + component: About + }, +]; + +const router = new VueRouter({ + mode: 'hash', + base: process.env.BASE_URL, + routes +}); + +export default router; diff --git a/app/client/src/js/vuetify.js b/app/client/src/js/vuetify.js new file mode 100644 index 0000000..cd63687 --- /dev/null +++ b/app/client/src/js/vuetify.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuetify from 'vuetify/lib'; + +import 'roboto-fontface/css/roboto/roboto-fontface.css'; +import '@mdi/font/css/materialdesignicons.css'; + +Vue.use(Vuetify); + +export default new Vuetify({ + theme: { + dark: true, + options: { + customProperties: true + }, + } +}); diff --git a/app/client/src/locales/ar.json b/app/client/src/locales/ar.json new file mode 100644 index 0000000..3ba6305 --- /dev/null +++ b/app/client/src/locales/ar.json @@ -0,0 +1,171 @@ +{ + "Home": "القائمة الرئيسية", + "Browse": "تصفح", + "Library": "المكتبة", + "Tracks": "أغاني", + "Playlists": "قوائم تشغيل", + "Albums": "البومات", + "Artists": "فنانون", + "More": "المزيد", + "Settings": "الإعدادات", + "Downloads": "التنزيلات", + "Search or paste Deezer URL. Use / to quickly focus.": "ابحث أو الصق رابط ديزر, استخدم \"/\" للتركيز السريع.", + "Play": "تشغيل", + "Add to library": "إضافة إلى المكتبة", + "Download": "تنزيل", + "fans": "المتابِعين", + "tracks": "أغاني", + "Quality": "الجودة", + "Estimated size:": "الحجم المتوقع:", + "Start downloading": "بدء التنزيل", + "Cancel": "الغاء", + "Stream logging is disabled!": "تسجيل البث معطل!", + "Enable it in settings for history to work properly.": "فعله في الإعدادات لتفعيل تاريخ السماع بشكل صحيح.", + "History": "تاريخ السماع", + "Create new playlist": "انشاء قائمة تشغيل جديدة", + "TRACKS": "أغاني", + "Sort by": "ترتيب حسب", + "Date Added": "تاريخ الإضافة", + "Name (A-Z)": "الإسم (أ - ي)", + "Artist (A-Z)": "الفنان (أ - ي)", + "Album (A-Z)": "الألبوم (أ - ي)", + "Error loading lyrics or lyrics not found!": "خطأ في تحميل كلمات الاغنية او الكلمات غير موجودة!", + "Create playlist": "إنشاء قائمة التشغيل", + "Create": "إنشاء", + "Add to playlist": "اضافة الى قائمة التشغيل", + "Create new": "إنشاء جديد", + "Remove": "إزالة", + "Play next": "شغل التالي", + "Add to queue": "إضافة إلى قائمة الانتظار", + "Remove from library": "إزالة من المكتبة", + "Remove from playlist": "إزالة من قائمة التشغيل", + "Play track mix": "تشغيل مزيج الاغاني", + "Go to": "الذهاب الى", + "Track Mix": "مزيج الاغاني", + "Duration": "المدة", + "Released": "تم إصداره", + "Disk": "القرص", + "albums": "البومات", + "Play top": "تشغيل الأفضل", + "Radio": "راديو", + "Show all albums": "اضهار كل الالبومات", + "Show all singles": "إظهار كل الأغاني المنفردة", + "Show more": "اظهار المزيد", + "Downloaded": "تم التنزيل", + "Queue": "قائمة الانتظار", + "Total": "المجموع", + "Stop": "إيقاف", + "Start": "بدء", + "Show folder": "عرض المجلدات", + "Clear queue": "تفريغ قائمة الإنتظار", + "Playing from": "التشغيل من", + "Info": "معلومات", + "Lyrics": "كلمات الأغنية", + "Track number": "رقم الأغنية", + "Disk number": "رقم القرص", + "Explicit": "صريحة (شتم)", + "Source": "المصدر", + "ID": "الرقم التعريفي", + "Error logging in!": "خطأ في تسجيل الدخول!", + "Please try again later, or try another account.": "الرجاء المحاولة مرة أخرى لاحقا، أو حاول حساب آخر.", + "Logout": "تسجيل الخروج", + "Login using browser": "تسجيل الدخول باستخدام المتصفح", + "Please login using your Deezer account:": "يرجى تسجيل الدخول باستخدام حساب ديزر الخاص بك:", + "...or paste your ARL/Token below:": "...أو لصق ARL/الرمز الخاص بك أدناه:", + "ARL/Token": "ARL/الرمز المميز", + "Login": "تسجيل الدخول", + "By using this program, you disagree with Deezer's ToS.": "باستخدام هذا البرنامج، أنت لا توافق على شروط خدمة ديزر.", + "Only in Electron version!": "فقط في إصدار إلكترون!", + "Search results for:": "نتائج البحث عن:", + "Error loading data!": "خطأ في تحميل البيانات!", + "Try again later!": "حاول مرة اخرى لاحقا!", + "Search": "بحث", + "Streaming Quality": "جودة التشغيل", + "Download Quality": "جودة التنزيل", + "Downloads Directory": "مسار التنزيل", + "Simultaneous downloads": "عدد التحميلات في نفس الوقت", + "Always show download confirm dialog before downloading.": "اضهار مربع تأكيد التنزيل دائماً قبل التنزيل.", + "Show download dialog": "عرض مربع تأكيد التنزيل", + "Create folders for artists": "إنشاء ملفات للفنان", + "Create folders for albums": "إنشاء ملفات للالبوم", + "Download lyrics": "تنزيل ملف كلمات الاغنية. Lrc", + "Variables": "المتغيرات", + "UI": "واجهة المستخدم", + "Show autocomplete in search": "إظهار الإكمال التلقائي في البحث", + "Integrations": "الدمج", + "This allows listening history, flow and recommendations to work properly.": "وهذا يتيح لسجل الاستماع و فلو والتوصيات, العمل على نحو سليم.", + "Log track listens to Deezer": "سِجِل استماع الاغاني الى ديزر", + "Connect your LastFM account to allow scrobbling.": "قم بتوصيل حساب LastFM الخاص بك للسماح بالتسجيل.", + "Login with LastFM": "تسجيل الدخول في LastFM", + "Disconnect LastFM": "تسجيل الخروج من LastFm", + "Requires restart to apply!": "يتطلب إعادة التشغيل من أجل التطبيق!", + "Enable Discord Rich Presence, requires restart to toggle!": "تمكين فعالية دسكورد، يتطلب إعادة تشغيل للتبديل!", + "Discord Rich Presence": "فعالية دسكورد", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "تمكين زر انضمام دسكورد لمزامنة الاغاني، يتطلب إعادة تشغيل للتبديل!", + "Discord Join Button": "زر الانضمام في دسكورد", + "Other": "أخرى", + "Minimize to tray": "تصغير إلى شريط المهام", + "Don't minimize to tray": "عدم التصغير إلى شريط المهام", + "Close on exit": "إغلاق عند الخروج", + "Settings saved!": "تم حفظ الإعدادات!", + "Available only in Electron version!": "متاح فقط في اصدار الإلكترون!", + "Crossfade (ms)": "التلاشي (ملي ثانية)", + "Select primary color": "تحديد اللون الأساسي", + "Light theme": "المظهر الفاتح", + "Create folders for playlists": "إنشاء ملفات لقائمة التشغيل", + "About": "حول البرنامج", + "Links:": "الروابط:", + "Telegram Releases": "إصدارات على تيليجرام", + "Telegram Group": "مجموعة التليجرام", + "Discord": "دسكورد", + "Telegram Android Group": "مجموعة تيليجرام (أندرويد)", + "Credits:": "المساهمون:", + "Agree": "قبول", + "Dismiss": "تجاهل", + "Added to playlist!": "تمت الإضافة إلى قائمة التشغيل!", + "Added to library!": "تمت الاضافة الى المكتبة!", + "Removed from library!": "الإزالة من المكتبة!", + "Removed from playlist!": "تمت الإزالة من قائمة التشغيل!", + "Playlist deleted!": "تم حذف قائمة التشغيل!", + "Delete": "حذف", + "Are you sure you want to delete this playlist?": "هل أنت متأكد من أنك تريد حذف قائمة التشغيل هذه؟", + "Force white tray icon": "فرض أيقونة شريط المهام البيضاء", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "فرض ايقونة شريط المهام (البيضاء) الافتراضية إذا تم الكشف عن الثيم بشكل غير صحيح. يتطلب إعادة التشغيل.", + "Share": "مشاركة", + "Settings quality": "جودة الإعدادات", + "Content language": "لغة المحتوى", + "Content country": "بلد المحتوى", + "Website": "الموقع الالكتروني", + "Visit website": "زيارة الموقع الإلكتروني", + "New update available:": "تحديث جديد متوفر:", + "Shuffle": "خلط عشوائي", + "Download album cover": "تنزيل صورة الألبوم", + "Art Resolution": "دقة صورة الالبوم", + "Public": "عامة", + "Private": "خاص", + "Collaborative": "التعاونيه", + "Edit playlist": "تعديل قائمة التشغيل", + "Save": "حفظ", + "Edit": "حرّر", + "Importer": "المستورد", + "Enter URL": "أدخل الرابط", + "Currently only Spotify is supported and limited to 100 tracks.": "حاليا فقط يدعم سبوتيقاي ومحدود إلى 100 اغنية.", + "Import into playlist": "استيراد إلى قائمة التشغيل", + "Keep sidebar open": "إبقاء الشريط الجانبي مفتوح", + "WARNING: Might require reload to work properly!": "تحذير: قد تتطلب إعادة تحميل للعمل بشكل صحيح!", + "An error occured, URL might be invalid or unsupported.": "حدث خطأ، قد يكون الرابط غير صالح أو غير مدعوم.", + "Top tracks": "افضل الاغاني", + "Show all top tracks": "عرض كل الاغاني المفضلة", + "Singles": "الأغاني", + "Album:": "ألبوم:", + "Artists:": "الفنانين:", + "Yes": "نعم", + "No": "لا", + "Download Filename": "تنزيل اسم الملف", + "Language": "اللغة", + "Background Image": "صورة الخلفية", + "Enter URL or absolute path. WARNING: Requires reload!": "أدخل عنوان URL أو المسار المطلق. تحذير: يتطلب إعادة التحميل!", + "LGBT Mode": "وضع LGBT", + "Native top bar": "الشريط العلوي الأصلي", + "Requires restart of Freezer!": "يتطلب إعادة تشغيل فريزر!" +} \ No newline at end of file diff --git a/app/client/src/locales/ast.json b/app/client/src/locales/ast.json new file mode 100644 index 0000000..76702bb --- /dev/null +++ b/app/client/src/locales/ast.json @@ -0,0 +1,171 @@ +{ + "Home": "Aniciu", + "Browse": "Restolar", + "Library": "Biblioteca", + "Tracks": "Pistes", + "Playlists": "Llistes", + "Albums": "Álbumes", + "Artists": "Artistes", + "More": "Más", + "Settings": "Axustes", + "Downloads": "Descargues", + "Search or paste Deezer URL. Use / to quickly focus.": "Busca o apiega una URL de Deezer. Usa «/» pa enfocar aína esta barra.", + "Play": "Reproducir", + "Add to library": "Amestar a la biblioteca", + "Download": "Baxar", + "fans": "siguidores", + "tracks": "pistes", + "Quality": "Calidá", + "Estimated size:": "Tamañu estimáu:", + "Start downloading": "Aniciar la descarga", + "Cancel": "Encaboxar", + "Stream logging is disabled!": "¡El rexistru de tresmisiones ta desactiváu!", + "Enable it in settings for history to work properly.": "Actívalu nos axustes pa que l'historial funcione afayadizamente.", + "History": "Historial", + "Create new playlist": "Crear una llista", + "TRACKS": "PISTES", + "Sort by": "Ordenar por", + "Date Added": "Data d'amiestu", + "Name (A-Z)": "Nome (A-Z)", + "Artist (A-Z)": "Artista (A-Z)", + "Album (A-Z)": "Álbum (A-Z)", + "Error loading lyrics or lyrics not found!": "¡Hebo un fallu al cargar la lletra o nun s'atopó!", + "Create playlist": "Creación d'una llista", + "Create": "Crear", + "Add to playlist": "Amestar a una llista", + "Create new": "Crear una", + "Remove": "Quitar", + "Play next": "Reproducir darréu", + "Add to queue": "Amestar a la cola", + "Remove from library": "Quitar de la biblioteca", + "Remove from playlist": "Quitar de la llista", + "Play track mix": "Reproducir el mecíu de pistes", + "Go to": "Dir a", + "Track Mix": "Mecíu de pistes", + "Duration": "Duración", + "Released": "Data de llanzamientu", + "Disk": "Discu", + "albums": "álbumes", + "Play top": "Reproducir lo destacao", + "Radio": "Radio", + "Show all albums": "Amosar tolos álbumes", + "Show all singles": "Amosar tolos singles", + "Show more": "Amosar más", + "Downloaded": "Baxó", + "Queue": "Cola", + "Total": "En total", + "Stop": "Parar", + "Start": "Aniciar", + "Show folder": "Amosar la carpeta", + "Clear queue": "Llimpiar la cola", + "Playing from": "Reproduciendo dende", + "Info": "Información", + "Lyrics": "Lletra", + "Track number": "Númberu de la pista", + "Disk number": "Númberu del discu", + "Explicit": "+18", + "Source": "Orixe", + "ID": "ID", + "Error logging in!": "¡Hebo un fallu al aniciar sesión!", + "Please try again later, or try another account.": "Volvi tentalo dempués o prueba con otra cuenta, por favor.", + "Logout": "Zarrar sesión", + "Login using browser": "Aniciar sesión col restolador", + "Please login using your Deezer account:": "Usa la to cuenta de Deezer…", + "...or paste your ARL/Token below:": "…o apiega la ARL/pase embaxo:", + "ARL/Token": "ARL/Pase", + "Login": "Aniciar sesión", + "By using this program, you disagree with Deezer's ToS.": "Col usu d'esti programa refugues los Términos del Serviciu de Deezer.", + "Only in Electron version!": "¡Namás na versión d'Electron!", + "Search results for:": "Resultaos pa la busca de:", + "Error loading data!": "¡Hebo un fallu al cargar los datos!", + "Try again later!": "¡Volvi tentalo dempués!", + "Search": "Buscar", + "Streaming Quality": "Calidá de les tresmisiones", + "Download Quality": "Calidá de les descargues", + "Downloads Directory": "Direutoriu de les descargues", + "Simultaneous downloads": "Descargues simultánees", + "Always show download confirm dialog before downloading.": "Amosar siempres el diálogu de confirmación de les descargues enantes de baxar.", + "Show download dialog": "Amosar el diálogu de descarga", + "Create folders for artists": "Crear carpetes pa los artistes", + "Create folders for albums": "Crear carpetes pa los álbumes", + "Download lyrics": "Baxar les lletres", + "Variables": "Variables", + "UI": "IU", + "Show autocomplete in search": "Amosar el completáu automáticu na busca", + "Integrations": "Integraciones", + "This allows listening history, flow and recommendations to work properly.": "Esto permite que l'historial, fluxu y recomendaciones d'escuches funcionen afayadizamente.", + "Log track listens to Deezer": "Rexistrar les pistes sentíes en Deezer", + "Connect your LastFM account to allow scrobbling.": "Conecta la to cuenta de LastFM pa permitir la sincronización de pistes.", + "Login with LastFM": "Aniciar sesión en LastFM", + "Disconnect LastFM": "Desconectase de LastFM", + "Requires restart to apply!": "¡Rique'l reaniciu p'aplicar!", + "Enable Discord Rich Presence, requires restart to toggle!": "Activa la presencia arriquecida de Discord. ¡Rique reaniciar l'aplicación p'alternar!", + "Discord Rich Presence": "Presencia arriquecida de Discord", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Activa'l botón de xunión de Discord pa sincronizar pistes. ¡Rique reaniciar l'aplicación p'alternar!", + "Discord Join Button": "Botón de xunión de Discord", + "Other": "Otres opciones", + "Minimize to tray": "Minimizar a la bandexa", + "Don't minimize to tray": "Nun minimiza a la bandexa", + "Close on exit": "Zarrar al colar", + "Settings saved!": "¡Guardáronse los axustes!", + "Available only in Electron version!": "¡Namás disponible na versión d'Electron!", + "Crossfade (ms)": "Transición (ms)", + "Select primary color": "Esbillar el color primariu", + "Light theme": "Estilu claru", + "Create folders for playlists": "Crear carpetes pa les llistes", + "About": "Tocante a", + "Links:": "Enllaces:", + "Telegram Releases": "Llanzamientos en Telegram", + "Telegram Group": "Grupu de Telegram", + "Discord": "Discord", + "Telegram Android Group": "Grupu d'Android en Telegram", + "Credits:": "Creitos:", + "Agree": "Acepto", + "Dismiss": "Escartar", + "Added to playlist!": "¡Amestóse a la llista!", + "Added to library!": "¡Amestóse a la biblioteca!", + "Removed from library!": "¡Quitóse de la biblioteca!", + "Removed from playlist!": "¡Quitóse de la llista!", + "Playlist deleted!": "¡Desanicióse la llista!", + "Delete": "Desaniciar", + "Are you sure you want to delete this playlist?": "¿De xuru que quies desaniciar esta llista?", + "Force white tray icon": "Forciar l'iconu de la bandexa blancu", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forcia l'iconu (blancu) predetermináu de la bandexa si nun se detecta correutamente l'estilu. Rique'l reaniciu.", + "Share": "Compartir", + "Settings quality": "Axustes de la calidá", + "Content language": "Llingua del conteníu", + "Content country": "País del conteníu", + "Website": "Sitiu web", + "Visit website": "Visitar el sitiu web", + "New update available:": "Anovamientu disponible:", + "Shuffle": "Al debalu", + "Download album cover": "Baxar la portada de los álbumes", + "Art Resolution": "Resolución", + "Public": "Pública", + "Private": "Privada", + "Collaborative": "En comuña", + "Edit playlist": "Edición d'una llista", + "Save": "Guardar", + "Edit": "Editar", + "Importer": "Importador", + "Enter URL": "Introduz una URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Anguaño namás se sofita Spotify y con una llende de 100 pistes.", + "Import into playlist": "Importar a una llista", + "Keep sidebar open": "Caltener la barra llateral abierta", + "WARNING: Might require reload to work properly!": "ALVERTENCIA: ¡Reanicia l'aplicación pa que funcione afayadizamente!", + "An error occured, URL might be invalid or unsupported.": "Asocedió un fallu, quiciabes la URL nun seya válida o nun se sofite.", + "Top tracks": "Pistes destacaes", + "Show all top tracks": "Amosar toles pistes destacaes", + "Singles": "Singles", + "Album:": "Álbum:", + "Artists:": "Artistes:", + "Yes": "Sí", + "No": "Non", + "Download Filename": "Nome de les descargues", + "Language": "Llingua", + "Background Image": "Imaxe del fondu", + "Enter URL or absolute path. WARNING: Requires reload!": "Introduz una URL o un camín absolutu. ALVERTENCIA: ¡Ha reaniciase l'aplicación!", + "LGBT Mode": "Mou LGBT", + "Native top bar": "Barra cimera nativa", + "Requires restart of Freezer!": "¡Tienes de reaniciar Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/bg.json b/app/client/src/locales/bg.json new file mode 100644 index 0000000..fbcb3e2 --- /dev/null +++ b/app/client/src/locales/bg.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Tracks", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artists", + "More": "More", + "Settings": "Settings", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Add to library", + "Download": "Download", + "fans": "fans", + "tracks": "tracks", + "Quality": "Quality", + "Estimated size:": "Estimated size:", + "Start downloading": "Start downloading", + "Cancel": "Cancel", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Create new playlist", + "TRACKS": "TRACKS", + "Sort by": "Sort by", + "Date Added": "Date Added", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Artist (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Create playlist", + "Create": "Create", + "Add to playlist": "Add to playlist", + "Create new": "Create new", + "Remove": "Remove", + "Play next": "Play next", + "Add to queue": "Add to queue", + "Remove from library": "Remove from library", + "Remove from playlist": "Remove from playlist", + "Play track mix": "Play track mix", + "Go to": "Go to", + "Track Mix": "Track Mix", + "Duration": "Duration", + "Released": "Released", + "Disk": "Disk", + "albums": "albums", + "Play top": "Play top", + "Radio": "Radio", + "Show all albums": "Show all albums", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Please try again later, or try another account.", + "Logout": "Logout", + "Login using browser": "Login using browser", + "Please login using your Deezer account:": "Please login using your Deezer account:", + "...or paste your ARL/Token below:": "...or paste your ARL/Token below:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "Search results for:", + "Error loading data!": "Error loading data!", + "Try again later!": "Try again later!", + "Search": "Search", + "Streaming Quality": "Streaming Quality", + "Download Quality": "Download Quality", + "Downloads Directory": "Downloads Directory", + "Simultaneous downloads": "Simultaneous downloads", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "Create folders for artists", + "Create folders for albums": "Create folders for albums", + "Download lyrics": "Download lyrics", + "Variables": "Variables", + "UI": "UI", + "Show autocomplete in search": "Show autocomplete in search", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "Login with LastFM", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "Requires restart to apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "Other", + "Minimize to tray": "Minimize to tray", + "Don't minimize to tray": "Don't minimize to tray", + "Close on exit": "Close on exit", + "Settings saved!": "Settings saved!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "Light theme", + "Create folders for playlists": "Create folders for playlists", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Group", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Agree", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Added to library!", + "Removed from library!": "Removed from library!", + "Removed from playlist!": "Removed from playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Share", + "Settings quality": "Settings quality", + "Content language": "Content language", + "Content country": "Content country", + "Website": "Website", + "Visit website": "Visit website", + "New update available:": "New update available:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/cs.json b/app/client/src/locales/cs.json new file mode 100644 index 0000000..fbcb3e2 --- /dev/null +++ b/app/client/src/locales/cs.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Tracks", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artists", + "More": "More", + "Settings": "Settings", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Add to library", + "Download": "Download", + "fans": "fans", + "tracks": "tracks", + "Quality": "Quality", + "Estimated size:": "Estimated size:", + "Start downloading": "Start downloading", + "Cancel": "Cancel", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Create new playlist", + "TRACKS": "TRACKS", + "Sort by": "Sort by", + "Date Added": "Date Added", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Artist (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Create playlist", + "Create": "Create", + "Add to playlist": "Add to playlist", + "Create new": "Create new", + "Remove": "Remove", + "Play next": "Play next", + "Add to queue": "Add to queue", + "Remove from library": "Remove from library", + "Remove from playlist": "Remove from playlist", + "Play track mix": "Play track mix", + "Go to": "Go to", + "Track Mix": "Track Mix", + "Duration": "Duration", + "Released": "Released", + "Disk": "Disk", + "albums": "albums", + "Play top": "Play top", + "Radio": "Radio", + "Show all albums": "Show all albums", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Please try again later, or try another account.", + "Logout": "Logout", + "Login using browser": "Login using browser", + "Please login using your Deezer account:": "Please login using your Deezer account:", + "...or paste your ARL/Token below:": "...or paste your ARL/Token below:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "Search results for:", + "Error loading data!": "Error loading data!", + "Try again later!": "Try again later!", + "Search": "Search", + "Streaming Quality": "Streaming Quality", + "Download Quality": "Download Quality", + "Downloads Directory": "Downloads Directory", + "Simultaneous downloads": "Simultaneous downloads", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "Create folders for artists", + "Create folders for albums": "Create folders for albums", + "Download lyrics": "Download lyrics", + "Variables": "Variables", + "UI": "UI", + "Show autocomplete in search": "Show autocomplete in search", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "Login with LastFM", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "Requires restart to apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "Other", + "Minimize to tray": "Minimize to tray", + "Don't minimize to tray": "Don't minimize to tray", + "Close on exit": "Close on exit", + "Settings saved!": "Settings saved!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "Light theme", + "Create folders for playlists": "Create folders for playlists", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Group", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Agree", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Added to library!", + "Removed from library!": "Removed from library!", + "Removed from playlist!": "Removed from playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Share", + "Settings quality": "Settings quality", + "Content language": "Content language", + "Content country": "Content country", + "Website": "Website", + "Visit website": "Visit website", + "New update available:": "New update available:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/de.json b/app/client/src/locales/de.json new file mode 100644 index 0000000..a1de5bc --- /dev/null +++ b/app/client/src/locales/de.json @@ -0,0 +1,171 @@ +{ + "Home": "Startseite", + "Browse": "Durchsuchen", + "Library": "Mediathek", + "Tracks": "Songs", + "Playlists": "Playlists", + "Albums": "Alben", + "Artists": "Künstler", + "More": "Mehr", + "Settings": "Einstellungen", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Suchen oder Deezer URL einfügen. Benutze \"/\", um schnell zu fokussieren.", + "Play": "Wiedergeben", + "Add to library": "Zur Mediathek hinzufügen", + "Download": "Download", + "fans": "Fans", + "tracks": "Titel", + "Quality": "Qualität", + "Estimated size:": "Geschätzte Größe:", + "Start downloading": "Download beginnen", + "Cancel": "Abbrechen", + "Stream logging is disabled!": "Streamprotokollierung ist deaktiviert!", + "Enable it in settings for history to work properly.": "Aktiviere es in den Einstellungen, damit der Verlauf korrekt funktioniert.", + "History": "Verlauf", + "Create new playlist": "Neue Playlist erstellen", + "TRACKS": "SONGS", + "Sort by": "Sortieren nach", + "Date Added": "Hinzugefügt am", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Künstler (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Fehler beim Laden der Songtexte bzw. Songtexte nicht gefunden!", + "Create playlist": "Playlist erstellen", + "Create": "Erstellen", + "Add to playlist": "Zur Playlist hinzufügen", + "Create new": "Neu erstellen", + "Remove": "Entfernen", + "Play next": "Als Nächstes abspielen", + "Add to queue": "In die Wiedergabeliste", + "Remove from library": "Aus der Mediathek entfernen", + "Remove from playlist": "Aus Playlist entfernen", + "Play track mix": "Songmix abspielen", + "Go to": "Gehe zu", + "Track Mix": "Songmix", + "Duration": "Dauer", + "Released": "Veröffentlicht", + "Disk": "Disk", + "albums": "Alben", + "Play top": "Top abspielen", + "Radio": "Radio", + "Show all albums": "Zeige alle Alben", + "Show all singles": "Zeige alle Singles", + "Show more": "Mehr anzeigen", + "Downloaded": "Heruntergeladen", + "Queue": "Wiedergabeliste", + "Total": "Gesamt", + "Stop": "Stopp", + "Start": "Start", + "Show folder": "Ordner anzeigen", + "Clear queue": "Warteschleife löschen", + "Playing from": "Wiedergabe von", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Songnummer", + "Disk number": "Disk-Nummer", + "Explicit": "Explizit", + "Source": "Quelle", + "ID": "ID", + "Error logging in!": "Fehler beim Einloggen!", + "Please try again later, or try another account.": "Bitte versuche es später noch einmal oder versuche es mit einem anderen Konto.", + "Logout": "Abmelden", + "Login using browser": "Anmeldung über Browser", + "Please login using your Deezer account:": "Bitte melde dich mit deinem Deezer-Konto an:", + "...or paste your ARL/Token below:": "...oder füge deine ARL/deinen Token unten ein:", + "ARL/Token": "ARL/Token", + "Login": "Anmeldung", + "By using this program, you disagree with Deezer's ToS.": "Durch die Verwendung dieses Programms lehnst du die Nutzungsbedingungen von Deezer ab.", + "Only in Electron version!": "Nur in der Electron-Version!", + "Search results for:": "Suchergebnisse für:", + "Error loading data!": "Fehler beim Laden der Daten!", + "Try again later!": "Versuch's später nochmal!", + "Search": "Suche", + "Streaming Quality": "Streamqualität", + "Download Quality": "Downloadqualität", + "Downloads Directory": "Downloadverzeichnis", + "Simultaneous downloads": "Gleichzeitige Downloads", + "Always show download confirm dialog before downloading.": "Downloadbestätigungsdialog immer vor dem Download anzeigen.", + "Show download dialog": "Download-Dialog anzeigen", + "Create folders for artists": "Ordner für Künstler erstellen", + "Create folders for albums": "Ordner für Alben erstellen", + "Download lyrics": "Lyrics herunterladen", + "Variables": "Variablen", + "UI": "Benutzeroberfläche", + "Show autocomplete in search": "Auto-Vervollständigung in der Suche anzeigen", + "Integrations": "Integrationen", + "This allows listening history, flow and recommendations to work properly.": "Dies ermöglicht, dass der Wiedergabeverlauf, Flow und Empfehlungen korrekt funktionieren.", + "Log track listens to Deezer": "Prokotolliere gehörte Songs auf Deezer", + "Connect your LastFM account to allow scrobbling.": "Verbinde dein LastFM-Konto, um das Scrobbling zu erlauben.", + "Login with LastFM": "Anmelden mit LastFM", + "Disconnect LastFM": "LastFM trennen", + "Requires restart to apply!": "Erfordert einen Neustart der App!", + "Enable Discord Rich Presence, requires restart to toggle!": "Discord Rich Presence aktivieren, erfordert einen Neustart zum Umschalten!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Aktiviere Discord Join für die Synchronisierung von Titel, benötigt einen Neustart zum Umschalten!", + "Discord Join Button": "Discord Join Button", + "Other": "Andere", + "Minimize to tray": "In die Statusleiste minimieren", + "Don't minimize to tray": "Nicht in die Statusleiste minimieren", + "Close on exit": "Beim Beenden schließen", + "Settings saved!": "Einstellungen gespeichert!", + "Available only in Electron version!": "Nur in der Electron-Version verfügbar!", + "Crossfade (ms)": "Überblendung (ms)", + "Select primary color": "Hauptfarbe auswählen", + "Light theme": "Helles Thema", + "Create folders for playlists": "Ordner für Playlists erstellen", + "About": "Über", + "Links:": "Links:", + "Telegram Releases": "Telegram Veröffentlichungen", + "Telegram Group": "Telegram-Gruppe", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android-Gruppe", + "Credits:": "Credits:", + "Agree": "Einverstanden", + "Dismiss": "Verwerfen", + "Added to playlist!": "Zur Playlist hinzugefügt!", + "Added to library!": "Zur Mediathek hinzugefügt!", + "Removed from library!": "Aus der Mediathek entfernt!", + "Removed from playlist!": "Aus der Playlist entfernt!", + "Playlist deleted!": "Playlist gelöscht!", + "Delete": "Löschen", + "Are you sure you want to delete this playlist?": "Bist du sicher, dass du diese Playlist löschen willst?", + "Force white tray icon": "Erzwinge weißes Tray-Icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Standardsymbol (weiß) in der Kontrollleiste erzwingen, wenn Design falsch erkannt wurde. Neustart erforderlich.", + "Share": "Teilen", + "Settings quality": "Audioqualität-Einstellungen", + "Content language": "Sprache der Inhalte", + "Content country": "Land der Inhalte", + "Website": "Webseite", + "Visit website": "Webseite besuchen", + "New update available:": "Neues Update verfügbar:", + "Shuffle": "Zufällige Wiedergabe", + "Download album cover": "Albumcover herunterladen", + "Art Resolution": "Auflösung der Albumcover", + "Public": "Öffentlich", + "Private": "Privat", + "Collaborative": "Zusammen", + "Edit playlist": "Playlist bearbeiten", + "Save": "Speichern", + "Edit": "Bearbeiten", + "Importer": "Importierer", + "Enter URL": "URL eingeben", + "Currently only Spotify is supported and limited to 100 tracks.": "Derzeit wird nur Spotify unterstützt und ist auf 100 Tracks beschränkt.", + "Import into playlist": "In Playlist importieren", + "Keep sidebar open": "Sidebar offen lassen", + "WARNING: Might require reload to work properly!": "WARNUNG: Eventuell muss neu geladen werden, um richtig zu funktionieren!", + "An error occured, URL might be invalid or unsupported.": "Ein Fehler ist aufgetreten, die URL ist ungültig oder wird nicht unterstützt.", + "Top tracks": "Top-Titel", + "Show all top tracks": "Zeige alle Top-Titel", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Künstler:", + "Yes": "Ja", + "No": "Nein", + "Download Filename": "Dateiname herunterladen", + "Language": "Sprache", + "Background Image": "Hintergrundbild", + "Enter URL or absolute path. WARNING: Requires reload!": "URL oder absoluten Pfad eingeben. WARNUNG: Neuladen erforderlich!", + "LGBT Mode": "LGBT-Modus", + "Native top bar": "System-Titelleiste verwenden", + "Requires restart of Freezer!": "Benötigt Neustart von Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/el.json b/app/client/src/locales/el.json new file mode 100644 index 0000000..0e9407d --- /dev/null +++ b/app/client/src/locales/el.json @@ -0,0 +1,171 @@ +{ + "Home": "Αρχική", + "Browse": "Περιήγηση", + "Library": "Βιβλιοθήκη", + "Tracks": "Κομμάτια", + "Playlists": "Λίστες αναπαραγωγής", + "Albums": "Άλμπουμ", + "Artists": "Καλλιτέχνες", + "More": "Περισσότερα", + "Settings": "Ρυθμίσεις", + "Downloads": "Λήψεις", + "Search or paste Deezer URL. Use / to quickly focus.": "Αναζήτηση ή επικόλληση διεύθυνσης URL Deezer. Χρησιμοποιήστε το \"/\" για γρήγορη εστίαση.", + "Play": "Αναπαραγωγή", + "Add to library": "Προσθήκη στη βιβλιοθήκη", + "Download": "Λήψη", + "fans": "θαυμαστές", + "tracks": "κομμάτια", + "Quality": "Ποιότητα", + "Estimated size:": "Εκτιμώμενος χρόνος:", + "Start downloading": "Έναρξη λήψης", + "Cancel": "Άκυρο", + "Stream logging is disabled!": "Η καταγραφή ροής είναι ανενεργή!", + "Enable it in settings for history to work properly.": "Ενεργοποιήστε το στις ρυθμίσεις για την σωστή λειτουργία του ιστορικού.", + "History": "Ιστορικό", + "Create new playlist": "Δημιουργία λίστας αναπαραγωγής", + "TRACKS": "ΤΡΑΓΟΥΔΙΑ", + "Sort by": "Ταξινόμηση κατά", + "Date Added": "Ημερομηνία Προσθήκης", + "Name (A-Z)": "Όνομα (Α-Ω)", + "Artist (A-Z)": "Καλλιτέχνης (Α-Ω)", + "Album (A-Z)": "Άλμπουμ (Α-Ω)", + "Error loading lyrics or lyrics not found!": "Σφάλμα κατά τη φόρτωση στίχων ή αδυναμία εύρεσης στίχων!", + "Create playlist": "Δημιουργία λίστας αναπαραγωγής", + "Create": "Δημιουργία", + "Add to playlist": "Προσθήκη στην λίστα αναπαραγωγής", + "Create new": "Δημιουργία νέου", + "Remove": "Αφαίρεση", + "Play next": "Παίξε αμέσως μετά", + "Add to queue": "Προσθήκη στην ουρά", + "Remove from library": "Κατάργηση από τη βιβλιοθήκη", + "Remove from playlist": "Κατάργηση από τη λίστα αναπαραγωγής", + "Play track mix": "Αναπαραγωγή μίξης τραγουδιών", + "Go to": "Πήγαινε σε", + "Track Mix": "Μίξη Τραγουδιών", + "Duration": "Διάρκεια", + "Released": "Κυκλοφόρησε", + "Disk": "Δίσκος", + "albums": "άλμπουμ", + "Play top": "Αναπαραγωγή κορυφαίου", + "Radio": "Ραδιόφωνο", + "Show all albums": "Εμφάνιση όλων των άλμπουμ", + "Show all singles": "Εμφάνιση όλων των single", + "Show more": "Εμφάνιση περισσότερων", + "Downloaded": "Ελήφθησαν", + "Queue": "Ουρά", + "Total": "Σύνολο ", + "Stop": "Διακοπή", + "Start": "Έναρξη", + "Show folder": "Εμφάνιση φακέλου", + "Clear queue": "Εκκαθάριση ουράς", + "Playing from": "Αναπαραγωγή από", + "Info": "Πληροφορίες", + "Lyrics": "Στίχοι", + "Track number": "Αριθμός τραγουδιού", + "Disk number": "Αριθμός δίσκου", + "Explicit": "Άσεμνο περιεχόμενο", + "Source": "Πηγή", + "ID": "ID", + "Error logging in!": "Σφάλμα εισόδου!", + "Please try again later, or try another account.": "Δοκιμάστε ξανά αργότερα ή δοκιμάστε έναν άλλο λογαριασμό.", + "Logout": "Αποσύνδεση", + "Login using browser": "Σύνδεση χρησιμοποιώντας το πρόγραμμα περιήγησης", + "Please login using your Deezer account:": "Συνδεθείτε χρησιμοποιώντας τον λογαριασμό σας στο Deezer:", + "...or paste your ARL/Token below:": "... ή επικολλήστε το ARL/Token σας παρακάτω:", + "ARL/Token": "ARL/Token", + "Login": "Σύνδεση", + "By using this program, you disagree with Deezer's ToS.": "Χρησιμοποιώντας αυτό το πρόγραμμα, διαφωνείτε με τους όρους χρήσης του Deezer.", + "Only in Electron version!": "Μόνο στην έκδοση Electron!", + "Search results for:": "Αποτελέσματα αναζήτησης για:", + "Error loading data!": "Σφάλμα φόρτωσης δεδομένων!", + "Try again later!": "Δοκιμάστε ξανά αργότερα!", + "Search": "Αναζήτηση", + "Streaming Quality": "Ποιότητα ροής", + "Download Quality": "Ποιότητα λήψης", + "Downloads Directory": "Κατάλογος Λήψεων", + "Simultaneous downloads": "Ταυτόχρονες λήψεις", + "Always show download confirm dialog before downloading.": "Να εμφανίζεται πάντα το παράθυρο διαλόγου επιβεβαίωσης πριν από τη λήψη.", + "Show download dialog": "Εμφάνιση παραθύρου διαλόγου επιβεβαίωσης", + "Create folders for artists": "Δημιουργία φακέλου για καλλιτέχνη", + "Create folders for albums": "Δημιουργία φακέλων για άλμπουμ", + "Download lyrics": "Λήψη στίχων", + "Variables": "Μεταβλητές", + "UI": "Περιβάλλον Χρήστη", + "Show autocomplete in search": "Εμφάνιση αυτόματων συμπληρώσεων στην αναζήτηση", + "Integrations": "Ενσωματώσεις", + "This allows listening history, flow and recommendations to work properly.": "Επιτρέπει στο ιστορικό ακρόασης, το flow και τις προτάσεις να λειτουργούν σωστά.", + "Log track listens to Deezer": "Καταγραφή ακρόασης κομματιών στο Deezer", + "Connect your LastFM account to allow scrobbling.": "Συνδέστε τον λογαριασμό σας LastFM για να επιτρέψετε το scrobbling.", + "Login with LastFM": "Σύνδεση με LastFM", + "Disconnect LastFM": "Αποσύνδεση από LastFM", + "Requires restart to apply!": "Απαιτείται επανεκκίνηση για την εφαρμογή!", + "Enable Discord Rich Presence, requires restart to toggle!": "Ενεργοποίηση Discord Rich Presence, απαιτείται επανεκκίνηση!", + "Discord Rich Presence": "Ενεργοποίηση Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Ενεργοποιήστε το κουμπί συμμετοχής Discord για συγχρονισμό κομματιών, απαιτείται επανεκκίνηση!", + "Discord Join Button": "Κουμπί συμμετοχής Discord", + "Other": "Άλλα", + "Minimize to tray": "Ελαχιστοποίηση σε εικονίδιο", + "Don't minimize to tray": "Μην ελαχιστοποιείτε σε εικονίδιο", + "Close on exit": "Κλείσιμο κατά την έξοδο", + "Settings saved!": "Οι ρυθμίσεις αποθηκεύτηκαν!", + "Available only in Electron version!": "Διαθέσιμο μόνο στην έκδοση Electron!", + "Crossfade (ms)": "Μίξη ομαλής μετάβασης (ms)", + "Select primary color": "Επιλογή κύριου χρώματος", + "Light theme": "Φωτεινό θέμα", + "Create folders for playlists": "Δημιουργία φακέλων για λίστες αναπαραγωγής", + "About": "Σχετικά", + "Links:": "Σύνδεσμοι:", + "Telegram Releases": "Κυκλοφορίες Telegram", + "Telegram Group": "Ομάδα Telegram", + "Discord": "Discord", + "Telegram Android Group": "Ομάδα Android Telegram", + "Credits:": "Συντελεστές:", + "Agree": "Αποδοχή", + "Dismiss": "Απόρριψη", + "Added to playlist!": "Προστέθηκε σε λίστα αναπαραγωγής!", + "Added to library!": "Προστέθηκε στη βιβλιοθήκη!", + "Removed from library!": "Καταργήθηκε από τη βιβλιοθήκη!", + "Removed from playlist!": "Καταργήθηκε από τη λίστα αναπαραγωγής!", + "Playlist deleted!": "Η λίστα αναπαραγωγής διαγράφηκε!", + "Delete": "Διαγραφή", + "Are you sure you want to delete this playlist?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την λίστα αναπαραγωγής;", + "Force white tray icon": "Εξαναγκασμός λευκού εικονιδίου", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Επαναφορά προεπιλογής (λευκού) εικονιδίου σε περίπτωση σφάλματος θέματος. Απαιτείται επανεκκίνηση.", + "Share": "Κοινοποίηση", + "Settings quality": "Επιλογή ρυθμίσεων ποιότητας", + "Content language": "Γλώσσα περιεχομένου", + "Content country": "Χώρα περιεχομένου", + "Website": "Ιστοσελίδα", + "Visit website": "Μετάβαση στην Ιστοσελίδα", + "New update available:": "Διαθέσιμη νέα ενημέρωση:", + "Shuffle": "Ανάμιξη", + "Download album cover": "Λήψη εξώφυλλου άλμπουμ", + "Art Resolution": "Ανάλυση εξώφυλλου", + "Public": "Δημόσιο", + "Private": "Ιδιωτικό", + "Collaborative": "Συνεργατικό", + "Edit playlist": "Επεξεργασία λίστας αναπαραγωγής", + "Save": "Αποθήκευση", + "Edit": "Επεξεργασία", + "Importer": "Εισαγωγέας", + "Enter URL": "Εισαγωγή URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Επί του παρόντος, υποστηρίζεται μόνο το Spotify και περιορίζεται σε 100 κομμάτια.", + "Import into playlist": "Εισαγωγή στη λίστα αναπαραγωγής", + "Keep sidebar open": "Κρατήστε την πλαϊνή μπάρα ανοιχτή", + "WARNING: Might require reload to work properly!": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Μπορεί να χρειαστεί επαναφόρτωση για να λειτουργήσει σωστά!", + "An error occured, URL might be invalid or unsupported.": "Παρουσιάστηκε σφάλμα, το URL ενδέχεται να μην είναι έγκυρο ή να μην υποστηρίζεται.", + "Top tracks": "Κορυφαία κομμάτια", + "Show all top tracks": "Εμφάνιση όλων των κορυφαίων κομματιών", + "Singles": "Σίνγκλ", + "Album:": "Άλμπουμ:", + "Artists:": "Καλλιτέχνες:", + "Yes": "Ναι", + "No": "Όχι", + "Download Filename": "Όνομα Αρχείου Λήψης", + "Language": "Γλώσσα", + "Background Image": "Εικόνα Φόντου", + "Enter URL or absolute path. WARNING: Requires reload!": "Εισάγετε URL ή απόλυτη διαδρομή. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Απαιτείται επαναφόρτωση!", + "LGBT Mode": "Λειτουργία LGBT", + "Native top bar": "Εγγενής επάνω γραμμή", + "Requires restart of Freezer!": "Απαιτεί επανεκκίνηση του Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/en.json b/app/client/src/locales/en.json new file mode 100644 index 0000000..fbcb3e2 --- /dev/null +++ b/app/client/src/locales/en.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Tracks", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artists", + "More": "More", + "Settings": "Settings", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Add to library", + "Download": "Download", + "fans": "fans", + "tracks": "tracks", + "Quality": "Quality", + "Estimated size:": "Estimated size:", + "Start downloading": "Start downloading", + "Cancel": "Cancel", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Create new playlist", + "TRACKS": "TRACKS", + "Sort by": "Sort by", + "Date Added": "Date Added", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Artist (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Create playlist", + "Create": "Create", + "Add to playlist": "Add to playlist", + "Create new": "Create new", + "Remove": "Remove", + "Play next": "Play next", + "Add to queue": "Add to queue", + "Remove from library": "Remove from library", + "Remove from playlist": "Remove from playlist", + "Play track mix": "Play track mix", + "Go to": "Go to", + "Track Mix": "Track Mix", + "Duration": "Duration", + "Released": "Released", + "Disk": "Disk", + "albums": "albums", + "Play top": "Play top", + "Radio": "Radio", + "Show all albums": "Show all albums", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Please try again later, or try another account.", + "Logout": "Logout", + "Login using browser": "Login using browser", + "Please login using your Deezer account:": "Please login using your Deezer account:", + "...or paste your ARL/Token below:": "...or paste your ARL/Token below:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "Search results for:", + "Error loading data!": "Error loading data!", + "Try again later!": "Try again later!", + "Search": "Search", + "Streaming Quality": "Streaming Quality", + "Download Quality": "Download Quality", + "Downloads Directory": "Downloads Directory", + "Simultaneous downloads": "Simultaneous downloads", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "Create folders for artists", + "Create folders for albums": "Create folders for albums", + "Download lyrics": "Download lyrics", + "Variables": "Variables", + "UI": "UI", + "Show autocomplete in search": "Show autocomplete in search", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "Login with LastFM", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "Requires restart to apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "Other", + "Minimize to tray": "Minimize to tray", + "Don't minimize to tray": "Don't minimize to tray", + "Close on exit": "Close on exit", + "Settings saved!": "Settings saved!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "Light theme", + "Create folders for playlists": "Create folders for playlists", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Group", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Agree", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Added to library!", + "Removed from library!": "Removed from library!", + "Removed from playlist!": "Removed from playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Share", + "Settings quality": "Settings quality", + "Content language": "Content language", + "Content country": "Content country", + "Website": "Website", + "Visit website": "Visit website", + "New update available:": "New update available:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/es.json b/app/client/src/locales/es.json new file mode 100644 index 0000000..c5f0263 --- /dev/null +++ b/app/client/src/locales/es.json @@ -0,0 +1,171 @@ +{ + "Home": "Inicio", + "Browse": "Explorar", + "Library": "Biblioteca", + "Tracks": "Canciones", + "Playlists": "Listas de reproducción", + "Albums": "Álbumes", + "Artists": "Artistas", + "More": "Más", + "Settings": "Ajustes", + "Downloads": "Descargas", + "Search or paste Deezer URL. Use / to quickly focus.": "Busca o pega la URL de Deezer. Usa \"/\" para empezar a buscar.", + "Play": "Reproducir", + "Add to library": "Agregar a la biblioteca", + "Download": "Descargar", + "fans": "seguidores", + "tracks": "canciones", + "Quality": "Calidad", + "Estimated size:": "Tamaño estimado:", + "Start downloading": "Comenzar descarga", + "Cancel": "Cancelar", + "Stream logging is disabled!": "¡El registro de reproducción está deshabilitado!", + "Enable it in settings for history to work properly.": "Habilítalo en los ajustes para que el historial funcione correctamente.", + "History": "Historial", + "Create new playlist": "Crear nueva lista de reproducción", + "TRACKS": "CANCIONES", + "Sort by": "Ordenar por", + "Date Added": "Fecha de adición", + "Name (A-Z)": "Nombre (A-Z)", + "Artist (A-Z)": "Artista (A-Z)", + "Album (A-Z)": "Álbum (A-Z)", + "Error loading lyrics or lyrics not found!": "¡Error al cargar letras o no encontradas!", + "Create playlist": "Crear lista de reproducción", + "Create": "Crear", + "Add to playlist": "Agregar a la lista de reproducción", + "Create new": "Crear nuevo", + "Remove": "Quitar", + "Play next": "Reproducir siguiente", + "Add to queue": "Agregar a la cola de reproducción", + "Remove from library": "Remover de la biblioteca", + "Remove from playlist": "Remover de la lista de reproducción", + "Play track mix": "Reproducir mezcla de canciones", + "Go to": "Ir a", + "Track Mix": "Mezcla de canciones", + "Duration": "Duración", + "Released": "Publicado", + "Disk": "Disco", + "albums": "álbumes", + "Play top": "Reproducir top", + "Radio": "Radio", + "Show all albums": "Mostrar todos los álbumes", + "Show all singles": "Mostrar todos los singles", + "Show more": "Mostrar más", + "Downloaded": "Descargadas", + "Queue": "Cola", + "Total": "Total", + "Stop": "Parar", + "Start": "Iniciar", + "Show folder": "Mostrar carpeta", + "Clear queue": "Limpiar lista", + "Playing from": "Reproduciendo desde", + "Info": "Info", + "Lyrics": "Letras", + "Track number": "Número de la canción", + "Disk number": "Número del disco", + "Explicit": "Explícito", + "Source": "Fuente", + "ID": "ID", + "Error logging in!": "¡Error al iniciar sesión!", + "Please try again later, or try another account.": "Por favor, inténtalo de nuevo más tarde, o prueba con otra cuenta.", + "Logout": "Cerrar sesión", + "Login using browser": "Iniciar sesión con navegador", + "Please login using your Deezer account:": "Por favor, inicia sesión con tu cuenta de Deezer:", + "...or paste your ARL/Token below:": "...o pega tu ARL/Token a continuación:", + "ARL/Token": "ARL/Token", + "Login": "Iniciar sesión", + "By using this program, you disagree with Deezer's ToS.": "Al usar este programa, usted no está de acuerdo con los Términos de Servicio de Deezer.", + "Only in Electron version!": "¡Sólo en la versión Electron!", + "Search results for:": "Resultados de la búsqueda para:", + "Error loading data!": "¡Error al cargar datos!", + "Try again later!": "¡Inténtalo más tarde!", + "Search": "Buscar", + "Streaming Quality": "Calidad de reproducción", + "Download Quality": "Calidad de descarga", + "Downloads Directory": "Carpeta de descargas", + "Simultaneous downloads": "Descargas simultáneas", + "Always show download confirm dialog before downloading.": "Mostrar siempre una confirmación de descarga antes de descargar.", + "Show download dialog": "Mostrar diálogo de descargas", + "Create folders for artists": "Crear carpetas por artistas", + "Create folders for albums": "Crear carpetas por álbumes", + "Download lyrics": "Descargar letras", + "Variables": "Variables", + "UI": "Interfaz de Usuario", + "Show autocomplete in search": "Mostrar autocompletado al buscar", + "Integrations": "Integraciones", + "This allows listening history, flow and recommendations to work properly.": "Esto permite registrar el historial, para que flow y las recomendaciones funcionen correctamente.", + "Log track listens to Deezer": "Registrar la canción que escucha a Deezer", + "Connect your LastFM account to allow scrobbling.": "Conecta tu cuenta de LastFM para permitir sincronizar tus canciones.", + "Login with LastFM": "Iniciar sesión con LastFM", + "Disconnect LastFM": "Desconectar LastFM", + "Requires restart to apply!": "¡Requiere reiniciar para aplicar!", + "Enable Discord Rich Presence, requires restart to toggle!": "¡Activar Rich Presence de Discord requiere reiniciar para cambiar!", + "Discord Rich Presence": "Rich Presence de Discord", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Activar el botón de unión de Discord para sincronizar las canciones, ¡requiere reiniciar para cambiarlo!", + "Discord Join Button": "Botón Unirse en Discord", + "Other": "Otro", + "Minimize to tray": "Minimizar a la bandeja del sistema", + "Don't minimize to tray": "No minimizar a la bandeja del sistema", + "Close on exit": "Cerrar al salir", + "Settings saved!": "¡Configuraciones guardadas!", + "Available only in Electron version!": "¡Disponible sólo en la versión Electron!", + "Crossfade (ms)": "Transición (ms)", + "Select primary color": "Seleccionar color primario", + "Light theme": "Tema claro", + "Create folders for playlists": "Crear carpetas para listas de reproducción", + "About": "Acerca de", + "Links:": "Enlaces:", + "Telegram Releases": "Lanzamientos en Telegram", + "Telegram Group": "Grupo en Telegram", + "Discord": "Discord", + "Telegram Android Group": "Grupo de Android en Telegram", + "Credits:": "Créditos:", + "Agree": "Acepto", + "Dismiss": "Descartar", + "Added to playlist!": "¡Añadido a la lista de reproducción!", + "Added to library!": "¡Agregado a la biblioteca!", + "Removed from library!": "¡Eliminado de la biblioteca!", + "Removed from playlist!": "¡Eliminado de la lista de reproducción!", + "Playlist deleted!": "¡Lista de reproducción eliminada!", + "Delete": "Eliminar", + "Are you sure you want to delete this playlist?": "¿Está seguro de querer eliminar la lista de reproducción?", + "Force white tray icon": "Forzar icono blanco en la bandeja", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forzar icono predeterminado en bandeja (blanco) si el tema no es detectado correctamente. Requiere reinicio.", + "Share": "Compartir", + "Settings quality": "Ajustes de calidad", + "Content language": "Idioma del contenido", + "Content country": "País del contenido", + "Website": "Sitio Web", + "Visit website": "Visita la página web", + "New update available:": "Nueva actualización disponible:", + "Shuffle": "Aleatorio", + "Download album cover": "Descargar portada del álbum", + "Art Resolution": "Resolución de Arte", + "Public": "Público", + "Private": "Privado", + "Collaborative": "Colaborativo", + "Edit playlist": "Editar lista de reproducción", + "Save": "Guardar", + "Edit": "Editar", + "Importer": "Importador", + "Enter URL": "Introducir URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Actualmente solo Spotify está soportado y limitado a 100 pistas.", + "Import into playlist": "Importar a lista de reproducción", + "Keep sidebar open": "Mantener barra lateral abierta", + "WARNING: Might require reload to work properly!": "ADVERTENCIA: ¡Puede requerir recarga para trabajar correctamente!", + "An error occured, URL might be invalid or unsupported.": "Ocurrió un error, la URL puede ser inválida o no soportada.", + "Top tracks": "Mejores canciones", + "Show all top tracks": "Mostrar las mejores canciones", + "Singles": "Sencillos", + "Album:": "Álbum:", + "Artists:": "Artistas:", + "Yes": "Sí", + "No": "No", + "Download Filename": "Nombre del archivo de descarga", + "Language": "Idioma", + "Background Image": "Imagen de fondo", + "Enter URL or absolute path. WARNING: Requires reload!": "Introducir la URL o ruta absoluta. ADVERTENCIA: ¡Necesita reiniciar!", + "LGBT Mode": "Modo LGBT", + "Native top bar": "Barra superior nativa", + "Requires restart of Freezer!": "¡Se necesita reiniciar Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/fa.json b/app/client/src/locales/fa.json new file mode 100644 index 0000000..fc13580 --- /dev/null +++ b/app/client/src/locales/fa.json @@ -0,0 +1,171 @@ +{ + "Home": "صفحه اصلی", + "Browse": "مرور", + "Library": "مجموعه", + "Tracks": "قطعه ها", + "Playlists": "لیست های پخش", + "Albums": "آلبوم ها", + "Artists": "صاحب آثار", + "More": "ادامه", + "Settings": "تنظیمات", + "Downloads": "بارگیری ها", + "Search or paste Deezer URL. Use / to quickly focus.": "جست و جو یا وارد کردن لینک دیزر، برای دقت بیشتر از \"/\" استفاده کنید.", + "Play": "پخش", + "Add to library": "به مجموعه اضافه کن", + "Download": "بارگیری", + "fans": "طرفداران", + "tracks": "قطعه ها", + "Quality": "کیفیت", + "Estimated size:": "حجم محاسبه شده:", + "Start downloading": "آغاز بارگیری", + "Cancel": "لغو", + "Stream logging is disabled!": "ثبت جریان پخش غیر فعال شده است!", + "Enable it in settings for history to work properly.": "برای کارکرد مناسب تاریخچه از بخش تنظیمات فعال کنید.", + "History": "تاریخچه", + "Create new playlist": "ایجاد لیست پخش جدید", + "TRACKS": "قطعه ها", + "Sort by": "مرتب‌سازی براساس", + "Date Added": "تاریخ اضافه شده", + "Name (A-Z)": "نام (A - Z)", + "Artist (A-Z)": "صاحب آثار (A-Z)", + "Album (A-Z)": "آلبوم (A-Z)", + "Error loading lyrics or lyrics not found!": "مشکل در بارگیری اشعار یا یافت نشد!", + "Create playlist": "ایجاد لیست پخش", + "Create": "ایجاد", + "Add to playlist": "به لیست پخش اضافه کن", + "Create new": "ایجاد جدید", + "Remove": "حذف", + "Play next": "بعد از این پخش کن", + "Add to queue": "به صف انتظار اضافه کن", + "Remove from library": "حذف از مجموعه", + "Remove from playlist": "از لیست پخش حذف شود", + "Play track mix": "پخش ترکیبی قطعه", + "Go to": "برو به", + "Track Mix": "ترکیب قطعه", + "Duration": "مدت زمان", + "Released": "منتشر شده‌ها", + "Disk": "نوار", + "albums": "آلبوم ها", + "Play top": "پخش محبوب ترین", + "Radio": "رادیو", + "Show all albums": "همه ی آلبوم ها را نشان بده", + "Show all singles": "همه ی قطعات تکی را نشان بده", + "Show more": "بیشتر نشان بده", + "Downloaded": "بارگیری شد", + "Queue": "صف انتظار", + "Total": "مجموع", + "Stop": "توقف", + "Start": "شروع", + "Show folder": "نمایش پوشه", + "Clear queue": "تخلیه صف انتظار", + "Playing from": "پخش از", + "Info": "اطلاعات", + "Lyrics": "اشعار", + "Track number": "شماره قطعه", + "Disk number": "شماره ی دیسک", + "Explicit": "فحاشی", + "Source": "منبع", + "ID": "شناسه", + "Error logging in!": "مشکل در وارد شدن!", + "Please try again later, or try another account.": "لطفا بعدا امتحان کنید یا با اکانت دیگری وارد شوید.", + "Logout": "خروج از حساب", + "Login using browser": "وارد شدن توسط مرورگر", + "Please login using your Deezer account:": "لطفاً با حساب کاربری دیزر خود وارد شوید:", + "...or paste your ARL/Token below:": "...یا با استفاده از ARL/Token پایین:", + "ARL/Token": "ARL/Token", + "Login": "وارد شدن", + "By using this program, you disagree with Deezer's ToS.": "با استفاده از این برنامه، شما قوانین دیزر را زیر پا میگذارید.", + "Only in Electron version!": "فقط در نسخه ی الکترون!", + "Search results for:": "نتایج جستجو برای:", + "Error loading data!": "مشکل در بالا آوردن اطلاعات!", + "Try again later!": "بعداً دوباره تلاش کنید!", + "Search": "جست‌وجو", + "Streaming Quality": "کیفیت پخش", + "Download Quality": "کیفیت بارگیری", + "Downloads Directory": "مسیر بارگیری", + "Simultaneous downloads": "بارگیری همزمان", + "Always show download confirm dialog before downloading.": "همیشه پیام تایید بارگیری قبل از بارگیری را نشان بده.", + "Show download dialog": "نمایش پنجره ی بارگیری", + "Create folders for artists": "برای صاحب آثار پوشه بساز", + "Create folders for albums": "برای آلبوم ها پوشه بساز", + "Download lyrics": "بارگیری اشعار", + "Variables": "متغیرها", + "UI": "واسطه کاربری", + "Show autocomplete in search": "حدس کلمه در جست و جو", + "Integrations": "یکپارچه سازی ها", + "This allows listening history, flow and recommendations to work properly.": "این کمک میکند که تاریخچه، جریان و پیشنهادات پخش به درستی کار کند.", + "Log track listens to Deezer": "بارگذاری پخش شده ها به دیزر", + "Connect your LastFM account to allow scrobbling.": "برای اسکراب کردن قطعه ها به اکانت لست اف ام متصل شوید.", + "Login with LastFM": "وارد شدن به حساب لست اف ام", + "Disconnect LastFM": "خارج شدن از حساب لست اف ام", + "Requires restart to apply!": "برای اعمال تغییرات اجرای دوباره برنامه نیاز است!", + "Enable Discord Rich Presence, requires restart to toggle!": "فعال کردن همسان سازی با دیسکورد، نیازمند اجرای دوباره!", + "Discord Rich Presence": "همسان سازی با دیسکورد", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "دکمه وارد شدن به دیسکورد را برای همسان سازی قطعه ها فعال کن، اجرای دوباره نیازمند است!", + "Discord Join Button": "دکمه وارد شدن به دیسکورد", + "Other": "ديگر", + "Minimize to tray": "کوچک کردن به Tray", + "Don't minimize to tray": "کوچک نکردن به Tray", + "Close on exit": "بستن و خروج", + "Settings saved!": "تنظیمات ذخیره شد!", + "Available only in Electron version!": "فقط در نسخه ی الکترون موجود است!", + "Crossfade (ms)": "پخش بعدی قبل از اتمام فعلی (میلی ثانیه)", + "Select primary color": "رنگ اصلی را انتخاب کنید", + "Light theme": "پوسته روشن", + "Create folders for playlists": "برای لیست پخش پوشه بساز", + "About": "درباره", + "Links:": "پیوند‌ها:", + "Telegram Releases": "عریضه های تلگرامی", + "Telegram Group": "گروه تلگرامی", + "Discord": "دیسکورد", + "Telegram Android Group": "گروه تلگرامی اندرویید", + "Credits:": "سازندگان:", + "Agree": "پذیرش", + "Dismiss": "رد", + "Added to playlist!": "به لیست پخش اضافه شد!", + "Added to library!": "به مجموعه اضافه شد!", + "Removed from library!": "از مجموعه حذف شد!", + "Removed from playlist!": "از لیست پخش حذف شد!", + "Playlist deleted!": "لیست پخش حذف شد!", + "Delete": "حذف", + "Are you sure you want to delete this playlist?": "مطمئنید که می خواهید این لیست پخش را حذف کنید?", + "Force white tray icon": "اجبار نمایش آیکون روشن در tray", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "اجبار آیکون پیشفرض (روشن) در tray زمانی که پوسته اشتباه به نمایش درآمد، نیازمند اجرای دوباره.", + "Share": "اشتراک", + "Settings quality": "تنظیمات پیشفرض", + "Content language": "زبان محتوی", + "Content country": "کشور محتوی", + "Website": "وب سایت", + "Visit website": "بازدید از وبگاه", + "New update available:": "به روز رسانی جدید در دسترس است:", + "Shuffle": "پخش تصادفی", + "Download album cover": "دانلود تصویر آلبوم", + "Art Resolution": "وضوح تصویر", + "Public": "عمومی", + "Private": "خصوصی", + "Collaborative": "چند همکاری", + "Edit playlist": "ویرایش لیست پخش", + "Save": "ذخیره", + "Edit": "ویرایش", + "Importer": "وارد کننده", + "Enter URL": "وارد کردن لینک", + "Currently only Spotify is supported and limited to 100 tracks.": "در حال حاظر فقط اسپاتیفای با محدودیت 100 قطعه ساپورت میشود.", + "Import into playlist": "وارد کردن به لیست پخش", + "Keep sidebar open": "نوار کناری را باز نگه دار", + "WARNING: Might require reload to work properly!": "اخطار: برای کارکرد مناسب نیازمند ریستارت است!", + "An error occured, URL might be invalid or unsupported.": "خطا رخ داد، لینک وارد شده اشتباه است یا پشتیبانی نمیشود.", + "Top tracks": "ترانه های برتر", + "Show all top tracks": "نمایش همه ترانه های برتر", + "Singles": "تکی ها", + "Album:": "آلبوم:", + "Artists:": "صاحب آثار:", + "Yes": "بله", + "No": "نه", + "Download Filename": "بارگیری اسم فایل", + "Language": "زبان", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/fil.json b/app/client/src/locales/fil.json new file mode 100644 index 0000000..f95d84a --- /dev/null +++ b/app/client/src/locales/fil.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Mga kanta", + "Playlists": "Mga playlist", + "Albums": "Mga album", + "Artists": "Mga artista", + "More": "More", + "Settings": "Settings", + "Downloads": "Mga download", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Idagdag sa library", + "Download": "I-download", + "fans": "fans", + "tracks": "mga kanta", + "Quality": "Kalidad", + "Estimated size:": "Tinantyang laki:", + "Start downloading": "Simulan ang download", + "Cancel": "I-kansel", + "Stream logging is disabled!": "Naka-disable ang stream logging!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Gumawa ng bagong playlist", + "TRACKS": "TRACKS", + "Sort by": "Ayusin ayon sa", + "Date Added": "Petsa kung kailan idinagdag", + "Name (A-Z)": "Pangalan (A-Z)", + "Artist (A-Z)": "Artista (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Gumawa ng playlist", + "Create": "Gumawa", + "Add to playlist": "Idagdag sa playlist", + "Create new": "Gumawa ng bago", + "Remove": "Tanggalin", + "Play next": "I-play ang kasunod", + "Add to queue": "Idagdag sa queue", + "Remove from library": "Tanggalin sa library", + "Remove from playlist": "Tanggalin mula sa playlist", + "Play track mix": "Play track mix", + "Go to": "Pumunta sa", + "Track Mix": "Track Mix", + "Duration": "Tagal", + "Released": "Released", + "Disk": "Disk", + "albums": "Mga album", + "Play top": "Play top", + "Radio": "Radyo", + "Show all albums": "Ipakita lahat ng album", + "Show all singles": "Ipakita ang lahat ng mga single", + "Show more": "Show more", + "Downloaded": "Mga na-download", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Liriko", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Pinagmulan", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Paki-subukan ulit mamaya, o mag-try ng ibang account.", + "Logout": "Mag-logout", + "Login using browser": "Mag-login gamit ang browser", + "Please login using your Deezer account:": "Paki-login ang iyong Deezer account:", + "...or paste your ARL/Token below:": "...o ilagay ang iyong ARL/Token sa baba:", + "ARL/Token": "ARL/Token", + "Login": "Mag-login", + "By using this program, you disagree with Deezer's ToS.": "Sa paggamit ng program na ito, ikaw ay hindi sumasang-ayon sa ToS ng Deezer.", + "Only in Electron version!": "Sa Electron version lamang!", + "Search results for:": "Maghanap ng resulta para sa:", + "Error loading data!": "May problema habang naglo-load ng mga datos!", + "Try again later!": "Paki-subukan ulit mamaya!", + "Search": "Maghanap", + "Streaming Quality": "Kalidad ng streaming", + "Download Quality": "Kalidad ng download", + "Downloads Directory": "Lalagyan ng mga download", + "Simultaneous downloads": "Sabay-sabay na download", + "Always show download confirm dialog before downloading.": "Laging ipakita ang confirm dialog bago mag-download.", + "Show download dialog": "Ipakita ang download dialog", + "Create folders for artists": "Gumawa ng folder para sa mga artista", + "Create folders for albums": "Gumawa ng folder para sa mga album", + "Download lyrics": "I-download ang lyrics", + "Variables": "Mga variable", + "UI": "UI", + "Show autocomplete in search": "Ipakita ang autocomplete sa search", + "Integrations": "Mga integration", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Ikabit ang iyong LastFM account para sa scrobbling.", + "Login with LastFM": "Mag-login gamit ang LastFM", + "Disconnect LastFM": "Tanggalin ang LastFM", + "Requires restart to apply!": "Kailangan i-restart para ma-apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "I-enable ang Discord Rich Presence, kailangan i-restart para mabago!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "I-enable ang Discord join button para sa pag-sync ng mga kanta, kailangan i-restart para mabago!", + "Discord Join Button": "Discord Join Button", + "Other": "Iba pa", + "Minimize to tray": "I-minimize sa tray", + "Don't minimize to tray": "Huwag i-minimize sa tray", + "Close on exit": "Isara sa pag-pindot ng X", + "Settings saved!": "Na-save ang settings!", + "Available only in Electron version!": "Meron lamang sa Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Pumili ng pangunahing kulay", + "Light theme": "Maliwanag na tema", + "Create folders for playlists": "Gumawa ng folder para sa mga playlist", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Grupo sa Telegram", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Sumang-ayon", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Idinagdag na sa library!", + "Removed from library!": "Tinanggal sa library!", + "Removed from playlist!": "Tinanggal sa playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Ibahagi", + "Settings quality": "Settings quality", + "Content language": "Wika ng nilalaman", + "Content country": "Bansa ng nilalaman", + "Website": "Website", + "Visit website": "Bisitahin ang website", + "New update available:": "May bagong update na:", + "Shuffle": "Paghaluin", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Patok na mga kanta", + "Show all top tracks": "Ipakita lahat ng mga kanta", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Oo", + "No": "Hindi", + "Download Filename": "Download Filename", + "Language": "Wika", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/fr.json b/app/client/src/locales/fr.json new file mode 100644 index 0000000..e23abc2 --- /dev/null +++ b/app/client/src/locales/fr.json @@ -0,0 +1,171 @@ +{ + "Home": "Accueil", + "Browse": "Explorer", + "Library": "Bibliothèque", + "Tracks": "Titres", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artistes", + "More": "Plus", + "Settings": "Paramètres", + "Downloads": "Téléchargements", + "Search or paste Deezer URL. Use / to quickly focus.": "Recherchez ou collez une URL Deezer. Utilisez \"/\" pour lancer rapidement la recherche.", + "Play": "Lire", + "Add to library": "Ajouter à la bibliothèque", + "Download": "Télécharger", + "fans": "fans", + "tracks": "titres", + "Quality": "Qualité", + "Estimated size:": "Durée estimée:", + "Start downloading": "Lancer le téléchargement", + "Cancel": "Annuler", + "Stream logging is disabled!": "La journalisation du stream est désactivée !", + "Enable it in settings for history to work properly.": "Activez-le dans les paramètres pour que l'historique fonctionne correctement.", + "History": "Historique", + "Create new playlist": "Créer une nouvelle playlist", + "TRACKS": "TITRES", + "Sort by": "Trier par", + "Date Added": "Ajouté le", + "Name (A-Z)": "Nom (A-Z)", + "Artist (A-Z)": "Artiste (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Erreur lors du chargement des paroles ou paroles introuvables !", + "Create playlist": "Créer une playlist", + "Create": "Créer", + "Add to playlist": "Ajouter à une playlist", + "Create new": "Créer un nouveau", + "Remove": "Supprimer", + "Play next": "Lire juste après", + "Add to queue": "Ajouter à la file d'attente", + "Remove from library": "Supprimer de la bibliothèque", + "Remove from playlist": "Supprimer de la playlist", + "Play track mix": "Jouer un mélange de titres", + "Go to": "Aller à", + "Track Mix": "Mélange de titres", + "Duration": "Durée", + "Released": "Publié", + "Disk": "Disque", + "albums": "albums", + "Play top": "Lire en haut", + "Radio": "Radio", + "Show all albums": "Afficher tous les albums", + "Show all singles": "Afficher tous les singles", + "Show more": "Afficher plus", + "Downloaded": "Téléchargés", + "Queue": "File d'attente", + "Total": "Total", + "Stop": "Arrêter", + "Start": "Lancer", + "Show folder": "Afficher le dossier", + "Clear queue": "Effacer la liste d'attente", + "Playing from": "Lecture depuis", + "Info": "Infos", + "Lyrics": "Paroles", + "Track number": "Numéro de piste", + "Disk number": "Numéro de disque", + "Explicit": "Explicite", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Erreur de connexion !", + "Please try again later, or try another account.": "Veuillez réessayer plus tard, ou essayez avec un autre compte.", + "Logout": "Déconnexion", + "Login using browser": "Connexion via navigateur", + "Please login using your Deezer account:": "Veuillez vous connecter en utilisant votre compte Deezer:", + "...or paste your ARL/Token below:": "...ou copiez votre ARL/Token ici:", + "ARL/Token": "ARL/Token", + "Login": "Connexion", + "By using this program, you disagree with Deezer's ToS.": "En utilisant ce programme, vous désagréez avec les conditions générales d'utilisation et de vente de Deezer.", + "Only in Electron version!": "Uniquement en version Electron !", + "Search results for:": "Résultats de la recherche pour:", + "Error loading data!": "Erreur lors du chargement des données !", + "Try again later!": "Réessayez plus tard !", + "Search": "Recherche", + "Streaming Quality": "Qualité en streaming", + "Download Quality": "Qualité de téléchargement", + "Downloads Directory": "Chemin de sauvegarde", + "Simultaneous downloads": "Limite téléchargements simultanés", + "Always show download confirm dialog before downloading.": "Toujours afficher une boîte de dialogue pour confirmer le téléchargement avant de le commencer.", + "Show download dialog": "Afficher une boîte de dialogue pour chaque téléchargement", + "Create folders for artists": "Générer des dossiers par artiste", + "Create folders for albums": "Générer des dossiers par album", + "Download lyrics": "Télécharger les paroles (.LRC)", + "Variables": "Variables", + "UI": "Interface", + "Show autocomplete in search": "Afficher la saisie automatique dans la recherche", + "Integrations": "Intégrations", + "This allows listening history, flow and recommendations to work properly.": "Cela permet à l'historique des titres écoutés, au flow et aux recommandations de fonctionner correctement.", + "Log track listens to Deezer": "Journaliser sur Deezer les titres écoutés", + "Connect your LastFM account to allow scrobbling.": "Connectez votre compte LastFM pour autoriser le scrobbling.", + "Login with LastFM": "Se connecter avec LastFM", + "Disconnect LastFM": "Déconnecté LastFM", + "Requires restart to apply!": "Redémarrage nécessaire pour prendre effet !", + "Enable Discord Rich Presence, requires restart to toggle!": "Activer la présence Discord, nécessite un redémarrage pour prendre effet !", + "Discord Rich Presence": "Présence sur Discord", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Activer le bouton \"rejoindre\" sur Discord pour synchroniser les titres, nécessite un redémarrage pour prendre effet !", + "Discord Join Button": "Bouton rejoindre sur Discord", + "Other": "Autre", + "Minimize to tray": "Réduire dans la zone de notification", + "Don't minimize to tray": "Ne pas réduire dans la zone de notification", + "Close on exit": "Fermer en quittant", + "Settings saved!": "Paramètres sauvegardés !", + "Available only in Electron version!": "Uniquement disponible en version Electron !", + "Crossfade (ms)": "Fondu enchaîné (ms)", + "Select primary color": "Sélectionner la couleur principale", + "Light theme": "Thème clair", + "Create folders for playlists": "Créer des dossiers par playlist", + "About": "À propos", + "Links:": "Liens:", + "Telegram Releases": "Publications Telegram", + "Telegram Group": "Groupe Telegram", + "Discord": "Discord", + "Telegram Android Group": "Groupe Telegram Android", + "Credits:": "Crédits:", + "Agree": "Accepter", + "Dismiss": "Cacher", + "Added to playlist!": "Ajouté à la playlist !", + "Added to library!": "Ajouté à la bibliothèque !", + "Removed from library!": "Supprimé de la bibliothèque !", + "Removed from playlist!": "Supprimé de la playlist !", + "Playlist deleted!": "Playlist supprimée !", + "Delete": "Supprimer", + "Are you sure you want to delete this playlist?": "Voulez-vous vraiment supprimer cette liste de lecture ?", + "Force white tray icon": "Forcer l'icône blanche dans la zone de notification", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forcer l'icône blanche par défaut dans la zone de notification si le thème n'est pas correctement détecté. Nécessite un redémarrage.", + "Share": "Partager", + "Settings quality": "Qualité définie dans les paramètres", + "Content language": "Langue du contenu", + "Content country": "Pays du contenu", + "Website": "Site internet", + "Visit website": "Visiter le site internet", + "New update available:": "Nouvelle mise à jour disponible :", + "Shuffle": "Mode aléatoire", + "Download album cover": "Télécharger la pochette de l'album", + "Art Resolution": "Résolution", + "Public": "Publique", + "Private": "Privée", + "Collaborative": "Collaborative", + "Edit playlist": "Editer la playlist", + "Save": "Enregistrer", + "Edit": "Éditer", + "Importer": "Importer", + "Enter URL": "Entrer l'URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Actuellement, seul Spotify est pris en charge et limité à 100 titres.", + "Import into playlist": "Importer dans la playlist", + "Keep sidebar open": "Garder le panneau latéral ouvert", + "WARNING: Might require reload to work properly!": "AVERTISSEMENT : Il peut être nécessaire de redémarrer pour fonctionner correctement !", + "An error occured, URL might be invalid or unsupported.": "Une erreur est survenue, l'URL est peut-être invalide ou non prise en charge.", + "Top tracks": "Titres populaires", + "Show all top tracks": "Afficher tous les titres populaires", + "Singles": "Singles", + "Album:": "Album :", + "Artists:": "Artistes :", + "Yes": "Oui", + "No": "Non", + "Download Filename": "Nom du fichier à télécharger", + "Language": "Langue", + "Background Image": "Image d'arrière-plan", + "Enter URL or absolute path. WARNING: Requires reload!": "Entrez l'URL ou le chemin absolu. ATTENTION : Nécessite un redémarrage !", + "LGBT Mode": "Mode LGBT", + "Native top bar": "Barre supérieure native", + "Requires restart of Freezer!": "Nécessite le redémarrage de Freezer !" +} \ No newline at end of file diff --git a/app/client/src/locales/he.json b/app/client/src/locales/he.json new file mode 100644 index 0000000..d6c5485 --- /dev/null +++ b/app/client/src/locales/he.json @@ -0,0 +1,171 @@ +{ + "Home": "מסך הבית", + "Browse": "חיפוש", + "Library": "ספרייה", + "Tracks": "רצועות", + "Playlists": "רשימות השמעה", + "Albums": "אלבומים", + "Artists": "אמנים", + "More": "עוד", + "Settings": "הגדרות", + "Downloads": "הורדות", + "Search or paste Deezer URL. Use / to quickly focus.": "חפש או הדבק קישור מ־Deezer. השתמש ב־\" / \" להתמקדות מהירה.", + "Play": "הפעל", + "Add to library": "הוסף לספרייה", + "Download": "הורדה", + "fans": "תומכים", + "tracks": "רצועות", + "Quality": "איכות", + "Estimated size:": "גודל משוער:", + "Start downloading": "מתחיל הורדה", + "Cancel": "ביטול", + "Stream logging is disabled!": "הזרמת רישומים מושבתת!", + "Enable it in settings for history to work properly.": "אפשר זאת בהגדרות כדי שההיסטוריה תפעל כראוי.", + "History": "היסטוריה", + "Create new playlist": "צור רשימת השמעה חדשה", + "TRACKS": "רצועות", + "Sort by": "מיון לפי", + "Date Added": "תאריך הוספה", + "Name (A-Z)": "שם (א—ת)", + "Artist (A-Z)": "אמן (א—ת)", + "Album (A-Z)": "אלבום (א—ת)", + "Error loading lyrics or lyrics not found!": "שגיאה בניסיון לטעון את המילים או שלא נמצאו!", + "Create playlist": "צור רשימת השמעה", + "Create": "צור", + "Add to playlist": "הוסף לרשימת השמעה", + "Create new": "צור חדש", + "Remove": "הסר", + "Play next": "נגן הבא בתור", + "Add to queue": "הוסף לתור", + "Remove from library": "הסר מהספרייה", + "Remove from playlist": "הסר מרשימת ההשמעה", + "Play track mix": "נגן track mix", + "Go to": "עבור אל", + "Track Mix": "תמהיל רצועות", + "Duration": "משך זמן", + "Released": "שוחרר", + "Disk": "דיסק", + "albums": "אלבומים", + "Play top": "נגן את העליון", + "Radio": "רדיו", + "Show all albums": "הצג את כל האלבומים", + "Show all singles": "הצג את כל השירים", + "Show more": "הצג עוד", + "Downloaded": "ההורדה בוצעה", + "Queue": "תור", + "Total": "סה״כ", + "Stop": "עצור", + "Start": "התחל", + "Show folder": "הצג תיקייה", + "Clear queue": "נקה תור", + "Playing from": "מנגן מ", + "Info": "מידע", + "Lyrics": "מילות שיר", + "Track number": "מספר רצועה", + "Disk number": "מספר דיסק", + "Explicit": "בוטה", + "Source": "מקור", + "ID": "מספר מזהה", + "Error logging in!": "שגיאה בהתחברות!", + "Please try again later, or try another account.": "אנא נסה שוב מאוחר יותר, או נסה חשבון אחר.", + "Logout": "התנתקות", + "Login using browser": "התחבר/י דרך הדפדפן", + "Please login using your Deezer account:": "בבקשה התחבר/י באמצעות חשבון ה־Deezer שלך:", + "...or paste your ARL/Token below:": "...או הדבק/י את ה־ARL/Token שלך כאן:", + "ARL/Token": "ARL/Token", + "Login": "התחברות", + "By using this program, you disagree with Deezer's ToS.": "בשימוש בתוכנה זו, את/ה לא מסכימ/ה עם תנאי השירות של Deezer.", + "Only in Electron version!": "רק בגרסת Electron!", + "Search results for:": "תוצאות חיפוש עבור:", + "Error loading data!": "שגיאה בטעינת הנתונים!", + "Try again later!": "נסה שוב מאוחר יותר!", + "Search": "חיפוש", + "Streaming Quality": "איכות הזרמה", + "Download Quality": "איכות הורדה", + "Downloads Directory": "מיקום ההורדות", + "Simultaneous downloads": "מספר הורדות במקביל", + "Always show download confirm dialog before downloading.": "תמיד הראה תיבת אישור הורדה לפני ההורדה.", + "Show download dialog": "הצג תיבת דו שיח להורדה", + "Create folders for artists": "צור תיקייה לאמנים", + "Create folders for albums": "צור תיקייה לאלבומים", + "Download lyrics": "הורד קובץ מילים", + "Variables": "משתנים", + "UI": "ממשק", + "Show autocomplete in search": "הצג השלמה אוטומטית בחיפוש", + "Integrations": "שילובים", + "This allows listening history, flow and recommendations to work properly.": "מאפשר להיסטוריית ההשמעות, הזרימה וההמלצות לעבוד כראוי.", + "Log track listens to Deezer": "רישומי השמעות של רצועות ב־Deezer", + "Connect your LastFM account to allow scrobbling.": "חבר/י את חשבון LastFM שלך כדי לאפשר מיזוג.", + "Login with LastFM": "התחבר/י באמצעות LastFM", + "Disconnect LastFM": "התנתקות מ־LastFM", + "Requires restart to apply!": "יש להפעיל מחדש על מנת להחיל את השינויים!", + "Enable Discord Rich Presence, requires restart to toggle!": "אפשור חיבור ל־Discord Rich Presence, מצריך הפעלה מחדש!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "אפשר את כפתור ההצטרפות של דיסקורד למטרת סנכרון רצונות. מצריך הפעלה מחדש!", + "Discord Join Button": "כפתור הצטרפות של Discord", + "Other": "אחר", + "Minimize to tray": "מזער למגש המערכת", + "Don't minimize to tray": "אל תמזער למגש המערכת", + "Close on exit": "סגור ביציאה", + "Settings saved!": "ההגדרות נשמרו בהצלחה!", + "Available only in Electron version!": "זמין רק בגרסת Electron!", + "Crossfade (ms)": "קרוספייד (מ\"ש)", + "Select primary color": "בחר צבע ראשי", + "Light theme": "ערכת נושא בהירה", + "Create folders for playlists": "צור תיקייה עבור רשימות השמעה", + "About": "אודות", + "Links:": "קישורים:", + "Telegram Releases": "שחרורי גרסאות ב־Telegram", + "Telegram Group": "קבוצת Telegram", + "Discord": "Discord", + "Telegram Android Group": "קבוצת Telegram למשתמשי Android", + "Credits:": "תודות:", + "Agree": "מסכים", + "Dismiss": "התעלם", + "Added to playlist!": "נוסף לרשימת ההשמעה!", + "Added to library!": "נוסף לספרייה!", + "Removed from library!": "הוסר מהספרייה!", + "Removed from playlist!": "הוסר מרשימת ההשמעה!", + "Playlist deleted!": "רשימת ההשמעה נמחקה!", + "Delete": "מחיקה", + "Are you sure you want to delete this playlist?": "האם את/ה בטוח/ה שברצונך למחוק את רשימת ההשמעה הזו?", + "Force white tray icon": "הכרח ערכת נושא בהירה על סמל מגש המערכת", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "הכרח ברירת מחדל (לבן) על סמל מגש המערכת אם ערכת הנושא זוהתה באופן שגוי. מצריך הפעלה מחדש.", + "Share": "שיתוף", + "Settings quality": "הגדרות איכות", + "Content language": "שפת תוכן", + "Content country": "מדינת תוכן", + "Website": "אתר", + "Visit website": "בקר באתר", + "New update available:": "גרסה חדשה זמינה:", + "Shuffle": "אקראי", + "Download album cover": "הורד עטיפת אלבום", + "Art Resolution": "רזולוציה", + "Public": "ציבורי", + "Private": "פרטי", + "Collaborative": "תורמים", + "Edit playlist": "עריכת רשימת ההשמעה", + "Save": "שמור", + "Edit": "ערוך", + "Importer": "מייבא רשימות השמעה", + "Enter URL": "הזן קישור", + "Currently only Spotify is supported and limited to 100 tracks.": "כרגע, רק Spotify נתמך ומוגבל ל־100 רצועות.", + "Import into playlist": "יבא לכדי רשימת השמעה", + "Keep sidebar open": "השאר את הסרגל פתוח", + "WARNING: Might require reload to work properly!": "אזהרה: מצריך טעינה מחדש כדי שיעבוד כראוי!", + "An error occured, URL might be invalid or unsupported.": "שגיאה התרחשה, הקישור כנראה שגוי או לא נתמך.", + "Top tracks": "רצועות מובילות", + "Show all top tracks": "הראה את כל הרצועות המובילות", + "Singles": "שירים", + "Album:": "אלבום:", + "Artists:": "אומנים:", + "Yes": "כן", + "No": "לא", + "Download Filename": "הורד/י את שם הקובץ", + "Language": "שפה", + "Background Image": "תמונת רקע", + "Enter URL or absolute path. WARNING: Requires reload!": "הזן כתובת אתר או נתיב מוחלט. אזהרה: מחייב טעינה מחדש!", + "LGBT Mode": "מצב להט\"ב", + "Native top bar": "סרגל עליון מקומי", + "Requires restart of Freezer!": "דורש הפעלה מחדש של Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/hi.json b/app/client/src/locales/hi.json new file mode 100644 index 0000000..fbcb3e2 --- /dev/null +++ b/app/client/src/locales/hi.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Tracks", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artists", + "More": "More", + "Settings": "Settings", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Add to library", + "Download": "Download", + "fans": "fans", + "tracks": "tracks", + "Quality": "Quality", + "Estimated size:": "Estimated size:", + "Start downloading": "Start downloading", + "Cancel": "Cancel", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Create new playlist", + "TRACKS": "TRACKS", + "Sort by": "Sort by", + "Date Added": "Date Added", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Artist (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Create playlist", + "Create": "Create", + "Add to playlist": "Add to playlist", + "Create new": "Create new", + "Remove": "Remove", + "Play next": "Play next", + "Add to queue": "Add to queue", + "Remove from library": "Remove from library", + "Remove from playlist": "Remove from playlist", + "Play track mix": "Play track mix", + "Go to": "Go to", + "Track Mix": "Track Mix", + "Duration": "Duration", + "Released": "Released", + "Disk": "Disk", + "albums": "albums", + "Play top": "Play top", + "Radio": "Radio", + "Show all albums": "Show all albums", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Please try again later, or try another account.", + "Logout": "Logout", + "Login using browser": "Login using browser", + "Please login using your Deezer account:": "Please login using your Deezer account:", + "...or paste your ARL/Token below:": "...or paste your ARL/Token below:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "Search results for:", + "Error loading data!": "Error loading data!", + "Try again later!": "Try again later!", + "Search": "Search", + "Streaming Quality": "Streaming Quality", + "Download Quality": "Download Quality", + "Downloads Directory": "Downloads Directory", + "Simultaneous downloads": "Simultaneous downloads", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "Create folders for artists", + "Create folders for albums": "Create folders for albums", + "Download lyrics": "Download lyrics", + "Variables": "Variables", + "UI": "UI", + "Show autocomplete in search": "Show autocomplete in search", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "Login with LastFM", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "Requires restart to apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "Other", + "Minimize to tray": "Minimize to tray", + "Don't minimize to tray": "Don't minimize to tray", + "Close on exit": "Close on exit", + "Settings saved!": "Settings saved!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "Light theme", + "Create folders for playlists": "Create folders for playlists", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Group", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Agree", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Added to library!", + "Removed from library!": "Removed from library!", + "Removed from playlist!": "Removed from playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Share", + "Settings quality": "Settings quality", + "Content language": "Content language", + "Content country": "Content country", + "Website": "Website", + "Visit website": "Visit website", + "New update available:": "New update available:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/hr.json b/app/client/src/locales/hr.json new file mode 100644 index 0000000..18f29f7 --- /dev/null +++ b/app/client/src/locales/hr.json @@ -0,0 +1,171 @@ +{ + "Home": "Početna", + "Browse": "Pretraži", + "Library": "Biblioteka", + "Tracks": "Pjesme", + "Playlists": "Popisi za reprodukciju", + "Albums": "Albumi", + "Artists": "Izvođači", + "More": "Više", + "Settings": "Postavke", + "Downloads": "Preuzimanja", + "Search or paste Deezer URL. Use / to quickly focus.": "Pretražite ili kopirajte Deezer URL. Koristite \"/\" za brzo fokusiranje.", + "Play": "Reproduciraj", + "Add to library": "Dodaj u biblioteku", + "Download": "Preuzmi", + "fans": "obožavatelji", + "tracks": "pjesme", + "Quality": "Kvaliteta", + "Estimated size:": "Predviđena veličina:", + "Start downloading": "Započni preuzimanje", + "Cancel": "Poništi", + "Stream logging is disabled!": "Bilježenje strujanja je onemogućeno!", + "Enable it in settings for history to work properly.": "Uključite u postavkama kako bi povijest funkcionirala normalno.", + "History": "Povijest", + "Create new playlist": "Kreirajte novi popis za reprodukciju", + "TRACKS": "PJESME", + "Sort by": "Sortiraj po", + "Date Added": "Datum dodavanja", + "Name (A-Z)": "Naziv (A-Z)", + "Artist (A-Z)": "Izvođač (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Pogreška kod učitavanja tekstova pjesme ili tekstovi pjesme nisu pronađeni!", + "Create playlist": "Napravi popis za reprodukciju", + "Create": "Stvori", + "Add to playlist": "Dodaj u popis za reprodukciju", + "Create new": "Stvori novi", + "Remove": "Ukloni", + "Play next": "Reproduciraj sljedeće", + "Add to queue": "Dodaj u red", + "Remove from library": "Ukloni iz biblioteke", + "Remove from playlist": "Ukloni iz popisa za reprodukciju", + "Play track mix": "Sviraj miks pjesme", + "Go to": "Idi u", + "Track Mix": "Miks pjesme", + "Duration": "Trajanje", + "Released": "Objavljeno", + "Disk": "Disk", + "albums": "albumi", + "Play top": "Sviraj najpopularnije", + "Radio": "Radio", + "Show all albums": "Prikaži sve albume", + "Show all singles": "Pokaži sve singlove", + "Show more": "Pokaži više", + "Downloaded": "Preuzeto", + "Queue": "Red", + "Total": "Ukupno", + "Stop": "Zaustavi", + "Start": "Započni", + "Show folder": "Pokaži mapu", + "Clear queue": "Očisti red", + "Playing from": "Svira iz", + "Info": "Info", + "Lyrics": "Tekst pjesme", + "Track number": "Broj pjesme", + "Disk number": "Broj diska", + "Explicit": "Eksplicitno", + "Source": "Izvor", + "ID": "ID", + "Error logging in!": "Pogreška prilikom prijavljivanja!", + "Please try again later, or try another account.": "Molimo pokušajte ponovno kasnije ili pokušajte sa drugim računom.", + "Logout": "Odjava", + "Login using browser": "Prijava pomoću preglednika", + "Please login using your Deezer account:": "Molimo vas da se prijavite pomoću vašeg Deezer računa:", + "...or paste your ARL/Token below:": "...ili zalijepite svoj ARL/Token ispod:", + "ARL/Token": "ARL/Token", + "Login": "Prijava", + "By using this program, you disagree with Deezer's ToS.": "Korištenjem ovog programa, ne prihvaćate Deezerove Uvjete pružanja usluge.", + "Only in Electron version!": "Samo u Electron verziji!", + "Search results for:": "Rezultati pretrage za:", + "Error loading data!": "Greška pri učitavanju podataka!", + "Try again later!": "Pokušajte ponovno kasnije!", + "Search": "Pretraga", + "Streaming Quality": "Kvaliteta strujanja", + "Download Quality": "Kvaliteta preuzimanja", + "Downloads Directory": "Direktorij preuzimanja", + "Simultaneous downloads": "Istovremena preuzimanja", + "Always show download confirm dialog before downloading.": "Uvijek prikaži dijaloški okvir potvrde prije preuzimanja.", + "Show download dialog": "Prikaži dijalog za preuzimanje", + "Create folders for artists": "Napravi mape za izvođače", + "Create folders for albums": "Naprave mape za albume", + "Download lyrics": "Preuzmi tekstove pjesama", + "Variables": "Varijable", + "UI": "Korisničko sučelje", + "Show autocomplete in search": "Pokaži samodovršavanje u pretrazi", + "Integrations": "Integracije", + "This allows listening history, flow and recommendations to work properly.": "Ovo omogućava da povijest slušanja, flow i preporuke rade ispravno.", + "Log track listens to Deezer": "Bilježi slušanje pjesama prema Deezeru", + "Connect your LastFM account to allow scrobbling.": "Spojite svoj LastFM račun da biste omogućili skroblanje.", + "Login with LastFM": "Prijavite se sa LastFM", + "Disconnect LastFM": "Odspojite LastFM", + "Requires restart to apply!": "Zahtjeva ponovno pokretanje da bi se primijenilo!", + "Enable Discord Rich Presence, requires restart to toggle!": "Omogući Obogaćeno Discord Prisustvo, zahtijeva ponovno pokretanje kako biste mogli prebaciti!", + "Discord Rich Presence": "Obogaćeno Discord Prisutstvo", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Omogući gumb Discord pridruživanje za sinkroniziranje pjesama, zahtijeva ponovno pokretanje kako biste mogli prebaciti!", + "Discord Join Button": "Gumb Discord pridruživanje", + "Other": "Ostalo", + "Minimize to tray": "Umanjite na alatnu traku", + "Don't minimize to tray": "Nemoj umanjiti na alatnu traku", + "Close on exit": "Zatvori na izlasku", + "Settings saved!": "Postavke spremljene!", + "Available only in Electron version!": "Dostupno samo u Electron verziji!", + "Crossfade (ms)": "Utišavanje/pretapanje (ms)", + "Select primary color": "Izaberi primarnu boju", + "Light theme": "Svijetla tema", + "Create folders for playlists": "Stvori mape za popise za reprodukciju", + "About": "O aplikaciji", + "Links:": "Poveznice:", + "Telegram Releases": "Telegram izdanja", + "Telegram Group": "Telegram grupa", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android grupa", + "Credits:": "Zasluge:", + "Agree": "Slažem se", + "Dismiss": "Odbaci", + "Added to playlist!": "Dodano u popis za reprodukciju!", + "Added to library!": "Dodano u biblioteku!", + "Removed from library!": "Uklonjeno iz biblioteke!", + "Removed from playlist!": "Uklonjeno iz popisa za reprodukciju!", + "Playlist deleted!": "Popis za reprodukciju izbrisan!", + "Delete": "Izbriši", + "Are you sure you want to delete this playlist?": "Jeste li sigurni da želite izbrisati ovaj popis za reprodukciju?", + "Force white tray icon": "Prisili bijelu ikonu u alatnoj traci", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Prisili zadanu (bijelu) ikonu alatne trake ako je tema neispravno detektirana. Zahtijeva ponovno pokretanje.", + "Share": "Podijeli", + "Settings quality": "Postavke kvalitete", + "Content language": "Jezik sadržaja", + "Content country": "Zemlja sadržaja", + "Website": "Web stranica", + "Visit website": "Posjeti web-stranicu", + "New update available:": "Dostupno je novo ažuriranje:", + "Shuffle": "Nasumično", + "Download album cover": "Skini naslovnicu albuma", + "Art Resolution": "Rezolucija naslovnice", + "Public": "Javno", + "Private": "Privatno", + "Collaborative": "Suradnja", + "Edit playlist": "Uredi popis za reprodukciju", + "Save": "Spremi", + "Edit": "Uredi", + "Importer": "Uvoznik", + "Enter URL": "Unesite URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Trenutno je podržan samo Spotify i limitiran na 100 pjesama.", + "Import into playlist": "Uvezi u popis za reprodukciju", + "Keep sidebar open": "Drži bočnu traku otvorenom", + "WARNING: Might require reload to work properly!": "UPOZORENJE: Možda će zahtijevati ponovno pokretanje da bi radilo ispravno!", + "An error occured, URL might be invalid or unsupported.": "Dogodila se pogreška, URL je neispravan ili nepodržan.", + "Top tracks": "Najslušanije pjesme", + "Show all top tracks": "Prikaži sve najslušanije pjesme", + "Singles": "Singlovi", + "Album:": "Album:", + "Artists:": "Izvođači:", + "Yes": "Da", + "No": "Ne", + "Download Filename": "Naziv datoteke preuzimanja", + "Language": "Jezik", + "Background Image": "Pozadinska slika", + "Enter URL or absolute path. WARNING: Requires reload!": "Unesire URL ili apsolutnu putanju. UPOZORENJE: Zahtijeva ponovno pokretanje!", + "LGBT Mode": "LGBT način", + "Native top bar": "Izvorna gornja traka", + "Requires restart of Freezer!": "Zahtijeva ponovno pokretanje Freezera!" +} \ No newline at end of file diff --git a/app/client/src/locales/hu.json b/app/client/src/locales/hu.json new file mode 100644 index 0000000..92e0de1 --- /dev/null +++ b/app/client/src/locales/hu.json @@ -0,0 +1,171 @@ +{ + "Home": "Kezdőlap", + "Browse": "Böngészés", + "Library": "Könyvtár", + "Tracks": "Dalok", + "Playlists": "Lejátszási listák", + "Albums": "Albumok", + "Artists": "Előadók", + "More": "Továbbiak", + "Settings": "Beállítások", + "Downloads": "Letöltések", + "Search or paste Deezer URL. Use / to quickly focus.": "Keressen, vagy illesszen be Deezer URL linket. Használja a \"/\" karaktert, hogy megfelelően rámutasson.", + "Play": "Lejátszás", + "Add to library": "Hozzáadás a könyvtárhoz", + "Download": "Letöltés", + "fans": "rajongók", + "tracks": "dalok", + "Quality": "Minőség", + "Estimated size:": "Becsült fájl méret:", + "Start downloading": "Letöltés indítása", + "Cancel": "Mégse", + "Stream logging is disabled!": "A stream naplózás le van tiltva!", + "Enable it in settings for history to work properly.": "Engedélyezze a beállításokban, hogy az előzmények megfelelően működjenek.", + "History": "Előzmények", + "Create new playlist": "Új lejátszási lista létrehozása", + "TRACKS": "DALOK", + "Sort by": "Rendezési elv", + "Date Added": "Hozzáadás dátuma", + "Name (A-Z)": "Név (A-Z)", + "Artist (A-Z)": "Előadó (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Hiba a dalszöveg betöltésekor, vagy nem található dalszöveg!", + "Create playlist": "Lejátszási lista létrehozása", + "Create": "Létrehozás", + "Add to playlist": "Hozzáadás a lejátszási listához", + "Create new": "Új létrehozása", + "Remove": "Eltávolítás", + "Play next": "Következő lejátszása", + "Add to queue": "Hozzáadás a listához", + "Remove from library": "Eltávolítás a könyvtárból", + "Remove from playlist": "Eltávolítás a lejátszási listáról", + "Play track mix": "Dal Mix lejátszása", + "Go to": "Ugrás", + "Track Mix": "Dal Mix", + "Duration": "Időtartam", + "Released": "Megjelent", + "Disk": "Lemez", + "albums": "albumok", + "Play top": "Legfelső lejátszása", + "Radio": "Rádió", + "Show all albums": "Összes album megjelenítése", + "Show all singles": "Összes dal megjelenítése", + "Show more": "Több megjelenítése", + "Downloaded": "Letöltött", + "Queue": "Várólista", + "Total": "Összes", + "Stop": "Megállít", + "Start": "Elindít", + "Show folder": "Mappa megjelenítése", + "Clear queue": "Lejátszási lista kiürítése", + "Playing from": "Lejátszás innentől", + "Info": "Információ", + "Lyrics": "Dalszöveg", + "Track number": "Dal sorszáma", + "Disk number": "Lemez száma", + "Explicit": "Szókimondó", + "Source": "Forrás", + "ID": "Azonosító", + "Error logging in!": "Hiba történt a bejelentkezéskor!", + "Please try again later, or try another account.": "Kérjük próbálja újra, vagy próbálkozzon egy másik fiókkal.", + "Logout": "Kijelentkezés", + "Login using browser": "Belépés böngészővel", + "Please login using your Deezer account:": "Kérjük lépjen be Deezer fiókjával:", + "...or paste your ARL/Token below:": "... vagy illessze be az ARL/Token-t:", + "ARL/Token": "ARL/Token", + "Login": "Belépés", + "By using this program, you disagree with Deezer's ToS.": "Szoftverünk használatával elutasítja a Deezer általános felhasználási feltételeit.", + "Only in Electron version!": "Csak az Electron verzióban elérhető!", + "Search results for:": "Keresési találatok:", + "Error loading data!": "Hiba az adatok betöltése közben!", + "Try again later!": "Kérjük próbálja meg később!", + "Search": "Keresés", + "Streaming Quality": "Streamelés minősége", + "Download Quality": "Letöltés minősége", + "Downloads Directory": "Letöltések helye", + "Simultaneous downloads": "Egyidejű letöltések", + "Always show download confirm dialog before downloading.": "Mindig mutassa a letöltési megerősítést letöltés előtt.", + "Show download dialog": "Letöltés párbeszédpanel mutatása", + "Create folders for artists": "Mappa létrehozása az előadónként", + "Create folders for albums": "Mappa létrehozása az albumonként", + "Download lyrics": "Dalszöveg letöltése", + "Variables": "Változók", + "UI": "Kezelőfelület", + "Show autocomplete in search": "Kereső találat kiegészítése", + "Integrations": "Integrációk", + "This allows listening history, flow and recommendations to work properly.": "Ezzel engedélyezi az előzményeket és, hogy a Flow illetve az ajánlások működjenek.", + "Log track listens to Deezer": "Dal előzmények rögzítése a Deezer számára", + "Connect your LastFM account to allow scrobbling.": "Csatlakoztassa LastFM fiókját, hogy engedélyezze az ajánlatokat.", + "Login with LastFM": "Belépés LastFM fiókkal", + "Disconnect LastFM": "LastFM fiók eltávolítása", + "Requires restart to apply!": "Újraindítás szükséges!", + "Enable Discord Rich Presence, requires restart to toggle!": "Discord Rich Presence engedélyezése, újraindítás szükséges!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Discord csatlakozás gomb engedélyezése dalok szinkronizálásához, újraindítás szükséges!", + "Discord Join Button": "Discord csatlakozás gomb", + "Other": "Egyéb", + "Minimize to tray": "Tálcára letétel", + "Don't minimize to tray": "Ne tegye le a tálcára", + "Close on exit": "Kilépés bezáráskor", + "Settings saved!": "Beállítások elmentve!", + "Available only in Electron version!": "Csak teljes változatban érhető el!", + "Crossfade (ms)": "Átfedés (ms)", + "Select primary color": "Elsődleges szín kiválasztása", + "Light theme": "Világos kinézet", + "Create folders for playlists": "Mappa létrehozása a lejátszólistákhoz", + "About": "Info", + "Links:": "Linkek:", + "Telegram Releases": "Telegram Kiadások", + "Telegram Group": "Telegram csoport", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android csoport", + "Credits:": "Köszönet:", + "Agree": "Elfogad", + "Dismiss": "Elvet", + "Added to playlist!": "Lejátszó listához adva!", + "Added to library!": "Könyvtárhoz adva!", + "Removed from library!": "Eltávolítva a könyvtárból!", + "Removed from playlist!": "Eltávolítva a lejátszási listából!", + "Playlist deleted!": "Lejátszási lista törölve!", + "Delete": "Töröl", + "Are you sure you want to delete this playlist?": "Biztosan ki szeretné törölni a lejátszási listát?", + "Force white tray icon": "Fehér tálcaikon használata mindenképpen", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Alap (fehér) tálcaikon használata ha a kinézet hibásan is lett észlelve. Újraindítás szükséges.", + "Share": "Megosztás", + "Settings quality": "Letöltési minőség", + "Content language": "Tartalom nyelve", + "Content country": "Tartalom országa", + "Website": "Weboldal", + "Visit website": "Weboldal megnyitása", + "New update available:": "Újabb frissítés elérhető:", + "Shuffle": "Véletlenszerű lejátszás", + "Download album cover": "Album borító letöltése", + "Art Resolution": "Borító felbontása", + "Public": "Nyilvános", + "Private": "Privát", + "Collaborative": "Együttműködés", + "Edit playlist": "Lejátszó lista szerkesztése", + "Save": "Mentés", + "Edit": "Szerkesztés", + "Importer": "Importáló", + "Enter URL": "Írjon be egy URL-t", + "Currently only Spotify is supported and limited to 100 tracks.": "Jelenleg csak a Spotify támogatott és 100 zeneszámra le van korlátozva.", + "Import into playlist": "Importálás a lejátszási listába", + "Keep sidebar open": "Oldalsó sáv nyitva tartása", + "WARNING: Might require reload to work properly!": "Figyelem: Alkalmazás újraindítása szükséges!", + "An error occured, URL might be invalid or unsupported.": "Egy hiba történet: a megadott URL érvénytelen vagy nem támogatott.", + "Top tracks": "Legjobb dalok", + "Show all top tracks": "Mutasd az összes legjobb dalt", + "Singles": "Kislemezek", + "Album:": "Album:", + "Artists:": "Előadó:", + "Yes": "Igen", + "No": "Nem", + "Download Filename": "Letöltési fájlnév", + "Language": "Nyelv", + "Background Image": "Háttérkép", + "Enter URL or absolute path. WARNING: Requires reload!": "Adjon meg URL címet vagy teljes elérési utat. FIGYELEM: Újraindítás szükséges!", + "LGBT Mode": "LGBT mód", + "Native top bar": "Natív felső sáv", + "Requires restart of Freezer!": "Újraindítás szükséges!" +} \ No newline at end of file diff --git a/app/client/src/locales/id.json b/app/client/src/locales/id.json new file mode 100644 index 0000000..b17f4ef --- /dev/null +++ b/app/client/src/locales/id.json @@ -0,0 +1,171 @@ +{ + "Home": "Beranda", + "Browse": "Telusuri", + "Library": "Koleksi", + "Tracks": "Lagu", + "Playlists": "Daftar Putar", + "Albums": "Album", + "Artists": "Artis", + "More": "Lebih banyak", + "Settings": "Pengaturan", + "Downloads": "Unduhan", + "Search or paste Deezer URL. Use / to quickly focus.": "Cari atau tempel URL Deezer. Gunakan \"/\" untuk fokus dengan cepat.", + "Play": "Putar", + "Add to library": "Tambahkan ke koleksi", + "Download": "Unduh", + "fans": "penggemar", + "tracks": "lagu", + "Quality": "Kualitas", + "Estimated size:": "Perkiraan ukuran:", + "Start downloading": "Mulai mengunduh", + "Cancel": "Batalkan", + "Stream logging is disabled!": "Catatan pemutaran di nonaktifkan!", + "Enable it in settings for history to work properly.": "Aktifkan di pengaturan agar riwayat berfungsi dengan benar.", + "History": "Riwayat", + "Create new playlist": "Buat daftar putar baru", + "TRACKS": "LAGU", + "Sort by": "Urut berdasarkan", + "Date Added": "Tanggal Ditambahkan", + "Name (A-Z)": "Nama (A-Z)", + "Artist (A-Z)": "Artis (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Gagal memuat lirik atau lirik tidak tersedia!", + "Create playlist": "Buat daftar putar", + "Create": "Buat", + "Add to playlist": "Tambahkan ke daftar putar", + "Create new": "Buat baru", + "Remove": "Hapus", + "Play next": "Putar selanjutnya", + "Add to queue": "Tambahkan ke antrean", + "Remove from library": "Hapus dari koleksi", + "Remove from playlist": "Hapus dari daftar putar", + "Play track mix": "Putar lagu campuran", + "Go to": "Pergi ke", + "Track Mix": "Lagu Campuran", + "Duration": "Durasi", + "Released": "Dirilis", + "Disk": "Disk", + "albums": "album", + "Play top": "Mainkan populer", + "Radio": "Radio", + "Show all albums": "Tampilkan semua album", + "Show all singles": "Tampilkan semua single", + "Show more": "Tampilkan lebih banyak", + "Downloaded": "Terunduh", + "Queue": "Antrean", + "Total": "Jumlah", + "Stop": "Berhenti", + "Start": "Mulai", + "Show folder": "Tampilkan folder", + "Clear queue": "Bersihkan antrean", + "Playing from": "Memainkan dari", + "Info": "Info", + "Lyrics": "Lirik", + "Track number": "Nomor lagu", + "Disk number": "Nomor disk", + "Explicit": "Eksplisit", + "Source": "Sumber", + "ID": "ID", + "Error logging in!": "Gagal masuk!", + "Please try again later, or try another account.": "Coba lagi nanti, atau coba akun lain.", + "Logout": "Keluar", + "Login using browser": "Masuk menggunakan browser", + "Please login using your Deezer account:": "Silakan masuk menggunakan akun Deezer anda:", + "...or paste your ARL/Token below:": "...atau tempelkan ARL/Token dibawah ini:", + "ARL/Token": "ARL/Token", + "Login": "Masuk", + "By using this program, you disagree with Deezer's ToS.": "Dengan menggunakan program ini, kamu tidak setuju dengan ToS Deezer.", + "Only in Electron version!": "Hanya dalam versi Electron!", + "Search results for:": "Hasil pencarian untuk:", + "Error loading data!": "Gagal memuat data!", + "Try again later!": "Coba lagi nanti!", + "Search": "Cari", + "Streaming Quality": "Kualitas pemutaran", + "Download Quality": "Kualitas unduhan", + "Downloads Directory": "Folder Unduhan", + "Simultaneous downloads": "Unduhan serentak", + "Always show download confirm dialog before downloading.": "Selalu tampilkan dialog konfirmasi unduhan sebelum mengunduh.", + "Show download dialog": "Tampilkan dialog unduhan", + "Create folders for artists": "Buat folder untuk artis", + "Create folders for albums": "Buat folder untuk album", + "Download lyrics": "Unduh lirik", + "Variables": "Variabel", + "UI": "UI", + "Show autocomplete in search": "Tampilkan isi otomatis di pencarian", + "Integrations": "Integrasi", + "This allows listening history, flow and recommendations to work properly.": "Hal ini memungkinkan riwayat mendengarkan, aliran, dan rekomendasi berfungsi dengan baik.", + "Log track listens to Deezer": "Catat aktifitas mendengarkan lagu ke Deezer", + "Connect your LastFM account to allow scrobbling.": "Hubungkan ke akun LastFM mu untuk mengijinkan scrobbling.", + "Login with LastFM": "Masuk dengan LastFM", + "Disconnect LastFM": "Putuskan LastFM", + "Requires restart to apply!": "Mulai ulang untuk menerapkan!", + "Enable Discord Rich Presence, requires restart to toggle!": "Aktifkan Discord Rich Presence, perlu dimulai ulang untuk beralih!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Aktifkan tombol bergabung Discord untuk menyinkronkan lagu, perlu dimulai ulang untuk beralih!", + "Discord Join Button": "Tombol Bergabung Discord", + "Other": "Lainnya", + "Minimize to tray": "Minimalkan ke Tray", + "Don't minimize to tray": "Jangan minimalkan ke tray", + "Close on exit": "Tutup saat keluar", + "Settings saved!": "Pengaturan tersimpan!", + "Available only in Electron version!": "Hanya tersedia di versi Electron!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Pilih warna utama", + "Light theme": "Tema cerah", + "Create folders for playlists": "Buat folder untuk daftar putar", + "About": "Tentang", + "Links:": "Tautan:", + "Telegram Releases": "Telegram Rilis", + "Telegram Group": "Telegram Grub", + "Discord": "Discord", + "Telegram Android Group": "Telegram Grub Android", + "Credits:": "Kredit:", + "Agree": "Setuju", + "Dismiss": "Abaikan", + "Added to playlist!": "Ditambahkan ke daftar putar!", + "Added to library!": "Ditambahkan ke koleksi!", + "Removed from library!": "Dihapus dari koleksi!", + "Removed from playlist!": "Dihapus dari daftar putar!", + "Playlist deleted!": "Daftar putar dihapus!", + "Delete": "Hapus", + "Are you sure you want to delete this playlist?": "Apakah kamu yakin ingin menghapus daftar putar ini?", + "Force white tray icon": "Paksa ikon baki putih", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Paksa default (putih) ikon baki jika tema tidak terdeteksi dengan benar. Membutuhkan restart.", + "Share": "Bagikan", + "Settings quality": "Pengaturan kualitas", + "Content language": "Bahasa konten", + "Content country": "Wilayah konten", + "Website": "Situs", + "Visit website": "Kunjungi situs web", + "New update available:": "Pembaruan tersedia:", + "Shuffle": "Putar acak", + "Download album cover": "Unduh cover album", + "Art Resolution": "Resolusi gambar", + "Public": "Publik", + "Private": "Pribadi", + "Collaborative": "Kolaboratif", + "Edit playlist": "Edit daftar putar", + "Save": "Simpan", + "Edit": "Edit", + "Importer": "Telah di impor", + "Enter URL": "Masukkan URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Saat ini hanya mendukung Spotify dan hanya terbatas 100 lagu.", + "Import into playlist": "Impor ke daftar putar", + "Keep sidebar open": "Biarkan sidebar tetap terbuka", + "WARNING: Might require reload to work properly!": "PERINGATAN: Mungkin perlu memuat ulang agar berfungsi dengan benar!", + "An error occured, URL might be invalid or unsupported.": "Terjadi kesalahan, URL mungkin tidak valid atau tidak didukung.", + "Top tracks": "Lagu teratas", + "Show all top tracks": "Tampilkan semua lagu teratas", + "Singles": "Single", + "Album:": "Album:", + "Artists:": "Artis:", + "Yes": "Iya", + "No": "Tidak", + "Download Filename": "Unduh Nama File", + "Language": "Bahasa", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/it.json b/app/client/src/locales/it.json new file mode 100644 index 0000000..84eca59 --- /dev/null +++ b/app/client/src/locales/it.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Sfoglia", + "Library": "Libreria", + "Tracks": "Brani", + "Playlists": "Playlist", + "Albums": "Album", + "Artists": "Artisti", + "More": "Altro", + "Settings": "Impostazioni", + "Downloads": "Download", + "Search or paste Deezer URL. Use / to quickly focus.": "Cerca qui o incolla un URL di Deezer. Usa \"/\" per mettere a fuoco questa barra.", + "Play": "Play", + "Add to library": "Aggiungi alla libreria", + "Download": "Scarica", + "fans": "fan", + "tracks": "brani", + "Quality": "Qualità", + "Estimated size:": "Dimensione stimata:", + "Start downloading": "Inizia il download", + "Cancel": "Annulla", + "Stream logging is disabled!": "Il logging delle stream è disabilitato!", + "Enable it in settings for history to work properly.": "Abilitalo nelle impostazioni per permettere alla cronologia di funzionare correttamente.", + "History": "Cronologia", + "Create new playlist": "Crea una playlist", + "TRACKS": "BRANI", + "Sort by": "Ordina per", + "Date Added": "Data di aggiunta", + "Name (A-Z)": "Nome (A-Z)", + "Artist (A-Z)": "Artista (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Errore nel caricare i testi o testi non trovati!", + "Create playlist": "Crea playlist", + "Create": "Crea", + "Add to playlist": "Aggiungi a playlist", + "Create new": "Crea nuova", + "Remove": "Rimuovi", + "Play next": "Riproduci subito dopo", + "Add to queue": "Aggiungi alla coda", + "Remove from library": "Rimuovi dalla libreria", + "Remove from playlist": "Rimuovi dalla playlist", + "Play track mix": "Riproduci mix di brani", + "Go to": "Vai a", + "Track Mix": "Mix Di Tracce", + "Duration": "Durata", + "Released": "Data di uscita", + "Disk": "Disco", + "albums": "album", + "Play top": "Riproduci dall'inizio", + "Radio": "Radio", + "Show all albums": "Mostra tutti gli album", + "Show all singles": "Mostra tutti i singoli", + "Show more": "Mostra di più", + "Downloaded": "Scaricato", + "Queue": "Coda", + "Total": "Totale", + "Stop": "Stop", + "Start": "Avvia", + "Show folder": "Mostra cartella", + "Clear queue": "Pulisci la coda", + "Playing from": "Riproduzione da", + "Info": "Informazioni", + "Lyrics": "Testo", + "Track number": "Numero della traccia", + "Disk number": "Numero del disco", + "Explicit": "Esplicito", + "Source": "Fonte", + "ID": "ID", + "Error logging in!": "Errore durante il login!", + "Please try again later, or try another account.": "Riprova più tardi, o prova un altro account.", + "Logout": "Disconnettiti", + "Login using browser": "Accedi utilizzando il browser", + "Please login using your Deezer account:": "Effettua il login usando il tuo account Deezer:", + "...or paste your ARL/Token below:": "...o incolla il tuo ARL/Token qui sotto:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "Utilizzando questo programma, non sei d'accordo con il ToS di Deezer.", + "Only in Electron version!": "Solo nella versione Electron!", + "Search results for:": "Risultati della ricerca per:", + "Error loading data!": "Errore nel caricamento dei dati!", + "Try again later!": "Riprova più tardi!", + "Search": "Cerca", + "Streaming Quality": "Qualità Streaming", + "Download Quality": "Qualità Download", + "Downloads Directory": "Cartella Download", + "Simultaneous downloads": "Download simultanei", + "Always show download confirm dialog before downloading.": "Mostra sempre la finestra di conferma download prima di scaricare.", + "Show download dialog": "Mostra finestra di download", + "Create folders for artists": "Crea cartelle per gli artisti", + "Create folders for albums": "Crea cartelle per gli album", + "Download lyrics": "Scarica testi .LRC", + "Variables": "Variabili", + "UI": "Interfaccia", + "Show autocomplete in search": "Mostra elenco autocompletamento", + "Integrations": "Integrazioni", + "This allows listening history, flow and recommendations to work properly.": "Abilitalo se vuoi che la cronologia, il Flow e i raccomandazioni funzionino correttamente.", + "Log track listens to Deezer": "Logging cronologia degli ascolti", + "Connect your LastFM account to allow scrobbling.": "Collega il tuo account LastFM per consentire lo scrobbling.", + "Login with LastFM": "Accedi con LastFM", + "Disconnect LastFM": "Disconnetti LastFM", + "Requires restart to apply!": "Richiede un riavvio!", + "Enable Discord Rich Presence, requires restart to toggle!": "Abilita Discord Rich Presence, richiede il riavvio!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Abilita il pulsante \"join\" di Discord per sincronizzare le tracce, richiede il riavvio!", + "Discord Join Button": "Pulsante Unisciti di Discord", + "Other": "Altro", + "Minimize to tray": "Minimizza ad icona", + "Don't minimize to tray": "Non minimizzare a icona", + "Close on exit": "Chiudi all'uscita", + "Settings saved!": "Impostazioni salvate!", + "Available only in Electron version!": "Disponibile solo nella versione Electron!", + "Crossfade (ms)": "Dissolvenza (ms)", + "Select primary color": "Seleziona colore principale", + "Light theme": "Tema chiaro", + "Create folders for playlists": "Crea cartelle per le playlist", + "About": "Informazioni", + "Links:": "Link:", + "Telegram Releases": "Canale Telegram (releases)", + "Telegram Group": "Gruppo Telegram", + "Discord": "Discord", + "Telegram Android Group": "Gruppo Telegram per Android", + "Credits:": "Crediti:", + "Agree": "Accetta", + "Dismiss": "Chiudi", + "Added to playlist!": "Aggiunto alla playlist!", + "Added to library!": "Aggiunto alla libreria!", + "Removed from library!": "Rimosso dalla libreria!", + "Removed from playlist!": "Rimosso dalla playlist!", + "Playlist deleted!": "Playlist eliminata!", + "Delete": "Elimina", + "Are you sure you want to delete this playlist?": "Sei sicuro di voler eliminare questa playlist?", + "Force white tray icon": "Forza icona bianca nell'area notifiche", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forza l'icona predefinita (bianca) nell'area notifiche se il tema è stato rilevato in modo errato. Richiede il riavvio.", + "Share": "Condividi", + "Settings quality": "Qualità delle Impostazioni", + "Content language": "Lingua dei contenuti", + "Content country": "Paese dei contenuti", + "Website": "Sito Web", + "Visit website": "Visita il sito", + "New update available:": "Nuovo aggiornamento disponibile:", + "Shuffle": "Riproduzione casuale", + "Download album cover": "Scarica copertina album", + "Art Resolution": "Risoluzione immagini", + "Public": "Pubblica", + "Private": "Privata", + "Collaborative": "Collaborativa", + "Edit playlist": "Modifica playlist", + "Save": "Salva", + "Edit": "Modifica", + "Importer": "Importazione", + "Enter URL": "Inserisci URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Attualmente solo Spotify è supportato e limitato a 100 brani.", + "Import into playlist": "Importa nella playlist", + "Keep sidebar open": "Mantieni la barra laterale aperta", + "WARNING: Might require reload to work properly!": "ATTENZIONE: Potrebbe essere necessario ricaricare per funzionare correttamente!", + "An error occured, URL might be invalid or unsupported.": "Si è verificato un errore, l'URL potrebbe non essere valido o non supportato.", + "Top tracks": "Tracce più ascoltate", + "Show all top tracks": "Mostra tutte le tracce più ascoltate", + "Singles": "Singoli", + "Album:": "Album:", + "Artists:": "Artisti:", + "Yes": "Sì", + "No": "No", + "Download Filename": "Nome del file scaricato", + "Language": "Lingua", + "Background Image": "Immagine di sfondo", + "Enter URL or absolute path. WARNING: Requires reload!": "Inserisci l'URL o il percorso assoluto. ATTENZIONE: Richiede il ricaricamento!", + "LGBT Mode": "Modalità LGBT", + "Native top bar": "Barra superiore nativa", + "Requires restart of Freezer!": "Richiede il riavvio di Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/ko.json b/app/client/src/locales/ko.json new file mode 100644 index 0000000..1a792c9 --- /dev/null +++ b/app/client/src/locales/ko.json @@ -0,0 +1,171 @@ +{ + "Home": "홈", + "Browse": "탐색", + "Library": "라이브러리", + "Tracks": "트랙", + "Playlists": "재생목록", + "Albums": "앨범", + "Artists": "아티스트", + "More": "더 보기", + "Settings": "설정", + "Downloads": "다운로드", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "재생", + "Add to library": "라이브러리에 추가", + "Download": "다운로드", + "fans": "fans", + "tracks": "트랙", + "Quality": "음질", + "Estimated size:": "예상 용량:", + "Start downloading": "다운로드 시작", + "Cancel": "취소", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "기록", + "Create new playlist": "새 재생목록 만들기", + "TRACKS": "트랙", + "Sort by": "정렬 기준", + "Date Added": "추가된 날짜", + "Name (A-Z)": "이름 (A-Z)", + "Artist (A-Z)": "아티스트 (A-Z)", + "Album (A-Z)": "앨범 (A-Z)", + "Error loading lyrics or lyrics not found!": "가사 로딩 오류 또는 가사 없음!", + "Create playlist": "재생목록 생성", + "Create": "생성", + "Add to playlist": "재생목록에 추가", + "Create new": "새로 만들기", + "Remove": "제거", + "Play next": "다음 재생", + "Add to queue": "대기열에 추가", + "Remove from library": "라이브러리에서 삭제", + "Remove from playlist": "재생목록에서 삭제", + "Play track mix": "Play track mix", + "Go to": "$로 이동", + "Track Mix": "Track Mix", + "Duration": "길이", + "Released": "출시됨", + "Disk": "Disk", + "albums": "앨범", + "Play top": "Play top", + "Radio": "라디오", + "Show all albums": "모든 앨범 보기", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "총", + "Stop": "정지", + "Start": "시작", + "Show folder": "폴더 보기", + "Clear queue": "대기열 비우기", + "Playing from": "$부터 재생:", + "Info": "정보", + "Lyrics": "가사", + "Track number": "트랙 번호", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "아이디", + "Error logging in!": "로그인 오류", + "Please try again later, or try another account.": "나중에 시도하거나 다른 계정으로 시도해주세요.", + "Logout": "로그아웃", + "Login using browser": "브라우저를 사용하여 로그인", + "Please login using your Deezer account:": "Deezer 계정을 사용하여 로그인하십시오.", + "...or paste your ARL/Token below:": "...혹은 아래에 ARL/Token을 붙여넣으세요", + "ARL/Token": "ARL/Token", + "Login": "로그인", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "다음에 대한 검색 결과:", + "Error loading data!": "데이터 로딩 오류!", + "Try again later!": "나중에 다시 시도해주세요!", + "Search": "검색", + "Streaming Quality": "스트리밍 음질", + "Download Quality": "다운로드 음질", + "Downloads Directory": "다운로드 위치", + "Simultaneous downloads": "동시 다운로드", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "가수 용 폴더 만들기", + "Create folders for albums": "앨범 용 폴더 만들기", + "Download lyrics": "가사 다운로드", + "Variables": "변수", + "UI": "인터페이스", + "Show autocomplete in search": "검색에 자동완성 보이기", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "LastFM에 로그인", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "변경사항을 적용하려면 재시작이 필요합니다!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "기타", + "Minimize to tray": "트레이로 최소화", + "Don't minimize to tray": "트레이로 최소화하지 않기", + "Close on exit": "종료시 닫기", + "Settings saved!": "설정이 저장되었습니다!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "밝은 테마", + "Create folders for playlists": "플레이리스트 용 폴더 만들기", + "About": "정보", + "Links:": "링크:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "텔레그램 그룹", + "Discord": "Discord", + "Telegram Android Group": "안드로이드 텔레그램 그룹", + "Credits:": "크레딧:", + "Agree": "동의", + "Dismiss": "무시하기", + "Added to playlist!": "재생목록에 추가되었습니다!", + "Added to library!": "라이브러리에 추가되었습니다!", + "Removed from library!": "라이브러리에서 삭제되었습니다!", + "Removed from playlist!": "재생목록에서 삭제되었습니다!", + "Playlist deleted!": "재생목록이 삭제되었습니다!", + "Delete": "삭제", + "Are you sure you want to delete this playlist?": "이 재생목록을 삭제하시겠습니까?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "공유", + "Settings quality": "Settings quality", + "Content language": "콘텐츠 언어", + "Content country": "콘텐츠 국가", + "Website": "홈페이지", + "Visit website": "홈페이지 방문하기", + "New update available:": "사용 가능한 업데이트가 있습니다:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/nl.json b/app/client/src/locales/nl.json new file mode 100644 index 0000000..86140ef --- /dev/null +++ b/app/client/src/locales/nl.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Bladeren", + "Library": "Bibliotheek", + "Tracks": "Nummers", + "Playlists": "Afspeellijsten", + "Albums": "Albums", + "Artists": "Artiesten", + "More": "Meer", + "Settings": "Instellingen", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Zoek of plak Deezer URL. Gebruik \"/\" om snel te focussen.", + "Play": "Afspelen", + "Add to library": "Aan bibliotheek toevoegen", + "Download": "Downloaden", + "fans": "fans", + "tracks": "nummers", + "Quality": "Kwaliteit", + "Estimated size:": "Geschatte grootte:", + "Start downloading": "Downloaden starten", + "Cancel": "Annuleren", + "Stream logging is disabled!": "Stream logboek is uitgeschakeld!", + "Enable it in settings for history to work properly.": "Schakel het in bij instellingen om de geschiedenis correct te laten werken.", + "History": "Geschiedenis", + "Create new playlist": "Nieuwe afspeellijst aanmaken", + "TRACKS": "NUMMERS", + "Sort by": "Sorteren op", + "Date Added": "Datum Toegevoegd", + "Name (A-Z)": "Naam (A-Z)", + "Artist (A-Z)": "Artiest (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Fout bij het laden van songteksten of songteksten niet gevonden!", + "Create playlist": "Afspeellijst aanmaken", + "Create": "Aanmaken", + "Add to playlist": "Aan afspeellijst toevoegen", + "Create new": "Nieuw aanmaken", + "Remove": "Verwijderen", + "Play next": "Volgende nummer afspelen", + "Add to queue": "Aan wachtrij toevoegen", + "Remove from library": "Uit bibliotheek verwijderen", + "Remove from playlist": "Uit afspeellijst verwijderen", + "Play track mix": "Speel nummer mix", + "Go to": "Ga naar", + "Track Mix": "Nummer Mix", + "Duration": "Tijdsduur", + "Released": "Gepubliceerd", + "Disk": "Schijf", + "albums": "albums", + "Play top": "Top nummers afspelen", + "Radio": "Radio", + "Show all albums": "Alle albums tonen", + "Show all singles": "Alle singles weergeven", + "Show more": "Meer tonen", + "Downloaded": "Gedownload", + "Queue": "Wachtrij", + "Total": "Totaal", + "Stop": "Stoppen", + "Start": "Starten", + "Show folder": "Map weergeven", + "Clear queue": "Wachtrij wissen", + "Playing from": "Afspelen van", + "Info": "Informatie", + "Lyrics": "Songteksten", + "Track number": "Liednummer", + "Disk number": "Schijfnummer", + "Explicit": "Expliciet", + "Source": "Bron", + "ID": "ID", + "Error logging in!": "Fout bij aanmelden!", + "Please try again later, or try another account.": "Probeer het later opnieuw of probeer een andere account.", + "Logout": "Afmelden", + "Login using browser": "Aanmelden via browser", + "Please login using your Deezer account:": "Log in met je Deezer account:", + "...or paste your ARL/Token below:": "...of plak je ARL/Token hieronder:", + "ARL/Token": "ARL/Token", + "Login": "Aanmelden", + "By using this program, you disagree with Deezer's ToS.": "Door dit programma te gebruiken, ben je het niet eens met Deezer's ToS.", + "Only in Electron version!": "Alleen in de Electron-versie!", + "Search results for:": "Zoekresultaten voor:", + "Error loading data!": "Fout bij laden van gegevens!", + "Try again later!": "Probeer het later opnieuw!", + "Search": "Zoeken", + "Streaming Quality": "Stream Kwaliteit", + "Download Quality": "Download Kwaliteit", + "Downloads Directory": "Download map", + "Simultaneous downloads": "Gelijktijdige downloads", + "Always show download confirm dialog before downloading.": "Toon altijd het download bevestigingsvenster voordat je downloadt.", + "Show download dialog": "Downloadvenster tonen", + "Create folders for artists": "Mappen voor artiest aanmaken", + "Create folders for albums": "Mappen voor albums aanmaken", + "Download lyrics": "Songteksten downloaden", + "Variables": "Variabelen", + "UI": "Gebruikersinterface", + "Show autocomplete in search": "Automatisch aanvullen weergeven bij zoeken", + "Integrations": "Integraties", + "This allows listening history, flow and recommendations to work properly.": "Dit zorgt ervoor dat je luistergeschiedenis, flow and aanbevelingen correct werken.", + "Log track listens to Deezer": "Log geluisterde nummers naar Deezer", + "Connect your LastFM account to allow scrobbling.": "Koppel je LastFM account om scrobbling toe te staan.", + "Login with LastFM": "Aanmelden bij LastFM", + "Disconnect LastFM": "Afbreken van LastFM-connectie", + "Requires restart to apply!": "Vereist herstart om toe te passen!", + "Enable Discord Rich Presence, requires restart to toggle!": "Schakel Discord Rich Presence in, herstart vereist om het in te schakelen!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Schakel Discord-join knop in voor het synchroniseren van nummers, herstart vereist om het in te schakelen!", + "Discord Join Button": "Discord Join Knop", + "Other": "Overig", + "Minimize to tray": "Minimaliseer naar systeemvak", + "Don't minimize to tray": "Niet minimaliseren naar systeemvak", + "Close on exit": "Sluiten bij afsluiten", + "Settings saved!": "Instellingen opgeslagen!", + "Available only in Electron version!": "Alleen beschikbaar in de Electron versie!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Selecteer primaire kleur", + "Light theme": "Lichte thema", + "Create folders for playlists": "Mappen voor afspeellijsten maken", + "About": "Over", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Groep", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Groep", + "Credits:": "Met dank aan:", + "Agree": "Akkoord gaan", + "Dismiss": "Negeren", + "Added to playlist!": "Aan afspeellijst toegevoegd!", + "Added to library!": "Aan bibliotheek toegevoegd!", + "Removed from library!": "Uit de bibliotheek verwijderd!", + "Removed from playlist!": "Uit afspeellijst verwijderd!", + "Playlist deleted!": "Afspeellijst verwijderd!", + "Delete": "Verwijderen", + "Are you sure you want to delete this playlist?": "Weet je zeker dat je deze afspeellijst wil verwijderen?", + "Force white tray icon": "Forceer wit systeemvak icoon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forceer standaard (wit) pictogram in het systeemvak als thema onjuist gedetecteerd wordt. Herstart vereist.", + "Share": "Delen", + "Settings quality": "Kwaliteit instellingen", + "Content language": "Taal van inhoud", + "Content country": "Land van inhoud", + "Website": "Website", + "Visit website": "Website bezoeken", + "New update available:": "Nieuwe update beschikbaar:", + "Shuffle": "Shuffle", + "Download album cover": "Albumhoes downloaden", + "Art Resolution": "Illustratie resolutie", + "Public": "Openbaar", + "Private": "Privé", + "Collaborative": "Samenwerkend", + "Edit playlist": "Afspeellijst bewerken", + "Save": "Opslaan", + "Edit": "Bewerken", + "Importer": "Importeerder", + "Enter URL": "URL invoeren", + "Currently only Spotify is supported and limited to 100 tracks.": "Momenteel wordt alleen Spotify ondersteund; deze is beperkt tot 100 nummers.", + "Import into playlist": "In afspeellijst importeren", + "Keep sidebar open": "Zijbalk open laten", + "WARNING: Might require reload to work properly!": "WAARSCHUWING: Herladen mogelijk vereist om correct te werken!", + "An error occured, URL might be invalid or unsupported.": "Er is een fout opgetreden, URL kan ongeldig zijn of wordt niet ondersteund.", + "Top tracks": "Top nummers", + "Show all top tracks": "Alle topnummers weergeven", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artiesten:", + "Yes": "Ja", + "No": "Nee", + "Download Filename": "Bestandsnaam Downloaden", + "Language": "Taal", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/pl.json b/app/client/src/locales/pl.json new file mode 100644 index 0000000..ccbe9fd --- /dev/null +++ b/app/client/src/locales/pl.json @@ -0,0 +1,171 @@ +{ + "Home": "Strona główna", + "Browse": "Przeglądaj", + "Library": "Biblioteka", + "Tracks": "Utwory", + "Playlists": "Playlisty", + "Albums": "Albumy", + "Artists": "Wykonawcy", + "More": "Więcej", + "Settings": "Ustawienia", + "Downloads": "Pobrane", + "Search or paste Deezer URL. Use / to quickly focus.": "Wyszukaj lub wklej adres URL Deezera. Wciśnij \"/\" aby szybko uaktywnić pasek wyszukiwania.", + "Play": "Odtwórz", + "Add to library": "Dodaj do biblioteki", + "Download": "Pobierz", + "fans": "fani", + "tracks": "utwory", + "Quality": "Jakość", + "Estimated size:": "Szacowany rozmiar:", + "Start downloading": "Rozpocznij pobieranie", + "Cancel": "Anuluj", + "Stream logging is disabled!": "Rejestrowanie strumieniowania jest wyłączone!", + "Enable it in settings for history to work properly.": "Włącz to w ustawieniach, aby historia działała prawidłowo.", + "History": "Historia", + "Create new playlist": "Utwórz nową playlistę", + "TRACKS": "UTWORY", + "Sort by": "Sortuj wg", + "Date Added": "Data dodania", + "Name (A-Z)": "Nazwa (A-Z)", + "Artist (A-Z)": "Artysta (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Wystąpił błąd podczas ładowania tekstu lub tekst nie został znaleziony!", + "Create playlist": "Utwórz playlistę", + "Create": "Utwórz", + "Add to playlist": "Dodaj do playlisty", + "Create new": "Utwórz nową", + "Remove": "Usuń", + "Play next": "Odtwarzaj następne", + "Add to queue": "Dodaj do kolejki", + "Remove from library": "Usuń z biblioteki", + "Remove from playlist": "Usuń z playlisty", + "Play track mix": "Odtwarzaj losowo", + "Go to": "Przejdź do", + "Track Mix": "Mieszaj utwory", + "Duration": "Czas trwania", + "Released": "Wydano", + "Disk": "Płyta", + "albums": "albumy", + "Play top": "Odtwarzaj topkę", + "Radio": "Radio", + "Show all albums": "Pokaż wszystkie albumy", + "Show all singles": "Pokaż wszystkie single", + "Show more": "Pokaż więcej", + "Downloaded": "Pobrane", + "Queue": "Kolejka", + "Total": "Łącznie", + "Stop": "Zatrzymaj", + "Start": "Rozpocznij", + "Show folder": "Pokaż folder", + "Clear queue": "Wyczyść kolejkę", + "Playing from": "Odtwarzanie z", + "Info": "Info", + "Lyrics": "Tekst", + "Track number": "Numer utworu", + "Disk number": "Numer płyty", + "Explicit": "Wulgarne", + "Source": "Źródło", + "ID": "ID", + "Error logging in!": "Błąd podczas logowania!", + "Please try again later, or try another account.": "Spróbuj ponownie później lub spróbuj innego konta.", + "Logout": "Wyloguj", + "Login using browser": "Zaloguj się za pomocą przeglądarki", + "Please login using your Deezer account:": "Zaloguj się używając swojego konta Deezer:", + "...or paste your ARL/Token below:": "...lub wklej ARL/Token poniżej:", + "ARL/Token": "ARL/Token", + "Login": "Zaloguj", + "By using this program, you disagree with Deezer's ToS.": "Korzystając z tego programu, nie zgadzasz się z ToS Deezera.", + "Only in Electron version!": "Tylko w wersji Electron!", + "Search results for:": "Wyniki wyszukiwania dla:", + "Error loading data!": "Błąd ładowania danych!", + "Try again later!": "Spróbuj ponownie później!", + "Search": "Szukaj", + "Streaming Quality": "Jakość odtwarzania", + "Download Quality": "Jakość pobierania", + "Downloads Directory": "Katalog pobierania", + "Simultaneous downloads": "Jednoczesne pobieranie", + "Always show download confirm dialog before downloading.": "Zawsze proś o potwierdzenie przed pobieraniem.", + "Show download dialog": "Pokazuj okno dialogowe pobierania", + "Create folders for artists": "Twórz foldery wykonawców", + "Create folders for albums": "Twórz foldery albumów", + "Download lyrics": "Pobierz tekst", + "Variables": "Zmienne", + "UI": "Interfejs", + "Show autocomplete in search": "Pokaż autouzupełnianie w wyszukiwarce", + "Integrations": "Połącz", + "This allows listening history, flow and recommendations to work properly.": "Pozwala na działanie historii odtwarzania, rekomendacji i automatycznych playlist.", + "Log track listens to Deezer": "Zapisuj historię odtwarzania na koncie Deezer", + "Connect your LastFM account to allow scrobbling.": "Połącz swoje konto LastFM, aby umożliwić scrobbling.", + "Login with LastFM": "Zaloguj używając LastFM", + "Disconnect LastFM": "Odłącz LastFM", + "Requires restart to apply!": "Zmiany wymagają ponownego uruchomienia!", + "Enable Discord Rich Presence, requires restart to toggle!": "Włącz Szczegółowy Widok Discord, wymaga ponownego uruchomienia!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Włącz w Discordzie przycisk dołączenia, aby synchronizować utwory, wymaga ponownego uruchomienia!", + "Discord Join Button": "Pokaż przycisk dołączenia do Discord", + "Other": "Inne", + "Minimize to tray": "Minimalizuj do zasobnika", + "Don't minimize to tray": "Nie minimalizuj do zasobnika", + "Close on exit": "Wyłącz po zamknięciu okna", + "Settings saved!": "Ustawienia zapisane!", + "Available only in Electron version!": "Dostępne tylko w wersji Electron!", + "Crossfade (ms)": "Przejście (ms)", + "Select primary color": "Wybierz podstawowy kolor", + "Light theme": "Wybierz jasny motyw", + "Create folders for playlists": "Utwórz folder dla playlist", + "About": "O programie", + "Links:": "Adresy:", + "Telegram Releases": "Nowe wydania w Telegram", + "Telegram Group": "Grupa w Telegramie", + "Discord": "Discord", + "Telegram Android Group": "Grupa Telegram dla wydań na Android", + "Credits:": "Twórcy:", + "Agree": "Akceptuję", + "Dismiss": "Odrzuć", + "Added to playlist!": "Dodano do playlisty!", + "Added to library!": "Dodano do biblioteki!", + "Removed from library!": "Usunięto z biblioteki!", + "Removed from playlist!": "Usunięto z playlisty!", + "Playlist deleted!": "Playlista została usunięta!", + "Delete": "Usuń", + "Are you sure you want to delete this playlist?": "Na pewno chcesz usunąć tę playlistę?", + "Force white tray icon": "Wymuś białą ikonę w zasobniku", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Wymuś domyślną (białą) ikonę w zasobniku paska zadań jeśli motyw został nieprawidłowo odczytany. Wymaga ponownego uruchomienia.", + "Share": "Udostępnij", + "Settings quality": "Jakość z ustawień", + "Content language": "Język treści", + "Content country": "Kraj treści", + "Website": "Strona internetowa", + "Visit website": "Odwiedź stronę internetową", + "New update available:": "Dostępna jest nowa aktualizacja:", + "Shuffle": "Losowo", + "Download album cover": "Pobierz okładkę albumu", + "Art Resolution": "Rozdzielczość Obrazu", + "Public": "Publiczne", + "Private": "Prywatne", + "Collaborative": "Współpraca", + "Edit playlist": "Edytuj playlistę", + "Save": "Zapisz", + "Edit": "Edytuj", + "Importer": "Importer", + "Enter URL": "Wprowadź adres URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Obecnie obsługuje tylko Spotify i maksymalnie 100 utworów.", + "Import into playlist": "Importuj na playlistę", + "Keep sidebar open": "Panel boczny zawsze otwarty", + "WARNING: Might require reload to work properly!": "UWAGA: Może wymagać ponownego uruchomienia, by działać poprawnie!", + "An error occured, URL might be invalid or unsupported.": "Wystąpił błąd, adres URL może być nieprawidłowy lub nieobsługiwany.", + "Top tracks": "Najpopularniejsze utwory", + "Show all top tracks": "Pokaż wszystkie najpopularniejsze utwory", + "Singles": "Single", + "Album:": "Album:", + "Artists:": "Wykonawcy:", + "Yes": "Tak", + "No": "Nie", + "Download Filename": "Nazwa pobieranego pliku", + "Language": "Język", + "Background Image": "Obraz w tle", + "Enter URL or absolute path. WARNING: Requires reload!": "Wprowadź adres URL lub całą ścieżkę pliku. UWAGA: Wymaga ponownego uruchomienia!", + "LGBT Mode": "Tryb LGBT", + "Native top bar": "Natywny górny pasek", + "Requires restart of Freezer!": "Wymaga ponownego uruchomiania Freezera!" +} \ No newline at end of file diff --git a/app/client/src/locales/pt.json b/app/client/src/locales/pt.json new file mode 100644 index 0000000..9a4f06f --- /dev/null +++ b/app/client/src/locales/pt.json @@ -0,0 +1,171 @@ +{ + "Home": "Início", + "Browse": "Navegar", + "Library": "Biblioteca", + "Tracks": "Faixas", + "Playlists": "Playlists", + "Albums": "Álbuns", + "Artists": "Artistas", + "More": "Mais", + "Settings": "Configurações", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Pesquise ou cole a URL do Deezer. Use \"/\" para focar rapidamente.", + "Play": "Reproduzir", + "Add to library": "Adicionar à biblioteca", + "Download": "Download", + "fans": "fans", + "tracks": "faixas", + "Quality": "Qualidade", + "Estimated size:": "Tempo estimado:", + "Start downloading": "Iniciar download", + "Cancel": "Cancelar", + "Stream logging is disabled!": "Registro de transmissão está desativado!", + "Enable it in settings for history to work properly.": "Habilite nas configurações para que o histórico funcione corretamente.", + "History": "Histórico", + "Create new playlist": "Criar nova playlist", + "TRACKS": "FAIXAS", + "Sort by": "Ordenar por", + "Date Added": "Data de adição", + "Name (A-Z)": "Nome (A-Z)", + "Artist (A-Z)": "Artista (A-Z)", + "Album (A-Z)": "Álbum (A-Z)", + "Error loading lyrics or lyrics not found!": "Erro ao carregar letra ou letra não encontrada!", + "Create playlist": "Criar playlist", + "Create": "Criar", + "Add to playlist": "Adicionar à playlist", + "Create new": "Criar nova", + "Remove": "Remover", + "Play next": "Reproduzir à seguir", + "Add to queue": "Adicionar à fila", + "Remove from library": "Remover da biblioteca", + "Remove from playlist": "Remover da playlist", + "Play track mix": "Reproduzir mix da faixa", + "Go to": "Ir para", + "Track Mix": "Mix da Faixa", + "Duration": "Duração", + "Released": "Lançamento", + "Disk": "Disco", + "albums": "álbuns", + "Play top": "Reproduzir top", + "Radio": "Rádio", + "Show all albums": "Mostrar todos os álbuns", + "Show all singles": "Mostrar todos os singles", + "Show more": "Mostrar mais", + "Downloaded": "Baixados", + "Queue": "Fila de Reprodução", + "Total": "Total", + "Stop": "Parar", + "Start": "Começar", + "Show folder": "Mostrar pasta", + "Clear queue": "Limpar fila", + "Playing from": "Reproduzindo de", + "Info": "Informações", + "Lyrics": "Letra", + "Track number": "Número da faixa", + "Disk number": "Número do disco", + "Explicit": "Explícito", + "Source": "Fonte", + "ID": "Identificação", + "Error logging in!": "Erro no login!", + "Please try again later, or try another account.": "Por favor, tente novamente mais tarde ou tente outra conta.", + "Logout": "Desconectar", + "Login using browser": "Login usando o navegador", + "Please login using your Deezer account:": "Faça login usando sua conta do Deezer:", + "...or paste your ARL/Token below:": "...ou cole seu ARL/Token abaixo:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "Ao usar este programa, você discorda dos termos e condições de uso do Deezer.", + "Only in Electron version!": "Somente na versão Electron!", + "Search results for:": "Resultado de pesquisa para:", + "Error loading data!": "Erro ao carregar dados!", + "Try again later!": "Tente novamente mais tarde!", + "Search": "Pesquisa", + "Streaming Quality": "Qualidade da Transmissão", + "Download Quality": "Qualidade do Download", + "Downloads Directory": "Diretório de Downloads", + "Simultaneous downloads": "Downloads simultâneos", + "Always show download confirm dialog before downloading.": "Sempre mostrar diálogo de confirmação de download antes de baixar.", + "Show download dialog": "Mostrar diálogo de download", + "Create folders for artists": "Criar pastas para artistas", + "Create folders for albums": "Criar pastas para álbuns", + "Download lyrics": "Baixar letra", + "Variables": "Variáveis", + "UI": "Interface do Usuário", + "Show autocomplete in search": "Mostrar autocompletar na pesquisa", + "Integrations": "Integrações", + "This allows listening history, flow and recommendations to work properly.": "Isto permite que o histórico de ouvidas, flow e recomendações funcionem corretamente.", + "Log track listens to Deezer": "Log de faixas ouvidas para o Deezer", + "Connect your LastFM account to allow scrobbling.": "Conecte sua conta do LastFM para permitir o scrobbling.", + "Login with LastFM": "Login com LastFM", + "Disconnect LastFM": "Desconectar LastFM", + "Requires restart to apply!": "Requer reinicialização para aplicar!", + "Enable Discord Rich Presence, requires restart to toggle!": "Ativar o Rich Presence do Discord, requer reinicialização para alternar!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Ativar o botão de entrar no Discord para sincronizar faixas, requer reinicialização para alternar!", + "Discord Join Button": "Botão de Entrar do Discord", + "Other": "Outro", + "Minimize to tray": "Minimizar para a bandeja", + "Don't minimize to tray": "Não minimizar para a bandeja", + "Close on exit": "Fechar ao sair", + "Settings saved!": "Configurações salvas!", + "Available only in Electron version!": "Disponível somente na versão Electron!", + "Crossfade (ms)": "Transição gradual (ms)", + "Select primary color": "Selecione a cor primária", + "Light theme": "Tema Claro", + "Create folders for playlists": "Criar pastas para playlists", + "About": "Sobre", + "Links:": "Links:", + "Telegram Releases": "Versões no Telegram", + "Telegram Group": "Grupo do Telegram", + "Discord": "Discord", + "Telegram Android Group": "Grupo do Android no Telegram", + "Credits:": "Créditos:", + "Agree": "Concordo", + "Dismiss": "Dispensar", + "Added to playlist!": "Adicionar à playlist!", + "Added to library!": "Adicionado à biblioteca!", + "Removed from library!": "Removido da biblioteca!", + "Removed from playlist!": "Removido da playlist!", + "Playlist deleted!": "Playlist deletada!", + "Delete": "Deletar", + "Are you sure you want to delete this playlist?": "Você tem certeza que deseja excluir esta playlist?", + "Force white tray icon": "Forçar ícone de bandeja branco", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forçar ícone de bandeja (branco) padrão se o tema for detectado incorretamente. Requer reinicialização.", + "Share": "Compartilhar", + "Settings quality": "Configurações de qualidade", + "Content language": "Linguagem do conteúdo", + "Content country": "País do conteúdo", + "Website": "Site", + "Visit website": "Visite o site", + "New update available:": "Nova atualização disponível:", + "Shuffle": "Aleatório", + "Download album cover": "Baixar arte do álbum", + "Art Resolution": "Resolução da Arte", + "Public": "Pública", + "Private": "Privada", + "Collaborative": "Colaborativa", + "Edit playlist": "Editar playlist", + "Save": "Salvar", + "Edit": "Editar", + "Importer": "Importador", + "Enter URL": "Introduzir ARL", + "Currently only Spotify is supported and limited to 100 tracks.": "Atualmente apenas o Spotify é suportado e limitado a 100 faixas.", + "Import into playlist": "Importar playlist", + "Keep sidebar open": "Manter a barra lateral aberta", + "WARNING: Might require reload to work properly!": "AVISO: Pode ser necessário recarregar para funcionar corretamente!", + "An error occured, URL might be invalid or unsupported.": "Ocorreu um erro, a URL pode ser inválida ou não suportada.", + "Top tracks": "Top faixas", + "Show all top tracks": "Mostrar todas as faixas", + "Singles": "Singles", + "Album:": "Álbum:", + "Artists:": "Artistas:", + "Yes": "Sim", + "No": "Não", + "Download Filename": "Nomenclatura de download", + "Language": "Idioma", + "Background Image": "Imagem de Fundo", + "Enter URL or absolute path. WARNING: Requires reload!": "Digite URL ou caminho absoluto. ATENÇÃO: Requer recarregar!", + "LGBT Mode": "Modo LGBT", + "Native top bar": "Barra superior nativa", + "Requires restart of Freezer!": "Requer reinicialização do Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/ro.json b/app/client/src/locales/ro.json new file mode 100644 index 0000000..c9a30c8 --- /dev/null +++ b/app/client/src/locales/ro.json @@ -0,0 +1,171 @@ +{ + "Home": "Acasă", + "Browse": "Caută", + "Library": "Librărie", + "Tracks": "Piese", + "Playlists": "Playlist-uri", + "Albums": "Albume", + "Artists": "Artiști", + "More": "Mai mult", + "Settings": "Setări", + "Downloads": "Descărcări", + "Search or paste Deezer URL. Use / to quickly focus.": "Caută sau lipește URL-ul Deezer. Folosește \"/\" pentru a se focaliza rapid.", + "Play": "Play", + "Add to library": "Adaugă la librărie", + "Download": "Descărcați", + "fans": "fani", + "tracks": "piese", + "Quality": "Calitate", + "Estimated size:": "Dimensiune estimată:", + "Start downloading": "Începe descărcarea", + "Cancel": "Anulează", + "Stream logging is disabled!": "Stream logging-ul este dezactivat!", + "Enable it in settings for history to work properly.": "Activați-l în setări pentru ca istoricul să funcționeze corect.", + "History": "Istoric", + "Create new playlist": "Crează un nou playlist", + "TRACKS": "PIESE", + "Sort by": "Sortează după", + "Date Added": "Dată Adăugare", + "Name (A-Z)": "Nume (A-Z)", + "Artist (A-Z)": "Artiști (A-Z)", + "Album (A-Z)": "Albume (A-Z)", + "Error loading lyrics or lyrics not found!": "Eroare la încărcarea versurilor sau versurile nu au fost găsite!", + "Create playlist": "Crează un playlist", + "Create": "Creează", + "Add to playlist": "Adaugă la un playlist", + "Create new": "Crează nou", + "Remove": "Șterge", + "Play next": "Redă următorul", + "Add to queue": "Adaugă la coadă", + "Remove from library": "Șterge din librărie", + "Remove from playlist": "Șterge din playlist", + "Play track mix": "Redă mix-ul piesei", + "Go to": "Accesați", + "Track Mix": "Mix-ul Piesei", + "Duration": "Durată", + "Released": "Lansat", + "Disk": "Disc", + "albums": "albume", + "Play top": "Redă de la început", + "Radio": "Radio", + "Show all albums": "Afișează toate albumele", + "Show all singles": "Arată toate melodiile", + "Show more": "Arată mai multe", + "Downloaded": "Descărcate", + "Queue": "Coadă", + "Total": "Total", + "Stop": "Stop", + "Start": "Începe", + "Show folder": "Arată folder-ul", + "Clear queue": "Șterge coada", + "Playing from": "Redare din", + "Info": "Informații", + "Lyrics": "Versuri", + "Track number": "Numărul piesei", + "Disk number": "Numărul discului", + "Explicit": "Explicit", + "Source": "Sursă", + "ID": "ID", + "Error logging in!": "Eroare la autentificare!", + "Please try again later, or try another account.": "Te rugăm să încerci din nou mai târziu, sau încearcă cu un alt cont.", + "Logout": "Deconectează-te", + "Login using browser": "Autentificare utilizând browserul", + "Please login using your Deezer account:": "Te rugăm să te conectezi utilizând contul tau Deezer:", + "...or paste your ARL/Token below:": "...sau lipiți ARL/Token-ul mai jos:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "Folosind acest program, nu sunteți de acord cu ToS-ul Deezer.", + "Only in Electron version!": "Doar în versiunea Electron!", + "Search results for:": "Rezultatele căutării pentru:", + "Error loading data!": "Eroare la încărcarea datelor!", + "Try again later!": "Încearcă din nou mai târziu!", + "Search": "Caută", + "Streaming Quality": "Calitatea streaming-ului", + "Download Quality": "Calitatea descărcărilor", + "Downloads Directory": "Descărcați in", + "Simultaneous downloads": "Descărcări simultane", + "Always show download confirm dialog before downloading.": "Arată întotdeauna confirmarea a descărcării înainte de descărcare.", + "Show download dialog": "Arată pagina de download", + "Create folders for artists": "Crează foldere pentru artiști", + "Create folders for albums": "Crează foldere pentru albume", + "Download lyrics": "Descărcați versurile .LRC", + "Variables": "Variabile", + "UI": "Interfață", + "Show autocomplete in search": "Afișează lista de autocompletare", + "Integrations": "Integrări", + "This allows listening history, flow and recommendations to work properly.": "Aceasta permite folosirea istoricului, Flow-ului și recomandările pentru a funcționa corect.", + "Log track listens to Deezer": "Înregistrează ascultările la Deezer", + "Connect your LastFM account to allow scrobbling.": "Conectați-vă contul LastFM pentru a permite scrobbling-ul.", + "Login with LastFM": "Conectează-te cu LastFM", + "Disconnect LastFM": "Deconectează LastFM", + "Requires restart to apply!": "Necesită repornirea pentru a aplica!", + "Enable Discord Rich Presence, requires restart to toggle!": "Activează Discord Rich Presence, necesită repornirea pentru a comuta!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Activează butonul de join la Discord pentru sincronizarea pieselor, necesită repornire în comutator!", + "Discord Join Button": "Butonul de join Discord", + "Other": "Altele", + "Minimize to tray": "Minimizează în bara de programe", + "Don't minimize to tray": "Nu minimiza în bara de programe", + "Close on exit": "Închide la ieșire", + "Settings saved!": "Setările au fost salvate!", + "Available only in Electron version!": "Disponibil doar în versiunea Electron!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Selectaţi culoarea primară", + "Light theme": "Temă luminoasă", + "Create folders for playlists": "Creați foldere pentru playlist-uri", + "About": "Despre", + "Links:": "Link-uri:", + "Telegram Releases": "Lansări Telegram", + "Telegram Group": "Grup Telegram", + "Discord": "Discord", + "Telegram Android Group": "Group Android Telegram", + "Credits:": "Contribuţii:", + "Agree": "Permite", + "Dismiss": "Respingeți", + "Added to playlist!": "Adăugat la playlist!", + "Added to library!": "Adăugat la bibliotecă!", + "Removed from library!": "Eliminat din bibliotecă!", + "Removed from playlist!": "Eliminat din playlist!", + "Playlist deleted!": "Playlist detectat!", + "Delete": "Ștergeți", + "Are you sure you want to delete this playlist?": "Ești sigur că dorești să ștergi acest playlist?", + "Force white tray icon": "Forțează icon alb", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Forțează icon implici (alb) dacă tema este detectată incorect. Necesită repornire a aplicației.", + "Share": "Distribuiți", + "Settings quality": "Setări Calitate", + "Content language": "Limbajul conținutului", + "Content country": "Țara conținutului", + "Website": "Website", + "Visit website": "Vizitați site-ul web", + "New update available:": "Actualizare nouă disponibilă:", + "Shuffle": "Amestecare", + "Download album cover": "Descarcă coperta albumului", + "Art Resolution": "Rezoluția Imaginii", + "Public": "Public", + "Private": "Privat", + "Collaborative": "Colaborativ", + "Edit playlist": "Editează playlist-ul", + "Save": "Salvează", + "Edit": "Editează", + "Importer": "Importator", + "Enter URL": "Introduceţi URL-ul", + "Currently only Spotify is supported and limited to 100 tracks.": "La moment, suportăm doar Spotify cu o limită de 100 de piese.", + "Import into playlist": "Importă în playlist", + "Keep sidebar open": "Păstrați meniul lateral deschis", + "WARNING: Might require reload to work properly!": "ATENȚIE: S-ar putea să fie necesar reîncărcarea aplicației pentru a funcționa adecvat!", + "An error occured, URL might be invalid or unsupported.": "A apărut o eroare, URL-ul ar putea fi invalid sau nesuportat.", + "Top tracks": "Piesele de top", + "Show all top tracks": "Arată toate piesele de top", + "Singles": "Single-uri", + "Album:": "Albumul:", + "Artists:": "Artiștii:", + "Yes": "Da", + "No": "Nu", + "Download Filename": "Denumirea Descărcării", + "Language": "Limba", + "Background Image": "Imagine de fundal", + "Enter URL or absolute path. WARNING: Requires reload!": "Introduceţi URL-ul sau calea completă. ATENŢIE: Necesită reîncărcare!", + "LGBT Mode": "Regim LGBT", + "Native top bar": "Bara de sus", + "Requires restart of Freezer!": "Necesită repornirea aplicației!" +} \ No newline at end of file diff --git a/app/client/src/locales/ru.json b/app/client/src/locales/ru.json new file mode 100644 index 0000000..69795ca --- /dev/null +++ b/app/client/src/locales/ru.json @@ -0,0 +1,171 @@ +{ + "Home": "Главная", + "Browse": "Обзор", + "Library": "Избранное", + "Tracks": "Треки", + "Playlists": "Плейлисты", + "Albums": "Альбомы", + "Artists": "Артисты", + "More": "Ещё", + "Settings": "Настройки", + "Downloads": "Загрузки", + "Search or paste Deezer URL. Use / to quickly focus.": "Введите запрос или ссылку. \"/\" для быстрого поиска.", + "Play": "Воспроизвести", + "Add to library": "Добавить в Избранное", + "Download": "Скачать", + "fans": "поклонники", + "tracks": "треки", + "Quality": "Качество звука", + "Estimated size:": "Приблизительный размер:", + "Start downloading": "Начать загрузку", + "Cancel": "Отмена", + "Stream logging is disabled!": "Отправка статистики отключена!", + "Enable it in settings for history to work properly.": "Включите её в настройках для работы рекомендаций.", + "History": "История", + "Create new playlist": "Новый плейлист", + "TRACKS": "Треки", + "Sort by": "Сортировать по", + "Date Added": "Дата добавления", + "Name (A-Z)": "Название (А - Я)", + "Artist (A-Z)": "Исполнитель (А - Я)", + "Album (A-Z)": "Альбом (A - Я)", + "Error loading lyrics or lyrics not found!": "Ошибка получения текста!", + "Create playlist": "Создать плейлист", + "Create": "Создать", + "Add to playlist": "Добавить в плейлист", + "Create new": "Создать новый", + "Remove": "Удалить", + "Play next": "Играть следующим", + "Add to queue": "Добавить в очередь", + "Remove from library": "Удалить из Избранного", + "Remove from playlist": "Удалить из плейлиста", + "Play track mix": "Воспроизвести микс", + "Go to": "Перейти к", + "Track Mix": "Микс", + "Duration": "Продолжительность", + "Released": "Релиз", + "Disk": "Диск", + "albums": "альбомы", + "Play top": "Играть популярные", + "Radio": "Радио", + "Show all albums": "Показать все", + "Show all singles": "Показать все синглы", + "Show more": "Ещё", + "Downloaded": "Загрузки", + "Queue": "Очередь", + "Total": "Всего", + "Stop": "Остановить", + "Start": "Пуск", + "Show folder": "Открыть папку", + "Clear queue": "Очистить очередь", + "Playing from": "Сейчас играет", + "Info": "Инфо", + "Lyrics": "Текст песни", + "Track number": "Дорожка", + "Disk number": "Номер диска", + "Explicit": "18+", + "Source": "Источник", + "ID": "ID", + "Error logging in!": "Ошибка авторизации!", + "Please try again later, or try another account.": "Пожалуйста, повторите попытку позже или попробуйте другой аккаунт.", + "Logout": "Выход", + "Login using browser": "Войти через браузер", + "Please login using your Deezer account:": "Войдите, используя свой аккаунт Deezer:", + "...or paste your ARL/Token below:": "...или вставьте ваш токен (ARL) ниже:", + "ARL/Token": "Токен (ARL) ", + "Login": "Вход", + "By using this program, you disagree with Deezer's ToS.": "Используя эту программу, вы не соглашаетесь с правилами использования Deezer.", + "Only in Electron version!": "Только в версии Electron!", + "Search results for:": "Результаты поиска для:", + "Error loading data!": "Ошибка при загрузке данных!", + "Try again later!": "Повторите попытку позже!", + "Search": "Поиск", + "Streaming Quality": "Качество при воспроизведении", + "Download Quality": "Качество при загрузке", + "Downloads Directory": "Папка загрузок", + "Simultaneous downloads": "Количество одновременных загрузок", + "Always show download confirm dialog before downloading.": "Подтверждать загрузки.", + "Show download dialog": "Подтверждение", + "Create folders for artists": "Создавать папки для исполнителей", + "Create folders for albums": "Создавать папки для альбомов", + "Download lyrics": "Скачивать тексты", + "Variables": "Переменные", + "UI": "Интерфейс", + "Show autocomplete in search": "Подсказки при поиске", + "Integrations": "Интеграции", + "This allows listening history, flow and recommendations to work properly.": "Для правильной работы Flow, рекомендаций и истории.", + "Log track listens to Deezer": "Отправлять статистику", + "Connect your LastFM account to allow scrobbling.": "Подключите ваш аккаунт LastFM, чтобы разрешить скробблинг.", + "Login with LastFM": "Авторизоваться через LastFM", + "Disconnect LastFM": "Отключить LastFM", + "Requires restart to apply!": "Требуется перезапуск приложения!", + "Enable Discord Rich Presence, requires restart to toggle!": "Включить Discord Rich Presence, требуется перезапуск!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Включить кнопку вступления Discord для синхронизации треков, требуется перезапуск!", + "Discord Join Button": "Кнопка \"Вступить\" в Discord", + "Other": "Другое", + "Minimize to tray": "Сворачивать в трей", + "Don't minimize to tray": "Не сворачивать в трей", + "Close on exit": "Закрывать при выходе", + "Settings saved!": "Настройки сохранены!", + "Available only in Electron version!": "Доступно только в версии на Electron!", + "Crossfade (ms)": "Кроссфейд (мс)", + "Select primary color": "Выберите основной цвет", + "Light theme": "Светлая тема", + "Create folders for playlists": "Создавать папки для плейлистов", + "About": "О приложении", + "Links:": "Ссылки:", + "Telegram Releases": "Релизы в Telegram", + "Telegram Group": "Группа в Telegram", + "Discord": "Discord", + "Telegram Android Group": "Обсуждение Freezer Android", + "Credits:": "Благодарности:", + "Agree": "Принять", + "Dismiss": "Отмена", + "Added to playlist!": "Добавлено в плейлист!", + "Added to library!": "Добавлено в Избранное!", + "Removed from library!": "Удалено из Избранного!", + "Removed from playlist!": "Удалено из плейлиста!", + "Playlist deleted!": "Плейлист удален!", + "Delete": "Удалить", + "Are you sure you want to delete this playlist?": "Вы точно хотите удалить этот плейлист?", + "Force white tray icon": "Белый значок в трее", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Принудительно использовать белый значок, если тема определена неправильно. Требуется перезапуск.", + "Share": "Поделиться", + "Settings quality": "Качество настроек", + "Content language": "Язык контента", + "Content country": "Страна контента", + "Website": "Веб-сайт", + "Visit website": "Посетить веб-сайт", + "New update available:": "Доступна новая версия:", + "Shuffle": "Перемешать", + "Download album cover": "Загрузить обложку альбома", + "Art Resolution": "Разрешение обложки", + "Public": "Публичный", + "Private": "Приватный", + "Collaborative": "Совместное", + "Edit playlist": "Изменить плейлист", + "Save": "Сохранить", + "Edit": "Редактировать", + "Importer": "Импорт плейлистов", + "Enter URL": "Введите ссылку", + "Currently only Spotify is supported and limited to 100 tracks.": "В настоящее время поддерживается только Spotify. Не более 100 треков в плейлисте.", + "Import into playlist": "Импортировать в плейлист", + "Keep sidebar open": "Не закрывать боковое меню", + "WARNING: Might require reload to work properly!": "Внимание! Может потребоваться перезапуск для правильной работы.", + "An error occured, URL might be invalid or unsupported.": "Ошибка, URL недействителен или не поддерживается.", + "Top tracks": "Популярные треки", + "Show all top tracks": "Показать все популярные треки", + "Singles": "Синглы", + "Album:": "Альбом:", + "Artists:": "Исполнители:", + "Yes": "Да", + "No": "Нет", + "Download Filename": "Скачать шаблон для названия", + "Language": "Язык", + "Background Image": "Фоновое изображение", + "Enter URL or absolute path. WARNING: Requires reload!": "Введите URL или полный путь. ВНИМАНИЕ: Требуется перезагрузка!", + "LGBT Mode": "Режим ЛГБТ", + "Native top bar": "Верхнюю панель", + "Requires restart of Freezer!": "Требуется перезагрузка!" +} \ No newline at end of file diff --git a/app/client/src/locales/sk.json b/app/client/src/locales/sk.json new file mode 100644 index 0000000..e5b1219 --- /dev/null +++ b/app/client/src/locales/sk.json @@ -0,0 +1,171 @@ +{ + "Home": "Domov", + "Browse": "Prehliadať", + "Library": "Knižnica", + "Tracks": "Skladby", + "Playlists": "Playlisty", + "Albums": "Albumy", + "Artists": "Umelci", + "More": "Viac", + "Settings": "Nastavenia", + "Downloads": "Na stiahnutie", + "Search or paste Deezer URL. Use / to quickly focus.": "Vyhľadať alebo vložiť Deezer URL. Použite \"/\" pre rýchly náhľad.", + "Play": "Prehrať", + "Add to library": "Pridať do knižnice", + "Download": "Stiahnuť", + "fans": "fanúšikov", + "tracks": "skladieb", + "Quality": "Kvalita", + "Estimated size:": "Odhadovaná veľkosť:", + "Start downloading": "Spustiť sťahovanie", + "Cancel": "Zrušiť", + "Stream logging is disabled!": "Zaznamenávanie histórie pre Deezer je zakázané!", + "Enable it in settings for history to work properly.": "Povoliť v nastaveniach pre správne fungovanie histórie.", + "History": "História", + "Create new playlist": "Vytvoriť nový playlist", + "TRACKS": "SKLADBY", + "Sort by": "Zoradiť podľa", + "Date Added": "Dátum pridania", + "Name (A-Z)": "Meno (A-Z)", + "Artist (A-Z)": "Umelec (А-Z)", + "Album (A-Z)": "Album (А-Z)", + "Error loading lyrics or lyrics not found!": "Chyba načítania textu alebo, text nie je dostupný!", + "Create playlist": "Vytvoriť playlist", + "Create": "Vytvoriť", + "Add to playlist": "Pridať do playlistu", + "Create new": "Vytvoriť nový", + "Remove": "Odstrániť", + "Play next": "Prehrať ako ďalšie", + "Add to queue": "Pridať do poradia", + "Remove from library": "Odstrániť z knižnice", + "Remove from playlist": "Odstrániť z playlistu", + "Play track mix": "Prehrať mix skladieb", + "Go to": "Ísť na", + "Track Mix": "Mix skladieb", + "Duration": "Trvanie", + "Released": "Vydané", + "Disk": "Disk", + "albums": "albumy", + "Play top": "Prehrať najlepšie", + "Radio": "Rádio", + "Show all albums": "Zobraziť všetky albumy", + "Show all singles": "Zobraziť všetky single", + "Show more": "Zobraziť viac", + "Downloaded": "Stiahnuté", + "Queue": "Poradie", + "Total": "Spolu", + "Stop": "Stop", + "Start": "Spustiť sťahovanie", + "Show folder": "Zobraziť priečinok", + "Clear queue": "Vyčistiť poradie", + "Playing from": "Prehráva sa", + "Info": "Informácie", + "Lyrics": "Texty", + "Track number": "Číslo skladby", + "Disk number": "Číslo disku", + "Explicit": "Pre dospelých", + "Source": "Zdroj", + "ID": "ID", + "Error logging in!": "Chyba prihlásenia!", + "Please try again later, or try another account.": "Prosím skúste znova alebo použite iný účet.", + "Logout": "Odhlásiť", + "Login using browser": "Prihlásenie cez prehliadač", + "Please login using your Deezer account:": "Prosím prihláste sa s použitím Deezer účtu:", + "...or paste your ARL/Token below:": "...alebo použite váš ARL/Token nižšie:", + "ARL/Token": "ARL/Token", + "Login": "Prihlásiť", + "By using this program, you disagree with Deezer's ToS.": "Použitím tejto aplikácie nesúhlasíte s Deezer ToS.", + "Only in Electron version!": "Iba vo verzii Electron!", + "Search results for:": "Výsledok hľadanie pre:", + "Error loading data!": "Chyba načítania dát!", + "Try again later!": "Skúste znova neskôr!", + "Search": "Hľadať", + "Streaming Quality": "Kvalita streamu", + "Download Quality": "Kvalita sťahovania", + "Downloads Directory": "Priečinok sťahovania", + "Simultaneous downloads": "Súbežné sťahovanie", + "Always show download confirm dialog before downloading.": "Pred stiahnutím vždy zobraziť dialógové okno s potvrdením stiahnutia.", + "Show download dialog": "Zobraziť dialógové okno sťahovania", + "Create folders for artists": "Vytvoriť pričinky pre umelcov", + "Create folders for albums": "Vytvoriť pričinky pre albumy", + "Download lyrics": "Sťahovať texty", + "Variables": "Premenné", + "UI": "Používateľské rozhranie", + "Show autocomplete in search": "Automatické dopĺňanie pri vyhľadávaní", + "Integrations": "Integrácia", + "This allows listening history, flow and recommendations to work properly.": "Umožňuje správne fungovanie histórie, flow a odporúčaných skladieb.", + "Log track listens to Deezer": "Zaznamenávanie histórie pre Deezer", + "Connect your LastFM account to allow scrobbling.": "Pripojte sa na váš LastFM účet pre použitie scrobblingu.", + "Login with LastFM": "Prihlásiť s LastFM", + "Disconnect LastFM": "Odpojiť od LastFM", + "Requires restart to apply!": "Vyžadovaný reštart pre použitie!", + "Enable Discord Rich Presence, requires restart to toggle!": "Povoliť funkciu Discord Rich Presence, na prepnutie je potrebný reštart!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Povoliť tlačidlo Discord join na synchronizáciu skladieb, na prepnutie je potrebný reštart!", + "Discord Join Button": "Discord Join tlačidlo", + "Other": "Iné", + "Minimize to tray": "Minimalizovať do lišty", + "Don't minimize to tray": "Neminimalizovať do lišty", + "Close on exit": "Zatvoriť pri ukončení", + "Settings saved!": "Nastavenia uložené!", + "Available only in Electron version!": "Iba vo verzii Electron!", + "Crossfade (ms)": "Prelínanie (ms)", + "Select primary color": "Hlavná farba", + "Light theme": "Svetlá téma", + "Create folders for playlists": "Vytvoriť priečinky pre playlisty", + "About": "O aplikácii", + "Links:": "Odkazy:", + "Telegram Releases": "Telegram vydania", + "Telegram Group": "Telegram skupina", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android skupina", + "Credits:": "Autori:", + "Agree": "Súhlasím", + "Dismiss": "Odmietnuť", + "Added to playlist!": "Pridané do playlistu!", + "Added to library!": "Pridané do knižnice!", + "Removed from library!": "Odstránené z knižnice!", + "Removed from playlist!": "Odstránené z playlistu!", + "Playlist deleted!": "Playlist odstránený!", + "Delete": "Odstrániť", + "Are you sure you want to delete this playlist?": "Naozaj chcete odstrániť tento playlist?", + "Force white tray icon": "Vynútiť bielu ikonu v lište", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Vynútiť predvolenú (bielu) ikonu v lište, ak je motív nesprávne zistený. Vyžaduje sa reštart.", + "Share": "Zdieľať", + "Settings quality": "Nastavenie kvality", + "Content language": "Jazyk obsahu", + "Content country": "Krajina obsahu", + "Website": "Webová stránka", + "Visit website": "Navštíviť webovú stránku", + "New update available:": "Nová aktualizácia k dispozícii:", + "Shuffle": "Náhodne prehrať", + "Download album cover": "Prevziať obal albumu", + "Art Resolution": "Rozlíšenie", + "Public": "Verejné", + "Private": "Sukromné", + "Collaborative": "Spolupráca", + "Edit playlist": "Upraviť playlist", + "Save": "Uložiť", + "Edit": "Upraviť", + "Importer": "Importovať", + "Enter URL": "Zadajte URL adresu", + "Currently only Spotify is supported and limited to 100 tracks.": "Momentálne je podporované iba Spotify s limitom 100 skladieb.", + "Import into playlist": "Importovať do playlistu", + "Keep sidebar open": "Nechať bočný panel otvorený", + "WARNING: Might require reload to work properly!": "UPOZORNENIE: Môže vyžadovať obnovenie pre správnu funkčnosť!", + "An error occured, URL might be invalid or unsupported.": "Chybná alebo nepodporovaná URL adresa.", + "Top tracks": "Najlepšie skladby", + "Show all top tracks": "Zobraziť všetky najlepšie skladby", + "Singles": "Single", + "Album:": "Album:", + "Artists:": "Interpreti:", + "Yes": "Áno", + "No": "Nie", + "Download Filename": "Stiahnuť názov súboru", + "Language": "Jazyk", + "Background Image": "Obrázok pozadia", + "Enter URL or absolute path. WARNING: Requires reload!": "Vložte URL alebo cestu. VAROVANIE: Vyžaduje obnovenie!", + "LGBT Mode": "LGBT mód", + "Native top bar": "Natívna horná lišta", + "Requires restart of Freezer!": "Vyžaduje sa reštart aplikácie!" +} \ No newline at end of file diff --git a/app/client/src/locales/sl.json b/app/client/src/locales/sl.json new file mode 100644 index 0000000..ec6c94e --- /dev/null +++ b/app/client/src/locales/sl.json @@ -0,0 +1,171 @@ +{ + "Home": "Domov", + "Browse": "Iskanje", + "Library": "Knjižnica", + "Tracks": "Skladbe", + "Playlists": "Seznami predvajanja", + "Albums": "Albumi", + "Artists": "Izvajalci", + "More": "Več", + "Settings": "Nastavitve", + "Downloads": "Prenosi", + "Search or paste Deezer URL. Use / to quickly focus.": "Išči ali prilepi Deezer URL.", + "Play": "Predvajaj", + "Add to library": "Dodaj v knjižnico", + "Download": "Prenos", + "fans": "oboževalci", + "tracks": "skladbe", + "Quality": "Kakovost", + "Estimated size:": "Predvidena velikost:", + "Start downloading": "Začni prenašati", + "Cancel": "Prekliči", + "Stream logging is disabled!": "Beleženje dejanj je izklopljeno!", + "Enable it in settings for history to work properly.": "Omogočite v nastavitvah za pravilno delovanje zgodovine.", + "History": "Zgodovina", + "Create new playlist": "Nov seznam predvajanja", + "TRACKS": "SKLADBE", + "Sort by": "Razvrsti po", + "Date Added": "Dodano", + "Name (A-Z)": "Ime (A-Z)", + "Artist (A-Z)": "Izvajalec (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Napaka pri nalaganju besedila ali pa besedilo ni bilo najdeno!", + "Create playlist": "Nov seznam predvajanja", + "Create": "Ustvari", + "Add to playlist": "Dodaj na seznam predvajanja", + "Create new": "Ustvari novo", + "Remove": "Odstrani", + "Play next": "Predvajaj naslednjega", + "Add to queue": "Dodaj v čakalno vrsto", + "Remove from library": "Odstrani iz knjižnice", + "Remove from playlist": "Odstrani iz seznama predvajanja", + "Play track mix": "Predvajaj miks", + "Go to": "Pojdi na", + "Track Mix": "Miks", + "Duration": "Trajanje", + "Released": "Objavljeno", + "Disk": "Disk", + "albums": "albumi", + "Play top": "Predvajaj najboljše", + "Radio": "Radio", + "Show all albums": "Pokaži vse albume", + "Show all singles": "Prikaži vse posnetke", + "Show more": "Prikaži več", + "Downloaded": "Prenešeno", + "Queue": "Čakalna vrsta", + "Total": "Skupaj", + "Stop": "Ustavi", + "Start": "Začni", + "Show folder": "Prikaži v mapi", + "Clear queue": "Počisti čakalno vrsto", + "Playing from": "Predvajaj iz", + "Info": "Informacije", + "Lyrics": "Besedilo", + "Track number": "Številka pesmi", + "Disk number": "Številka na disku", + "Explicit": "Izrecno", + "Source": "Vir", + "ID": "ID", + "Error logging in!": "Napaka pri prijavi!", + "Please try again later, or try another account.": "Poskusite znova pozneje ali pa uporabite drug račun.", + "Logout": "Odjava", + "Login using browser": "Prijava preko brskalnika", + "Please login using your Deezer account:": "Prosimo prijavite se s svojim Deezer računom:", + "...or paste your ARL/Token below:": "... ali prilepite svoj ARL/žeton:", + "ARL/Token": "ARL/Žeton", + "Login": "Prijava", + "By using this program, you disagree with Deezer's ToS.": "Z uporabo tega programa boste kršili pogoje uporabe od Dezzer-ja.", + "Only in Electron version!": "Samo v verziji ki jo poganja elektron!", + "Search results for:": "Rezultati iskanja za:", + "Error loading data!": "Napaka pri nalaganju podatkov!", + "Try again later!": "Poskusite znova pozneje!", + "Search": "Išči", + "Streaming Quality": "Kvaliteta pretakanja", + "Download Quality": "Kvaliteta prejemanja", + "Downloads Directory": "Mapa za prenose", + "Simultaneous downloads": "Sočasnih prejemov", + "Always show download confirm dialog before downloading.": "Vedno pokaži okence za potrditev prenosa pred prenosom.", + "Show download dialog": "Pokaži okno o prejemanju", + "Create folders for artists": "Ustvari mape za izvajalce", + "Create folders for albums": "Ustvari mape za albume", + "Download lyrics": "Prenesi besedilo", + "Variables": "Spremenljivke", + "UI": "Vmesnik", + "Show autocomplete in search": "Pokaži predloge v polju za iskanje", + "Integrations": "Integracije", + "This allows listening history, flow and recommendations to work properly.": "To omogoča zgodovino poslušanja in priporočila.", + "Log track listens to Deezer": "Shranjuj poslušanja v Deezer-u", + "Connect your LastFM account to allow scrobbling.": "Poveži svoj LastFM račun da omogočite poslušanje.", + "Login with LastFM": "Prijava z LastFM računom", + "Disconnect LastFM": "Odklopi LastFM", + "Requires restart to apply!": "Uveljavitev zahteva ponovni zagon!", + "Enable Discord Rich Presence, requires restart to toggle!": "Omogoči Discord Rich Presence, zahteva ponovni zagon!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Omogoči Discord pridruži se gumb, zahteva ponovni zagon!", + "Discord Join Button": "Discord pridruži se gumb", + "Other": "Drugo", + "Minimize to tray": "Pomanjšaj v orodno vrstico", + "Don't minimize to tray": "Ne pomanjšaj v orodno vrstico", + "Close on exit": "Zapri ob izhodu", + "Settings saved!": "Nastavitve shranjene!", + "Available only in Electron version!": "Samo v verziji, ki jo poganja elektron!", + "Crossfade (ms)": "Navzkrižno pojemanje t.i. Crossfade (ms)", + "Select primary color": "Izberi barvo", + "Light theme": "Svetla tema", + "Create folders for playlists": "Ustvari mape za sezname predvajanja", + "About": "O programu", + "Links:": "Povezave:", + "Telegram Releases": "Telegram kanal", + "Telegram Group": "Telegram skupina", + "Discord": "Discord", + "Telegram Android Group": "Telegram skupina za Android", + "Credits:": "Zasluge:", + "Agree": "Strinjam se", + "Dismiss": "Opusti", + "Added to playlist!": "Dodaj na seznam predvajanja!", + "Added to library!": "Dodano v knjižnico!", + "Removed from library!": "Odstranjeno iz knjižnice!", + "Removed from playlist!": "Odstranjeno iz seznama predvajanja!", + "Playlist deleted!": "Seznam predvajanja je izbrisan!", + "Delete": "Izbriši", + "Are you sure you want to delete this playlist?": "Ali ste prepričani da želite izbrisati ta seznam predvajanja?", + "Force white tray icon": "Vsili belo ikono v orodni vrstici", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Vsili privzeto (belo) ikono v orodni vrstici, če je tema napačno zaznana. Zahteva ponovni zagon.", + "Share": "Deli", + "Settings quality": "Nastavitve kvalitete", + "Content language": "Jezik vsebine", + "Content country": "Koda države", + "Website": "Spletna stran", + "Visit website": "Obišči spletno stran", + "New update available:": "Na voljo je nova posodobitev:", + "Shuffle": "Naključno", + "Download album cover": "Shrani sličico albuma", + "Art Resolution": "Ločljivost sličice", + "Public": "Javno", + "Private": "Zasebno", + "Collaborative": "Sodelovalno", + "Edit playlist": "Uredi seznam predvajanja", + "Save": "Shrani", + "Edit": "Uredi", + "Importer": "Uvoznik", + "Enter URL": "Vnesite URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Trenutno je podprt samo Spotify in ima omejitev 100 pesmi.", + "Import into playlist": "Uvozi na seznam predvajanja", + "Keep sidebar open": "Obdrži odprto stransko vrstico", + "WARNING: Might require reload to work properly!": "OPOZORILO: Potrebuje ponovni zagon aplikacije za učinkovanje!", + "An error occured, URL might be invalid or unsupported.": "Pojavila se je napaka, URL je lahko napačen ali nepodprt.", + "Top tracks": "Najboljše skladbe", + "Show all top tracks": "Prikaži vse najboljše skaldbe", + "Singles": "Singli", + "Album:": "Album:", + "Artists:": "Izvajalci:", + "Yes": "Da", + "No": "Ne", + "Download Filename": "Imena prenosa", + "Language": "Jezik", + "Background Image": "Slika za ozadje", + "Enter URL or absolute path. WARNING: Requires reload!": "Vpiši URL ali absolutno pot. OPOZORILO: Zahtev ponovno nalaganje!", + "LGBT Mode": "LGBT način", + "Native top bar": "Sistemska zgornja vrstica", + "Requires restart of Freezer!": "Potrebuje ponovni zagon Freezerja!" +} \ No newline at end of file diff --git a/app/client/src/locales/tr.json b/app/client/src/locales/tr.json new file mode 100644 index 0000000..1997650 --- /dev/null +++ b/app/client/src/locales/tr.json @@ -0,0 +1,171 @@ +{ + "Home": "Ana Sayfa", + "Browse": "Göz at", + "Library": "Kütüphane", + "Tracks": "Parçalar", + "Playlists": "Çalma listeleri", + "Albums": "Albümler", + "Artists": "Sanatçılar", + "More": "Daha Fazla", + "Settings": "Ayarlar", + "Downloads": "İndirilenler", + "Search or paste Deezer URL. Use / to quickly focus.": "Arayın veya Deezer URL'sini yapıştırın. Hızlıca odaklanmak için \"/\" kullanın.", + "Play": "Oynat", + "Add to library": "Kütüphaneye ekle", + "Download": "İndir", + "fans": "hayranlar", + "tracks": "parçalar", + "Quality": "Kalite", + "Estimated size:": "Tahmini süre:", + "Start downloading": "İndirmeyi başlat", + "Cancel": "İptal Et", + "Stream logging is disabled!": "Günlük akışı devre dışı bırakıldı!", + "Enable it in settings for history to work properly.": "Geçmişin düzgün çalışması için ayarlardan etkinleştirin.", + "History": "Geçmiş", + "Create new playlist": "Yeni çalma listesi oluştur", + "TRACKS": "PARÇALAR", + "Sort by": "Sırala", + "Date Added": "Eklenme Tarihi", + "Name (A-Z)": "Ad (A-Z)", + "Artist (A-Z)": "Sanatçı (A-Z)", + "Album (A-Z)": "Albüm (A-Z)", + "Error loading lyrics or lyrics not found!": "Şarkı sözleri bulunamadı veya yüklenirken hata oluştu!", + "Create playlist": "Çalma listesi oluştur", + "Create": "Oluştur", + "Add to playlist": "Çalma listesine ekle", + "Create new": "Yeni oluştur", + "Remove": "Kaldır", + "Play next": "Sonrakini çal", + "Add to queue": "Sıraya ekle", + "Remove from library": "Kütüphaneden kaldır", + "Remove from playlist": "Çalma listesinden kaldır", + "Play track mix": "Play track mix", + "Go to": "Git", + "Track Mix": "Track Mix", + "Duration": "Süre", + "Released": "Yayınlandı", + "Disk": "Disk", + "albums": "albümler", + "Play top": "Popüler olanları çal", + "Radio": "Radyo", + "Show all albums": "Tüm albümleri göster", + "Show all singles": "Tüm şarkıları göster", + "Show more": "Daha fazla göster", + "Downloaded": "İndirildi", + "Queue": "Sıra", + "Total": "Toplam", + "Stop": "Durdur", + "Start": "Başlat", + "Show folder": "Klasörü göster", + "Clear queue": "Sırayı temizle", + "Playing from": "Şuradan oynatılıyor:", + "Info": "Bilgi", + "Lyrics": "Şarkı sözleri", + "Track number": "Parça Numarası", + "Disk number": "Disk numarası", + "Explicit": "Sakıncalı", + "Source": "Kaynak", + "ID": "ID", + "Error logging in!": "Oturum açma hatası!", + "Please try again later, or try another account.": "Lütfen daha sonra tekrar deneyin veya başka bir hesap deneyin.", + "Logout": "Çıkış Yap", + "Login using browser": "Tarayıcı kullanarak giriş yapın", + "Please login using your Deezer account:": "Lütfen Deezer hesabınızı kullanarak giriş yapın:", + "...or paste your ARL/Token below:": "yada ARL/Token aşağıya yapıştırın:", + "ARL/Token": "ARL/Token", + "Login": "Giriş", + "By using this program, you disagree with Deezer's ToS.": "Bu programı kullanarak Deezer'in Hizmet Şartları'na katılmıyorsunuz.", + "Only in Electron version!": "Sadece Electron versiyonunda!", + "Search results for:": "Arama sonuçları:", + "Error loading data!": "Veri yükleme hatası!", + "Try again later!": "Daha sonra yeniden deneyin!", + "Search": "Ara", + "Streaming Quality": "Akış Kalitesi", + "Download Quality": "İndirme Kalitesi", + "Downloads Directory": "İndirme Dizini", + "Simultaneous downloads": "Eşzamanlı indirmeler", + "Always show download confirm dialog before downloading.": "İndirmeden önce her zaman indirme onayı iletişim kutusunu gösterin.", + "Show download dialog": "İndirme iletişim kutusunu göster", + "Create folders for artists": "Sanatçılar için klasörler oluştur", + "Create folders for albums": "Albümler için klasörler oluştur", + "Download lyrics": "Şarkı sözlerini indir", + "Variables": "Değişkenler", + "UI": "Arayüz", + "Show autocomplete in search": "Aramada otomatik tamamlamayı göster", + "Integrations": "Entegrasyonlar", + "This allows listening history, flow and recommendations to work properly.": "Bu, dinleme geçmişinin, akışının ve önerilerin düzgün çalışmasını sağlar.", + "Log track listens to Deezer": "Dinlediğin şarkılar Deezer'da yayınlansın", + "Connect your LastFM account to allow scrobbling.": "Scrobbling'e izin vermek için LastFM hesabınızı bağlayın.", + "Login with LastFM": "LastFM ile giriş yapın", + "Disconnect LastFM": "LastFM bağlantısını kes", + "Requires restart to apply!": "Yeniden başlatma gerekli!", + "Enable Discord Rich Presence, requires restart to toggle!": "Discord zengin içeriği etkinleştirin, geçiş yapmak için yeniden başlatma gerekir!", + "Discord Rich Presence": "Discord Zengin İçerik", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Şarkıları senkronize etmek için Discord katıl düğmesini etkinleştirin, değişiklik yapmak yeniden başlatma gerektirir!", + "Discord Join Button": "Discord Katılma Düğmesi", + "Other": "Diğer", + "Minimize to tray": "Sistem tepsisine küçült", + "Don't minimize to tray": "Sistem tepsisine küçültülmez", + "Close on exit": "Çıkışta kapat", + "Settings saved!": "Ayarlar kaydedildi!", + "Available only in Electron version!": "Sadece Electron versiyonunda mevcuttur!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Ana rengi seçin", + "Light theme": "Aydınlık tema", + "Create folders for playlists": "Çalma listesi için klasör oluştur", + "About": "Hakkında", + "Links:": "Bağlantılar:", + "Telegram Releases": "Telegram Sürümleri", + "Telegram Group": "Telegram Grubu", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Grubu", + "Credits:": "Katkıda Bulunanlar:", + "Agree": "Katılıyorum", + "Dismiss": "Reddet", + "Added to playlist!": "Çalma listesine eklendi!", + "Added to library!": "Kütüphaneye eklendi!", + "Removed from library!": "Kütüphaneden silindi!", + "Removed from playlist!": "Çalma listesinden silindi!", + "Playlist deleted!": "Çalma listesi silindi!", + "Delete": "Sil", + "Are you sure you want to delete this playlist?": "Bu çalma listesini silmek istediğinizden emin misiniz?", + "Force white tray icon": "Tepsi simgesini beyaz olmaya zorla", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Tema yanlış algılanırsa varsayılan (beyaz) tepsi simgesini zorlar. Yeniden başlatma gerektirir.", + "Share": "Paylaş", + "Settings quality": "Kalite ayarları", + "Content language": "İçerik dili", + "Content country": "İçerik ülkesi", + "Website": "İnternet sitesi", + "Visit website": "Web sitesine git", + "New update available:": "Yeni güncelleme mevcut:", + "Shuffle": "Karışık çal", + "Download album cover": "Albüm kapağını indir", + "Art Resolution": "Art Çözüm", + "Public": "Herkese Açık", + "Private": "Gizli", + "Collaborative": "Paylaşılan", + "Edit playlist": "Çalma listesini düzenle", + "Save": "Kaydet", + "Edit": "Düzenle", + "Importer": "Aktarıcı", + "Enter URL": "URL girin", + "Currently only Spotify is supported and limited to 100 tracks.": "Şimdilik sadece Spotify destekleniyor ve 100 parça sınırı mevcut.", + "Import into playlist": "Çalma listesine aktar", + "Keep sidebar open": "Kenar çubuğu açık kalsın", + "WARNING: Might require reload to work properly!": "DİKKAT: Düzgün çalışması için yeniden yükleme gerekebilir!", + "An error occured, URL might be invalid or unsupported.": "Bir hata oluştu, URL geçersiz veya desteklenmiyor olabilir.", + "Top tracks": "Popüler şarkılar", + "Show all top tracks": "En çok dinlenenleri göster", + "Singles": "Single'lar", + "Album:": "Albümler:", + "Artists:": "Sanatçılar:", + "Yes": "Evet", + "No": "Hayır", + "Download Filename": "Adlandırma taslağı indir", + "Language": "Dil", + "Background Image": "Arka plan resmi", + "Enter URL or absolute path. WARNING: Requires reload!": "URL'yi veya mutlak yolu girin. UYARI: yeniden yükleme gerektirir!", + "LGBT Mode": "LGBT Modu", + "Native top bar": "Yerel üst çubuk", + "Requires restart of Freezer!": "Freezer'ın yeniden başlatılmasını gerektirir!" +} \ No newline at end of file diff --git a/app/client/src/locales/uk.json b/app/client/src/locales/uk.json new file mode 100644 index 0000000..2e20403 --- /dev/null +++ b/app/client/src/locales/uk.json @@ -0,0 +1,171 @@ +{ + "Home": "Головна", + "Browse": "Перегляд", + "Library": "Бібліотека", + "Tracks": "Треки", + "Playlists": "Плейлисти", + "Albums": "Альбоми", + "Artists": "Виконавці", + "More": "Більше", + "Settings": "Налаштування", + "Downloads": "Завантаження", + "Search or paste Deezer URL. Use / to quickly focus.": "Знайдіть або вставте URL Deezer. Використовуйте \"/\" для швидкого фокусування.", + "Play": "Відтворити", + "Add to library": "Додати до бібліотеки", + "Download": "Завантажити", + "fans": "фани", + "tracks": "треки", + "Quality": "Якість", + "Estimated size:": "Приблизний розмір:", + "Start downloading": "Почати завантаження", + "Cancel": "Скасувати", + "Stream logging is disabled!": "Журнал трансляції відключений!", + "Enable it in settings for history to work properly.": "Увімкніть це в налаштуваннях, щоб історія працювала належним чином.", + "History": "Історія", + "Create new playlist": "Створити новий плейлист", + "TRACKS": "ТРЕКИ", + "Sort by": "Сортувати за", + "Date Added": "Дата додавання", + "Name (A-Z)": "Назва (А-Я)", + "Artist (A-Z)": "Виконавець (А-Я)", + "Album (A-Z)": "Альбом (А-Я)", + "Error loading lyrics or lyrics not found!": "Помилка при завантаженні текстів або тексту не знайдено!", + "Create playlist": "Створити плейлист", + "Create": "Створити", + "Add to playlist": "Додати до плейлиста", + "Create new": "Створити новий", + "Remove": "Видалити", + "Play next": "Відтворити наступний", + "Add to queue": "Додати до черги", + "Remove from library": "Видалити з бібліотеки", + "Remove from playlist": "Видалити з плейлиста", + "Play track mix": "Відтворити мікс треків", + "Go to": "Перейти до", + "Track Mix": "Трек Мікс", + "Duration": "Тривалість", + "Released": "Реліз", + "Disk": "Диск", + "albums": "альбоми", + "Play top": "Відтворити топ", + "Radio": "Радіо", + "Show all albums": "Показати всі альбоми", + "Show all singles": "Показати всі композиції", + "Show more": "Показати більше", + "Downloaded": "Завантажено", + "Queue": "Черга", + "Total": "Всього", + "Stop": "Зупинити", + "Start": "Почати", + "Show folder": "Показати теку", + "Clear queue": "Очистити чергу", + "Playing from": "Відтворення з", + "Info": "Інфо", + "Lyrics": "Текст", + "Track number": "Номер треку", + "Disk number": "Номер диску", + "Explicit": "18+", + "Source": "Джерело", + "ID": "ID", + "Error logging in!": "Помилка входу!", + "Please try again later, or try another account.": "Будь ласка, повторіть спробу пізніше, або спробуйте інший обліковий запис.", + "Logout": "Вийти", + "Login using browser": "Вхід через браузер", + "Please login using your Deezer account:": "Будь ласка, увійдіть, використовуючи свій обліковий запис Deezer:", + "...or paste your ARL/Token below:": "...або вставте свій ARL/токен нижче:", + "ARL/Token": "ARL/токен", + "Login": "Увійти", + "By using this program, you disagree with Deezer's ToS.": "Використовуючи цю програму, ви не погоджуєтесь із умовами використання Deezer.", + "Only in Electron version!": "Тільки в версії Electron!", + "Search results for:": "Результати пошуку для:", + "Error loading data!": "Помилка завантаження даних!", + "Try again later!": "Повторіть спробу пізніше!", + "Search": "Пошук", + "Streaming Quality": "Якість потоку", + "Download Quality": "Якість завантаження", + "Downloads Directory": "Тека для завантажень", + "Simultaneous downloads": "Одночасних завантажень", + "Always show download confirm dialog before downloading.": "Завжди показувати вікно підтвердження перед завантаженням.", + "Show download dialog": "Показувати діалогове вікно завантаження", + "Create folders for artists": "Створити теки для виконавців", + "Create folders for albums": "Створити теки для альбомів", + "Download lyrics": "Завантажити тексти пісень", + "Variables": "Змінні", + "UI": "Інтерфейс користувача", + "Show autocomplete in search": "Автозаповнення при пошуку", + "Integrations": "Інтеграція", + "This allows listening history, flow and recommendations to work properly.": "Для правильної роботи Flow, рекомендацій та історії.", + "Log track listens to Deezer": "Відправляти статистику", + "Connect your LastFM account to allow scrobbling.": "Підключити обліковий запис LastFM, щоб дозволити scrobbling.", + "Login with LastFM": "Увійти через LastFM", + "Disconnect LastFM": "Від'єднати LastFM", + "Requires restart to apply!": "Необхідно перезапустити для застосування!", + "Enable Discord Rich Presence, requires restart to toggle!": "Увімкнути Discord Rich Presence, для застосування потрібен перезапуск!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Увімкнути приєднання до Discord для синхронізації треків, для застосування потрібен перезапуск!", + "Discord Join Button": "Кнопка приєднання до Discord", + "Other": "Інше", + "Minimize to tray": "Згорнути в трей", + "Don't minimize to tray": "Не згортати в трей", + "Close on exit": "Закрити при виході", + "Settings saved!": "Налаштування збережено!", + "Available only in Electron version!": "Доступно лише в Electron версії!", + "Crossfade (ms)": "Плавний перехід (мс)", + "Select primary color": "Вибрати основний колір", + "Light theme": "Світла тема", + "Create folders for playlists": "Створити теки для плейлистів", + "About": "Про додаток", + "Links:": "Лінки:", + "Telegram Releases": "Telegram релізи", + "Telegram Group": "Група в Telegram", + "Discord": "Discord", + "Telegram Android Group": "Група Android в Telegram", + "Credits:": "Автори:", + "Agree": "Погоджуюсь", + "Dismiss": "Відхилити", + "Added to playlist!": "Додано до плейлиста!", + "Added to library!": "Додано до бібліотеки!", + "Removed from library!": "Видалено з бібліотеки!", + "Removed from playlist!": "Видалено з плейлиста!", + "Playlist deleted!": "Плейлист видалено!", + "Delete": "Видалити", + "Are you sure you want to delete this playlist?": "Ви впевнені, що хочете видалити цей плейлист?", + "Force white tray icon": "Примусово використовувати білий значок трею", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Примусово використовувати стандартний (білий) значок, якщо тема визначена неправильно. Необхідне перезавантаження.", + "Share": "Поділитись", + "Settings quality": "Налаштування якості", + "Content language": "Мова контенту", + "Content country": "Країна контенту", + "Website": "Веб-сторінка", + "Visit website": "Відвідати веб-сторінку", + "New update available:": "Доступне оновлення:", + "Shuffle": "Перемішати", + "Download album cover": "Завантажити обкладинку альбому", + "Art Resolution": "Розширення обкладинки", + "Public": "Публічний", + "Private": "Приватний", + "Collaborative": "Спільний", + "Edit playlist": "Редагувати плейлист", + "Save": "Зберегти", + "Edit": "Редагувати", + "Importer": "Імпорт плейлистів", + "Enter URL": "Введіть посилання", + "Currently only Spotify is supported and limited to 100 tracks.": "Наразі підтримується тільки Spotify і з обмеженям в 100 треків.", + "Import into playlist": "Імпортувати в плейлист", + "Keep sidebar open": "Не закривати бокове меню", + "WARNING: Might require reload to work properly!": "УВАГА: Можливо потрібно перезапустити програму для коректної роботи!", + "An error occured, URL might be invalid or unsupported.": "Виникла помилка, можливо посилання є недійсним або не підтримується.", + "Top tracks": "Популярні треки", + "Show all top tracks": "Показати всі популярні треки", + "Singles": "Композиції", + "Album:": "Альбом:", + "Artists:": "Виконавці:", + "Yes": "Так", + "No": "Ні", + "Download Filename": "Ім'я файлу завантаження", + "Language": "Мова", + "Background Image": "Фонове зображення", + "Enter URL or absolute path. WARNING: Requires reload!": "Введіть URL або абсолютний шлях. УВАГА: необхідне перезавантаження!", + "LGBT Mode": "Режим збоченців", + "Native top bar": "Власний top bar", + "Requires restart of Freezer!": "Потребує перезавантаження Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/ur.json b/app/client/src/locales/ur.json new file mode 100644 index 0000000..fbcb3e2 --- /dev/null +++ b/app/client/src/locales/ur.json @@ -0,0 +1,171 @@ +{ + "Home": "Home", + "Browse": "Browse", + "Library": "Library", + "Tracks": "Tracks", + "Playlists": "Playlists", + "Albums": "Albums", + "Artists": "Artists", + "More": "More", + "Settings": "Settings", + "Downloads": "Downloads", + "Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.", + "Play": "Play", + "Add to library": "Add to library", + "Download": "Download", + "fans": "fans", + "tracks": "tracks", + "Quality": "Quality", + "Estimated size:": "Estimated size:", + "Start downloading": "Start downloading", + "Cancel": "Cancel", + "Stream logging is disabled!": "Stream logging is disabled!", + "Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.", + "History": "History", + "Create new playlist": "Create new playlist", + "TRACKS": "TRACKS", + "Sort by": "Sort by", + "Date Added": "Date Added", + "Name (A-Z)": "Name (A-Z)", + "Artist (A-Z)": "Artist (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!", + "Create playlist": "Create playlist", + "Create": "Create", + "Add to playlist": "Add to playlist", + "Create new": "Create new", + "Remove": "Remove", + "Play next": "Play next", + "Add to queue": "Add to queue", + "Remove from library": "Remove from library", + "Remove from playlist": "Remove from playlist", + "Play track mix": "Play track mix", + "Go to": "Go to", + "Track Mix": "Track Mix", + "Duration": "Duration", + "Released": "Released", + "Disk": "Disk", + "albums": "albums", + "Play top": "Play top", + "Radio": "Radio", + "Show all albums": "Show all albums", + "Show all singles": "Show all singles", + "Show more": "Show more", + "Downloaded": "Downloaded", + "Queue": "Queue", + "Total": "Total", + "Stop": "Stop", + "Start": "Start", + "Show folder": "Show folder", + "Clear queue": "Clear queue", + "Playing from": "Playing from", + "Info": "Info", + "Lyrics": "Lyrics", + "Track number": "Track number", + "Disk number": "Disk number", + "Explicit": "Explicit", + "Source": "Source", + "ID": "ID", + "Error logging in!": "Error logging in!", + "Please try again later, or try another account.": "Please try again later, or try another account.", + "Logout": "Logout", + "Login using browser": "Login using browser", + "Please login using your Deezer account:": "Please login using your Deezer account:", + "...or paste your ARL/Token below:": "...or paste your ARL/Token below:", + "ARL/Token": "ARL/Token", + "Login": "Login", + "By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.", + "Only in Electron version!": "Only in Electron version!", + "Search results for:": "Search results for:", + "Error loading data!": "Error loading data!", + "Try again later!": "Try again later!", + "Search": "Search", + "Streaming Quality": "Streaming Quality", + "Download Quality": "Download Quality", + "Downloads Directory": "Downloads Directory", + "Simultaneous downloads": "Simultaneous downloads", + "Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.", + "Show download dialog": "Show download dialog", + "Create folders for artists": "Create folders for artists", + "Create folders for albums": "Create folders for albums", + "Download lyrics": "Download lyrics", + "Variables": "Variables", + "UI": "UI", + "Show autocomplete in search": "Show autocomplete in search", + "Integrations": "Integrations", + "This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.", + "Log track listens to Deezer": "Log track listens to Deezer", + "Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.", + "Login with LastFM": "Login with LastFM", + "Disconnect LastFM": "Disconnect LastFM", + "Requires restart to apply!": "Requires restart to apply!", + "Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!", + "Discord Join Button": "Discord Join Button", + "Other": "Other", + "Minimize to tray": "Minimize to tray", + "Don't minimize to tray": "Don't minimize to tray", + "Close on exit": "Close on exit", + "Settings saved!": "Settings saved!", + "Available only in Electron version!": "Available only in Electron version!", + "Crossfade (ms)": "Crossfade (ms)", + "Select primary color": "Select primary color", + "Light theme": "Light theme", + "Create folders for playlists": "Create folders for playlists", + "About": "About", + "Links:": "Links:", + "Telegram Releases": "Telegram Releases", + "Telegram Group": "Telegram Group", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android Group", + "Credits:": "Credits:", + "Agree": "Agree", + "Dismiss": "Dismiss", + "Added to playlist!": "Added to playlist!", + "Added to library!": "Added to library!", + "Removed from library!": "Removed from library!", + "Removed from playlist!": "Removed from playlist!", + "Playlist deleted!": "Playlist deleted!", + "Delete": "Delete", + "Are you sure you want to delete this playlist?": "Are you sure you want to delete this playlist?", + "Force white tray icon": "Force white tray icon", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Force default (white) tray icon if theme incorrectly detected. Requires restart.", + "Share": "Share", + "Settings quality": "Settings quality", + "Content language": "Content language", + "Content country": "Content country", + "Website": "Website", + "Visit website": "Visit website", + "New update available:": "New update available:", + "Shuffle": "Shuffle", + "Download album cover": "Download album cover", + "Art Resolution": "Art Resolution", + "Public": "Public", + "Private": "Private", + "Collaborative": "Collaborative", + "Edit playlist": "Edit playlist", + "Save": "Save", + "Edit": "Edit", + "Importer": "Importer", + "Enter URL": "Enter URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Currently only Spotify is supported and limited to 100 tracks.", + "Import into playlist": "Import into playlist", + "Keep sidebar open": "Keep sidebar open", + "WARNING: Might require reload to work properly!": "WARNING: Might require reload to work properly!", + "An error occured, URL might be invalid or unsupported.": "An error occured, URL might be invalid or unsupported.", + "Top tracks": "Top tracks", + "Show all top tracks": "Show all top tracks", + "Singles": "Singles", + "Album:": "Album:", + "Artists:": "Artists:", + "Yes": "Yes", + "No": "No", + "Download Filename": "Download Filename", + "Language": "Language", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/vi.json b/app/client/src/locales/vi.json new file mode 100644 index 0000000..fec6f32 --- /dev/null +++ b/app/client/src/locales/vi.json @@ -0,0 +1,171 @@ +{ + "Home": "Trang chủ", + "Browse": "Duyệt", + "Library": "Thư viện", + "Tracks": "Bài hát", + "Playlists": "Danh sách phát", + "Albums": "Album", + "Artists": "Nghệ sĩ", + "More": "Thêm", + "Settings": "Cài đặt", + "Downloads": "Danh sách tải", + "Search or paste Deezer URL. Use / to quickly focus.": "Tìm kiếm hoặc dán URL Deezer.", + "Play": "Phát", + "Add to library": "Thêm vào thư viện", + "Download": "Tải xuống", + "fans": "người hâm mộ", + "tracks": "bài hát", + "Quality": "Chất lượng", + "Estimated size:": "Kích thước dự tính:", + "Start downloading": "Bắt đầu tải xuống", + "Cancel": "Hủy", + "Stream logging is disabled!": "Đã tắt nhật kí phát trực tuyến!", + "Enable it in settings for history to work properly.": "Kích hoạt chế độ này trong cài đặt để lịch sử nghe hoạt động bình thường.", + "History": "Lịch sử", + "Create new playlist": "Tạo danh sách phát mới", + "TRACKS": "BÀI HÁT", + "Sort by": "Sắp xếp theo", + "Date Added": "Ngày thêm vào", + "Name (A-Z)": "Tên (A-Z)", + "Artist (A-Z)": "Nghệ sĩ (A-Z)", + "Album (A-Z)": "Album (A-Z)", + "Error loading lyrics or lyrics not found!": "Lỗi tải lời bài hát hoặc lời bài hát không tồn tại!", + "Create playlist": "Tạo danh sách phát", + "Create": "Tạo", + "Add to playlist": "Thêm vào danh sách phát", + "Create new": "Tạo mới", + "Remove": "Loại bỏ", + "Play next": "Phát kế tiếp", + "Add to queue": "Thêm vào hàng chờ", + "Remove from library": "Xoá khỏi Thư Viện", + "Remove from playlist": "Xóa khỏi danh sách phát", + "Play track mix": "Phát trộn bài hát", + "Go to": "Đi đến", + "Track Mix": "Trộn bài hát", + "Duration": "Thời lượng", + "Released": "Phát hành", + "Disk": "Đĩa", + "albums": "album", + "Play top": "Phát từ đầu", + "Radio": "Radio", + "Show all albums": "Hiện tất cả album", + "Show all singles": "Hiển thị tất cả đĩa đơn", + "Show more": "Hiển thị thêm", + "Downloaded": "Đã tải xuống", + "Queue": "Hàng chờ", + "Total": "Tổng", + "Stop": "Dừng", + "Start": "Bắt đầu", + "Show folder": "Hiển thị trong thư mục", + "Clear queue": "Xóa hàng chờ", + "Playing from": "Phát từ", + "Info": "Thông tin", + "Lyrics": "Lời bài hát", + "Track number": "Số thứ tự bài hát", + "Disk number": "Số thứ tự đĩa", + "Explicit": "18+", + "Source": "Nguồn", + "ID": "ID", + "Error logging in!": "Lỗi đăng nhập!", + "Please try again later, or try another account.": "Vui lòng thử lại sau, hoặc thử lại bằng tài khoản khác.", + "Logout": "Đăng xuất", + "Login using browser": "Đăng nhập bằng trình duyệt", + "Please login using your Deezer account:": "Xin vui lòng đăng nhập bằng tài khoản Deezer của bạn:", + "...or paste your ARL/Token below:": "...hoặc dán mã ARL/Token vào đây:", + "ARL/Token": "ARL/Token", + "Login": "Đăng nhập", + "By using this program, you disagree with Deezer's ToS.": "Với việc sử dụng ứng dụng này, bạn đã từ chối điều khoản dịch vụ của Deezer.", + "Only in Electron version!": "Chỉ có trong phiên bản electron!", + "Search results for:": "Kết quả tìm kiếm cho:", + "Error loading data!": "Lỗi khi tải dữ liệu!", + "Try again later!": "Thử lại sau!", + "Search": "Tìm kiếm", + "Streaming Quality": "Chất lượng phát trực tuyến", + "Download Quality": "Chất lượng tải xuống", + "Downloads Directory": "Thư mục tải xuống", + "Simultaneous downloads": "Tải xuống đồng thời", + "Always show download confirm dialog before downloading.": "Luôn hiển thị hộp thoại xác nhận tải xuống trước khi tải xuống.", + "Show download dialog": "Hiển thị hộp thoại tải xuống", + "Create folders for artists": "Tạo thư mục theo tên nghệ sĩ", + "Create folders for albums": "Tạo thư mục theo tên album", + "Download lyrics": "Tải xuống lời bài hát", + "Variables": "Các biến số", + "UI": "Giao diện", + "Show autocomplete in search": "Hiển thị tự động hoàn thành trong tìm kiếm", + "Integrations": "Tích hợp", + "This allows listening history, flow and recommendations to work properly.": "Điều này giúp lịch sử nghe, phát nhạc thông minh (flow) và đề xuất hoạt động bình thường.", + "Log track listens to Deezer": "Lưu nhật ký nhạc đã nghe cho Deezer", + "Connect your LastFM account to allow scrobbling.": "Kết nối tài khoản LastFM của bạn để sử dụng hệ thống phát thông minh của LastFM (scrobbling).", + "Login with LastFM": "Đăng nhập tài khoản LastFM", + "Disconnect LastFM": "Đăng xuất LastFM", + "Requires restart to apply!": "Khởi động lại ứng dụng để áp dụng!", + "Enable Discord Rich Presence, requires restart to toggle!": "Bật tính năng Discord Rich Presence, yêu cầu khởi động lại để chuyển đổi!", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "Bật nút tham gia Discord để đồng bộ các bài hát, yêu cầu khởi động lại để chuyển đổi!", + "Discord Join Button": "Nút tham gia Discord", + "Other": "Khác", + "Minimize to tray": "Thu xuống khay hệ thống", + "Don't minimize to tray": "Không thu xuống khay hệ thống", + "Close on exit": "Đóng khi thoát", + "Settings saved!": "Cài đặt đã lưu!", + "Available only in Electron version!": "Chỉ có ở phiên bản Electron!", + "Crossfade (ms)": "Khoảng lặng (ms)", + "Select primary color": "Chọn màu chính", + "Light theme": "Màu sáng", + "Create folders for playlists": "Tạo thư mục theo danh sách phát", + "About": "Giới thiệu", + "Links:": "Liên kết:", + "Telegram Releases": "Các bản phát hành trên Telegram", + "Telegram Group": "Nhóm Telegram", + "Discord": "Discord", + "Telegram Android Group": "Nhóm Telegram Android", + "Credits:": "Đóng góp:", + "Agree": "Đồng ý", + "Dismiss": "Đóng", + "Added to playlist!": "Đã thêm vào danh sách phát!", + "Added to library!": "Đã thêm vào Thư viện!", + "Removed from library!": "Đã xoá khỏi thư viện!", + "Removed from playlist!": "Đã xóa khỏi danh sách phát!", + "Playlist deleted!": "Đã xoá danh sách phát!", + "Delete": "Xóa", + "Are you sure you want to delete this playlist?": "Bạn có chắc chắn muốn xóa danh sách phát này?", + "Force white tray icon": "Buộc biểu tượng khay hệ thống màu trắng", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "Buộc biểu tượng khay mặc định (trắng) nếu không phát hiện được chủ đề. Khởi động lại để áp dụng.", + "Share": "Chia sẻ", + "Settings quality": "Cài đặt chất lượng", + "Content language": "Ngôn ngữ nội dung", + "Content country": "Quốc gia của nội dung", + "Website": "Trang web", + "Visit website": "Truy cập trang web", + "New update available:": "Có bản cập nhật mới:", + "Shuffle": "Trộn", + "Download album cover": "Tải xuống ảnh bìa album", + "Art Resolution": "Độ Phân Giải", + "Public": "Công khai", + "Private": "Riêng tư", + "Collaborative": "Hợp tác", + "Edit playlist": "Chỉnh sửa danh sách phát", + "Save": "Lưu", + "Edit": "Chỉnh sửa", + "Importer": "Công cụ nhập", + "Enter URL": "Nhập URL", + "Currently only Spotify is supported and limited to 100 tracks.": "Hiện chỉ hỗ trợ Spotify và giới hạn 100 bài hát.", + "Import into playlist": "Nhập vào danh sách phát", + "Keep sidebar open": "Giữ thanh bên luôn mở", + "WARNING: Might require reload to work properly!": "CẢNH BÁO: Có thể phải tải lại để hoạt động bình thường!", + "An error occured, URL might be invalid or unsupported.": "Lỗi, URL sai hoặc không được hỗ trợ.", + "Top tracks": "Bài hát hàng đầu", + "Show all top tracks": "Hiện tất cả bài hát hàng đầu", + "Singles": "Đĩa đơn", + "Album:": "Album:", + "Artists:": "Nghệ sĩ:", + "Yes": "Có", + "No": "Không", + "Download Filename": "Tải xuống tên tệp", + "Language": "Ngôn ngữ", + "Background Image": "Ảnh nền", + "Enter URL or absolute path. WARNING: Requires reload!": "Nhập URL hoặc đường dẫn hợp lệ. CẢNH BÁO: Yêu cầu tải lại!", + "LGBT Mode": "Chế độ LGBT", + "Native top bar": "Thanh trên cùng mặc định", + "Requires restart of Freezer!": "Cần khởi động lại Freezer!" +} \ No newline at end of file diff --git a/app/client/src/locales/zh.json b/app/client/src/locales/zh.json new file mode 100644 index 0000000..ff5aef8 --- /dev/null +++ b/app/client/src/locales/zh.json @@ -0,0 +1,171 @@ +{ + "Home": "主页", + "Browse": "浏览", + "Library": "曲库", + "Tracks": "歌曲", + "Playlists": "播放列表", + "Albums": "专辑", + "Artists": "歌手", + "More": "更多", + "Settings": "设置", + "Downloads": "下载", + "Search or paste Deezer URL. Use / to quickly focus.": "搜索或输入 Deezer 链接。使用“/”来快速对焦。", + "Play": "播放", + "Add to library": "添加到曲库", + "Download": "下载", + "fans": "粉丝", + "tracks": "歌曲", + "Quality": "音质", + "Estimated size:": "预计大小:", + "Start downloading": "开始下载", + "Cancel": "取消", + "Stream logging is disabled!": "在线播放日志已禁用", + "Enable it in settings for history to work properly.": "请在设置中开启这个功能", + "History": "播放记录", + "Create new playlist": "新建播放列表", + "TRACKS": "歌曲", + "Sort by": "排序方式", + "Date Added": "添加日期", + "Name (A-Z)": "标题(A - Z)", + "Artist (A-Z)": "歌手 (A - Z)", + "Album (A-Z)": "专辑(A - Z)", + "Error loading lyrics or lyrics not found!": "加载歌词错误或歌词不存在", + "Create playlist": "创建播放列表", + "Create": "创建", + "Add to playlist": "添加到播放列表", + "Create new": "新建", + "Remove": "移除", + "Play next": "播放下一首", + "Add to queue": "加入队列", + "Remove from library": "从曲库中删除", + "Remove from playlist": "从播放列表中删除", + "Play track mix": "播放歌曲电台", + "Go to": "转到", + "Track Mix": "曲目电台", + "Duration": "时长", + "Released": "已发布", + "Disk": "专辑碟", + "albums": "专辑", + "Play top": "播放热门", + "Radio": "电台", + "Show all albums": "显示所有专辑", + "Show all singles": "显示所有单曲", + "Show more": "更多", + "Downloaded": "已下载", + "Queue": "队列", + "Total": "全部", + "Stop": "停止", + "Start": "开始", + "Show folder": "打开文件夹", + "Clear queue": "清除队列", + "Playing from": "正在播放自", + "Info": "详情", + "Lyrics": "歌词", + "Track number": "歌曲编号", + "Disk number": "专辑碟号", + "Explicit": "露骨", + "Source": "来源", + "ID": "ID", + "Error logging in!": "登录错误", + "Please try again later, or try another account.": "请稍后再试,或尝试另一个帐户。", + "Logout": "注销", + "Login using browser": "使用浏览器登录", + "Please login using your Deezer account:": "请使用您的 Deezer 帐户登录:", + "...or paste your ARL/Token below:": "或者在下面输入 ARL/Token:", + "ARL/Token": "ARL/Token", + "Login": "登录", + "By using this program, you disagree with Deezer's ToS.": "使用这个软件,代表您不同意 Deezer 的使用条款", + "Only in Electron version!": "仅支持 Electron 版本", + "Search results for:": "搜索结果:", + "Error loading data!": "加载数据错误", + "Try again later!": "稍后再试", + "Search": "搜索", + "Streaming Quality": "在线播放音质", + "Download Quality": "下载音质", + "Downloads Directory": "下载目录", + "Simultaneous downloads": "同时下载任务数", + "Always show download confirm dialog before downloading.": "下载前总是显示下载确认对话框", + "Show download dialog": "显示下载对话框", + "Create folders for artists": "创建艺术家文件夹", + "Create folders for albums": "创建专辑文件夹", + "Download lyrics": "下载歌词", + "Variables": "变量", + "UI": "用户界面", + "Show autocomplete in search": "在搜索中显示自动补全", + "Integrations": "整合", + "This allows listening history, flow and recommendations to work properly.": "这个选项开启播放历史、Flow 和推荐功能", + "Log track listens to Deezer": "发送播放记录到 Deezer", + "Connect your LastFM account to allow scrobbling.": "登录您的 LastFM 账户同步播放记录", + "Login with LastFM": "登录 LastFM", + "Disconnect LastFM": "登出 LastFM", + "Requires restart to apply!": "需要重启生效", + "Enable Discord Rich Presence, requires restart to toggle!": "开启 Discord Rich Presence,需要重启生效", + "Discord Rich Presence": "Discord Rich Presence", + "Enable Discord join button for syncing tracks, requires restart to toggle!": "启用进入 Discord 按钮以显示正在播放的歌曲,需要重启生效", + "Discord Join Button": "Discord 加入按钮", + "Other": "其他", + "Minimize to tray": "最小化到托盘", + "Don't minimize to tray": "不要最小化到托盘", + "Close on exit": "直接关闭", + "Settings saved!": "设置已保存", + "Available only in Electron version!": "仅在 Electron 版本中可用!", + "Crossfade (ms)": "平缓过渡(毫秒)", + "Select primary color": "选择主色调", + "Light theme": "浅色主题", + "Create folders for playlists": "创建播放列表文件夹", + "About": "关于", + "Links:": "链接:", + "Telegram Releases": "到 Telegram 下载", + "Telegram Group": "Telegram 群组", + "Discord": "Discord", + "Telegram Android Group": "Telegram Android 群组", + "Credits:": "贡献者:", + "Agree": "同意", + "Dismiss": "忽略", + "Added to playlist!": "已添加到播放列表", + "Added to library!": "已添加到曲库", + "Removed from library!": "已从曲库中移除", + "Removed from playlist!": "已从播放列表中移除", + "Playlist deleted!": "已删除播放列表", + "Delete": "删除", + "Are you sure you want to delete this playlist?": "确定要删除这个播放列表?", + "Force white tray icon": "强制使用白色托盘图标", + "Force default (white) tray icon if theme incorrectly detected. Requires restart.": "如果主题检测不正确,强制使用默认(白色)托盘图标,需要重启", + "Share": "分享", + "Settings quality": "预设音质", + "Content language": "语种", + "Content country": "国家", + "Website": "网站", + "Visit website": "访问网站", + "New update available:": "检测到新版本", + "Shuffle": "随机", + "Download album cover": "下载专辑封面", + "Art Resolution": "专辑封面分辨率", + "Public": "公开", + "Private": "私有", + "Collaborative": "协作", + "Edit playlist": "编辑播放列表", + "Save": "保存", + "Edit": "编辑", + "Importer": "导入", + "Enter URL": "输入链接", + "Currently only Spotify is supported and limited to 100 tracks.": "目前只支持 Spotify ,并限制最多 100 首歌曲", + "Import into playlist": "导入到播放列表", + "Keep sidebar open": "保持侧栏打开", + "WARNING: Might require reload to work properly!": "警告:可能需要重新加载才能正常工作!", + "An error occured, URL might be invalid or unsupported.": "出现错误,链接可能无效或不支持", + "Top tracks": "热门歌曲", + "Show all top tracks": "显示所有热门歌曲", + "Singles": "单曲", + "Album:": "专辑:", + "Artists:": "歌手:", + "Yes": "是", + "No": "否", + "Download Filename": "下载文件名", + "Language": "语言", + "Background Image": "Background Image", + "Enter URL or absolute path. WARNING: Requires reload!": "Enter URL or absolute path. WARNING: Requires reload!", + "LGBT Mode": "LGBT Mode", + "Native top bar": "Native top bar", + "Requires restart of Freezer!": "Requires restart of Freezer!" +} \ No newline at end of file diff --git a/app/client/src/main.js b/app/client/src/main.js new file mode 100644 index 0000000..b73f576 --- /dev/null +++ b/app/client/src/main.js @@ -0,0 +1,696 @@ +import Vue from 'vue'; +import App from './App.vue'; +import router from './js/router'; +import vuetify from './js/vuetify'; +import axios from 'axios'; +import VueEsc from 'vue-esc'; +import i18n from './js/i18n'; +import { io } from "socket.io-client"; + +//Globals +let ipcRenderer; +//Axios +let axiosInstance = axios.create({ + baseURL: process.env.NODE_ENV === 'development' ? "http://localhost:10069" : `${window.location.origin}`, + timeout: 16000, + responseType: 'json' +}); +Vue.prototype.$axios = axiosInstance; + +//Duration formatter +Vue.prototype.$duration = (ms) => { + if (isNaN(ms) || ms < 1) return '0:00'; + let s = Math.floor(ms / 1000); + let hours = Math.floor(s / 3600); + s %= 3600; + let min = Math.floor(s / 60); + let sec = s % 60; + if (hours == 0) return `${min}:${sec.toString().padStart(2, '0')}`; + return `${hours}:${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; +}; + +//Abbrevation +Vue.prototype.$abbreviation = (n) => { + if (!n || n == 0) return '0'; + var base = Math.floor(Math.log(Math.abs(n))/Math.log(1000)); + var suffix = 'KMB'[base-1]; + return suffix ? String(n/Math.pow(1000,base)).substring(0,3)+suffix : ''+n; +} + +//Add thousands commas +Vue.prototype.$numberString = (n) => { + if (!n || n == 0) return '0'; + return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + +//Filesize +Vue.prototype.$filesize = (bytes) => { + if (bytes === 0) return '0 B'; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i]; +} + +//Sockets +Vue.prototype.$io = io("http://localhost:10069", { + path: '/socket' +}); + +Vue.config.productionTip = false; +Vue.use(VueEsc); + +new Vue({ + data: { + //Globals + settings: {}, + profile: {}, + authorized: false, + loadingPromise: null, + + downloads: {}, + + //Player + track: null, + audio: null, + volume: 0.00, + //0 = Stopped, 1 = Paused, 2 = Playing, 3 = Loading + state: 0, + loaders: 0, + playbackInfo: {}, + position: 0, + muted: false, + //Gapless playback meta + gapless: { + promise: null, + audio: null, + info: null, + track: null + }, + + //0 - normal, 1 - repeat list, 2 - repeat track + repeat: 0, + shuffled: false, + + //Library cache + libraryTracks: [], + + //Queue data + queue: { + data: [], + index: -1, + source: { + text: 'None', + source: 'none', + data: 'none' + } + }, + + //Used to prevent double listen logging + logListenId: null, + + globalSnackbar: null + }, + + methods: { + // PLAYBACK METHODS + isPlaying() { + return this.state == 2; + }, + + play() { + if (!this.audio || this.state != 1) return; + this.audio.play(); + this.state = 2; + }, + pause() { + if (!this.audio || this.state != 2) return; + this.audio.pause(); + this.state = 1; + }, + toggle() { + if (this.isPlaying()) return this.pause(); + this.play(); + }, + seek(t) { + if (!this.audio || isNaN(t) || !t) return; + //ms -> s + this.audio.currentTime = (t / 1000); + this.position = t; + + this.updateState(); + }, + + //Current track duration + duration() { + //Prevent 0 division + if (!this.audio) return 1; + return this.audio.duration * 1000; + }, + + //Replace queue, has to make clone of data to not keep references + replaceQueue(newQueue) { + this.queue.data = Object.assign([], newQueue); + }, + //Add track to queue at index + addTrackIndex(track, index) { + this.queue.data.splice(index, 0, track); + }, + + //Play at index in queue + async playIndex(index) { + if (index >= this.queue.data.length || index < 0) return; + this.queue.index = index; + await this.playTrack(this.queue.data[this.queue.index]); + this.play(); + this.savePlaybackInfo(); + }, + //Skip n tracks, can be negative + async skip(n) { + let newIndex = this.queue.index + n; + //Out of bounds + if (newIndex < 0 || newIndex >= this.queue.data.length) return; + await this.playIndex(newIndex); + }, + shuffle() { + if (!this.shuffled) { + //Save positions + for (let i=0; i 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.queue.data[i], this.queue.data[j]] = [this.queue.data[j], this.queue.data[i]]; + } + //Update index + this.queue.index = this.queue.data.findIndex(t => t.id == this.track.id); + this.shuffled = true; + return; + } + //Restore unshuffled queue + if (this.shuffled) { + this.queue.data.sort((a, b) => (a._position || 10000) - (b._position || 10000)); + this.queue.index = this.queue.data.findIndex(t => t.id == this.track.id); + this.shuffled = false; + return; + } + }, + + //Skip wrapper + skipNext() { + this.skip(1); + this.savePlaybackInfo(); + }, + toggleMute() { + if (this.audio) this.audio.muted = !this.audio.muted; + this.muted = !this.muted; + }, + + async playTrack(track) { + if (!track || !track.streamUrl) return; + this.resetGapless(); + + this.track = track; + this.loaders++; + this.state = 3; + //Stop audio + let autoplay = (this.state == 2); + if (this.audio) this.audio.pause(); + if (this.audio) this.audio.currentTime = 0; + + //Load track meta + let playbackInfo = await this.loadPlaybackInfo(track.streamUrl, track.duration); + if (!playbackInfo) { + this.loaders--; + this.skipNext(); + return; + } + this.playbackInfo = playbackInfo; + + //Stream URL + let url; + if (this.playbackInfo.encrypted) + url = `${process.env.NODE_ENV === 'development' ? "http://localhost:10069" : window.location.origin}${this.playbackInfo.url}`; + else + url = this.playbackInfo.direct; + //Cancel loading + this.loaders--; + if (this.loaders > 0) { + return; + } + + //Audio + this.audio = new Audio(url); + this.configureAudio(); + this.state = 1; + if (autoplay) this.play(); + //MediaSession + this.updateMediaSession(); + + //Loads more tracks if end of list + this.loadSTL(); + }, + //Configure html audio element + configureAudio() { + //Listen position updates + this.audio.addEventListener('timeupdate', async () => { + this.position = this.audio.currentTime * 1000; + + //Gapless playback + if (this.position >= (this.duration() - (this.settings.crossfadeDuration + 7500)) && this.state == 2) { + if (this.repeat != 2) + this.loadGapless(); + } + + //Crossfade + if (this.settings.crossfadeDuration > 0 && this.position >= (this.duration() - this.settings.crossfadeDuration) && this.state == 2 && this.gapless.audio && !this.gapless.crossfade && this.gapless.track) { + this.gapless.crossfade = true; + let currentVolume = this.audio.volume; + let oldAudio = this.audio; + this.audio = this.gapless.audio; + this.audio.play(); + + //Update meta + this.playbackInfo = this.gapless.info; + this.track = this.gapless.track; + this.queue.index = this.gapless.index; + + this.configureAudio(); + this.updateMediaSession(); + + this.audio.volume = 0.0; + let volumeStep = currentVolume / (this.settings.crossfadeDuration / 50); + for (let i=0; i<(this.settings.crossfadeDuration / 50); i++) { + if ((oldAudio.volume - volumeStep) > 0) + oldAudio.volume -= volumeStep; + if ((this.audio.volume + volumeStep) >= 1.0 || (this.audio.volume + volumeStep) >= currentVolume) + break; + this.audio.volume += volumeStep; + await new Promise((res) => setTimeout(() => res(), 50)); + } + //Restore original volume + this.audio.voume = currentVolume; + + oldAudio.pause(); + + this.resetGapless(); + this.updateState(); + + //Save + await this.savePlaybackInfo(); + } + + //Scrobble/LogListen + if (this.position >= this.duration() * 0.75) { + this.logListen(); + } + }); + this.audio.muted = this.muted; + //Set volume + this.audio.volume = this.volume * this.volume; + + this.audio.addEventListener('ended', async () => { + if (this.gapless.crossfade) return; + + //Repeat track + if (this.repeat == 2) { + this.seek(0); + this.audio.play(); + this.updateState(); + return; + } + + //Repeat list + if (this.repeat == 1 && this.queue.index == this.queue.data.length - 1) { + this.skip(-(this.queue.data.length - 1)); + return; + } + + //End of queue + if (this.queue.index+1 == this.queue.data.length) { + this.state = 1; + return; + } + + //Skip to next track + this.skip(1); + this.savePlaybackInfo(); + }); + }, + //Update media session with current track metadata + updateMediaSession() { + if (!this.track || !('mediaSession' in navigator)) return; + + // eslint-disable-next-line no-undef + navigator.mediaSession.metadata = new MediaMetadata({ + title: this.track.title, + artist: this.track.artistString, + album: this.track.album.title, + artwork: [ + {src: this.getImageUrl(this.track.albumArt, 256), sizes: '256x256', type: 'image/jpeg'}, + {src: this.getImageUrl(this.track.albumArt, 512), sizes: '512x512', type: 'image/jpeg'} + ] + }); + //Controls + navigator.mediaSession.setActionHandler('play', this.play); + navigator.mediaSession.setActionHandler('pause', this.pause); + navigator.mediaSession.setActionHandler('nexttrack', this.skipNext); + navigator.mediaSession.setActionHandler('previoustrack', () => this.skip(-1)); + }, + //Get Deezer CDN image url + getImageUrl(img, size = 256) { + return `https://e-cdns-images.dzcdn.net/images/${img.type}/${img.hash}/${size}x${size}-000000-80-0-0.jpg` + }, + + async loadPlaybackInfo(streamUrl, duration) { + //Get playback info + let quality = this.settings.streamQuality; + let infoUrl = `/streaminfo/${streamUrl}?q=${quality}`; + let res; + try { + res = await this.$axios.get(infoUrl); + } catch (_) { + return null; + } + + let info = res.data; + //Generate qualityString + switch (info.quality) { + case 9: + info.qualityString = 'FLAC ' + Math.round((info.size*8) / duration) + 'kbps'; + break; + case 3: + info.qualityString = 'MP3 320kbps'; + break; + case 1: + info.qualityString = 'MP3 128kbps'; + break; + } + return info; + }, + + //Reset gapless playback meta + resetGapless() { + this.gapless = {crossfade: false,promise: null,audio: null,info: null,track: null,index:null}; + }, + //Load next track for gapless + async loadGapless() { + if (this.loaders != 0 || this.gapless.promise || this.gapless.audio || this.gapless.crossfade) return; + + //Repeat list + if (this.repeat == 1 && this.queue.index == this.queue.data.length - 1) { + this.gapless.track = this.queue.data[0]; + this.gapless.index = 0; + } else { + //Last song + if (this.queue.index+1 >= this.queue.data.length) return; + //Next song + this.gapless.track = this.queue.data[this.queue.index + 1]; + this.gapless.index = this.queue.index + 1; + } + + //Save promise + let resolve; + this.gapless.promise = new Promise((res) => {resolve = res}); + + //Load meta + let info = await this.loadPlaybackInfo(this.gapless.track.streamUrl, this.gapless.track.duration); + if (!info) { + this.resetGapless(); + if (this.gapless.promise) resolve(); + } + this.gapless.info = info + if (info.encrypted) + this.gapless.audio = new Audio(`${process.env.NODE_ENV === 'development' ? "http://localhost:10069" : window.location.origin}${info.url}`); + else + this.gapless.audio = new Audio(info.direct); + this.gapless.audio.volume = 0.00; + this.gapless.audio.preload = 'auto'; + this.gapless.crossfade = false; + + //Might get canceled + if (this.gapless.promise) resolve(); + }, + //Load more SmartTrackList tracks + async loadSTL() { + if (this.queue.data.length - 1 == this.queue.index && this.queue.source.source == 'smarttracklist') { + let data = await this.$axios.get('/smarttracklist/' + this.queue.source.data); + if (data.data) { + this.queue.data = this.queue.data.concat(data.data); + } + this.savePlaybackInfo(); + } + }, + + //Update & save settings + async saveSettings() { + this.settings.volume = this.volume; + await this.$axios.post('/settings', this.settings); + + //Update settings in electron + if (this.settings.electron) { + ipcRenderer.send('updateSettings', this.settings); + } + }, + + async savePlaybackInfo() { + let data = { + queue: this.queue, + position: this.position, + track: this.track, + repeat: this.repeat, + shuffled: this.shuffled + } + await this.$axios.post('/playback', data); + }, + //Get downloads from server + async getDownloads() { + let res = await this.$axios.get('/downloads'); + if (res.data) + this.downloads = res.data; + }, + //Start stop downloading + async toggleDownload() { + if (this.downloads.downloading) { + await this.$axios.delete('/download'); + } else { + await this.$axios.put('/download'); + } + }, + + //Deezer doesn't give information if items are in library, so it has to be cachced + async cacheLibrary() { + let res = await this.$axios.get(`/playlist/${this.profile.favoritesPlaylist}?full=idk`); + this.libraryTracks = res.data.tracks.map((t) => t.id); + }, + + //Log song listened to deezer, only if allowed + async logListen() { + if (this.logListenId == this.track.id) return; + if (!this.track || !this.track.id) return; + + this.logListenId = this.track.id; + await this.$axios.post(`/log`, this.track); + }, + //Send state update to integrations + async updateState() { + //Wait for duration + if (this.state == 2 && (this.duration() == null || isNaN(this.duration()))) { + setTimeout(() => { + this.updateState(); + }, 500); + return; + } + this.$io.emit('stateChange', { + position: this.position, + duration: this.duration(), + state: this.state, + track: this.track + }); + + //Update in electron + if (this.settings.electron) { + ipcRenderer.send('playing', this.state == 2); + } + }, + updateLanguage(l) { + i18n.locale = l; + }, + //For LGBT/Gayming mode + primaryColorRainbow() { + const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', + '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', + '#795548', '#607D8B', '#9E9E9E']; + let index = 0; + setInterval(() => { + this.$vuetify.theme.themes.dark.primary = colors[index]; + this.$vuetify.theme.themes.light.primary = colors[index]; + this.$root.settings.primaryColor = colors[index]; + index++; + if (index == colors.length) + index = 0; + }, 400); + } + }, + + computed: { + //Show or no topbar + topBar() { + if (!this.settings.electron) return false; + return !this.settings.nativeTopBar; + } + }, + + async created() { + //Load settings, create promise so `/login` can await it + let r; + this.loadingPromise = new Promise((resolve) => r = resolve); + let res = await this.$axios.get('/settings'); + this.settings = res.data; + this.$vuetify.theme.themes.dark.primary = this.settings.primaryColor; + this.$vuetify.theme.themes.light.primary = this.settings.primaryColor; + if (this.settings.lightTheme) + this.$vuetify.theme.dark = false; + + //Remove LGBT + const lgbt = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Gay_Pride_Flag.svg/1280px-Gay_Pride_Flag.svg.png'; + if (this.settings.backgroundImage == lgbt) + this.settings.backgroundImage = null; + + + i18n.locale = this.settings.language; + this.volume = this.settings.volume; + + //Restore playback data + let pd = await this.$axios.get('/playback'); + if (pd.data != {}) { + if (pd.data.queue) this.queue = pd.data.queue; + if (pd.data.track) this.track = pd.data.track; + if (pd.data.repeat) this.repeat = pd.data.repeat; + if (pd.data.shuffled) this.shuffled = pd.data.shuffled; + this.playTrack(this.track).then(() => { + this.seek(pd.data.position); + }); + } + + //Check for electron (src: npm isElectron) + this.settings.electron = (( + typeof window !== 'undefined' && + typeof window.process === 'object' && + window.process.type === 'renderer') || ( + typeof navigator === 'object' && typeof navigator.userAgent === 'string' && + navigator.userAgent.indexOf('Electron') >= 0 + )); + if (this.settings.electron) + ipcRenderer = window.require('electron').ipcRenderer; + + //Setup electron callbacks + if (this.settings.electron) { + //Save files on exit + ipcRenderer.on('onExit', async () => { + this.pause(); + await this.saveSettings(); + await this.savePlaybackInfo(); + ipcRenderer.send('onExit', ''); + }); + + //Control from electron + ipcRenderer.on('togglePlayback', () => { + this.toggle(); + }); + ipcRenderer.on('skipNext', () => { + this.skip(1); + }); + ipcRenderer.on('skipPrev', () => { + this.skip(-1); + }) + } + + //Get downloads + await this.getDownloads(); + + //Sockets + + //Queue change + this.$io.on('downloads', (data) => { + this.downloads = data; + }); + //Current download change + this.$io.on('currentlyDownloading', (data) => { + this.downloads.threads = data; + }); + //Play at offset (for integrations) + this.$io.on('playOffset', async (data) => { + this.queue.data.splice(this.queue.index + 1, 0, data.track); + await this.skip(1); + this.seek(data.position); + }); + + r(); + }, + + mounted() { + //Save settings on unload + window.addEventListener('beforeunload', () => { + this.savePlaybackInfo(); + this.saveSettings(); + }); + //Save size + window.addEventListener('resize', () => { + this.settings.width = window.innerWidth; + this.settings.height = window.innerHeight; + }); + + //Keystrokes + document.addEventListener('keyup', (e) => { + //Don't handle keystrokes in text fields + if (e.target.tagName == "INPUT") return; + //Don't handle if specials + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + + //K toggle playback + if (e.code == "KeyK" || e.code == "Space") this.$root.toggle(); + //L +10s (from YT) + if (e.code == "KeyL") this.$root.seek((this.position + 10000)); + //J -10s (from YT) + if (e.code == "KeyJ") this.$root.seek((this.position - 10000)); + //-> +5s (from YT) + if (e.code == "ArrowRight") this.$root.seek((this.position + 5000)); + //<- -5s (from YT) + if (e.code == "ArrowLeft") this.$root.seek((this.position - 5000)); + // ^ v - Volume + if (e.code == 'ArrowUp') { + if ((this.volume + 0.05) > 1) { + this.volume = 1.00; + return; + } + this.volume += 0.05; + } + if (e.code == 'ArrowDown') { + if ((this.volume - 0.05) < 0) { + this.volume = 0.00; + return; + } + this.volume -= 0.05; + } + }); + }, + + watch: { + //Watch state for integrations + state() { + this.updateMediaSession(); + this.updateState(); + }, + //Update volume with curve + volume() { + if (this.audio) + this.audio.volume = this.volume * this.volume; + } + }, + + router, + vuetify, + i18n, + render: function (h) { return h(App) } +}).$mount('#app'); diff --git a/app/client/src/views/About.vue b/app/client/src/views/About.vue new file mode 100644 index 0000000..1317de7 --- /dev/null +++ b/app/client/src/views/About.vue @@ -0,0 +1,144 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/AlbumPage.vue b/app/client/src/views/AlbumPage.vue new file mode 100644 index 0000000..6dcc72f --- /dev/null +++ b/app/client/src/views/AlbumPage.vue @@ -0,0 +1,159 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/ArtistPage.vue b/app/client/src/views/ArtistPage.vue new file mode 100644 index 0000000..8e045fc --- /dev/null +++ b/app/client/src/views/ArtistPage.vue @@ -0,0 +1,240 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/DeezerPage.vue b/app/client/src/views/DeezerPage.vue new file mode 100644 index 0000000..a148b6f --- /dev/null +++ b/app/client/src/views/DeezerPage.vue @@ -0,0 +1,102 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/DownloadsPage.vue b/app/client/src/views/DownloadsPage.vue new file mode 100644 index 0000000..e175c48 --- /dev/null +++ b/app/client/src/views/DownloadsPage.vue @@ -0,0 +1,85 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/FullscreenPlayer.vue b/app/client/src/views/FullscreenPlayer.vue new file mode 100644 index 0000000..29d9d31 --- /dev/null +++ b/app/client/src/views/FullscreenPlayer.vue @@ -0,0 +1,384 @@ + + + + + \ No newline at end of file diff --git a/app/client/src/views/HomeScreen.vue b/app/client/src/views/HomeScreen.vue new file mode 100644 index 0000000..7d37209 --- /dev/null +++ b/app/client/src/views/HomeScreen.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/Library.vue b/app/client/src/views/Library.vue new file mode 100644 index 0000000..a5dc267 --- /dev/null +++ b/app/client/src/views/Library.vue @@ -0,0 +1,107 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/Login.vue b/app/client/src/views/Login.vue new file mode 100644 index 0000000..fc346b2 --- /dev/null +++ b/app/client/src/views/Login.vue @@ -0,0 +1,127 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/PlaylistPage.vue b/app/client/src/views/PlaylistPage.vue new file mode 100644 index 0000000..f211033 --- /dev/null +++ b/app/client/src/views/PlaylistPage.vue @@ -0,0 +1,320 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/Search.vue b/app/client/src/views/Search.vue new file mode 100644 index 0000000..ecc67c3 --- /dev/null +++ b/app/client/src/views/Search.vue @@ -0,0 +1,151 @@ + + + \ No newline at end of file diff --git a/app/client/src/views/Settings.vue b/app/client/src/views/Settings.vue new file mode 100644 index 0000000..c785250 --- /dev/null +++ b/app/client/src/views/Settings.vue @@ -0,0 +1,521 @@ + + + \ No newline at end of file diff --git a/app/client/styles/bg-image.scss b/app/client/styles/bg-image.scss new file mode 100644 index 0000000..5e7990b --- /dev/null +++ b/app/client/styles/bg-image.scss @@ -0,0 +1,59 @@ +.theme--dark.v-tabs > .v-tabs-bar { + background-color: #00000000 !important; + backdrop-filter: blur(16px); +} +.theme--dark.v-list { + background-color: #00000000 !important; +} +.theme--light.v-list { + background-color: #ffffff00 !important; +} +.theme--dark.v-application { + background-color: #000000aa !important; +} +.theme--light.v-application { + background-color: #ffffff00 !important; +} +.v-list-item { + background-color: #00000000; +} +.v-list { + background-color: #000000aa !important; +} +.v-item-group { + background-color: #00000000 !important; + backdrop-filter: blur(16px); +} +.v-card { + background-color: #00000000 !important; + backdrop-filter: blur(16px); +} +.v-navigation-drawer { + background-color: #00000000 !important; +} +.theme--dark.v-navigation-drawer--is-mouseover { + background-color: #12121277 !important; + backdrop-filter: blur(16px); +} +.theme--light.v-navigation-drawer--is-mouseover { + background-color: #ffffff66 !important; + backdrop-filter: blur(16px); +} +.v-toolbar { + background-color: #00000000 !important; +} +.v-navigation-drawer--mini-variant { + background-color: #00000000 !important; +} +.v-footer { + background-color: #00000000 !important; +} +.theme--dark.v-select-list div { + background-color: #212121; +} +.theme--light.v-select-list div { + background-color: #ffffff; +} +.v-menu__content { + backdrop-filter: blur(16px); +} \ No newline at end of file diff --git a/app/client/styles/scrollbar.scss b/app/client/styles/scrollbar.scss new file mode 100644 index 0000000..7d75ac5 --- /dev/null +++ b/app/client/styles/scrollbar.scss @@ -0,0 +1,29 @@ +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar:horizontal { + height: 10px; +} + +::-webkit-scrollbar-track { + background-color: #10101044; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #363636; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #404040; +} + +::-webkit-scrollbar-thumb:horizontal { + background-color: #36363690; +} + +::-webkit-scrollbar-thumb:horizontal:hover { + background-color: #404040; +} \ No newline at end of file diff --git a/app/client/vue.config.js b/app/client/vue.config.js new file mode 100644 index 0000000..dd49452 --- /dev/null +++ b/app/client/vue.config.js @@ -0,0 +1,14 @@ +module.exports = { + "transpileDependencies": [ + "vuetify" + ], + + pluginOptions: { + i18n: { + locale: 'en', + fallbackLocale: 'en', + localeDir: 'locales', + enableInSFC: true + } + } +} diff --git a/app/main.js b/app/main.js new file mode 100644 index 0000000..93bec4d --- /dev/null +++ b/app/main.js @@ -0,0 +1,7 @@ +const {createServer} = require('./src/server'); + +createServer(false); + +/* +This script is used to start standalone server without electron +*/ \ No newline at end of file diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..467c9c6 --- /dev/null +++ b/app/package.json @@ -0,0 +1,33 @@ +{ + "name": "saturn", + "private": true, + "version": "1.0.0", + "description": "", + "main": "background.js", + "scripts": { + "start": "electron ." + }, + "author": "", + "license": "ISC", + "dependencies": { + "arg": "^5.0.0", + "axios": "^0.21.1", + "browser-id3-writer": "^4.4.0", + "chalk": "^4.1.1", + "cheerio": "^1.0.0-rc.9", + "compare-versions": "^3.6.0", + "cors": "^2.8.5", + "discord-rpc": "^3.2.0", + "express": "^4.17.1", + "lastfmapi": "^0.1.1", + "metaflac-js2": "^1.0.7", + "nedb": "^1.8.0", + "nodeezcryptor": "git+https://github.com/SaturnMusic/encryption.git", + "sanitize-filename": "^1.6.3", + "socket.io": "^4.1.2", + "winston": "^3.3.3" + }, + "devDependencies": { + "eslint": "^7.27.0" + } +} diff --git a/app/src/deezer.js b/app/src/deezer.js new file mode 100644 index 0000000..41288a0 --- /dev/null +++ b/app/src/deezer.js @@ -0,0 +1,485 @@ +const crypto = require('crypto'); +const axios = require('axios'); +const decryptor = require('nodeezcryptor'); +const querystring = require('querystring'); +const https = require('https'); +const {Transform, Readable} = require('stream'); +const {Track} = require('./definitions'); +const logger = require('./winston'); + +global.lasturl; + +class DeezerAPI { + + constructor(arl, electron = false) { + this.arl = arl; + this.electron = electron; + this.url = 'https://www.deezer.com/ajax/gw-light.php'; + } + + //Get headers + headers() { + let cookie = `arl=${this.arl}`; + if (this.sid) cookie += `; sid=${this.sid}`; + return { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", + "Content-Language": "en-US", + "Cache-Control": "max-age=0", + "Accept": "*/*", + "Accept-Charset": "utf-8,ISO-8859-1;q=0.7,*;q=0.3", + "Accept-Language": "en-US,en;q=0.9,en-US;q=0.8,en;q=0.7", + "Connection": 'keep-alive', + "Cookie": cookie + } + } + + //Wrapper for api calls, because axios doesn't work reliably with electron + async callApi(method, args = {}, gatewayInput = null) { + if (this.electron) return await this._callApiElectronNet(method, args, gatewayInput); + return await this._callApiAxios(method, args, gatewayInput); + } + + //gw_light api call using axios, unstable in electron + async _callApiAxios(method, args = {}, gatewayInput = null) { + let data = await axios({ + url: this.url, + method: 'POST', + headers: this.headers(), + responseType: 'json', + params: Object.assign({ + api_version: '1.0', + api_token: this.token ? this.token : 'null', + input: '3', + method: method, + }, + gatewayInput ? {gateway_input: JSON.stringify(gatewayInput)} : null + ), + data: args + }); + + //Save SID cookie to not get token error + if (method == 'deezer.getUserData') { + let sidCookie = data.headers['set-cookie'].filter((e) => e.startsWith('sid=')); + if (sidCookie.length > 0) { + sidCookie = sidCookie[0].split(';')[0]; + this.sid = sidCookie.split('=')[1]; + } + } + + //Invalid CSRF + if (data.data.error && data.data.error.VALID_TOKEN_REQUIRED) { + await this.authorize(); + return await this.callApi(method, args, gatewayInput); + } + + return data.data; + } + + //gw_light api call using electron net + async _callApiElectronNet(method, args = {}, gatewayInput = null) { + const net = require('electron').net; + let data = await new Promise((resolve, reject) => { + //Create request + let req = net.request({ + method: 'POST', + url: this.url + '?' + querystring.stringify(Object.assign({ + api_version: '1.0', + api_token: this.token ? this.token : 'null', + input: '3', + method: method, + }, + gatewayInput ? {gateway_input: JSON.stringify(gatewayInput)} : null + )), + }); + + req.on('response', (res) => { + let data = Buffer.alloc(0); + + //Save SID cookie + if (method == 'deezer.getUserData') { + let sidCookie = res.headers['set-cookie'].filter((e) => e.startsWith('sid=')); + if (sidCookie.length > 0) { + sidCookie = sidCookie[0].split(';')[0]; + this.sid = sidCookie.split('=')[1]; + } + } + + //Response data + res.on('data', (buffer) => { + data = Buffer.concat([data, buffer]); + }); + res.on('end', () => { + resolve(data); + }) + }); + req.on('error', (err) => { + reject(err); + }); + + //Write headers + let headers = this.headers(); + for(let key of Object.keys(headers)) { + req.setHeader(key, headers[key]); + } + req.write(JSON.stringify(args)); + req.end(); + }); + + data = JSON.parse(data.toString('utf-8')); + + //Invalid CSRF + if (data.error && data.error.VALID_TOKEN_REQUIRED) { + await this.authorize(); + return await this.callApi(method, args, gatewayInput); + } + + return data; + } + + //true / false if success + async authorize() { + let data = await this.callApi('deezer.getUserData'); + + this.plan = data.results.OFFER_NAME; + + this.token = data.results.checkForm; + this.userId = data.results.USER.USER_ID; + this.favoritesPlaylist = data.results.USER.LOVEDTRACKS_ID.toString(); + + if (!this.userId || this.userId == 0 || !this.token || this.plan == "Deezer Free") return false; + return true; + } + + //Wrapper because electron is piece of shit + async callPublicApi(path, params) { + if (this.electron) return await this._callPublicApiElectron(path, params); + return await this._callPublicApiAxios(path, params); + } + + async _callPublicApiElectron(path, params) { + const net = require('electron').net; + let data = await new Promise((resolve, reject) => { + //Create request + let req = net.request({ + method: 'GET', + url: `https://api.deezer.com/${encodeURIComponent(path)}/${encodeURIComponent(params)}` + }); + + req.on('response', (res) => { + let data = Buffer.alloc(0); + //Response data + res.on('data', (buffer) => { + data = Buffer.concat([data, buffer]); + }); + res.on('end', () => { + resolve(data); + }) + }); + req.on('error', (err) => { + reject(err); + }); + req.end(); + }); + + data = JSON.parse(data.toString('utf-8')); + return data; + } + + async _callPublicApiAxios(path, params) { + let res = await axios({ + url: `https://api.deezer.com/${encodeURIComponent(path)}/${encodeURIComponent(params)}`, + responseType: 'json', + method: 'GET' + }); + return res.data; + } + + //Get track URL + static getUrl(trackId, md5origin, mediaVersion, quality = 3) { + const magic = Buffer.from([0xa4]); + let step1 = Buffer.concat([ + Buffer.from(md5origin), + magic, + Buffer.from(quality.toString()), + magic, + Buffer.from(trackId), + magic, + Buffer.from(mediaVersion) + ]); + //MD5 + let md5sum = crypto.createHash('md5'); + md5sum.update(step1); + let step1md5 = md5sum.digest('hex'); + + let step2 = Buffer.concat([ + Buffer.from(step1md5), + magic, + step1, + magic + ]); + //Padding + while(step2.length%16 > 0) { + step2 = Buffer.concat([step2, Buffer.from('.')]); + } + //AES + let aesCipher = crypto.createCipheriv('aes-128-ecb', Buffer.from('jo6aey6haid2Teih'), Buffer.from('')); + let step3 = Buffer.concat([aesCipher.update(step2, 'binary'), aesCipher.final()]).toString('hex').toLowerCase(); + + global.lasturl = `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}` + + return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`; + } + + + // Updated URL gen from yours truly @ github.com/SluttySpider + // both tokens are refreshed on call so i might change that later on to speed the process up + async generateUrl(trackId, md5origin, mediaVersion, quality = 3) { + + if (quality != 1) { + + var trackData = await this.callApi('song.getData', {sng_id: trackId}); + var userdata = await this.callApi('deezer.getUserData'); + + var t = trackData.results.TRACK_TOKEN + var l = userdata.results.USER.OPTIONS.license_token + + try { + if (t.toString() != null && l.toString() != null) { + + var qty; + + //9 - FLAC + //3 - MP3 320 + //1 - MP3 128 + + if ( quality == 3 ) { + qty = "MP3_320" + } + else if ( quality == 1 ) { + qty = "MP3_128" + } + else if ( quality == 9 ) { + qty = "FLAC" + } + + let json = { + 'license_token': l, + 'media': [ + { + 'type': 'FULL', + 'formats': [{'cipher': 'BF_CBC_STRIPE', 'format': qty}] + } + ], + 'track_tokens': [t] + } + + try { + + var res = await axios({ + method: 'POST', + headers: this.headers(), + url: `https://media.deezer.com/v1/get_url`, + data: json, + responseType: 'json' + }); + + } catch (err) { "failed on request: " + err} + + if (qty != 'FLAC') { + global.lasturl = res.data.data[0].media[0].sources[1].url + return { + encrypted: true, + url: res.data.data[0].media[0].sources[1].url + }; + } else { + global.lasturl = res.data.data[0].media[0].sources[1].url + return { + encrypted: true, + url: res.data.data[0].media[0].sources[1].url + }; + } + + + } else { logger.warn("track token or license token are null") } + } catch (e) { + logger.warn('failed: ' + e); + } + } else { + return { + encrypted: true, + url: DeezerAPI.getUrl(trackId, md5origin, mediaVersion, quality) + }; + } + } + + + + async fallback(info, quality = 3) { + let qualityInfo = Track.getUrlInfo(info); + + //User uploaded MP3s + if (qualityInfo.trackId.startsWith('-')) { + qualityInfo.quality = 3; + return qualityInfo; + } + + //Quality fallback + let newQuality = await this.qualityFallback(qualityInfo, quality); + if (newQuality != null) { + return qualityInfo; + } + //ID Fallback + let trackData = await this.callApi('deezer.pageTrack', {sng_id: qualityInfo.trackId}); + try { + if (trackData.results.DATA.FALLBACK.SNG_ID.toString() != qualityInfo.trackId) { + let newId = trackData.results.DATA.FALLBACK.SNG_ID.toString(); + let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId}); + let newTrack = new Track(newTrackData.results.DATA); + return this.fallback(newTrack.streamUrl); + } + } catch (e) { + logger.warn('TrackID Fallback failed: ' + e + ' Original ID: ' + qualityInfo.trackId); + } + //ISRC Fallback + try { + let publicTrack = this.callPublicApi('track', 'isrc:' + trackData.results.DATA.ISRC); + let newId = publicTrack.id.toString(); + let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId}); + let newTrack = new Track(newTrackData.results.DATA); + return this.fallback(newTrack.streamUrl); + } catch (e) { + logger.warn('ISRC Fallback failed: ' + e + ' Original ID: ' + qualityInfo.trackId); + } + return null; + } + + //Fallback thru available qualities, -1 if none work + async qualityFallback(info, quality = 3) { + try { + let urlGen = await this.generateUrl(info.trackId, info.md5origin, info.mediaVersion, quality); + let res = await axios.head(urlGen.url).then(logger.warn(parseInt(quality.toString(), 10))); + if (res.status > 400) throw new Error(`Status code: ${res.status}`); + //Make sure it's an int + info.quality = parseInt(quality.toString(), 10); + info.size = parseInt(res.headers['content-length'], 10); + info.encrypted = urlGen.encrypted; + if (!info.encrypted) + info.direct = urlGen.url; + return info; + } catch (e) { + logger.warn('Quality fallback: ' + e); + //Fallback + //9 - FLAC + //3 - MP3 320 + //1 - MP3 128 + let nq = -1; + if (quality == 3) nq = 1; + if (quality == 9) nq = 3; + if (quality == 1) return null; + return this.qualityFallback(info, nq); + } + } + +} + +class DeezerStream extends Readable { + constructor(qualityInfo, options) { + super(options); + this.qualityInfo = qualityInfo; + this.ended = false; + } + + async open(offset, end) { + //Prepare decryptor + this.decryptor = new DeezerDecryptionStream(this.qualityInfo.trackId, {offset}); + this.decryptor.on('end', () => { + this.ended = true; + }); + + //Calculate headers + let offsetBytes = offset - (offset % 2048); + end = (end == -1) ? '' : end; + let url = global.lasturl; + + //Open request + await new Promise((res) => { + this.request = https.get(url, {headers: {'Range': `bytes=${offsetBytes}-${end}`}}, (r) => { + r.pipe(this.decryptor); + this.size = parseInt(r.headers['content-length'], 10) + offsetBytes; + res(); + }); + }); + } + + async _read() { + //Decryptor ended + if (this.ended) + return this.push(null); + + this.decryptor.once('readable', () => { + this.push(this.decryptor.read()); + }); + } + + _destroy(err, callback) { + this.request.destroy(); + this.decryptor.destroy(); + callback(); + } + +} + +class DeezerDecryptionStream extends Transform { + + constructor(trackId, options = {offset: 0}) { + super(); + //Offset as n chunks + this.offset = Math.floor(options.offset / 2048); + //How many bytes to drop + this.drop = options.offset % 2048; + this.buffer = Buffer.alloc(0); + + this.key = decryptor.getKey(trackId); + } + + _transform(chunk, encoding, next) { + //Restore leftovers + chunk = Buffer.concat([this.buffer, chunk]); + + while (chunk.length >= 2048) { + //Decrypt + let slice = chunk.slice(0, 2048); + if ((this.offset % 3) == 0) { + slice = decryptor.decryptBuffer(this.key, slice); + } + this.offset++; + + //Cut bytes + if (this.drop > 0) { + slice = slice.slice(this.drop); + this.drop = 0; + } + + this.push(slice); + + //Replace original buffer + chunk = chunk.slice(2048); + } + //Save leftovers + this.buffer = chunk; + + next(); + } + + //Last chunk + async _flush(cb) { + //drop should be 0, so it shouldnt affect + this.push(this.buffer.slice(this.drop)); + this.drop = 0; + this.buffer = Buffer.alloc(0); + cb(); + } +} + + +module.exports = {DeezerAPI, DeezerDecryptionStream, DeezerStream}; \ No newline at end of file diff --git a/app/src/definitions.js b/app/src/definitions.js new file mode 100644 index 0000000..eececdf --- /dev/null +++ b/app/src/definitions.js @@ -0,0 +1,322 @@ + +//Datatypes, constructor parameters = gw_light API call. +class Track { + constructor(json) { + this.id = json.SNG_ID.toString(); + this.title = `${json.SNG_TITLE}${json.VERSION ? ` ${json.VERSION}` : ''}`; + //Duration as ms for easier use in frontend + this.duration = parseInt(json.DURATION.toString(), 10) * 1000; + this.albumArt = new DeezerImage(json.ALB_PICTURE); + this.artists = (json.ARTISTS ? json.ARTISTS : [json]).map((a) => new Artist(a)); + //Helper + this.artistString = this.artists.map((a) => a.name).join(', '); + + this.album = new Album(json); + this.trackNumber = parseInt((json.TRACK_NUMBER || 1).toString(), 10); + this.diskNumber = parseInt((json.DISK_NUMBER || 1).toString(), 10); + this.explicit = json['EXPLICIT_LYRICS'] == 1 ? true:false; + this.lyricsId = json.LYRICS_ID; + + this.library = null; + + //Generate URL Part + //0 - 32 = MD5 ORIGIN + //33 - = 1/0 if md5origin ends with .mp3 + //34 - 35 = MediaVersion + //Rest = Track ID + let md5 = json.MD5_ORIGIN.replace('.mp3', ''); + let md5mp3bit = json.MD5_ORIGIN.includes('.mp3') ? '1' : '0'; + let mv = json.MEDIA_VERSION.toString().padStart(2, '0'); + this.streamUrl = `${md5}${md5mp3bit}${mv}${this.id}`; + } + + //Get Deezer CDN url by streamUrl + static getUrlInfo(info) { + let md5origin = info.substring(0, 32); + if (info.charAt(32) == '1') md5origin += '.mp3'; + let mediaVersion = parseInt(info.substring(33, 34)).toString(); + let trackId = info.substring(35); + return new QualityInfo(md5origin, mediaVersion, trackId); + } +} + +class Album { + constructor(json, tracksJson = {data: []}, library = false) { + this.id = json.ALB_ID.toString(); + this.title = json.ALB_TITLE; + this.art = new DeezerImage(json.ALB_PICTURE); + this.fans = json.NB_FAN; + this.tracks = tracksJson.data.map((t) => new Track(t)); + this.artists = (json.ARTISTS ? json.ARTISTS : [json]).map((a) => new Artist(a)); + this.releaseDate = json.DIGITAL_RELEASE_DATE; + + //Explicit + this.explicit = false; + if (json.EXPLICIT_LYRICS && json.EXPLICIT_LYRICS.toString() == "1") this.explicit = true; + if (json.EXPLICIT_ALBUM_CONTENT) { + if (json.EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS == 4) this.explicit = true; + if (json.EXPLICIT_ALBUM_CONTENT.EXPLICIT_LYRICS_STATUS == 1) this.explicit = true; + } + + //Type + this.type = 'album'; + if (json.TYPE && json.TYPE.toString() == "0") this.type = 'single'; + if (json.ROLE_ID == 5) this.type = 'featured'; + + //Helpers + this.artistString = this.artists.map((a) => a.name).join(', '); + + this.library = library; + } +} + +class Artist { + constructor(json, albumsJson = {data: []}, topJson = {data: []}, library = false) { + this.id = json.ART_ID.toString(); + this.name = json.ART_NAME; + this.fans = json.NB_FAN; + this.picture = new DeezerImage(json.ART_PICTURE, 'artist'); + this.albumCount = albumsJson.total; + this.albums = albumsJson.data.map((a) => new Album(a)); + this.topTracks = topJson.data.map((t) => new Track(t)); + this.radio = json.SMARTRADIO; + this.library = library; + } +} + +class Playlist { + constructor(json, tracksJson = {data: []}, library = false) { + this.id = json.PLAYLIST_ID.toString(), + this.title = json.TITLE, + this.trackCount = json.NB_SONG ? json.NB_SONG : tracksJson.total; + this.image = new DeezerImage(json.PLAYLIST_PICTURE, json.PICTURE_TYPE); + this.fans = json.NB_FAN; + this.duration = parseInt((json.DURATION ? json.DURATION : 0).toString(), 10) * 1000; + this.description = json.DESCRIPTION; + this.user = new User( + json.PARENT_USER_ID, + json.PARENT_USERNAME, + new DeezerImage(json.PARENT_USER_PICTURE, 'user') + ); + this.tracks = tracksJson.data.map((t) => new Track(t)); + this.library = library; + switch (json.STATUS) { + case 0: + this.type = 'public'; + break; + case 1: + this.type = 'private'; + break; + case 2: + this.type = 'collaborative' + break; + } + } + + //Extend tracks + extend(tracksJson = {data: []}) { + let tracks = tracksJson.data.map((t) => new Track(t)); + this.tracks.push(...tracks); + } +} + +class User { + constructor(id, name, picture) { + this.id = id; + this.name = name; + this.picture = picture; + } +} + +class DeezerImage { + constructor(hash, type='cover') { + this.hash = hash; + this.type = type; + //Create full and thumb, to standardize size and because functions aren't preserved + this.full = DeezerImage.url(this.hash, this.type, 1400); + this.thumb = DeezerImage.url(this.hash, this.type, 256); + } + static url(hash, type, size = 256) { + if (!hash) + return `https://e-cdns-images.dzcdn.net/images/${type}/${size}x${size}-000000-80-0-0.jpg`; + return `https://e-cdns-images.dzcdn.net/images/${type}/${hash}/${size}x${size}-000000-80-0-0.jpg`; + } +} + +class SearchResults { + constructor(json) { + this.albums = json.ALBUM.data.map((a) => new Album(a)); + this.artists = json.ARTIST.data.map((a) => new Artist(a)); + this.tracks = json.TRACK.data.map((t) => new Track(t)); + this.playlists = json.PLAYLIST.data.map((p) => new Playlist(p)); + this.top = json.TOP_RESULT; + } +} + +class DeezerProfile { + constructor(json) { + this.token = json.checkForm; + this.id = json.USER.USER_ID; + this.name = json.USER.BLOG_NAME; + this.favoritesPlaylist = json.USER.LOVEDTRACKS_ID; + this.picture = new DeezerImage(json.USER.USER_PICTURE, 'user'); + } +} + +class DeezerLibrary { + //Pass 'TAB' from API to parse + constructor(json, type='tracks') { + switch (type) { + case 'tracks': + // this.count = json.loved.total; + // this.data = json.loved.data.map((t) => new Track(t)); + this.count = json.total; + this.data = json.data.map((t) => new Track(t)); + break; + case 'albums': + this.count = json.albums.total; + this.data = json.albums.data.map((a) => new Album(a, {data: []}, true)); + break; + case 'artists': + this.count = json.artists.total; + this.data = json.artists.data.map((a) => new Artist(a, {data: []}, {data: []}, true)); + break; + case 'playlists': + this.count = json.playlists.total; + this.data = json.playlists.data.map((p) => new Playlist(p, {data: []}, true)); + break; + } + } +} + +class SmartTrackList { + constructor(json) { + this.title = json.TITLE; + this.subtitle = json.SUBTITLE; + this.description = json.DESCRIPTION; + this.id = json.SMARTTRACKLIST_ID + this.cover = new DeezerImage(json.COVER.MD5, json.COVER.TYPE); + } +} + +class DeezerPage { + constructor(json) { + this.title = json.title; + this.sections = json.sections.map((s) => new ChannelSection(s)); + } +} + +class DeezerChannel { + constructor(json, target) { + this.title = json.title; + this.image = new DeezerImage(json.pictures.md5, json.pictures.type); + this.color = json.background_color; + this.id = json.id; + this.slug = json.slug; //Hopefully it's used for path + this.target = target; + } +} + +class ChannelSection { + constructor(json) { + //Parse layout + switch (json.layout) { + case 'grid': this.layout = 'grid'; break; + case 'horizontal-grid': this.layout = 'row'; break; + default: this.layout = 'row'; break; + } + this.title = json.title; + this.hasMore = json.hasMoreItems ? true : false; + this.target = json.target; + this.items = json.items.map((i) => new ChannelSectionItem(i)); + } +} + +class ChannelSectionItem { + constructor(json) { + this.id = json.id; + this.title = json.title; + this.type = json.type; + this.subtitle = json.subtitle; + //Parse data + switch (this.type) { + case 'flow': + case 'smarttracklist': + this.data = new SmartTrackList(json.data); + break; + case 'playlist': + this.data = new Playlist(json.data); + break; + case 'artist': + this.data = new Artist(json.data); + break; + case 'channel': + // console.log("a", json.data) + this.data = new DeezerChannel(json.data, json.target); + break; + case 'album': + this.data = new Album(json.data); + break; + } + } +} + +class Lyrics { + constructor(json) { + this.id = json.LYRICS_ID; + this.writer = json.LYRICS_WRITERS; + this.text = []; + if (json.LYRICS_TEXT) { + this.text = json.LYRICS_TEXT.replace(new RegExp('\\r', 'g'), '').split('\n'); + } + + //Parse invidual lines + this.lyrics = []; + if (json.LYRICS_SYNC_JSON) { + for (let l of json.LYRICS_SYNC_JSON) { + let lyric = Lyric.parseJson(l); + if (lyric) this.lyrics.push(lyric); + } + } + + } +} + +class Lyric { + //NOT for parsing from deezer + constructor(offset, text, lrcTimestamp) { + this.offset = parseInt(offset.toString(), 10); + this.text = text; + this.lrcTimestamp = lrcTimestamp; + } + //Can return null if invalid lyric + static parseJson(json) { + if (!json.milliseconds || !json.line || json.line == '') return; + return new Lyric(json.milliseconds, json.line, json.lrc_timestamp); + } +} + +class QualityInfo { + constructor(md5origin, mediaVersion, trackId, quality = 1, source='stream') { + this.md5origin = md5origin; + this.mediaVersion = mediaVersion; + this.trackId = trackId; + this.quality = quality; + this.source = source; + //For FLAC bitrate calculation + this.size = 1; + + this.url = ''; + } + + //Generate direct stream URL + generateUrl() { + let md5 = this.md5origin.replace('.mp3', ''); + let md5mp3bit = this.md5origin.includes('.mp3') ? '1' : '0'; + let mv = this.mediaVersion.toString().padStart(2, '0'); + this.url = `/stream/${md5}${md5mp3bit}${mv}${this.trackId}?q=${this.quality}`; + } +} + +module.exports = {Track, Album, Artist, Playlist, User, SearchResults, + DeezerImage, DeezerProfile, DeezerLibrary, DeezerPage, Lyrics}; \ No newline at end of file diff --git a/app/src/downloads.js b/app/src/downloads.js new file mode 100644 index 0000000..90b3c3c --- /dev/null +++ b/app/src/downloads.js @@ -0,0 +1,587 @@ +const {DeezerAPI} = require('./deezer'); +const Datastore = require('nedb'); +const {Settings} = require('./settings'); +const fs = require('fs'); +const https = require('https'); +const logger = require('./winston'); +const path = require('path'); +const decryptor = require('nodeezcryptor'); +const sanitize = require('sanitize-filename'); +const ID3Writer = require('browser-id3-writer'); +const Metaflac = require('metaflac-js2'); +const { Track, Lyrics, DeezerImage } = require('./definitions'); + +let deezer; + +class DownloadManager { + + constructor(settings, callback) { + this.settings = settings; + this.downloading = false; + this.callback = callback; + + this.queue = []; + this.threads = []; + + this.updateRequests = 0; + } + + //Update DeezerAPI global + setDeezer(d) { + deezer = d; + } + + async load() { + this.db = new Datastore({filename: Settings.getDownloadsDB(), autoload: true}); + + //Load from DB + await new Promise((resolve) => { + this.db.find({state: 0}, (err, docs) => { + if (!err) { + this.queue = docs.map(d => Download.fromDB(d)); + } + resolve(); + }); + }); + + //Create temp dir + if (!fs.existsSync(Settings.getTempDownloads())) { + fs.promises.mkdir(Settings.getTempDownloads(), {recursive: true}); + } + } + + async start() { + this.downloading = true; + await this.updateQueue(); + } + + async stop() { + this.downloading = false; + //Stop all threads + let nThreads = this.threads.length; + for (let i=nThreads-1; i>=0; i--) { + await this.threads[i].stop(); + } + this.updateQueue(); + } + + //data: {tracks: [], quality, playlistName} + async addBatch(data) { + for (let track of data.tracks) { + let p = this.settings.downloadsPath; + if (data.playlistName && this.settings.playlistFolder) { + p = path.join(p, sanitize(data.playlistName)); + } + await this.add(track, data.quality, p); + } + } + + async add(track, quality, p) { + //Sanitize quality + let q = this.settings.downloadsQuality; + if (quality) + q = parseInt(quality.toString(), 10); + let download = new Download(track, q, 0, p); + + //Check if in queue + if (this.queue.some(d => d.track.id == track.id)) { + return; + } + + //Check if in DB + let dbDownload = await new Promise((resolve) => { + this.db.find({_id: download.track.id}, (err, docs) => { + if (err) return resolve(null); + if (docs.length == 0) return resolve(null); + + //Update download as not done, will be skipped while downloading + this.db.update({_id: download.track.id}, {state: 0, quality: download.quality}, {}, () => { + resolve(Download.fromDB(docs[0])); + }); + }); + }); + + //Insert to DB + if (!dbDownload) { + await new Promise((resolve) => { + this.db.insert(download.toDB(), () => { + resolve(); + }); + }); + } + + //Queue + this.queue.push(download); + this.updateQueue(); + } + + async delete(index) { + //-1 = Delete all + if (index == -1) { + let ids = this.queue.map(q => q.track.id); + this.queue = []; + //Remove from DB + await new Promise((res) => { + this.db.remove({_id: {$in: ids}}, {multi: true}, () => { + res(); + }) + }); + this.updateQueue(); + return; + } + + //Remove single item + let id = this.queue[index].track.id; + this.queue.splice(index, 1); + await new Promise((res) => { + this.db.remove({_id: id}, {}, () => { + res(); + }) + }) + this.updateQueue(); + } + + //Thread safe update + async updateQueue() { + this.updateRequests++; + if (this._updatePromise) return; + this._updatePromise = this._updateQueue(); + await this._updatePromise; + this._updatePromise = null; + this.updateRequests--; + if (this.updateRequests > 0) { + this.updateRequests--; + this.updateQueue(); + } + } + + async _updateQueue() { + //Finished downloads + if (this.threads.length > 0) { + for (let i=this.threads.length-1; i>=0; i--) { + if (this.threads[i].download.state == 3 || this.threads[i].download.state == -1) { + //Update DB + await new Promise((resolve) => { + this.db.update({_id: this.threads[i].download.track.id}, {state: this.threads[i].download.state}, {}, () => { + resolve(); + }); + }); + this.threads.splice(i, 1); + } else { + //Remove if stopped + if (this.threads[i].stopped) { + this.queue.unshift(this.threads[i].download); + this.threads.splice(i, 1); + } + } + } + } + //Create new threads + if (this.downloading) { + let nThreads = this.settings.downloadThreads - this.threads.length; + for (let i=0; i 0) { + let thread = new DownloadThread(this.queue[0], () => {this.updateQueue();}, this.settings); + thread.start(); + this.threads.push(thread); + this.queue.splice(0, 1); + } + } + } + //Stop downloading if queues empty + if (this.queue.length == 0 && this.threads.length == 0 && this.downloading) + this.downloading = false; + + //Update UI + if (this.callback) + this.callback(); + } +} + +class DownloadThread { + constructor (download, callback, settings) { + this.download = download; + this.callback = callback; + this.settings = settings; + this.stopped = true; + this.isUserUploaded = download.track.id.toString().startsWith('-'); + this.coverPath = null; + this.encrypted = false; + } + + //Callback wrapper + _cb() { + if (this.callback) this.callback(); + } + + async start() { + this.download.state = 1; + this.stopped = false; + + //Fallback + this.qualityInfo = await deezer.fallback(this.download.track.streamUrl, this.download.quality); + if (!this.qualityInfo) { + this.download.state = -1; + this._cb(); + return; + } + + //Get track info + if (!this.isUserUploaded) { + try { + this.rawTrack = await deezer.callApi('deezer.pageTrack', {'sng_id': this.qualityInfo.trackId}); + this.track = new Track(this.rawTrack.results.DATA); + this.publicTrack = await deezer.callPublicApi('track', this.track.id); + this.publicAlbum = await deezer.callPublicApi('album', this.track.album.id); + } catch (e) { + logger.error(`Error fetching metadata for ID: ${this.qualityInfo.trackId}, Error: ${e}`); + this.download.state = -1; + this._cb(); + return; + } + } + + //Check if exists + let outPath = this.generatePath(this.qualityInfo.quality); + try { + await fs.promises.access(outPath, fs.constants.R_OK); + //File exists + this.download.state = 3; + return this._cb(); + } catch (_) {} + + //Path to temp file + let tmp = path.join(Settings.getTempDownloads(), `${this.download.track.id}.ENC`); + //Get start offset + let start = 0; + try { + let stat = await fs.promises.stat(tmp); + if (stat.size) start = stat.size; + + // eslint-disable-next-line no-empty + } catch (e) {} + this.download.downloaded = start; + + //Download + let urlGen = await deezer.generateUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality); + this.encrypted = urlGen.encrypted; + + if (this.stopped) return; + this._request = https.get(urlGen.url, {headers: {'Range': `bytes=${start}-`}}, (r) => { + this._response = r; + let outFile = fs.createWriteStream(tmp, {flags: 'a'}); + + //On download done + r.on('end', () => { + if (this.download.size != this.download.downloaded) return; + + outFile.on('finish', () => { + outFile.close(() => { + this.postPromise = this._post(tmp); + }); + }); + outFile.end(); + }); + //Progress + r.on('data', (c) => { + outFile.write(c); + this.download.downloaded += c.length; + }); + + r.on('error', (e) => { + logger.error(`Download error: ${e}`); + //TODO: Download error handling + }) + + //Save size + this.size = parseInt(r.headers['content-length'], 10) + start; + this.download.size = this.size; + }); + } + + async stop() { + //If post processing, wait for it + if (this.postPromise) { + await this._postPromise; + return this._cb(); + } + + //Cancel download + if (this._response) + this._response.destroy(); + if (this._request) + this._request.destroy(); + + // this._response = null; + // this._request = null; + + this.stopped = true; + this.download.state = 0; + this._cb(); + } + + async _post(tmp) { + this.download.state = 2; + + //Decrypt + let outPath = this.generatePath(this.qualityInfo.quality); + await fs.promises.mkdir(path.dirname(outPath), {recursive: true}); + if (this.encrypted) { + decryptor.decryptFile(decryptor.getKey(this.qualityInfo.trackId), tmp, `${tmp}.DEC`); + await fs.promises.copyFile(`${tmp}.DEC`, outPath); + await fs.promises.unlink(`${tmp}.DEC`); + } else { + await fs.promises.copyFile(tmp, outPath); + } + await fs.promises.unlink(tmp); + + + let cover = null; + + if (!this.isUserUploaded) { + //Tag, returns cover to prevent double downlaoding + cover = await this.tagTrack(outPath); + + //Lyrics + if (this.settings.downloadLyrics) { + let lrcFile = outPath.split('.').slice(0, -1).join('.') + '.lrc'; + let lrc; + try { + lrc = this.generateLRC(); + } catch (e) { + logger.warn('Error getting lyrics! ' + e); + } + if (lrc) { + await fs.promises.writeFile(lrcFile, lrc, {encoding: 'utf-8'}); + } + } + } + + //Cover + if (this.coverPath) { + if (!fs.existsSync(this.coverPath)) { + //Create empty file to "lock" + fs.closeSync(fs.openSync(this.coverPath, 'w')); + + if (!cover) { + try { + cover = await this.downloadCover(DeezerImage.url(this.track.albumArt.hash, 'cover', this.settings.coverResolution)); + } catch (e) {} + } + if (!cover) { + logger.warn("Error downloading album art!"); + } else { + await fs.promises.writeFile(this.coverPath, cover); + } + } + } + + + this.download.state = 3; + this._cb(); + } + + async tagTrack(path) { + let cover; + try { + cover = await this.downloadCover(DeezerImage.url(this.track.albumArt.hash, 'cover', this.settings.coverResolution), 'cover', this.settings.coverResolution); + } catch (e) {} + + //Genre tag + let genres = []; + if (this.publicAlbum.genres && this.publicAlbum.genres.data) + genres = this.publicAlbum.genres.data.map(g => g.name); + + if (path.toLowerCase().endsWith('.mp3')) { + //Load + const audioData = await fs.promises.readFile(path); + const writer = new ID3Writer(audioData); + + writer.setFrame('TIT2', this.track.title); + writer.setFrame('TPE1', this.track.artists.map((a) => a.name)); + if (this.publicAlbum.artist) writer.setFrame('TPE2', this.publicAlbum.artist.name); + writer.setFrame('TALB', this.track.album.title); + writer.setFrame('TRCK', this.track.trackNumber); + writer.setFrame('TPOS', this.track.diskNumber); + writer.setFrame('TCON', genres); + let date = new Date(this.publicTrack.release_date); + writer.setFrame('TYER', date.getFullYear()); + writer.setFrame('TDAT', `${date.getMonth().toString().padStart(2, '0')}${date.getDay().toString().padStart(2, '0')}`); + if (this.publicTrack.bpm > 2) writer.setFrame('TBPM', this.publicTrack.bpm); + if (this.publicAlbum.label) writer.setFrame('TPUB', this.publicAlbum.label); + writer.setFrame('TSRC', this.publicTrack.isrc); + if (this.rawTrack.results.LYRICS) writer.setFrame('USLT', { + lyrics: this.rawTrack.results.LYRICS.LYRICS_TEXT, + language: 'eng', + description: 'Unsychronised lyrics' + }); + + if (cover) writer.setFrame('APIC', {type: 3, data: cover, description: 'Cover'}); + writer.addTag(); + + //Write + await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer)); + + return cover; + } + + //Tag FLAC + if (path.toLowerCase().endsWith('.flac')) { + const flac = new Metaflac(path); + flac.removeAllTags(); + + flac.setTag(`TITLE=${this.track.title}`); + flac.setTag(`ALBUM=${this.track.album.title}`); + flac.setTag(`ARTIST=${this.track.artistString}`); + flac.setTag(`TRACKNUMBER=${this.track.trackNumber}`); + flac.setTag(`DISCNUMBER=${this.track.diskNumber}`); + if (this.publicAlbum.artist) flac.setTag(`ALBUMARTIST=${this.publicAlbum.artist.name}`); + flac.setTag(`GENRE=${genres.join(", ")}`); + flac.setTag(`DATE=${this.publicTrack.release_date}`); + if (this.publicTrack.bpm > 2) flac.setTag(`BPM=${this.publicTrack.bpm}`); + if (this.publicAlbum.label) flac.setTag(`LABEL=${this.publicAlbum.label}`); + flac.setTag(`ISRC=${this.publicTrack.isrc}`); + if (this.publicAlbum.upc) flac.setTag(`BARCODE=${this.publicAlbum.upc}`); + if (this.rawTrack.results.LYRICS) flac.setTag(`LYRICS=${this.rawTrack.results.LYRICS.LYRICS_TEXT}`); + + if (cover) flac.importPicture(cover); + + flac.save(); + } + } + + async downloadCover(url) { + return await new Promise((res) => { + let out = Buffer.alloc(0); + https.get(url, (r) => { + r.on('data', (d) => { + out = Buffer.concat([out, d]); + }); + r.on('end', () => { + res(out); + }); + }); + }); + } + + generateLRC() { + //Check if exists + if (!this.rawTrack.results.LYRICS || !this.rawTrack.results.LYRICS.LYRICS_SYNC_JSON) return; + let lyrics = new Lyrics(this.rawTrack.results.LYRICS); + if (lyrics.lyrics.length == 0) return; + //Metadata + let out = `[ar:${this.track.artistString}]\r\n[al:${this.track.album.title}]\r\n[ti:${this.track.title}]\r\n`; + //Lyrics + for (let l of lyrics.lyrics) { + if (l.lrcTimestamp && l.text) + out += `${l.lrcTimestamp}${l.text}\r\n`; + } + return out; + } + + generatePath(quality) { + //Path + let folder = this.settings.downloadsPath; + if (this.download.path) + folder = this.download.path; + + //User uploaded mp3s + if (this.isUserUploaded) { + //Generate path + if (this.settings.createArtistFolder && this.download.track.artists[0].name.length > 0) + folder = path.join(folder, sanitize(this.download.track.artists[0].name)); + if (this.settings.createAlbumFolder && this.download.track.album.title.length > 0) + folder = path.join(folder, sanitize(this.download.track.album.title)); + //Filename + let out = path.join(folder, sanitize(this.download.track.title)); + if (!out.includes('.')) + out += '.mp3'; + return out; + } + + //Generate filename + let fn = this.settings.downloadFilename; + + //Disable feats for single artist + let feats = ''; + if (this.track.artists.length >= 2) + feats = this.track.artists.slice(1).map((a) => a.name).join(', '); + + //Date + let date = new Date(this.publicTrack.release_date); + + let props = { + '%title%': this.track.title, + '%artists%': this.track.artistString, + '%artist%': this.track.artists[0].name, + '%feats%': feats, + '%trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString(), + '%0trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString().padStart(2, '0'), + '%album%': this.track.album.title, + '%albumArtist%': this.track.album.artists[0].name, + '%albumArtists%': this.track.album.artistString, + '%year%': date.getFullYear().toString(), + '%label%': (this.publicAlbum.label) ? this.publicAlbum.label : '' + }; + for (let k of Object.keys(props)) { + fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k])); + } + //Generate folders + if (this.settings.createArtistFolder) folder = path.join(folder, sanitize(this.track.artists[0].name)); + if (this.settings.createAlbumFolder) { + folder = path.join(folder, sanitize(this.track.album.title)); + if (this.settings.downloadCover) { + this.coverPath = path.join(folder, "cover.jpg"); + } + } + + //Cut path to fit into windows limits + if (fn.length >= 249) { + fn = fn.substring(0, 249); + } + + //Extension + if (quality.toString() == '9') { + fn += '.flac'; + } else { + fn += '.mp3'; + } + + return path.join(folder, fn); + } +} + +class Download { + constructor (track, quality, state, path) { + this.track = track; + this.quality = quality; + this.path = path; + + // 0 - none + // 1 - downloading + // 2 - postprocess + // 3 - done + // -1 - error + this.state = state; + + //Updated from threads + this.downloaded = 0; + this.size = 1; + } + + toDB() { + return { + _id: this.track.id, + track: this.track, + quality: this.quality, + state: this.state, + path: this.path + } + } + + static fromDB(json) { + return new Download(json.track, json.quality, json.state, json.path); + } +} + +module.exports = {DownloadManager} \ No newline at end of file diff --git a/app/src/integrations.js b/app/src/integrations.js new file mode 100644 index 0000000..7badac2 --- /dev/null +++ b/app/src/integrations.js @@ -0,0 +1,144 @@ +const LastfmAPI = require('lastfmapi'); +const DiscordRPC = require('discord-rpc'); +const {EventEmitter} = require('events'); +const logger = require('./winston'); + +class Integrations extends EventEmitter { + + //LastFM, Discord etc + constructor(settings) { + super(); + + this.settings = settings; + this.discordReady = false; + this.discordRPC = null; + + //LastFM + this.lastfm = new LastfmAPI({ + api_key: settings.lastfmkey, + secret: settings.lastfmsecret + }); + this.authorizeLastFM(); + + //Discord + if (settings.enableDiscord) + this.connectDiscord(); + + } + + updateSettings(settings) { + this.settings = settings; + } + + //Autorize lastfm with saved credentials + authorizeLastFM() { + if (!this.settings.lastFM) return; + this.lastfm.setSessionCredentials(this.settings.lastFM.name, this.settings.lastFM.key); + } + + //Login to lastfm by token + async loginLastFM(token) { + let response = await new Promise((res) => { + this.lastfm.authenticate(token, (err, sess) => { + if (err) res(); + res({ + name: sess.username, + key: sess.key + }); + }); + }); + this.settings.lastFM = response; + this.authorizeLastFM(); + return response; + } + + //LastFM Scrobble + async scrobbleLastFM(title, album, artist) { + if (this.settings.lastFM) + this.lastfm.track.scrobble({ + artist, + track: title, + album, + timestamp: Math.floor((new Date()).getTime() / 1000) + }); + } + + //Connect to discord client + connectDiscord() { + + const CLIENTID = '882672432144592957'; + + this.discordReady = false; + DiscordRPC.register(CLIENTID); + this.discordRPC = new DiscordRPC.Client({transport: 'ipc'}); + this.discordRPC.on('connected', () => { + this.discordReady = true; + + //Allow discord "join" button + if (this.settings.discordJoin) { + //Always accept join requests + this.discordRPC.subscribe('ACTIVITY_JOIN_REQUEST', (user) => { + this.discordRPC.sendJoinInvite(user.user).catch((e) => { + logger.warn('Unable to accept Discord invite: ' + e); + }); + }); + //Joined + this.discordRPC.subscribe('ACTIVITY_JOIN', async (data) => { + let params = JSON.parse(data.secret); + this.emit('discordJoin', params); + }); + + } + }); + //Connect to discord + this.discordRPC.login({clientId: CLIENTID}).catch(() => { + //Wait 5s to retry + setTimeout(() => { + if (!this.discordReady) + this.connectDiscord(); + }, 5000); + }); + } + + //Called when playback state changed + async updateState(data) { + if (this.discordReady) { + if (data.state == 2){ + let richPresence = { + state: data.track.artistString, + details: data.track.title, + largeImageKey: 'icon', + instance: true, + } + //Show timestamp only if playing + if (data.state == 2) { + Object.assign(richPresence, { + startTimestamp: Date.now() - data.position, + endTimestamp: (Date.now() - data.position) + data.duration, + }); + } + //Enabled discord join + if (this.settings.discordJoin) { + Object.assign(richPresence, { + partySize: 1, + partyMax: 10, + matchSecret: 'match_secret_' + data.track.id, + joinSecret: JSON.stringify({ + pos: Math.floor(data.position), + ts: Date.now(), + id: data.track.id + }), + partyId: 'party_id_' + data.track.id + }); + } + //Set + this.discordRPC.setActivity(richPresence); + } else { + this.discordRPC.clearActivity(); + } + } + } + +} + +module.exports = {Integrations}; \ No newline at end of file diff --git a/app/src/server.js b/app/src/server.js new file mode 100644 index 0000000..8415c42 --- /dev/null +++ b/app/src/server.js @@ -0,0 +1,630 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const packageJson = require('../package.json'); +const fs = require('fs'); +const compareVersions = require('compare-versions'); +const axios = require('axios').default; +const logger = require('./winston'); +const {DeezerAPI, DeezerStream} = require('./deezer'); +const {Settings} = require('./settings'); +const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions'); +const {DownloadManager} = require('./downloads'); +const {Integrations} = require('./integrations'); + +let settings; +let deezer; +let downloadManager; +let integrations; + +let sockets = []; + +//Express +const app = express(); +app.use(express.json({limit: '50mb'})); +app.use(express.static(path.join(__dirname, '../client', 'dist'))); +app.use(cors({origin: 'http://localhost:8080'})); +//Server +const server = require('http').createServer(app); +const { Server } = require('socket.io'); +const io = new Server(server, { + path: '/socket', + //CORS for webpack debug + cors: { + origin: 'http://localhost:8080', + methods: ["GET", "POST"], + } +}); + +//Get playback info +app.get('/playback', async (req, res) => { + try { + let data = await fs.promises.readFile(Settings.getPlaybackInfoPath(), 'utf-8'); + return res.json(data); + // eslint-disable-next-line no-empty + } catch (e) {} + + return res.json({}); +}); + +//Save playback info +app.post('/playback', async (req, res) => { + if (req.body) { + let data = JSON.stringify(req.body); + await fs.promises.writeFile(Settings.getPlaybackInfoPath(), data, 'utf-8'); + } + res.status(200).send(''); +}); + +//Get settings +app.get('/settings', (req, res) => { + res.json(settings); +}); + +//Save settings +app.post('/settings', async (req, res) => { + if (req.body) { + Object.assign(settings, req.body); + downloadManager.settings = settings; + integrations.updateSettings(settings); + await settings.save(); + } + + res.status(200).send(''); +}); + +//Post with body {"arl": ARL} +app.post('/authorize', async (req, res) => { + if (!req.body.arl || req.body.arl.length < 100) return res.status(500).send('Invalid ARL'); + + //Check if ARL valid + let electron = deezer.electron; + deezer = new DeezerAPI(req.body.arl, electron); + settings.arl = req.body.arl; + + if (await (deezer.authorize())) { + //Update download manager + downloadManager.setDeezer(deezer); + + res.status(200).send('OK'); + return; + } + + res.status(500).send('Invalid ARL / Free Account'); +}); + +//Get track by id +app.get('/track/:id', async (req, res) => { + let data = await deezer.callApi('deezer.pageTrack', {sng_id: req.params.id.toString()}); + res.send(new Track(data.results.DATA)); +}); + +//Get album by id +app.get('/album/:id', async (req, res) => { + let data = await deezer.callApi('deezer.pageAlbum', {alb_id: req.params.id.toString(), lang: settings.contentLanguage}); + res.send(new Album(data.results.DATA, data.results.SONGS)); +}); + +//Get artist by id +app.get('/artist/:id', async (req, res) => { + let data = await deezer.callApi('deezer.pageArtist', {art_id: req.params.id.toString(), lang: settings.contentLanguage}); + res.send(new Artist(data.results.DATA, data.results.ALBUMS, data.results.TOP)); +}); + +//Get playlist by id +//start & full query parameters +app.get('/playlist/:id', async (req, res) => { + //Set anything to `full` query parameter to get entire playlist + let nb = req.query.full ? 100000 : 50; + let data = await deezer.callApi('deezer.pagePlaylist', { + playlist_id: req.params.id.toString(), + lang: settings.contentLanguage, + nb: nb, + start: req.query.start ? parseInt(req.query.start, 10) : 0, + tags: true + }); + return res.send(new Playlist(data.results.DATA, data.results.SONGS)); +}); + +//DELETE playlist +app.delete('/playlist/:id', async (req, res) => { + await deezer.callApi('playlist.delete', {playlist_id: req.params.id.toString()}); + res.sendStatus(200); +}); + +//POST create playlist +// { +// desciption, +// title, +// type: 0, 1, 2 = public, private, collab +// track: trackID +// } +app.post('/playlist', async (req, res) => { + await deezer.callApi('playlist.create', { + description: req.body.description, + title: req.body.title, + status: req.body.type, + songs: req.body.track ? [[req.body.track, 0]] : [] + }); + + res.sendStatus(200); +}); + +//PUT edit playlist, see above create +app.put('/playlist/:id', async (req, res) => { + await deezer.callApi('playlist.update', { + description: req.body.description, + title: req.body.title, + status: req.body.type, + playlist_id: parseInt(req.params.id.toString(), 10) + }); + res.sendStatus(200); +}); + +//POST track to playlist +//Body {"track": "trackId"} +app.post('/playlist/:id/tracks', async (req, res) => { + await deezer.callApi('playlist.addSongs', { + offset: -1, + playlist_id: req.params.id, + songs: [[req.body.track, 0]] + }); + + res.sendStatus(200); +}); + +//DELETE track from playlist +//Body {"track": "trackId"} +app.delete('/playlist/:id/tracks', async (req, res) => { + await deezer.callApi('playlist.deleteSongs', { + playlist_id: req.params.id, + songs: [[req.body.track, 0]] + }); + + res.sendStatus(200); +}); + +//Get more albums +//ID = artist id, QP start = offset +app.get('/albums/:id', async (req, res) => { + let data = await deezer.callApi('album.getDiscography', { + art_id: parseInt(req.params.id.toString(), 10), + discography_mode: "all", + nb: 25, + nb_songs: 200, + start: req.query.start ? parseInt(req.query.start, 10) : 0 + }); + + let albums = data.results.data.map((a) => new Album(a)); + res.send(albums); +}) + +//Get tracks from listening history +app.get('/history', async (req, res) => { + let data = await deezer.callApi('deezer.pageProfile', { + nb: 200, + tab: "history", + user_id: deezer.userId.toString() + }); + let tracks = data.results.TAB.history.data.map((t) => new Track(t)); + res.send(tracks); +}); + +//Search, q as query parameter +app.get('/search', async (req, res) => { + let data = await deezer.callApi('deezer.pageSearch', {query: req.query.q, nb: 100}); + res.send(new SearchResults(data.results)); +}); + +//Get user profile data +app.get('/profile', async (req, res) => { + let data = await deezer.callApi('deezer.getUserData'); + let profile = new DeezerProfile(data.results); + res.send(profile); +}); + +//Get shuffled library +app.get('/shuffle', async (req, res) => { + let data = await deezer.callApi('tracklist.getShuffledCollection', { + nb: 50, + start: 0 + }); + res.send(data.results.data.map((t) => new Track(t))); +}); + +//Get list of `type` from library +app.get('/library/:type', async (req, res) => { + let type = req.params.type; + //Normal + if (type != 'tracks') { + let data = await deezer.callApi('deezer.pageProfile', { + nb: 50, + tab: (type == 'tracks') ? 'loved' : type, + user_id: deezer.userId + }); + return res.send(new DeezerLibrary(data.results.TAB, type)).end(); + } + //Tracks + let data = await deezer.callApi('deezer.pagePlaylist', { + playlist_id: deezer.favoritesPlaylist, + lang: settings.contentLanguage, + nb: 50, + start: 0, + tags: true + }); + res.send(new DeezerLibrary(data.results.SONGS, type)); +}); + +//DELETE from library +app.delete('/library/:type', async (req, res) => { + let type = req.params.type; + let id = req.query.id; + + if (type == 'track') await deezer.callApi('favorite_song.remove', {SNG_ID: id}); + if (type == 'album') await deezer.callApi('album.deleteFavorite', {ALB_ID: id}); + if (type == 'playlist') await deezer.callApi('playlist.deleteFavorite', {playlist_id: parseInt(id, 10)}); + if (type == 'artist') await deezer.callApi('artist.deleteFavorite', {ART_ID: id}); + + res.sendStatus(200); +}); + +//PUT (add) to library +app.put('/library/:type', async (req, res) => { + let type = req.params.type; + let id = req.query.id; + + if (type == 'track') await deezer.callApi('favorite_song.add', {SNG_ID: id}); + if (type == 'album') await deezer.callApi('album.addFavorite', {ALB_ID: id}); + if (type == 'artist') await deezer.callApi('artist.addFavorite', {ART_ID: id}); + if (type == 'playlist') await deezer.callApi('playlist.addFavorite', {parent_playlist_id: parseInt(id)}); + + res.sendStatus(200); +}); + + +//Get streaming metadata, quality fallback +app.get('/streaminfo/:info', async (req, res) => { + let info = req.params.info; + let quality = req.query.q ? req.query.q : 3; + let qualityInfo = await deezer.fallback(info, quality); + + if (qualityInfo == null) + return res.sendStatus(404).end(); + + //Generate stream URL before sending + qualityInfo.generateUrl(); + return res.json(qualityInfo); +}); + +// S T R E A M I N G +app.get('/stream/:info', async (req, res) => { + //Parse stream info + let quality = req.query.q ? req.query.q : 3; + let streamInfo = Track.getUrlInfo(req.params.info); + streamInfo.quality = quality; + + //MIME type of audio + let mime = 'audio/mp3'; + if (quality == 9) mime = 'audio/flac'; + + //Parse range header + let range = 'bytes=0-'; + if (req.headers.range) range = req.headers.range; + let rangeParts = range.replace(/bytes=/, '').split('-'); + let start = parseInt(rangeParts[0], 10); + let end = -1; + if (rangeParts.length >= 2) end = rangeParts[1]; + if (end == '' || end == ' ') end = -1; + + //Create Stream + let stream = new DeezerStream(streamInfo, {}); + await stream.open(start, end); + + //Range header + if (req.headers.range) { + end = (end == -1) ? stream.size - 1 : end; + res.writeHead(206, { + 'Content-Range': `bytes ${start}-${end}/${stream.size}`, + 'Accept-Ranges': 'bytes', + 'Content-Length': stream.size - start, + 'Content-Type': mime + }); + + //Normal (non range) request + } else { + res.writeHead(200, { + 'Content-Length': stream.size, + 'Content-Type': mime + }); + } + + //Should force HTML5 to retry + stream.on('error', () => { + res.destroy(); + }); + + stream.pipe(res); +}); + +//Get deezer page +app.get('/page', async (req, res) => { + let target = req.query.target.replace(/"/g, ''); + + let st = ['album', 'artist', 'channel', 'flow', 'playlist', 'smarttracklist', 'track', 'user']; + let data = await deezer.callApi('page.get', {}, { + 'PAGE': target, + 'VERSION': '2.3', + 'SUPPORT': { + 'grid': st, + 'horizontal-grid': st, + 'item-highlight': ['radio'], + 'large-card': ['album', 'playlist', 'show', 'video-link'], + 'ads': [] //None + }, + 'LANG': settings.contentLanguage, + 'OPTIONS': [] + }); + + // logger.warn("data", data.results) + + res.send(new DeezerPage(data.results)); +}); + +//Get smart track list or flow tracks +app.get('/smarttracklist/:id', async (req, res) => { + let id = req.params.id; + + //Flow not normal STL + if (id == 'flow') { + let data = await deezer.callApi('radio.getUserRadio', { + user_id: deezer.userId + }); + let tracks = data.results.data.map((t) => new Track(t)); + return res.send(tracks); + } + + //Normal STL + let data = await deezer.callApi('smartTracklist.getSongs', { + smartTracklist_id: id + }); + //No more tracks + if (!data.results.data) { + logger.warn('No more STL tracks: ' + JSON.stringify(data.error)); + return res.send([]); + } + + let tracks = data.results.data.map((t) => new Track(t)); + return res.send(tracks); +}); + +//Artist smart radio +app.get('/smartradio/:id', async (req, res) => { + let data = await deezer.callApi('smart.getSmartRadio', {art_id: req.params.id}); + res.send(data.results.data.map(t => new Track(t))); +}); + +//Track Mix +app.get('/trackmix/:id', async (req, res) => { + let data = await deezer.callApi('song.getContextualTrackMix', {sng_ids: [req.params.id]}); + res.send(data.results.data.map(t => new Track(t))); +}); + +//Load lyrics, ID = SONG ID +app.get('/lyrics/:id', async (req, res) => { + let data = await deezer.callApi('song.getLyrics', { + sng_id: parseInt(req.params.id, 10) + }); + if (!data.results || data.error.length > 0) return res.status(502).send('Lyrics not found!'); + + res.send(new Lyrics(data.results)); +}); + +//Search Suggestions +app.get('/suggestions/:query', async (req, res) => { + let query = req.params.query; + try { + let data = await deezer.callApi('search_getSuggestedQueries', { + QUERY: query + }); + let out = data.results.SUGGESTION.map((s) => s.QUERY); + res.json(out); + } catch (e) { + res.json([]); + } +}); + +//Post list of tracks to download +app.post('/downloads', async (req, res) => { + downloadManager.addBatch(req.body); + + res.status(200).send('OK'); +}); + +//PUT to /download to start +app.put('/download', async (req, res) => { + await downloadManager.start(); + res.status(200).send('OK'); +}); + +//DELETE to /download to stop/pause +app.delete('/download', async (req, res) => { + await downloadManager.stop(); + res.status(200).send('OK'); +}) + +//Get all downloads +app.get('/downloads', async (req, res) => { + res.json({ + downloading: downloadManager.downloading, + queue: downloadManager.queue, + threads: downloadManager.threads.map(t => t.download) + }); +}); + +//Delete single download +app.delete('/downloads/:index', async (req, res) => { + let index = parseInt(req.params.index, 10); + await downloadManager.delete(index); + res.status(200).end(); +}); + +//Log listen to deezer & lastfm +app.post('/log', async (req, res) => { + //LastFM + integrations.scrobbleLastFM(req.body.title, req.body.album.title, req.body.artists[0].name); + + //Deezer + if (settings.logListen) + await deezer.callApi('log.listen', { + params: { + timestamp: Math.floor(new Date() / 1000), + ts_listen: Math.floor(new Date() / 1000), + type: 1, + stat: {seek: 0, pause: 0, sync: 0}, + media: {id: req.body.id, type: 'song', format: 'MP3_128'} + } + }); + res.status(200).end(); +}); + +//Last.FM authorization callback +app.get('/lastfm', async (req, res) => { + //Got token + if (req.query.token) { + let token = req.query.token; + //Authorize + let authinfo = await integrations.loginLastFM(token); + if (authinfo) { + settings.lastFM = authinfo; + settings.save(); + } + //Redirect to homepage + return res.redirect('/'); + } + + //Get auth url + res.json({ + url: integrations.lastfm.getAuthenticationUrl({cb: `http://${req.socket.remoteAddress}:${settings.port}/lastfm`}) + }).end(); +}); + +//Get URL from deezer.page.link +app.get('/fullurl', async (req, res) => { + let url = req.query.url; + let r = await axios.get(url, {validateStatus: null}); + res.json({url: r.request.res.responseUrl}); +}); + +//About page +app.get('/about', async (req, res) => { + res.json({ + version: packageJson.version + }); +}); + +app.get('/updates', async (req, res) => { + try { + let response = await axios.get('https://saturnclient.dev/api/versions'); + //New version + if (compareVersions(response.data.pc.latest, packageJson.version) >= 1) { + res.send(response.data.pc.versions[0]); + return; + } + res.status(404).end(); + return; + } catch (e) { + res.status(500).end(); + } +}); + +//Background image +app.get('/background', async (req, res) => { + //Missing + if (!settings.backgroundImage && !fs.existsSync(settings.backgroundImage)) { + return res.status(404).end(); + } + res.sendFile(path.resolve(settings.backgroundImage)); +}); + +//Redirect to index on unknown path +app.all('*', (req, res) => { + res.redirect('/'); +}); + +// S O C K E T S +io.on('connection', (socket) => { + sockets.push(socket); + //Remove on disconnect + socket.on('disconnect', () => { + sockets.splice(sockets.indexOf(socket), 1); + }); + //Send to integrations + socket.on('stateChange', (data) => { + integrations.updateState(data); + }); +}); + +//ecb = Error callback +async function createServer(electron = false, ecb, override = {}) { + //Prepare globals + settings = new Settings(electron); + settings.load(); + + deezer = new DeezerAPI(settings.arl, electron); + + //Prepare downloads + downloadManager = new DownloadManager(settings, () => { + //Emit queue change to socket + sockets.forEach((s) => { + s.emit('downloads', { + downloading: downloadManager.downloading, + queue: downloadManager.queue, + threads: downloadManager.threads.map(t => t.download) + }); + }); + }); + await downloadManager.load(); + downloadManager.setDeezer(deezer); + //Emit download progress updates + setInterval(() => { + sockets.forEach((s) => { + if (!downloadManager.downloading && downloadManager.threads.length == 0) + return; + + s.emit('currentlyDownloading', downloadManager.threads.map(t => t.download)); + }); + }, 400); + + //Integrations (lastfm, discord) + integrations = new Integrations(settings); + //Discord Join = Sync tracks + integrations.on('discordJoin', async (data) => { + let trackData = await deezer.callApi('deezer.pageTrack', {sng_id: data.id}); + let track = new Track(trackData.results.DATA); + let out = { + track: track, + position: (Date.now() - data.ts) + data.pos + } + //Emit to sockets + sockets.forEach((s) => { + s.emit('playOffset', out); + }); + }); + + //Error callback + server.on('error', (e) => { + if (ecb) + ecb(e); + logger.error(e); + }); + + //Start server + let serverIp = override.host ? override.host : settings.serverIp; + let port = override.port ? override.port: settings.port; + server.listen(port, serverIp); + console.log(`Running on: http://${serverIp}:${port}`); + + return settings; +} + +module.exports = {createServer}; \ No newline at end of file diff --git a/app/src/settings.js b/app/src/settings.js new file mode 100644 index 0000000..9ff6540 --- /dev/null +++ b/app/src/settings.js @@ -0,0 +1,137 @@ +const os = require('os'); +const path = require('path'); +const fs = require('fs'); + +class Settings { + + constructor(electron = false) { + //Defaults + this.port = 10069; + this.serverIp = '127.0.0.1'; + this.arl; + this.streamQuality = 3; + this.volume = 0.69; + this.electron = electron; + this.minimizeToTray = true; + this.closeOnExit = false; + this.width = 1280; + this.height = 720; + + this.downloadsPath = this.getDefaultDownloadPath(); + this.downloadsQuality = 3; + this.createAlbumFolder = true; + this.createArtistFolder = true; + this.downloadFilename = '%0trackNumber%. %artists% - %title%'; + this.downloadDialog = true; + this.downloadCover = true; + this.coverResolution = 1400; + + this.logListen = false; + this.lastFM = null; + this.enableDiscord = false; + this.discordJoin = false; + + this.showAutocomplete = true; + this.downloadThreads = 4; + this.downloadLyrics = true; + this.primaryColor = '#2196F3'; + this.language = 'en'; + + this.crossfadeDuration = 3000; + this.playlistFolder = false; + + this.forceWhiteTrayIcon = false; + this.contentLanguage = 'en'; + this.contentCountry = 'US'; + this.sidebarOpen = false; + this.nativeTopBar = false; + + this.lastfmkey = ''; + this.lastfmsecret = ''; + + //Has to be local path + this.backgroundImage = null; + this.lightTheme = false; + } + + //Based on electorn app.getPath + static getDir() { + let home = os.homedir(); + if (os.platform() === 'win32') { + return path.join(process.env.APPDATA, 'saturn'); + } + if (os.platform() === 'linux') { + return path.join(home, '.config', 'saturn'); + } + + //UNTESTED + if (os.platform() == 'darwin') { + return path.join(home, 'Library', 'Application Support', 'saturn'); + } + throw Error('Unsupported platform!'); + } + + //Get settings.json path + static getPath() { + return path.join(Settings.getDir(), 'settings.json'); + } + //Get path to playback.json + static getPlaybackInfoPath() { + return path.join(Settings.getDir(), 'playback.json'); + } + //Get path to downloads database + static getDownloadsDB() { + //Delete old DB if exists + let oldPath = path.join(Settings.getDir(), 'downloads.db'); + if (fs.existsSync(oldPath)) + fs.unlink(oldPath, () => {}); + + return path.join(Settings.getDir(), 'downloads2.db'); + } + //Get path to temporary / unfinished downlaods + static getTempDownloads() { + return path.join(Settings.getDir(), 'downloadsTemp'); + } + + getDefaultDownloadPath() { + return path.join(os.homedir(), 'SaturnMusic'); + } + + //Blocking load settings + load() { + //Preserve electorn option + let e = this.electron; + //Create dir if doesn't exist + try { + fs.mkdirSync(Settings.getDir(), {recursive: true}); + } catch (_) {} + + //Load settings from file + try { + if (fs.existsSync(Settings.getPath())) { + let data = fs.readFileSync(Settings.getPath(), 'utf-8'); + Object.assign(this, JSON.parse(data)); + } + } catch (e) { + console.error(`Error loading settings: ${e}. Using defaults.`); + this.save(); + } + this.electron = e; + + //Defaults for backwards compatibility + if (!this.downloadsPath) this.downloadsPath = this.getDefaultDownloadPath(); + } + + //ASYNC save settings + async save() { + //Create dir if doesn't exist + try { + await fs.promises.mkdir(Settings.getDir(), {recursive: true}); + } catch (_) {} + + await fs.promises.writeFile(Settings.getPath(), JSON.stringify(this, null, 2), 'utf-8'); + } + +} + +module.exports = {Settings}; \ No newline at end of file diff --git a/app/src/winston.js b/app/src/winston.js new file mode 100644 index 0000000..ac4b918 --- /dev/null +++ b/app/src/winston.js @@ -0,0 +1,23 @@ +const winston = require('winston'); +const path = require('path'); +const {Settings} = require('./settings'); +const { Transform } = require('stream'); + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.simple(), + transports: [ + new winston.transports.Console(), + new winston.transports.File({filename: path.join(Settings.getDir(), "saturn-server.log")}), + ] +}); + +//Node errors +process.on('uncaughtException', (err) => { + logger.error('Unhandled Exception: ' + err + "\nStack: " + err.stack); +}); +process.on('unhandledRejection', (err) => { + logger.error('Unhandled Rejection: ' + err + "\nStack: " + err.stack); +}); + +module.exports = logger; \ No newline at end of file diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..53cfb66 Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..8bd35c5 Binary files /dev/null and b/build/icon.png differ diff --git a/build/iconset/128x128.png b/build/iconset/128x128.png new file mode 100644 index 0000000..2ace948 Binary files /dev/null and b/build/iconset/128x128.png differ diff --git a/build/iconset/16x16.png b/build/iconset/16x16.png new file mode 100644 index 0000000..87f8027 Binary files /dev/null and b/build/iconset/16x16.png differ diff --git a/build/iconset/22x22.png b/build/iconset/22x22.png new file mode 100644 index 0000000..db440f3 Binary files /dev/null and b/build/iconset/22x22.png differ diff --git a/build/iconset/24x24.png b/build/iconset/24x24.png new file mode 100644 index 0000000..226a067 Binary files /dev/null and b/build/iconset/24x24.png differ diff --git a/build/iconset/256x256.png b/build/iconset/256x256.png new file mode 100644 index 0000000..aaf5cda Binary files /dev/null and b/build/iconset/256x256.png differ diff --git a/build/iconset/32x32.png b/build/iconset/32x32.png new file mode 100644 index 0000000..8ce03b7 Binary files /dev/null and b/build/iconset/32x32.png differ diff --git a/build/iconset/48x48.png b/build/iconset/48x48.png new file mode 100644 index 0000000..f31b22a Binary files /dev/null and b/build/iconset/48x48.png differ diff --git a/build/iconset/512x512.png b/build/iconset/512x512.png new file mode 100644 index 0000000..b8540dc Binary files /dev/null and b/build/iconset/512x512.png differ diff --git a/build/iconset/64x64.png b/build/iconset/64x64.png new file mode 100644 index 0000000..70263fd Binary files /dev/null and b/build/iconset/64x64.png differ diff --git a/build/installerIcon.ico b/build/installerIcon.ico new file mode 100644 index 0000000..53cfb66 Binary files /dev/null and b/build/installerIcon.ico differ diff --git a/build/uninstallIcon.ico b/build/uninstallIcon.ico new file mode 100644 index 0000000..53cfb66 Binary files /dev/null and b/build/uninstallIcon.ico differ diff --git a/build/uninstallerIcon.ico b/build/uninstallerIcon.ico new file mode 100644 index 0000000..d091f4d Binary files /dev/null and b/build/uninstallerIcon.ico differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..e11633f --- /dev/null +++ b/package.json @@ -0,0 +1,99 @@ +{ + "name": "saturn", + "private": true, + "version": "1.0.0", + "description": "Saturn PC", + "scripts": { + "pack": "electron-builder --dir", + "dist": "electron-builder", + "postinstall": "electron-builder install-app-deps", + "build": "cd app && npm i && cd client && npm i && npm run build && cd .. && cd .. && npm run dist" + }, + "license": "GPL-3.0-or-later", + "homepage": "https://saturnclient.dev", + "devDependencies": { + "electron": "^13.6.9", + "electron-builder": "^22.14.13" + }, + "build": { + "appId": "dev.arachnid.saturn", + "productName": "Saturn", + "extraResources": [ + { + "from": "app/assets/**", + "to": "assets/" + } + ], + "files": [ + "**/*", + "!app/client/", + "app/client/dist/**" + ], + "win": { + "target": [ + "portable", + "nsis" + ], + "icon": "build/icon.ico", + "asarUnpack": [ + "app/node_modules/nodeezcryptor/**" + ] + }, + "nsis": { + "oneClick": true, + "perMachine": false, + "allowElevation": false, + "allowToChangeInstallationDirectory": false + }, + "linux": { + "appId": "s.s.saturn", + "category": "AudioVideo;Network;Audio;FileTransfer;Player", + "description": "Electron-based Deezer client", + "desktop": { + "Version": "1.1", + "Type": "Application", + "Name": "Saturn", + "GenericName": "Electron-based Deezer client", + "Comment": "Desktop application for the Deezer audio streaming service", + "Icon": "saturn", + "Categories": "AudioVideo;Network;Audio;FileTransfer;Player;", + "MimeType": "application/http;", + "Keywords": "audio;download;flac;lyrics;mp3;music;spotify;stream;", + "StartupNotify": "true", + "StartupWMClass": "saturn", + "DBusActivatable": "false", + "Terminal": "false", + "NoDisplay": "false", + "Hidden": "false" + }, + "executableName": "saturn", + "icon": "build/iconset", + "maintainer": "github.com/SaturnMusic", + "mimeTypes": [ + "application/http" + ], + "synopsis": "Electron-based Deezer client", + "target": [ + "AppImage", + "deb", + "tar.xz" + ] + }, + "appImage": { + "desktop": { + "X-AppImage-Name": "Saturn" + } + }, + "deb": { + "packageCategory": "sound", + "priority": "optional", + "depends": [ + "libflac8", + "libnotify4", + "libnss3", + "libssl1.1 | libssl1.0.0", + "libxtst6" + ] + } + } +}