pax_global_header00006660000000000000000000000064152324003000014477gustar00rootroot0000000000000052 comment=b442feedb33bf8da5a5d8be89208072f8a7e8202 Jeffser-Nocturne-b442fee/000077500000000000000000000000001523240030000153535ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/.env000066400000000000000000000000601523240030000161400ustar00rootroot00000000000000PYTHONPATH=${PYTHONPATH}:${workspaceFolder}/src Jeffser-Nocturne-b442fee/.github/000077500000000000000000000000001523240030000167135ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/.github/FUNDING.yml000066400000000000000000000015371523240030000205360ustar00rootroot00000000000000# These are supported funding model platforms github: jeffser #patreon: # Replace with a single Patreon username #open_collective: # Replace with a single Open Collective username #ko_fi: # Replace with a single Ko-fi username #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry #liberapay: # Replace with a single Liberapay username #issuehunt: # Replace with a single IssueHunt username #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry #polar: # Replace with a single Polar username #buy_me_a_coffee: # Replace with a single Buy Me a Coffee username #thanks_dev: # Replace with a single thanks.dev username #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] Jeffser-Nocturne-b442fee/.github/ISSUE_TEMPLATE/000077500000000000000000000000001523240030000210765ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014041523240030000235670ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Package** - Which version and package are you using? (for example: Flatpak 1.2.2) - Which instance are you using? (for example: Navidrome, Jellyfin, Local Files, etc) - Any additional context of your setup if you think it might help **Additional context** Add any other context about the problem here. Jeffser-Nocturne-b442fee/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341523240030000246220ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. Jeffser-Nocturne-b442fee/.gitignore000066400000000000000000000000651523240030000173440ustar00rootroot00000000000000*.ui __pycache__ gschemas.compiled nocturne.gresourceJeffser-Nocturne-b442fee/.vscode/000077500000000000000000000000001523240030000167145ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/.vscode/launch.json000066400000000000000000000005511523240030000210620ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "Launch Nocturne (Python)", "type": "debugpy", "request": "launch", "program": "${env:HOME}/.local/bin/nocturne", "console": "integratedTerminal", "cwd": "${workspaceFolder}", "preLaunchTask": "meson: build and install", "justMyCode": false } ] } Jeffser-Nocturne-b442fee/.vscode/settings.json000066400000000000000000000005571523240030000214560ustar00rootroot00000000000000{ "python.analysis.extraPaths": [ "${workspaceFolder}" ], "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "python.analysis.stubPath": "stubs", "python.envFile": "${workspaceFolder}/.env", "python.terminal.activateEnvInCurrentTerminal": true, "cSpell.words": [ "jeffser", "navidrome" ] } Jeffser-Nocturne-b442fee/.vscode/tasks.json000066400000000000000000000010421523240030000207310ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "meson: build and install", "type": "shell", "command": "meson setup --reconfigure build --prefix=${env:HOME}/.local && ninja -C build && ninja install -C build", "options": { "env": { "PATH": "${env:PATH}:${env:HOME}/.local/bin" } }, "group": { "kind": "build", "isDefault": true }, "presentation": { "reveal": "always", "panel": "shared" }, "problemMatcher": [] } ] } Jeffser-Nocturne-b442fee/CODE_OF_CONDUCT.md000066400000000000000000000004341523240030000201530ustar00rootroot00000000000000Nocturne follows [GNOME's code of conduct](https://conduct.gnome.org/), please make sure to read it before interacting in any way with this repository. To report any misconduct please reach out via private message on - Mastodon: [@jeffser@floss.social](https://floss.social/@jeffser) Jeffser-Nocturne-b442fee/CONTRIBUTING.md000066400000000000000000000010131523240030000175770ustar00rootroot00000000000000# Contributing Rules - Before contributing be aware that Nocturne follows the [GNOME Code of Conduct](https://conduct.gnome.org). - Before contributing code or translations please open an issue explaining what you want to contribute. - AI generated code is not allowed, if you still plan on using AI make sure it follows the structure of the codebase and that it works. - Any contributions that makes significant changes to existing files or deletes existing code are subject to additional testing and might get rejected. Jeffser-Nocturne-b442fee/COPYING000066400000000000000000001045201523240030000164100ustar00rootroot00000000000000 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 . Jeffser-Nocturne-b442fee/Nocturne.doap000066400000000000000000000024351523240030000200210ustar00rootroot00000000000000 Nocturne An Adwaita Navidrome / Subsonic client Python GTK 4 Libadwaita Jeffry Samuel jeffser jeffser Jeffser-Nocturne-b442fee/README.md000066400000000000000000000132361523240030000166370ustar00rootroot00000000000000

Nocturne

Nocturne is a Jellyfin, OpenSubsonic and Bandcamp client that brings all your music together in one place, Nocturne not only connects to existing instances but it's capable of installing and managing its own Navidrome instance

Download on Flathub

--- > [!IMPORTANT] > Please be aware that [GNOME Code of Conduct](https://conduct.gnome.org) applies to Nocturne before interacting with this repository. > [!WARNING] > AI generated issues and PRs will be denied, repeated offence will result in a ban from the repository. ## Features - Exploration by songs, artists, albums, radios and playlists - Playlist management - Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files - Audio equalizer and audio visualizer - Mpris integration - Integrated Navidrome instance management - Automatic lyrics fetching - Downloads and offline mode - Cool interface ## Screenies HomePage | Song Queue | Lyrics | Song List | Album Page :------------------:|:-----------------:|:----------------:|:---------------------------:|:--------------------: ![screenie1](https://jeffser.com/images/nocturne/screenie1.png) | ![screenie2](https://jeffser.com/images/nocturne/screenie2.png) | ![screenie3](https://jeffser.com/images/nocturne/screenie3.png) | ![screenie4](https://jeffser.com/images/nocturne/screenie4.png) | ![screenie5](https://jeffser.com/images/nocturne/screenie5.png) ## Dependencies The following dependencies are requirements of the project. - `python3 >= 3.13` - `gtk4` - `libadwaita-1 >= 1.9` - `glib-2.0 >= 2.84.0` - `libsecret` - `gstreamer` - `blueprint-compiler >= 0.18.0` - `python-requests >= 2.33.1` - `python-colorthief >= 0.2.1` - `python-syncedlyrics >= 1.0.1` - `python-mpris-server >= 0.10.0` - `python-tinytag >= 2.2.1` - `gstreamer1.0-plugins-gstreamer-rs` (optional, needed for video rendering) ## Install ### Linux (Flatpak) Most Linux distributions come with Flatpak preinstalled, make sure your device has [the Flathub repo enabled](https://flathub.org/en/setup). ```sh flatpak install flathub com.jeffser.Nocturne ``` ### Arch Linux (AUR) Nocturne is packaged unofficially in the AUR, to install it first make sure you have an AUR helper such as [yay](https://github.com/jguer/yay). ```sh yay -S nocturne ``` ### Debian Nocturne is packaged unofficially in the official Debian archive (Available in Debian 14 'forky' and above). Install it via apt. ```sh apt install nocturne ``` ### NixOS/nix Nocturne is packaged unofficially in nixpkgs. You can either try it out using nix-shell: ```sh nix-shell -p nocturne ``` or add it to your sytem packages: ```nix environment.systemPackages = [ pkgs.nocturne ]; ``` ## Build ### Linux (Flatpak) Dependencies are automatically managed and built depending on host environment. ```sh flatpak-builder build com.jeffser.Nocturne.yml --force-clean --install-deps-from=flathub flatpak-builder --run build com.jeffser.Nocturne.yml nocturne ``` ### macOS #### 1. Install Dependencies with [Homebrew](https://brew.sh/) ```sh brew install python@3.14 meson ninja pkgconf \ glib gtk4 libadwaita pygobject3 gstreamer \ gobject-introspection libsecret \ desktop-file-utils ``` #### 2. Install Project & Packages ```sh # 1. Install blueprint-compiler git clone https://github.com/GNOME/blueprint-compiler cd blueprint-compiler meson build --prefix=/usr/local sudo ninja install -C build cd .. # 2. Clone the project git clone https://github.com/Jeffser/Nocturne/ cd Nocturne # 3. Install python packages python3 -m venv ./venv source ./venv/bin/activate pip install requests colorthief syncedlyrics tinytag mpris-server ``` #### 3. Build Project ```sh meson setup build --prefix=$HOME/.local ninja -C build ninja install -C build ``` #### 4. Run Development Build ```sh nocturne ``` ## Special Thanks ### Translators Language | Contributors :-----------------------|:----------- Spanish | [Jeffry Samuel](https://github.com/jeffser) Catalan | [Jordi Bultรณ](https://github.com/formajestically) Basque | [Ibai Oihanguren Sala](https://ibaios.eus) German | [Martin Prokoph](https://github.com/Motschen) Russian | [Aleksandr Shamaraev](https://github.com/AlexanderShad) [gttn-84](https://github.com/gttn-84) Simplified Chinese | [Saul Gman](https://github.com/Ja4e) Turkish | [Muhammed Emin Akalan](https://github.com/muhammedeminakalan) Traditional Chinese | [Yuan Chiu](https://yuaner.tw) Croatian | [Milo Ivir](https://github.com/milotype) Telugu | [Aryan Karamtoth](https://github.com/spaciouskarter78) Brazilian Portuguese | [Rikelry Souza](https://github.com/Rikelry) ## Legal Disclaimer - Nocturne is an independent application and is not affiliated with, endorsed, or sponsored by OpenSubsonic, Bandcamp, or Jellyfin. All logos and illustrative assets used within the app are the property of their respective owners. All rights are reserved by their respective holders, and will be taken down if requested. - Nocturne functions strictly as a client application. All network connections and data transfers are performed exclusively at the request and authorization of the server owner. Nocturne does not independently access or host any content. - Nocturne does not facilitate, encourage, or provide mechanisms for music piracy. Users are responsible for ensuring they have the legal right to access and stream the content hosted on the servers they connect to. Jeffser-Nocturne-b442fee/SECURITY.md000066400000000000000000000032731523240030000171510ustar00rootroot00000000000000# Security Policy ## Supported Packaging and Distribution Methods Nocturne only supports [Flatpak](https://flatpak.org/) officially, any other packaging methods might not behave as expected. Thus, official security-related support is only provided to the Flatpak distribution as of right now. This may be subject to change in the future. --- ## Data Handling ### Library - Any data related to the user's music library, history and playback is handled by the server and thus is out of the scope of Nocturne. - If the user chooses to use the integrated Navidrome instance data handling will be managed by Navidrome. ### Lyrics - Automatic lyric downloading is handled by [LRCLIB](https://lrclib.net/) via it's open API. - Nocturne is not responsible for checking the ownership of the lyrics being downloaded - Nocturne is not responsible for the data sent to LRCLIB in order to facilitate the lyrics - Every time a lyric is requested it is downloaded locally as to not make multiple requests to LRCLIB - [LRCLIB](https://github.com/tranxuanthang/lrclib) is an open source software project and thus can be subject to review by anyone ### Telemetry - Nocturne does **not** include any telemetry. - Servers like Navidrome or Subsonic might contain telemetry depending on the configuration. - Nocturne requests for the integrated Navidrome instance telemetry to be off by default on installation. ### Password - The password needed for connecting to a server is stored securely using the library `libsecret` which handles passwords in the device's keyring. --- ## What Nocturne Will **Never** Do - Share library information outside of server - client interactions - Collect usage data - Facilitate content piracy Jeffser-Nocturne-b442fee/com.jeffser.Nocturne.yml000066400000000000000000000154751523240030000221070ustar00rootroot00000000000000id: com.jeffser.Nocturne runtime: org.gnome.Platform runtime-version: '50' sdk: org.gnome.Sdk command: nocturne finish-args: - '--share=network' - '--share=ipc' - '--socket=fallback-x11' - '--device=dri' - '--socket=wayland' - '--socket=pulseaudio' cleanup: - /include - /lib/pkgconfig - /man - /share/doc - /share/gtk-docw - /share/man - /share/pkgconfig - '*.la' - '*.a' modules: - name: python3-requests buildsystem: simple build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "requests" --no-build-isolation sources: - type: file url: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl sha256: 3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a - type: file url: https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz sha256: ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 - type: file url: https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl sha256: 892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3 - type: file url: https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl sha256: 4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a - type: file url: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - name: python3-colorthief buildsystem: simple build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "colorthief" --no-build-isolation sources: - type: file url: https://files.pythonhosted.org/packages/56/18/be03b7058e65f9df479b14e7af4e73945ce311e07aaad45cf2536e14791a/colorthief-0.2.1-py2.py3-none-any.whl sha256: b04fc8ce5cf9c888768745e29cb19b7b688d5711af6fba26e8057debabec56b9 - type: file url: https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz sha256: a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6 - name: python3-syncedlyrics buildsystem: simple build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "syncedlyrics" --no-build-isolation sources: - type: file url: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl sha256: 0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb - type: file url: https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl sha256: 43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c only-arches: - x86_64 - type: file url: https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl sha256: cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457 only-arches: - aarch64 - type: file url: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl sha256: ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95 - type: file url: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - type: file url: https://files.pythonhosted.org/packages/7f/ad/62d2a1cd2a251ea2d43185f2b98badb8acf97a42278ce424ea8a97d8f911/syncedlyrics-1.0.1-py3-none-any.whl sha256: 31ca4322311b454f969e837c7350ecbef241f705f2122ea0183a2902a1a3791a - name: python3-mpris-server buildsystem: simple build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "mpris-server" --no-build-isolation sources: - type: file url: https://github.com/Jeffser/mpris_server/releases/download/v0.9.1/mpris_server-0.10.0-py3-none-any.whl sha256: 14b3c2fd057ca5cf8110d4fe0256a5bc6ad1ccf23474daf31c44728416e6cbbc - type: file url: https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl sha256: a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659 - type: file url: https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl sha256: c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021 - type: file url: https://files.pythonhosted.org/packages/e1/5e/4b5aaaabddfacfe36ba7768817bd1f71a7a810a43705e531f3ae4c690767/emoji-2.15.0-py3-none-any.whl sha256: 205296793d66a89d88af4688fa57fd6496732eb48917a87175a023c8138995eb - type: file url: https://files.pythonhosted.org/packages/92/56/27148014c2f85ce70332f18612f921f682395c7d4e91ec103783be4fce00/pydbus-0.6.0-py2.py3-none-any.whl sha256: 66b80106352a718d80d6c681dc2a82588048e30b75aab933e4020eb0660bf85e - name: python3-tinytag buildsystem: simple build-commands: - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "tinytag" --no-build-isolation sources: - type: file url: https://files.pythonhosted.org/packages/ce/34/d50e338631baaf65ec5396e70085e5de0b52b24b28db1ffbc1c6e82190dc/tinytag-2.2.1-py3-none-any.whl sha256: ed8b1e6d25367937e3321e054f4974f9abfde1a3e0a538824c87da377130c2b6 - name: libportal buildsystem: meson builddir: true config-opts: - -Ddocs=false - -Dtests=false - -Dvapi=false sources: - type: git url: https://github.com/flatpak/libportal.git tag: 0.10.0 commit: c23024018c8eb076549a1517fcb2d7f80d3e2ed5 - name: nocturne builddir: true buildsystem: meson sources: - type: dir path: . Jeffser-Nocturne-b442fee/data/000077500000000000000000000000001523240030000162645ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.SearchProvider.desktop.in000066400000000000000000000003621523240030000272010ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Nocturne Search Provider Icon=com.jeffser.Nocturne DBusActivatable=true NoDisplay=true X-GNOME-SearchProvider-Prefix=com.jeffser.Nocturne X-GNOME-SearchProvider-Path=/com/jeffser/Nocturne/SearchProvider/ Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.SearchProvider.service.in000066400000000000000000000001371523240030000271700ustar00rootroot00000000000000[D-BUS Service] Name=com.jeffser.Nocturne.SearchProvider Exec=@bindir@/nocturne_search_providerJeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.desktop.in000066400000000000000000000011311523240030000242550ustar00rootroot00000000000000[Desktop Entry] Name=Nocturne TryExec=nocturne Exec=nocturne %U Icon=com.jeffser.Nocturne Terminal=false Type=Application MimeType=audio/mpeg;audio/wav;audio/x-aac;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-m4a;audio/x-m4b;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpg;audio/x-mpeg;audio/x-mpegurl;audio/x-opus+ogg;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-wav;audio/x-speex;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wavpack;inode/directory; Categories=AudioVideo;Audio;Utility Keywords=GTK;Navidrome;Jellyfin;Music; StartupNotify=true DBusActivatable=true X-Purism-FormFactor=Workstation;Mobile; Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.gschema.xml000066400000000000000000000135701523240030000244170ustar00rootroot00000000000000 true false false false true 'home' 0 true true true true false false true true true false false false false 'big' 'blur' 'blur' 'blur' true '' '' 0 12 12 12 12 [] [] false 18 'wave' 'translucent' true true '0.11,0.44,0.85' true 0.25 'consecutive' 0.0 0.0 0.0 0.0 0.0 0.0 '' 'http://127.0.0.1:4533' '' '' false 800 600 '' 'grid' 'grid' 'list' 'list' Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.metainfo.xml.in000066400000000000000000000676051523240030000252270ustar00rootroot00000000000000 com.jeffser.Nocturne CC0-1.0 GPL-3.0-or-later Nocturne Bring your music library together

A modern music client

Features

  • Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files
  • Exploration by songs, artists, albums, radios and playlists
  • Playlist management
  • Audio equalizer and audio visualizer
  • Mpris integration
  • Integrated Navidrome instance management
  • Automatic lyrics fetching
  • Downloads and offline mode
  • Cool interface
Jeffry Samuel Eduarte Rojas nocturne com.jeffser.Nocturne AudioVideo Audio Utility 360 keyboard pointing touch https://jeffser.com/nocturne https://github.com/Jeffser/Nocturne https://github.com/Jeffser/Nocturne/issues https://github.com/Jeffser/Nocturne/discussions https://github.com/sponsors/Jeffser https://github.com/Jeffser/Nocturne/blob/main/CONTRIBUTING.md nocturne com.jeffser.Nocturne.desktop #8bf3f5 #08828a https://jeffser.com/images/nocturne/screenie1.png Main homepage https://jeffser.com/images/nocturne/screenie2.png Song queue https://jeffser.com/images/nocturne/screenie3.png Lyrics https://jeffser.com/images/nocturne/screenie4.png Song list https://jeffser.com/images/nocturne/screenie5.png Album page https://jeffser.com/images/nocturne/screenie6.png Mini Player opensubsonic bandcamp navidrome jellyfin music gtk https://github.com/Jeffser/Nocturne/releases/tag/1.5.1

New

  • Added page reload keyboard shortcut (ctrl+r)

Fix

  • Bottom padding is now removed when footer is not being shown
  • Fix list of songs not being shown in Nocturne Playback
https://github.com/Jeffser/Nocturne/releases/tag/1.5.0

New

  • Added option to add a welcome screen to homepage
  • Added option to show frequent albums in homepage
  • Better customization of sidebar
  • Dynamic background for main window is now on by default on new installations
  • OpenSubsonic downloads now include metadata (except BandCamp)
  • Added Favorite Artists page
  • Containers now load faster

Fix

  • Fixes to lyric fetcher / loader
https://github.com/Jeffser/Nocturne/releases/tag/1.4.3

New

  • Lyrics fetcher now uses the database to store downloaded lyrics
  • Added option to hide singles (albums with only one song)
  • Song switching is now faster
  • Optimisations to OpenSubsonic and Jellyfin instances

Fix

  • Fixed box shadow for carousels
  • Fixed list view for songs not working properly
https://github.com/Jeffser/Nocturne/releases/tag/1.4.2

Fix

  • Fixed error preventing the app from opening in certain launchers
https://github.com/Jeffser/Nocturne/releases/tag/1.4.1

New

  • Nocturne can now be controlled from CLI
  • Added swipe gestures in players
  • Added option to select all songs in a list
  • Most actions are now faster
  • Appearance of queue widgets have been improved
  • Images now have a cache database
  • Removed option to hide pan buttons in carousels (they have been redesigned)
  • Implemented recent and most played albums for Bandcamp
  • Implemented restore play queue for Bandcamp
  • Footer in popout window now handles small sizes better
  • Footers now hide previous / next buttons if they are small enough
  • Now Login button disables itself if entries are not filled

Fix

  • Fixed error when searching using OpenSubsonic instances
  • Fixed 'Favorite' button not working in control page
  • Removed the bottom margin from toast messages in popout window
  • General improvements to performance and stability
https://github.com/Jeffser/Nocturne/releases/tag/1.4.0

New

  • Added Bandcamp (Beta) integration
  • Added logos for instances in welcome page
  • Added option to auto download lyrics
  • Fallback to album cover if song does not have one
  • Removed music folder permission (Flatpak), now relying on portal paths
  • Redesigned carousel pan buttons
  • Connections to current song data are now faster
  • Added custom cache manager solution

Fix

  • Fixed lyrics getting mixed within songs
  • Prevent MPRIS from calling functions in instance constantly
  • Username and passwords are now stripped when accessing an instance
  • Instance row from preferences dialog now loads in the background
  • Fixed 'no songs' message appearing on playlists and albums for a split second
  • Fixes to radio playback and UI implementation
https://github.com/Jeffser/Nocturne/releases/tag/1.3.2

Fix

  • Fixed occasional crash when playing certain types of media
  • Fixed opus files not being recognized
https://github.com/Jeffser/Nocturne/releases/tag/1.3.1

New

  • Added Gnome Search Provider
  • Improved pop-out window sizing
  • Redesigned control page (moved rating to popup)
  • Added preferences for Gnome Search Provider
  • Removed "experimental" tag from window dynamic background
https://github.com/Jeffser/Nocturne/releases/tag/1.3.0

New

  • Improved automatic word-by-word lyrics
  • Redesigned fullscreen mode
  • Added toggle for sidebar in player
  • Moved edit lyrics away from context menu
  • Added dynamic sizing for video streams in fullscreen
  • Added rating to song context menu
  • Use requests session for API interaction
  • Toggles now follow dynamic styling
  • Improvements to music player (gst)
  • Randomized homepage
https://github.com/Jeffser/Nocturne/releases/tag/1.2.2

New

  • Added option to ignore gain in songs and albums
  • Allowed instances to provide cover art for radios
  • Added error details on failed login for troubleshooting

Fixes

  • Fixed bug where artist name is not updated with song if metadata is not available when the song changes
  • Fixed bug where Nocturne can not close gracefully sometimes
https://github.com/Jeffser/Nocturne/releases/tag/1.2.1

New

  • Added warning if volume is set to 0 and the song changes

Fixes

  • Added fallback when servers do not provide higher resolution cover art images for songs
  • Made video streaming dependency optional for packages other than Flatpak
https://github.com/Jeffser/Nocturne/releases/tag/1.2.0

New

  • Added compatibility with video streams
  • Added option to hide sidebar in fullscreen mode
  • Synced volume changes with the system audio mixer
  • Changed intensity of dynamic background
  • Made carousels scrollable with horizontal scroll wheels
  • Added option to change the size of buttons (artists, albums, songs, playlists)
  • Reduced max width for left sidebar
  • Added global search to homepage with a keyboard shortcut
  • Added option to see last Nocturne Playback

Fixes

  • Fixed metadata not being displayed properly in players sometimes
https://github.com/Jeffser/Nocturne/releases/tag/1.1.1

New

  • Added option to show pan buttons on carousels

Fixes

  • Fixed problems with JSON reads
  • Fixed capitalization errors in some tooltips
https://github.com/Jeffser/Nocturne/releases/tag/1.1.0

New

  • Added Nocturne Playback
  • Added Discord Rich Presence
  • Added shortcuts for pausing, playing and skipping songs
  • Added option to resume playlists
  • Added song details dialog
  • Added preferences to hide items from sidebar
  • Added accent color to the current song row

Internal

  • Standardized JSON handling in instances
  • Moved most local data to SQLite3

Fixes

  • Fixed incorrect time format handling
  • Fixed ratings not being loaded for local and Jellyfin instances
https://github.com/Jeffser/Nocturne/releases/tag/1.0.1

New

  • Added option to simulate word by word lyrics when none are available

Optimizations

  • Added dynamic image resolutions to save ram
  • Removed gdkPaintableBytes property to save ram

Fixes

  • Fixed sidebar player appearing on launch for a second even when disabled
  • Fixed 'null' appearing as title of radios sometimes
  • Fixed error when some OpenSubsonic instances use int as ID instead of str
https://github.com/Jeffser/Nocturne/releases/tag/1.0.0

New

  • Added option to show dynamic background in the main window
  • Added option to show player as a new sidebar
  • Added option to hide labels from buttons in multiple pages
  • Added support for grouping songs in album by their disc number
  • Added compatibility with word for word lyrics
  • Added support for gapless playback
  • Added support for changing max bitrate
  • Added support for replay gain (applies both at a track and album level)
  • Added dynamic load for playlist and album pages
  • Added support for huge play queues (100+)
  • Added dynamic title and artist handling for radios
  • Added tracking for monthly plays in every integration
  • Added shortcut for opening pop-out window
  • Added support for Quick Connect login in Jellyfin
  • Headerbar is now hidden when in fullscreen mode
  • Paginated playlists
  • Made resizing smoother

Fixes

  • Fixed code causing warnings when launching pop-out window
  • Fixed ratings in Jellyfin
  • Fixed stuttering problems when song does not have cover art
  • Fixed error of repeated songs in lists
  • Fixed multiple errors relating to radios
  • Fixed problems with Play Later and Play Next options
  • Fixed problems with lyrics when they use '/' in their name
  • Fixed self-signed ssl servers not being able to playback music
  • Fixed songs not playing in the correct order
  • Fixed playlist dialog not being scrollable
  • Fixed hide-on-close restore causing the queue to get restarted
  • Fixed no albums appearing in home page when using Jellyfin
https://github.com/Jeffser/Nocturne/releases/tag/0.9.7

Fixes

  • Fixed rating system not working in navidrome and Jellyfin
  • Fixed visual bug in downloads queue
  • Fixed problems related to playlist covers in navidrome
https://github.com/Jeffser/Nocturne/releases/tag/0.9.6

New

  • New background generation algorithm
  • Added downloads and offline mode
  • Redesign of equalizer

Fixes

  • Fixes to logout / login process
https://github.com/Jeffser/Nocturne/releases/tag/0.9.5

New

  • Redesigned login dialog
  • Redesigned preferences dialog
  • Added option to show progressbar in player footer
  • Added smooth transitions between color / blur changes
  • Added 'Top Songs' section to artist page
  • Added additional customization preferences

Fixes

  • Fixed Simplified Chinese translation not being included
  • Added error handling for edge cases in lyrics
  • Fixed errors with fullscreen actions
  • Fixed playback mode 'repeat-one' not working as expected
  • Fixed playback mode 'repeat-all' not working when the queue has only one item
https://github.com/Jeffser/Nocturne/releases/tag/0.9.0

New

  • Rewrote labels in welcome and login pages
  • Added key-capture widgets to searchable pages
  • Added audio equalizer
  • Added Simplified Chinese translation
  • Tweaked the opacity of dynamic backgrounds

Fixes

  • Pages are now reset when login out
  • Better performance and stability
  • Song switcher is now faster
  • Fixed search in local instances
https://github.com/Jeffser/Nocturne/releases/tag/0.8.5

New

  • Replaced local music tag reader for a faster one
  • Made song switcher faster
  • Visualizer now uses less RAM
  • Added full configuration preferences for visualizer
  • Added support for ListenBrainz scrobbing

Fixes

  • Fixed song getting stuck at beggining
  • Fixed problems with external file playback
  • Fixed error when popout window is closed after main window
https://github.com/Jeffser/Nocturne/releases/tag/0.8.0

New

  • Added audio visualizer
  • Changed max sheet size to 420
  • Added option to change default page for local instances
  • Add pagination to album pages
  • Added Russian translation

Fixes

  • Slight reduction in ram usage
  • Sidebar title now adapts correctly if title buttons are located on the left side
  • Fixed error on local server when songs have no album metadata
  • Miscellaneous fixes
https://github.com/Jeffser/Nocturne/releases/tag/0.7.1

New

  • Added loading indicator for local instance
  • Added option to change default page on launch (does not apply to Local instances)
  • Added option to restart Navidrome server from login
  • Made improvements to general stability

Fixes

  • Improved compatibility with more Mpris features
  • Fixed errors with radio creation
  • Fixed null title on radio stations that do not report song names
  • Removed favorite button from radio rows
  • Fixed problems relating to albums in local instance
https://github.com/Jeffser/Nocturne/releases/tag/0.7.0

New

  • UI tweaks to fullscreen mode
  • Added German translation
  • Added plain text auth as fallback
  • Added tooltip to context buttons in album / artist pages
  • Improved auto loading of search results
  • Added option to have playlists in the sidebar
  • Preload metada in local instance
  • Added "Favorite Songs" page
  • Added lyric fetcher for local Flac files
  • Added rating system to songs, albums and artists
  • Added context shortcuts for artists and albums in songs
  • Added additional metadata for mpris clients

Fixes

  • Fixed errors with lyric loader
  • Fixed mpris clients not being able to change volume
  • Fixed Nocturne appearing as "python3" in volume mixers
https://github.com/Jeffser/Nocturne/releases/tag/0.6.0

New

  • Added fullscreen mode
  • Added miniplayer mode
  • Accent color now follows song palette
  • Volume, playback mode and grid view are now remembered between sessions
  • Added option to go back to main lyrics menu
  • Better auto scrolling for lyrics
  • Playing a song from an album or playlist now adds the whole song list to queue

Fixes

  • Fixed MPRIS errors when login out
  • Fixed session not being remembered
  • Fixed song restart not working
  • Fixed some stability issues
https://github.com/Jeffser/Nocturne/releases/tag/0.5.0

New

  • Added dynamic backgrounds to multiple pages
  • Redesigned titlebar for multiple pages
  • Redesigned playlist and album buttons
  • Added the option to restore queues to Jellyfin integration
  • Added option to trust self-signed certificates
  • Added shortcut to preferences
  • Hide titlebar buttons in player
  • Retrieve lyrics from server before lrclib

Fixes

  • Fixed problems with metadata fetching
  • Fixed problems with stability
https://github.com/Jeffser/Nocturne/releases/tag/0.4.0

New

  • Added compatibility with Jellyfin (Experimental)
  • Give user the option to change local directory
  • Better login experience
  • Changed how radios are handled
  • Simplified object models to improve performance

Fixes

  • Made coverArt fetching more consistent
  • Fixed problems with stability
https://github.com/Jeffser/Nocturne/releases/tag/0.3.1

Fixes

  • Improvements to performance
  • Fixed fallback for systems with no keyrings
https://github.com/Jeffser/Nocturne/releases/tag/0.3.0

New

  • Added blur effect to album buttons
  • Improvements to lyrics editor
  • Added option to load lrc files directly
  • Added Spanish translation
  • Made startup sequence faster

Fixes

  • Added fallback for systems with no keyrings
  • Fixed buggy appearance of carousels
https://github.com/Jeffser/Nocturne/releases/tag/0.2.0

New

  • Added screen to indicate when no lyrics are found inside editor
  • Added transitions when logging in
  • Added option to update / delete Navidrome server
  • Added newest, recent and frequent album filtering for local instance
  • Better shadows for album buttons

Fixes

  • Remove dynamic background if song does not have a cover art
  • Only change song if current row is deleted
  • Fixed lyric editor margins
  • Items in sidebar are selected when changing pages
  • Fixed show sidebar button being shown in wide mode
  • Save lyrics as lrc from editor even when not fully synced
  • Fixed external files being saved in restore queue
  • Fixed wrong cover art being used when loading external music into Navidrome instance
https://github.com/Jeffser/Nocturne/releases/tag/0.1.0

First release of Nocturne, please report any bugs or features you might want to see in the next version

Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.search-provider.ini000066400000000000000000000002161523240030000260550ustar00rootroot00000000000000[Shell Search Provider] DesktopId=com.jeffser.Nocturne.desktop BusName=com.jeffser.Nocturne.SearchProvider ObjectPath=@object_path@ Version=2 Jeffser-Nocturne-b442fee/data/com.jeffser.Nocturne.service.in000066400000000000000000000001301523240030000242420ustar00rootroot00000000000000[D-BUS Service] Name=com.jeffser.Nocturne Exec=@bindir@/nocturne --gapplication-service Jeffser-Nocturne-b442fee/data/icons/000077500000000000000000000000001523240030000173775ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/000077500000000000000000000000001523240030000210365ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/scalable/000077500000000000000000000000001523240030000226045ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/scalable/apps/000077500000000000000000000000001523240030000235475ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/scalable/apps/com.jeffser.Nocturne.Devel.svg000066400000000000000000000236641523240030000313360ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/data/icons/hicolor/scalable/apps/com.jeffser.Nocturne.svg000066400000000000000000000111531523240030000302660ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/data/icons/hicolor/symbolic/000077500000000000000000000000001523240030000226575ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/symbolic/apps/000077500000000000000000000000001523240030000236225ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/data/icons/hicolor/symbolic/apps/com.jeffser.Nocturne-symbolic.svg000066400000000000000000000010461523240030000321600ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/data/icons/meson.build000066400000000000000000000006271523240030000215460ustar00rootroot00000000000000application_id = 'com.jeffser.Nocturne' scalable_dir = 'hicolor' / 'scalable' / 'apps' install_data( scalable_dir / ('@0@.svg').format(application_id), install_dir: get_option('datadir') / 'icons' / scalable_dir ) symbolic_dir = 'hicolor' / 'symbolic' / 'apps' install_data( symbolic_dir / ('@0@-symbolic.svg').format(application_id), install_dir: get_option('datadir') / 'icons' / symbolic_dir ) Jeffser-Nocturne-b442fee/data/meson.build000066400000000000000000000047511523240030000204350ustar00rootroot00000000000000desktop_file = i18n.merge_file( input: 'com.jeffser.Nocturne.desktop.in', output: 'com.jeffser.Nocturne.desktop', type: 'desktop', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'applications' ) searchprovider_desktop_file = i18n.merge_file( input: 'com.jeffser.Nocturne.SearchProvider.desktop.in', output: 'com.jeffser.Nocturne.SearchProvider.desktop', type: 'desktop', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'applications' ) desktop_utils = find_program('desktop-file-validate', required: false) if desktop_utils.found() test('Validate desktop file', desktop_utils, args: [desktop_file]) test('Validate search provider desktop file', desktop_utils, args: [searchprovider_desktop_file]) endif appstream_file = i18n.merge_file( input: 'com.jeffser.Nocturne.metainfo.xml.in', output: 'com.jeffser.Nocturne.metainfo.xml', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'metainfo' ) appstreamcli = find_program('appstreamcli', required: false, disabler: true) test('Validate appstream file', appstreamcli, args: ['validate', '--no-net', '--explain', appstream_file]) install_data('com.jeffser.Nocturne.gschema.xml', install_dir: get_option('datadir') / 'glib-2.0' / 'schemas' ) compile_schemas = find_program('glib-compile-schemas', required: false, disabler: true) test('Validate schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()]) service_conf = configuration_data() service_conf.set('bindir', get_option('prefix') / get_option('bindir')) configure_file( input: 'com.jeffser.Nocturne.service.in', output: 'com.jeffser.Nocturne.service', configuration: service_conf, install_dir: get_option('datadir') / 'dbus-1' / 'services' ) search_conf = configuration_data() search_conf.set('object_path', '/com/jeffser/Nocturne/SearchProvider') search_conf.set('bindir', get_option('prefix') / get_option('bindir')) configure_file( input: 'com.jeffser.Nocturne.search-provider.ini', output: 'com.jeffser.Nocturne.search-provider.ini', configuration: search_conf, install_dir: get_option('datadir') / 'gnome-shell' / 'search-providers', ) configure_file( input: 'com.jeffser.Nocturne.SearchProvider.service.in', output: 'com.jeffser.Nocturne.SearchProvider.service', configuration: service_conf, install_dir: get_option('datadir') / 'dbus-1' / 'services' ) subdir('icons') Jeffser-Nocturne-b442fee/meson.build000066400000000000000000000005431523240030000175170ustar00rootroot00000000000000project('nocturne', version: '1.5.1', meson_version: '>= 1.0.0', default_options: [ 'warning_level=2', 'werror=false', ], ) i18n = import('i18n') gnome = import('gnome') subdir('data') subdir('src') subdir('po') gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true, ) Jeffser-Nocturne-b442fee/nocturne-uninstalled000077500000000000000000000046471523240030000214710ustar00rootroot00000000000000#!/usr/bin/env python # nocturne.in # # Copyright 2026 Jeffry Samuel # Copyright 2026 Philippe Normand # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later import os import sys import gettext import subprocess import gi VERSION = 'uninstalled' gettext.install('nocturne', '') def mtime(path): return os.stat(path).st_mtime if __name__ == '__main__': # Make sure blp files are compiled. for (dirname, _subdirs, files) in os.walk('src/ui'): for filename in files: in_file = os.path.join(dirname, filename) basename, ext = os.path.splitext(in_file) if ext != '.blp': continue out_file = f'{basename}.ui' if os.path.isfile(out_file) and mtime(out_file) >= mtime(in_file): continue subprocess.check_call(['blueprint-compiler', 'compile', '--output', out_file, in_file]) # Make sure the GResource file is compiled. compiled_resource_file = os.path.join('src', 'nocturne.gresource') if not os.path.isfile(compiled_resource_file) or mtime(os.path.join('src', 'nocturne.gresource.xml')) > mtime(compiled_resource_file): subprocess.check_call(['glib-compile-resources', 'nocturne.gresource.xml'], cwd='src') # Make sure the GSettings file is compiled. compiled_gsettings_file = os.path.join('data', 'gschemas.compiled') if not os.path.isfile(compiled_gsettings_file) or mtime(os.path.join('data', 'com.jeffser.Nocturne.gschema.xml')) > mtime(compiled_gsettings_file): subprocess.check_call(['glib-compile-schemas', 'data']) os.environ['GSETTINGS_SCHEMA_DIR'] = 'data' from gi.repository import Gio resource = Gio.Resource.load(compiled_resource_file) resource._register() sys.path.insert(0, 'src') from src import main sys.exit(main.main(VERSION)) Jeffser-Nocturne-b442fee/po/000077500000000000000000000000001523240030000157715ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/po/LINGUAS000066400000000000000000000000521523240030000170130ustar00rootroot00000000000000ca de es eu hr ru te tr zh_CN zh_TW pt_BR Jeffser-Nocturne-b442fee/po/POTFILES.in000066400000000000000000000072101523240030000175460ustar00rootroot00000000000000data/com.jeffser.Nocturne.desktop.in data/com.jeffser.Nocturne.gschema.xml data/com.jeffser.Nocturne.metainfo.xml.in data/com.jeffser.Nocturne.SearchProvider.desktop.in data/com.jeffser.Nocturne.SearchProvider.service.in data/com.jeffser.Nocturne.service.in src/actions.py src/constants.py src/__init__.py src/integrations/base.py src/integrations/discord_rpc.py src/integrations/__init__.py src/integrations/jellyfin.py src/integrations/local.py src/integrations/models.py src/integrations/navidrome.py src/integrations/secret.py src/integrations/sql_instance.py src/main.py src/nocturne.gresource.xml src/nocturne.in src/nocturne_search_provider.py.in src/preferences.py src/ui/album/button.blp src/ui/album/page.blp src/ui/album/row.blp src/ui/artist/button.blp src/ui/artist/page.blp src/ui/artist/row.blp src/ui/containers/carousel.blp src/ui/containers/download_row.blp src/ui/containers/downloads_queue_button.blp src/ui/containers/page_dialog.blp src/ui/containers/wrapbox.blp src/ui/lyrics/dialog.blp src/ui/lyrics/edit_row.blp src/ui/pages/albums_all.blp src/ui/pages/albums.blp src/ui/pages/artists.blp src/ui/pages/home.blp src/ui/pages/login.blp src/ui/pages/playback.blp src/ui/pages/playlists.blp src/ui/pages/radios.blp src/ui/pages/setup.blp src/ui/pages/songs_all.blp src/ui/pages/starred.blp src/ui/pages/welcome.blp src/ui/playing/control_page.blp src/ui/playing/cover_art.blp src/ui/playing/equalizer_page.blp src/ui/playing/footer.blp src/ui/playing/lyrics_page.blp src/ui/playing/playback_mode_button.blp src/ui/playing/popout_window.blp src/ui/playing/queue_page.blp src/ui/playing/volume_button.blp src/ui/playlist/button.blp src/ui/playlist/dialog.blp src/ui/playlist/page.blp src/ui/playlist/row.blp src/ui/playlist/selector_row.blp src/ui/preferences.blp src/ui/shortcuts-dialog.blp src/ui/song/button.blp src/ui/song/details_dialog.blp src/ui/song/queue.blp src/ui/song/row.blp src/ui/song/small_row.blp src/ui/window.blp src/widgets/album/button.py src/widgets/album/__init__.py src/widgets/album/page.py src/widgets/album/row.py src/widgets/artist/button.py src/widgets/artist/__init__.py src/widgets/artist/page.py src/widgets/artist/row.py src/widgets/containers/carousel.py src/widgets/containers/context.py src/widgets/containers/download_row.py src/widgets/containers/downloads_queue_button.py src/widgets/containers/__init__.py src/widgets/containers/page_dialog.py src/widgets/containers/wrapbox.py src/widgets/__init__.py src/widgets/lyrics/dialog.py src/widgets/lyrics/helpers.py src/widgets/lyrics/__init__.py src/widgets/pages/albums_all.py src/widgets/pages/albums.py src/widgets/pages/artists.py src/widgets/pages/home.py src/widgets/pages/__init__.py src/widgets/pages/login.py src/widgets/pages/playback.py src/widgets/pages/playlists.py src/widgets/pages/radios.py src/widgets/pages/setup.py src/widgets/pages/songs_all.py src/widgets/pages/starred.py src/widgets/pages/welcome.py src/widgets/playing/control_page.py src/widgets/playing/cover_art.py src/widgets/playing/equalizer_page.py src/widgets/playing/footer.py src/widgets/playing/__init__.py src/widgets/playing/lyrics_page.py src/widgets/playing/playback_mode_button.py src/widgets/playing/player.py src/widgets/playing/popout_window.py src/widgets/playing/queue_page.py src/widgets/playing/spectrum.py src/widgets/playing/volume_button.py src/widgets/playlist/button.py src/widgets/playlist/dialog.py src/widgets/playlist/__init__.py src/widgets/playlist/page.py src/widgets/playlist/row.py src/widgets/playlist/selector_row.py src/widgets/song/button.py src/widgets/song/details_dialog.py src/widgets/song/__init__.py src/widgets/song/queue.py src/widgets/song/row.py src/widgets/song/small_row.py src/window.py Jeffser-Nocturne-b442fee/po/ca.po000066400000000000000000001463731523240030000167320ustar00rootroot00000000000000# Catalan translations for PACKAGE package. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Jordi Bultรณ , 2026. # msgid "" msgstr "" "Project-Id-Version: Nocturne\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-23 03:14+0200\n" "PO-Revision-Date: 2026-07-24 13:14+0200\n" "Last-Translator: Jordi Bultรณ \n" "Language-Team: Catalan\n" "Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 50.0\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Tota la teva mรบsica en un sol lloc" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern music client" msgstr "Client de mรบsica modern" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Caracterรญstiques" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files" msgstr "Compatible amb Jellyfin, OpenSubsonic, Bandcamp i fitxers locals" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "" "Exploraciรณ per canรงons, artistes, ร lbums, rร dios i llistes de reproducciรณ" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Playlist management" msgstr "Gestiรณ de llistes de reproducciรณ" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Equalitzador i visualitzador d'ร udio" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Integraciรณ MPRIS" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Gestiรณ integrada d'instร ncies de Navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Obtenciรณ automร tica de lletres" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "Baixades i mode sense connexiรณ" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Interfรญcie moderna" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Pร gina d'inici" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Cua de canรงons" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:93 src/ui/window.blp:185 #: src/ui/window.blp:237 msgid "Lyrics" msgstr "Lletres" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Llista de canรงons" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "ร€lbums" #: data/com.jeffser.Nocturne.metainfo.xml.in:86 msgid "Mini Player" msgstr "Minireproductor" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "opensubsonic" msgstr "opensubsonic" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "bandcamp" msgstr "bandcamp" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:95 msgid "music" msgstr "mรบsica" #: data/com.jeffser.Nocturne.metainfo.xml.in:96 msgid "gtk" msgstr "gtk" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "Proveรฏdor de cerca del Nocturne" #: src/actions.py:140 src/widgets/pages/login.py:133 msgid "Error" msgstr "Errada" #: src/actions.py:141 msgid "No details provided" msgstr "No s'ha proporcionat cap detall" #: src/actions.py:143 src/actions.py:284 src/preferences.py:486 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "Tanca" #: src/actions.py:181 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "No s'han trobat prou dades per a la reproducciรณ del Nocturne ({})" #: src/actions.py:281 src/ui/song/row.blp:72 msgid "External File" msgstr "Fitxer extern" #: src/actions.py:282 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Aquesta pista s'ha carregat des d'un fitxer extern, per tant, tindrร  menys " "funcionalitats que una pista de la biblioteca" #: src/actions.py:302 msgid "Navidrome server deleted successfully" msgstr "El servidor de Navidrome s'ha suprimit correctament" #: src/actions.py:311 msgid "Delete Navidrome Server" msgstr "Suprimeix el servidor de Navidrome" #: src/actions.py:312 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Voleu suprimir el servidor de Navidrome integrat?" #: src/actions.py:314 src/actions.py:453 src/actions.py:487 src/actions.py:665 #: src/actions.py:743 src/preferences.py:431 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:181 msgid "Cancel" msgstr "Cancelยทla" #: src/actions.py:315 msgid "Keep Data" msgstr "Mantรฉn les dades" #: src/actions.py:316 msgid "Delete Everything" msgstr "Suprimeix-ho tot" #: src/actions.py:401 msgid "No Name" msgstr "Sense nom" #: src/actions.py:420 msgid "Radio updated successfully" msgstr "La rร dio s'ha actualitzat correctament" #: src/actions.py:420 msgid "Radio added successfully" msgstr "La radio s'ha afegit correctament" #: src/actions.py:427 msgid "Error updating radio" msgstr "S'ha produรฏt un error en actualitzar la rร dio" #: src/actions.py:427 msgid "Error adding radio" msgstr "S'ha produรฏt un error en afegir la rร dio" #: src/actions.py:438 src/actions.py:657 msgid "Name" msgstr "Nom" #: src/actions.py:443 msgid "Stream Url" msgstr "URL de l'emissiรณ" #: src/actions.py:450 msgid "Update Radio Station" msgstr "Actualitza l'emissora de rร dio" #: src/actions.py:450 msgid "Add Radio Station" msgstr "Afegeix una emissora de rร dio" #: src/actions.py:454 src/preferences.py:432 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Desa" #: src/actions.py:467 msgid "Radio deleted successfully" msgstr "La rร dio s'ha suprimit correctament" #: src/actions.py:471 msgid "Error deleting radio" msgstr "S'ha produรฏt un error en suprimir la rร dio" #: src/actions.py:484 msgid "Delete Radio Station" msgstr "Suprimeix l'emissora de rร dio" #: src/actions.py:485 src/actions.py:741 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Esteu segur que voleu suprimir '{}'?" #: src/actions.py:488 src/actions.py:744 src/constants.py:534 #: src/constants.py:582 msgid "Delete" msgstr "Suprimeix" #: src/actions.py:507 src/actions.py:519 src/actions.py:521 src/actions.py:563 #: src/actions.py:614 msgid "Playing Next" msgstr "Es reproduirร  a continuaciรณ" #: src/actions.py:511 src/actions.py:526 src/actions.py:528 src/actions.py:570 #: src/actions.py:621 msgid "Playing Later" msgstr "Es reproduirร  mรฉs tard" #: src/actions.py:519 src/actions.py:526 src/actions.py:854 src/actions.py:979 #: src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} canรงons" #: src/actions.py:639 msgid "Playlist updated successfully" msgstr "La llista s'ha actualitzat correctament" #: src/actions.py:639 msgid "Playlist created successfully" msgstr "La llista s'ha creat correctament" #: src/actions.py:641 msgid "Error updating playlist" msgstr "S'ha produรฏt un error en actualitzar la llista" #: src/actions.py:641 msgid "Error creating playlist" msgstr "S'ha produรฏt un error en crear la llista" #: src/actions.py:662 msgid "Update Playlist" msgstr "Actualitza la llista" #: src/actions.py:662 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Crea una llista" #: src/actions.py:666 msgid "Update" msgstr "Actualitza" #: src/actions.py:666 msgid "Create" msgstr "Crea" #: src/actions.py:679 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "S'ha suprimit {} canรงรณ" msgstr[1] "S'han suprimit {} canรงons" #: src/actions.py:704 src/actions.py:718 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "S'ha afegit {} canรงรณ" msgstr[1] "S'han afegit {} canรงons" #: src/actions.py:722 src/actions.py:859 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "S'ha omรจs {} canรงรณ" msgstr[1] "S'han omรจs {} canรงons" #: src/actions.py:733 msgid "Playlist Deleted" msgstr "La llista s'ha eliminat" #: src/actions.py:740 msgid "Delete Playlist" msgstr "Suprimeix la llista" #: src/actions.py:790 msgid "No songs found" msgstr "No s'ha trobat cap canรงรณ" #: src/actions.py:831 src/actions.py:855 src/actions.py:890 src/actions.py:923 msgid "Download Started" msgstr "S'ha iniciat la baixada" #: src/actions.py:838 src/actions.py:866 src/actions.py:873 src/actions.py:906 #: src/actions.py:939 msgid "Already Downloaded" msgstr "Ja s'ha baixat" #: src/actions.py:860 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} canรงรณ ({})" msgstr[1] "{} canรงons ({})" #: src/actions.py:872 msgid "Download Skipped" msgstr "S'ha omรจs la baixada" #: src/actions.py:894 src/actions.py:927 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "S'ha iniciat la baixada (s'ha omรจs {} canรงรณ)" msgstr[1] "S'ha iniciat la baixada (s'han omรจs {} canรงons)" #: src/actions.py:963 src/actions.py:980 msgid "Deleted" msgstr "Eliminada" #: src/constants.py:344 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "En ordre" #: src/constants.py:348 msgid "Repeat All" msgstr "Repeteix-ho tot" #: src/constants.py:352 msgid "Repeat One" msgstr "Repeteix-ne una" #: src/constants.py:357 #, python-brace-format msgid "Low ({})" msgstr "Baixa ({})" #: src/constants.py:358 #, python-brace-format msgid "Medium ({})" msgstr "Mitjana ({})" #: src/constants.py:359 #, python-brace-format msgid "High ({})" msgstr "Alta ({})" #: src/constants.py:360 #, python-brace-format msgid "Ultra ({})" msgstr "Mร xima ({})" #: src/constants.py:361 msgid "Original File" msgstr "Fitxer original" #. Item #: src/constants.py:368 src/ui/pages/home.blp:5 msgid "Home" msgstr "Inici" #. Item #. list #: src/constants.py:373 src/integrations/models.py:163 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/pages/home.blp:65 src/ui/preferences.blp:287 msgid "Artists" msgstr "Artistes" #. Section #: src/constants.py:380 src/ui/artist/page.blp:164 #: src/ui/pages/albums_all.blp:5 src/ui/pages/albums_all.blp:28 #: src/ui/pages/home.blp:59 src/ui/preferences.blp:279 msgid "Albums" msgstr "ร€lbums" #. Item #: src/constants.py:383 src/constants.py:418 src/constants.py:438 #: src/window.py:155 msgid "All" msgstr "Tots" #. Item #: src/constants.py:388 msgid "Random" msgstr "Aleatoris" #. Item #: src/constants.py:393 src/constants.py:423 msgid "Favorites" msgstr "Preferits" #. Item #: src/constants.py:398 msgid "Recently Added" msgstr "Afegits recentment" #. Item #: src/constants.py:403 msgid "Recently Played" msgstr "Reproduรฏts recentment" #. Item #: src/constants.py:408 msgid "Most Played" msgstr "Mรฉs reproduรฏts" #. Section #: src/constants.py:415 src/ui/album/page.blp:160 src/ui/pages/home.blp:53 #: src/ui/pages/songs_all.blp:5 src/ui/pages/songs_all.blp:28 #: src/ui/preferences.blp:271 msgid "Songs" msgstr "Canรงons" #. Item #: src/constants.py:428 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Rร dios" #. Section #: src/constants.py:435 src/ui/pages/home.blp:71 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:295 msgid "Playlists" msgstr "Llistes" #: src/constants.py:448 src/constants.py:499 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:114 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play" msgstr "Reprodueix" #: src/constants.py:453 src/constants.py:486 src/constants.py:509 msgid "Shuffle" msgstr "Barreja" #: src/constants.py:458 src/constants.py:514 src/constants.py:547 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Next" msgstr "Reprodueix a continuaciรณ" #: src/constants.py:463 src/constants.py:519 src/constants.py:552 #: src/ui/song/queue.blp:71 src/ui/song/queue.blp:73 msgid "Play Later" msgstr "Reprodueix mรฉs tard" #: src/constants.py:468 src/constants.py:562 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Add To Playlist" msgstr "Afegeix a la llista" #: src/constants.py:473 src/constants.py:524 src/constants.py:567 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:87 #: src/ui/song/queue.blp:89 msgid "Download" msgstr "Baixa" #: src/constants.py:478 src/constants.py:577 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Mostra l'artista" #: src/constants.py:491 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Rร dio" #: src/constants.py:504 msgid "Resume" msgstr "Reprรจn" #: src/constants.py:529 src/constants.py:557 msgid "Edit" msgstr "Edita" #: src/constants.py:543 msgid "Select" msgstr "Selecciona" #: src/constants.py:572 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Mostra l'ร lbum" #: src/constants.py:588 src/ui/song/queue.blp:98 src/ui/song/queue.blp:100 msgid "Remove" msgstr "Suprimeix" #: src/constants.py:593 src/ui/song/queue.blp:109 src/ui/song/queue.blp:111 msgid "Delete Download" msgstr "Esborra la baixada" #: src/constants.py:599 src/main.py:156 msgid "Show Details" msgstr "Mostra els detalls" #: src/constants.py:610 msgid "Visit Webpage" msgstr "Visita la pร gina web" #: src/constants.py:616 msgid "Update Server" msgstr "Actualitza el servidor" #: src/constants.py:621 msgid "Delete Server" msgstr "Suprimeix el servidor" #: src/constants.py:630 msgid "Edit Lyrics" msgstr "Edita la lletra" #: src/constants.py:635 msgid "Remove Lyrics" msgstr "Suprimeix la lletra" #: src/integrations/base.py:306 msgid "Could not generate SQL database" msgstr "No s'ha pogut generar la base de dades SQL" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "Navegaciรณ" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "S'estร  escoltant mรบsica" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "Connecteu-vos a un servidor Jellyfin" #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "Utilitzeu una instร ncia ja existent de Jellyfin" #: src/integrations/jellyfin.py:295 msgid "Could not log in" msgstr "No s'ha pogut iniciar la sessiรณ" #: src/integrations/jellyfin.py:1135 msgid "Unknown" msgstr "Desconegut" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:691 msgid "Local Files" msgstr "Fitxers locals" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Permet que el Nocturne carregui els fitxers locals. Si la biblioteca รฉs " "gran, utilitzeu un servidor dedicat." #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Continua" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "Amb funcionalitat limitada" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "S'estan carregant les canรงons" #: src/integrations/local.py:138 src/integrations/navidrome.py:749 msgid "Could not locate path, try again" msgstr "No s'ha pogut trobar el camรญ. Torna-ho a provar" #: src/integrations/local.py:317 msgid "No Album" msgstr "Cap ร lbum" #: src/integrations/local.py:709 src/integrations/local.py:718 msgid "Offline Mode" msgstr "Mode sense connexiรณ" #: src/integrations/local.py:710 msgid "Access your downloads" msgstr "Accedeix a les teves baixades" #: src/integrations/models.py:142 src/ui/album/page.blp:5 #: src/widgets/album/page.py:128 msgid "Album" msgstr "ร€lbum" #: src/integrations/models.py:144 msgid "Display Artist" msgstr "Artista principal" #: src/integrations/models.py:146 msgid "MusicBrainz ID" msgstr "ID del MusicBrainz" #: src/integrations/models.py:147 msgid "Track Number" msgstr "Nรบmero de pista" #: src/integrations/models.py:148 msgid "Year" msgstr "Any" #: src/integrations/models.py:149 msgid "Size" msgstr "Mida" #: src/integrations/models.py:150 msgid "Suffix" msgstr "Extensiรณ" #: src/integrations/models.py:151 src/widgets/album/button.py:94 #: src/widgets/album/page.py:144 src/widgets/artist/page.py:109 #: src/widgets/playing/control_page.py:191 src/widgets/song/row.py:183 msgid "Favorite" msgstr "Preferit" #: src/integrations/models.py:152 msgid "Duration" msgstr "Durada" #: src/integrations/models.py:154 msgid "Bit Rate" msgstr "Taxa de bits" #: src/integrations/models.py:155 msgid "Bit Depth" msgstr "Profunditat de bits" #: src/integrations/models.py:156 msgid "Sampling Rate" msgstr "Freqรผรจncia de mostreig" #: src/integrations/models.py:157 msgid "Channel Count" msgstr "Nombre de canals" #: src/integrations/models.py:159 msgid "Path" msgstr "Camรญ" #: src/integrations/models.py:160 msgid "Disc Number" msgstr "Nรบmero de disc" #: src/integrations/models.py:161 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:162 msgid "Genres" msgstr "Gรจneres" #. list #: src/integrations/models.py:164 msgid "Track Gain" msgstr "Guany de la pista" #: src/integrations/models.py:165 msgid "Album Gain" msgstr "Guany de l'ร lbum" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Connecta un servidor OpenSubsonic com Navidrome" #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "Servidor extern" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic instance" msgstr "Utilitza una instร ncia existent d'OpenSubsonic" #: src/integrations/navidrome.py:173 msgid "Server returned an error" msgstr "El servidor ha retornat un error" #: src/integrations/navidrome.py:173 msgid "Unknown Error" msgstr "Error desconegut" #: src/integrations/navidrome.py:724 src/integrations/navidrome.py:733 msgid "Managed Server" msgstr "Servidor gestionat" #: src/integrations/navidrome.py:725 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "Connecteu-vos a una instร ncia de Navidrome gestionada directament pel " "Nocturne" #: src/integrations/navidrome.py:728 msgid "Manage Server" msgstr "Gestiona el servidor" #: src/integrations/navidrome.py:734 msgid "Create and use a Navidrome instance" msgstr "Creeu i utilitzeu una instร ncia de Navidrome" #: src/integrations/navidrome.py:793 msgid "Important: Use your Subsonic login information." msgstr "Important: utilitzeu les vostres dades d'inici de sessiรณ de Subsonic." #: src/integrations/navidrome.py:796 msgid "Bandcamp User Settings" msgstr "Parร metres d'usuari de Bandcamp" #: src/integrations/navidrome.py:802 msgid "Explore your online library" msgstr "Exploreu la biblioteca en lรญnia" #: src/main.py:94 msgid "Music is Playing" msgstr "Hi ha mรบsica en reproducciรณ" #: src/main.py:107 msgid "Fullscreen Player Active" msgstr "El reproductor a pantalla completa estร  actiu" #: src/main.py:153 msgid "Login Failed" msgstr "Ha fallat l'inici de sessiรณ" #: src/nocturne_search_provider.py.in:12 src/ui/album/button.blp:21 msgid "Play Album" msgstr "Reprodueix l'ร lbum" #: src/nocturne_search_provider.py.in:13 src/ui/song/button.blp:16 msgid "Play Song" msgstr "Reprodueix la canรงรณ" #: src/nocturne_search_provider.py.in:14 src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "Reprodueix la llista" #: src/nocturne_search_provider.py.in:113 msgid "Open" msgstr "Obre" #: src/preferences.py:420 msgid "Settings Page" msgstr "Configuraciรณ" #: src/preferences.py:423 msgid "User Token" msgstr "Testimoni d'usuari" #: src/preferences.py:427 msgid "Link ListenBrainz" msgstr "Enllaรงa amb ListenBrainz" #: src/preferences.py:428 msgid "Connect your ListenBrainz account with a user token" msgstr "Connecteu el compte de ListenBrainz amb un testimoni d'usuari" #: src/preferences.py:476 msgid "Flatpak Sandbox Warning" msgstr "Avรญs sobre l'aรฏllament de Flatpak" #: src/preferences.py:477 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "Per connectar-vos a Discord cal un permรญs addicional; un cop hร giu executat " "l'ordre segรผent, reinicieu el Nocturne" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:115 msgid "Toggle star" msgstr "Activa el preferit" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Carร tula de l'ร lbum" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "Obre l'ร lbum" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "Obre l'artista" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:228 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "1 estrella" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:237 msgid "2 Stars" msgstr "2 estrelles" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:246 msgid "3 Stars" msgstr "3 estrelles" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:255 msgid "4 Stars" msgstr "4 estrelles" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:264 msgid "5 Stars" msgstr "5 estrelles" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:130 msgid "Options" msgstr "Opcions" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Artista" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Amplia la biografia" #: src/ui/artist/page.blp:159 msgid "Top Songs" msgstr "Canรงons mรฉs populars" #: src/ui/artist/page.blp:169 msgid "Related Artists" msgstr "Artistes relacionats" #: src/ui/containers/carousel.blp:24 src/ui/containers/carousel.blp:26 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Llista" #: src/ui/containers/carousel.blp:41 msgid "Pan to the Left" msgstr "Desplaรงa cap a l'esquerra" #: src/ui/containers/carousel.blp:47 msgid "Pan to the Right" msgstr "Desplaรงa cap a la dreta" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "S'ha baixat" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Suprimeix de la cua" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "Cua de baixades" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "Les canรงons baixades estan disponibles en el mode sense connexiรณ" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Neteja les baixades finalitzades" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Editor de lletres" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:187 #: src/ui/playing/footer.blp:129 msgid "Pause" msgstr "Pausa" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:122 msgid "Current song progress bar" msgstr "Barra de progrรฉs de la canรงรณ actual" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Assigna la marca de temps de la lรญnia segรผent a l'actual" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Afegeix una lรญnia a la marca de temps" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Sense lletra" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "" "No s'ha trobat la lletra d'aquesta pista. Per comenรงar, afegiu una lรญnia nova" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Marca de temps" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Vรฉs a la marca de temps" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Assigna la marca de temps actual" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Suprimeix la lรญnia" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Graella" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Cerca ร lbums" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "S'ha arribat al final de la llista" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "No s'ha trobat cap ร lbum" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Cerca artistes" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "No s'ha trobat cap artista" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "Commuta la cerca" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "Cerca" #: src/ui/pages/home.blp:84 msgid "No Elements Found" msgstr "No s'ha trobat cap element" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 #: src/widgets/pages/login.py:38 src/widgets/pages/login.py:78 msgid "Login" msgstr "Inici de sessiรณ" #: src/ui/pages/login.blp:36 msgid "Server Status" msgstr "Estat del servidor" #: src/ui/pages/login.blp:37 src/widgets/pages/login.py:52 msgid "Not Running" msgstr "No s'estร  executant" #: src/ui/pages/login.blp:38 msgid "Restart Instance" msgstr "Reinicia la instร ncia" #: src/ui/pages/login.blp:51 msgid "Manage Navidrome Server" msgstr "Gestiona el servidor de Navidrome" #: src/ui/pages/login.blp:61 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:69 msgid "More Options" msgstr "Mรฉs opcions" #: src/ui/pages/login.blp:75 msgid "Trust Server Certificates" msgstr "Confia en els certificats del servidor" #: src/ui/pages/login.blp:83 msgid "Library Directory" msgstr "Directori de la biblioteca" #: src/ui/pages/login.blp:96 msgid "Username" msgstr "Nom d'usuari" #: src/ui/pages/login.blp:101 msgid "Password" msgstr "Contrasenya" #: src/ui/pages/login.blp:122 src/ui/pages/login.blp:123 msgid "Use Quick Connect" msgstr "Utilitza la connexiรณ rร pida" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:25 msgid "Playback" msgstr "Reproducciรณ" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "Vรฉs a l'inici" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "Mostra o amaga el visualitzador" #: src/ui/pages/playback.blp:54 src/window.py:213 msgid "Nocturne Playback" msgstr "Reproducciรณ del Nocturne" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "Aquestes sรณn canรงons mรฉs reproduรฏdes el mes passat" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "Desa la llista" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "S'ha afegit la llista" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "Grร cies per l'รบs del Nocturne!" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Cerca llistes" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "No s'ha trobat cap llista" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Cerca rร dios" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Afegeix una rร dio" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "No s'ha trobat cap rร dio" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Configuraciรณ" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Baixada del Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "El Nocturne baixarร  el Navidrome automร ticament" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Baixada (~20โ€ฏmb)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "S'estร  baixant" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "S'ha produรฏt un error" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Torna-ho a provar mรฉs tard" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "La baixada s'ha fet correctament" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Abans de continuar creeu un compte d'administrador i verifiqueu que les " "pistes es troben al directori de mรบsica" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Pร gina de la instร ncia" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Cerca canรงons" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:96 #: src/ui/song/queue.blp:137 msgid "No Songs Found" msgstr "No s'ha trobat cap canรงรณ" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Canรงons preferides" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "Cerca canรงons preferides" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Us donem la benvinguda" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Us donem la benvinguda al Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "Seleccioneu un tipus d'instร ncia per comenรงar" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "S'estร  reproduint" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "Vรฉs al web de l'emissora actual" #: src/ui/playing/control_page.blp:157 src/ui/playing/footer.blp:99 msgid "Previous" msgstr "Anterior" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:143 msgid "Next" msgstr "Segรผent" #: src/ui/playing/control_page.blp:221 msgid "Rate" msgstr "Valora" #: src/ui/playing/control_page.blp:277 msgid "Star" msgstr "Destacat" #: src/ui/playing/control_page.blp:284 msgid "Toggle Sidebar" msgstr "Mostra o amaga la barra lateral" #: src/ui/playing/cover_art.blp:35 msgid "Audio" msgstr "ร€udio" #: src/ui/playing/cover_art.blp:43 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Carร tula de la canรงรณ" #: src/ui/playing/cover_art.blp:49 src/ui/playing/cover_art.blp:57 msgid "Video" msgstr "Vรญdeo" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "Predefinits" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Pla" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Claredat vocal" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "Jazz suau" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rock" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Clร ssica" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Acรบstica" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Indicador de progrรฉs de la canรงรณ actual" #: src/ui/playing/footer.blp:45 msgid "Current song cover art" msgstr "Carร tula de la canรงรณ actual" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "Torna enrere" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "Opcions de la lletra" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "No s'ha trobat cap lletra" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "No s'ha baixat" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "La lletra d'aquesta canรงรณ encara no s'ha baixat, รฉs possible que estigui " "disponible per a la baixada" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "Carrega un fitxer de lletra" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Finestra emergent" #: src/ui/playing/popout_window.blp:67 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:209 msgid "Song Details" msgstr "Detalls de la canรงรณ" #: src/ui/playing/popout_window.blp:87 src/ui/window.blp:175 #: src/ui/window.blp:231 msgid "Queue" msgstr "Cua" #: src/ui/playing/popout_window.blp:99 src/ui/window.blp:191 #: src/ui/window.blp:243 msgid "Equalizer" msgstr "Equalitzador" #: src/ui/playing/queue_page.blp:19 msgid "Generating Next Queue" msgstr "S'estร  generant la cua segรผent" #: src/ui/playing/queue_page.blp:23 msgid "Autoplay" msgstr "Reproducciรณ automร tica" #: src/ui/playing/queue_page.blp:24 msgid "Generate a new queue when the current one ends" msgstr "Genereu una cua nova quan acabi l'actual" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Volum" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Silencia" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Volum mร xim" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "Mostra la llista" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Carร tula de la llista" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "Vรฉs a la llista" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:83 msgid "Playlist" msgstr "Llista" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Cerca (o crea una llista nova)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "S'estร  afegint mรบsica a la llista" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "Crea una llista nova i afegeix canรงons" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "General" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Comportament" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "Restaura la cua en iniciar" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Amaga en tancar" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "Simula la lletra paraula per paraula" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "S'aplica quan la lletra estร  disponible lรญnia per lรญnia" #: src/ui/preferences.blp:22 msgid "Auto Download Lyrics" msgstr "Baixa les lletres automร ticament" #: src/ui/preferences.blp:23 msgid "Used when the current instance does not provide lyrics" msgstr "S'utilitza quan la instร ncia actual no proporciona les lletres" #: src/ui/preferences.blp:26 msgid "Use Track and Album Gain" msgstr "Utilitza el guany de la pista i de l'ร lbum" #: src/ui/preferences.blp:27 msgid "Adjust the volume based on gain metadata" msgstr "Ajusteu el volum d'acord amb les metadades de guany" #: src/ui/preferences.blp:30 msgid "Default Page on Login" msgstr "Pร gina per defecte en iniciar la sessiรณ" #: src/ui/preferences.blp:35 msgid "Maximum Bitrate" msgstr "Taxa mร xima de bits" #: src/ui/preferences.blp:41 msgid "Gnome Search" msgstr "Cerca del Gnome" #: src/ui/preferences.blp:42 msgid "" "Search for your music directly from the system search bar while Nocturne is " "running. You can enable or disable this feature completely in System Settings" msgstr "" "Cerca mรบsica directament des de la barra de cerca del sistema mentre el " "Nocturne s'estร  executant. Pots activar o desactivar aquesta funciรณ " "completament als Parร metres del sistema" #: src/ui/preferences.blp:45 msgid "Include Artists" msgstr "Inclou artistes" #: src/ui/preferences.blp:48 msgid "Include Albums" msgstr "Inclou ร lbums" #: src/ui/preferences.blp:51 msgid "Include Songs" msgstr "Inclou canรงons" #: src/ui/preferences.blp:54 msgid "Include Playlists" msgstr "Inclou llistes" #: src/ui/preferences.blp:58 msgid "Session" msgstr "Sessiรณ" #: src/ui/preferences.blp:61 msgid "Discord Rich Presence" msgstr "Estat d'activitat a Discord" #: src/ui/preferences.blp:62 msgid "Share the currently playing song on Discord" msgstr "Compartiu la canรงรณ actual a Discord" #: src/ui/preferences.blp:66 msgid "Share Cover Art With Discord" msgstr "Compartiu la carร tula a Discord" #: src/ui/preferences.blp:67 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "Si els servidors de Discord no poden accedir a la instร ncia, es farร  servir " "CoverArtArchive en el seu lloc" #: src/ui/preferences.blp:73 msgid "ListenBrainz Link" msgstr "Enllaรงa amb ListenBrainz" #: src/ui/preferences.blp:74 msgid "Keep track of your playback history" msgstr "Manteniu un registre de l'historial de reproducciรณ" #: src/ui/preferences.blp:80 src/ui/preferences.blp:81 msgid "Link" msgstr "Enllaรงa" #: src/ui/preferences.blp:89 src/ui/preferences.blp:90 msgid "Unlink" msgstr "Desenllaรงa" #: src/ui/preferences.blp:102 msgid "User" msgstr "Usuari" #: src/ui/preferences.blp:122 msgid "Delete Image Cache" msgstr "Suprimeix la memรฒria cau d'imatges" #: src/ui/preferences.blp:129 msgid "Logout" msgstr "Desconnecta" #: src/ui/preferences.blp:139 msgid "Customization" msgstr "Personalitzaciรณ" #: src/ui/preferences.blp:142 msgid "Interface" msgstr "Interficie" #: src/ui/preferences.blp:144 msgid "Show Context Button in Rows" msgstr "Mostra el botรณ contextual a cada fila" #: src/ui/preferences.blp:145 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" "El menรบ tambรฉ es pot mostrar fent clic amb el botรณ dret o prement de manera " "mantinguda" #: src/ui/preferences.blp:148 msgid "Show Labels in Buttons of Pages" msgstr "Mostra les etiquetes als botons de les pร gines" #: src/ui/preferences.blp:151 msgid "Show Progressbar in Player Footer" msgstr "Mostra la barra de progrรฉs al reproductor" #: src/ui/preferences.blp:154 msgid "Make Background Translucent in Player" msgstr "Fes translรบcid el fons del reproductor" #: src/ui/preferences.blp:155 msgid "Does not affect sidebar player" msgstr "No afecta el reproductor de la barra lateral" #: src/ui/preferences.blp:158 msgid "Use Sidebar Player When Possible" msgstr "Utilitza el reproductor de la barra lateral si รฉs possible" #: src/ui/preferences.blp:159 msgid "Sidebar player is only available when the window is wide enough" msgstr "" "El reproductor de la barra lateral nomรฉs estร  disponible si la finestra รฉs " "prou ampla" #: src/ui/preferences.blp:162 msgid "Filter Singles" msgstr "Filtratge de senzills" #: src/ui/preferences.blp:163 msgid "Hides albums with only one song (does not affect homepage)" msgstr "Amaga els ร lbums amb una sola canรงรณ (no afecta la pร gina d'inici)" #: src/ui/preferences.blp:166 msgid "Button Size" msgstr "Mida del botรณ" #: src/ui/preferences.blp:167 msgid "Affects albums, artists, playlists and songs" msgstr "S'aplica a ร lbums, artistes, llistes i canรงons" #: src/ui/preferences.blp:173 src/ui/preferences.blp:174 msgid "Small" msgstr "Petit" #: src/ui/preferences.blp:178 src/ui/preferences.blp:179 msgid "Big" msgstr "Gran" #: src/ui/preferences.blp:187 msgid "Dynamic Background" msgstr "Fons dinร mic" #: src/ui/preferences.blp:188 msgid "Background generated based on the album art of the current song" msgstr "Fons generat a partir de la carร tula de la canรงรณ actual" #: src/ui/preferences.blp:191 msgid "Use in Main Window" msgstr "Utilitza'l a la finestra principal" #: src/ui/preferences.blp:197 src/ui/preferences.blp:198 #: src/ui/preferences.blp:221 src/ui/preferences.blp:222 #: src/ui/preferences.blp:245 src/ui/preferences.blp:246 msgid "Off" msgstr "Apagat" #: src/ui/preferences.blp:202 src/ui/preferences.blp:203 #: src/ui/preferences.blp:226 src/ui/preferences.blp:227 #: src/ui/preferences.blp:250 src/ui/preferences.blp:251 msgid "Gradient" msgstr "Degradat" #: src/ui/preferences.blp:207 src/ui/preferences.blp:208 #: src/ui/preferences.blp:231 src/ui/preferences.blp:232 #: src/ui/preferences.blp:255 src/ui/preferences.blp:256 msgid "Blur" msgstr "Difuminat" #: src/ui/preferences.blp:215 msgid "Use in Player" msgstr "Utilitza'l al reproductor" #: src/ui/preferences.blp:239 msgid "Use in Pop-out Window" msgstr "Utilitza'l a la finestra emergent" #: src/ui/preferences.blp:263 msgid "Use as Accent Color" msgstr "Utilitza'l com a color de realรงat" #: src/ui/preferences.blp:268 msgid "Homepage" msgstr "Pร gina d'inici" #: src/ui/preferences.blp:269 msgid "Reload homepage to see changes" msgstr "Restaureu la pร gina d'inici per veure els canvis" #: src/ui/preferences.blp:305 msgid "Sidebar" msgstr "Barra lateral" #: src/ui/preferences.blp:309 msgid "Visualizer" msgstr "Visualitzador" #: src/ui/preferences.blp:313 src/ui/window.blp:260 msgid "Preferences" msgstr "Parร metres" #: src/ui/preferences.blp:315 msgid "Show Visualizer in Player" msgstr "Mostra el visualitzador al reproductor" #: src/ui/preferences.blp:316 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" "Mostra un visualitzador de l'espectre d'ร udio sobre la carร tula de l'ร lbum " "del reproductor" #: src/ui/preferences.blp:321 msgid "Appearance" msgstr "Aparenรงa" #: src/ui/preferences.blp:325 msgid "Number of Bars" msgstr "Nรบmero de barres" #: src/ui/preferences.blp:336 msgid "Visualizer Type" msgstr "Tipus de visualitzador" #: src/ui/preferences.blp:342 src/ui/preferences.blp:343 msgid "Wave" msgstr "Ones" #: src/ui/preferences.blp:347 src/ui/preferences.blp:348 msgid "Bars" msgstr "Barres" #: src/ui/preferences.blp:352 src/ui/preferences.blp:353 msgid "Particles" msgstr "Partรญcules" #: src/ui/preferences.blp:360 msgid "Fill Mode" msgstr "Mode d'emplenament" #: src/ui/preferences.blp:366 src/ui/preferences.blp:367 msgid "Solid" msgstr "Sรฒlid" #: src/ui/preferences.blp:371 src/ui/preferences.blp:372 msgid "Translucent" msgstr "Translรบcid" #: src/ui/preferences.blp:376 src/ui/preferences.blp:377 msgid "Border" msgstr "Contorn" #: src/ui/preferences.blp:385 msgid "Color" msgstr "Color" #: src/ui/preferences.blp:389 msgid "Use Dynamic Color" msgstr "Utilitza un color dinร mic" #: src/ui/preferences.blp:392 msgid "Invert Color" msgstr "Inverteix el color" #: src/ui/preferences.blp:396 msgid "Manual Color" msgstr "Color manual" #: src/ui/preferences.blp:402 msgid "Visualizer Color" msgstr "Color del visualitzador" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Shortcuts" msgstr "Mostra les dreceres" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Preferences" msgstr "Mostra els parร metres" #: src/ui/shortcuts-dialog.blp:20 msgid "Quit" msgstr "Surt" #: src/ui/shortcuts-dialog.blp:27 msgid "Toggle Playback" msgstr "Commuta la reproducciรณ" #: src/ui/shortcuts-dialog.blp:31 msgid "Next Song" msgstr "Canรงรณ segรผent" #: src/ui/shortcuts-dialog.blp:35 msgid "Previous Song" msgstr "Canรงรณ anterior" #: src/ui/shortcuts-dialog.blp:39 msgid "Raise Volume" msgstr "Apuja el volum" #: src/ui/shortcuts-dialog.blp:43 msgid "Lower Volume" msgstr "Abaixa el volum" #: src/ui/shortcuts-dialog.blp:48 msgid "Window" msgstr "Finestra" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:270 #: src/widgets/playing/popout_window.py:48 msgid "Toggle Fullscreen" msgstr "Canvia a pantalla completa" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:265 msgid "Open Pop-Out Window" msgstr "Obre una finestra emergent" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Carร tula de la canรงรณ" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "Indicador de reproducciรณ" #: src/ui/song/queue.blp:47 src/ui/song/queue.blp:49 msgid "Select All" msgstr "Selecciona-ho tot" #: src/ui/song/row.blp:44 msgid "Drag icon" msgstr "Arrossega una icona" #: src/ui/song/row.blp:142 msgid "Selection box" msgstr "Casella de selecciรณ" #: src/ui/window.blp:94 msgid "Play Random Queue" msgstr "Reprodueix una cua aleatรฒria" #: src/ui/window.blp:102 msgid "Menu" msgstr "Menรบ" #: src/ui/window.blp:130 msgid "Random Albums" msgstr "ร€lbums aleatoris" #: src/ui/window.blp:134 msgid "Favorite Albums" msgstr "ร€lbums preferits" #: src/ui/window.blp:138 msgid "Newest Albums" msgstr "ร€lbums nous" #: src/ui/window.blp:142 msgid "Recent Albums" msgstr "ร€lbums recents" #: src/ui/window.blp:146 msgid "Frequent Albums" msgstr "ร€lbums freqรผents" #: src/ui/window.blp:169 msgid "Player" msgstr "Reproductor" #: src/ui/window.blp:275 msgid "See Last Nocturne Playback" msgstr "Mostra la darrera reproducciรณ del Nocturne" #: src/ui/window.blp:280 msgid "Keyboard Shortcuts" msgstr "Dreceres de teclat" #: src/ui/window.blp:285 msgid "About Nocturne" msgstr "Quant al Nocturne" #: src/widgets/album/button.py:99 src/widgets/album/page.py:149 #: src/widgets/artist/page.py:114 src/widgets/playing/control_page.py:196 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "No preferit" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "Disc {}" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} ร€lbum" msgstr[1] "{} ร€lbums" #: src/widgets/artist/page.py:97 msgid "Author" msgstr "Autor" #: src/widgets/containers/context.py:50 #, python-brace-format msgid "{} Stars" msgstr "{} estrelles" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Sense marca de temps" #: src/widgets/lyrics/dialog.py:195 msgid "Lyrics Saved" msgstr "S'ha desat la lletra" #: src/widgets/pages/login.py:52 src/widgets/pages/login.py:131 msgid "Running" msgstr "En funcionament" #: src/widgets/pages/login.py:85 msgid "Extra Menu" msgstr "Menรบ extra" #: src/widgets/pages/login.py:120 src/widgets/pages/setup.py:83 msgid "Music Library" msgstr "Biblioteca de mรบsica" #: src/widgets/pages/login.py:130 msgid "Restarted" msgstr "S'ha reiniciat" #: src/widgets/pages/login.py:172 msgid "Quick Connect" msgstr "Connexiรณ rร pida" #: src/widgets/pages/login.py:173 msgid "Error getting code" msgstr "S'ha produรฏt un error en obtenir el codi" #: src/widgets/pages/login.py:175 msgid "Quick Connect Page" msgstr "Pร gina de connexiรณ rร pida" #: src/widgets/pages/playback.py:29 #, python-brace-format msgid "Nocturne Playback ~ {}" msgstr "Reproducciรณ del Nocturne ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 #, python-brace-format msgid "{} Plays" msgstr "{} reproduccions" #: src/widgets/pages/setup.py:70 msgid "Installing" msgstr "S'estร  instalยทlant" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integraciรณ" #: src/widgets/playing/lyrics_page.py:220 msgid "LRC File" msgstr "Fitxer LRC" #: src/widgets/playing/player.py:625 msgid "Warning: Song changed but volume is set to 0" msgstr "Avรญs: la canรงรณ ha canviat perรฒ el volum estร  al 0" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:115 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "{} canรงรณ" msgstr[1] "{} canรงons" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Llista nova" #: src/widgets/playlist/selector_row.py:33 #, python-brace-format msgid "Add Songs to '{}'" msgstr "Afegeix canรงons a '{}'" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "Sรญ" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "No" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "No s'ha trobat" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "Diversos artistes" #: src/window.py:214 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "La cua de reproducciรณ {} ja es troba disponible!" #: src/window.py:216 msgid "Show" msgstr "Mostra" #: src/window.py:217 msgid "Later" msgstr "Mรฉs tard" #~ msgid "A modern Navidrome / Jellyfin client" #~ msgstr "Un client modern de Navidrome i Jellyfin" #~ msgid "Compatibility with Jellyfin, OpenSubsonic and local files" #~ msgstr "Compatible amb Jellyfin, OpenSubsonic i fitxers locals" #~ msgid "Use an existing OpenSubsonic / Navidrome instance" #~ msgstr "Utilitzeu una instร ncia ja existent de Navidrome / Subsonic" #~ msgid "Show Pan Buttons on Carousels" #~ msgstr "Mostra els botons de desplaรงament als carrusels" #~ msgid "Only visible when a carousel has 5 or more items" #~ msgstr "Nomรฉs sรณn visibles si el carrusel tรฉ 5 elements o mรฉs" #~ msgid "Experimental" #~ msgstr "Experimental" #~ msgid "Local Music Library" #~ msgstr "Biblioteca de mรบsica local" #~ msgid "Show Sidebar" #~ msgstr "Mostra la barra lateral" #~ msgid "Generating next queue" #~ msgstr "S'estร  generant la cua segรผent" #~ msgid "Create new playlist and add songs" #~ msgstr "Crea una llista nova i afegeix canรงons" #~ msgid "Show Playlists in Sidebar" #~ msgstr "Mostra les llistes a la barra lateral" #~ msgid "Shortcuts" #~ msgstr "Dreceres" #, python-brace-format #~ msgid "Add songs to '{}'" #~ msgstr "Afegeix canรงons a '{}'" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "S'han suprimit les canรงons {}" #~ msgid "Song Removed" #~ msgstr "S'ha eliminat la canรงรณ" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "Canรงons afegides {}" #~ msgid "1 Song Added" #~ msgstr "S'ha afegit una canรงรณ" #~ msgid "1 Song Skipped" #~ msgstr "S'ha saltat una canรงรณ" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "S'han saltat les canรงons {}" #, python-brace-format #~ msgid "{} Songs ({})" #~ msgstr "{} canรงons ({})" #, python-brace-format #~ msgid "1 Song ({})" #~ msgstr "1 canรงรณ ({})" #, python-brace-format #~ msgid "Download Started ({} Songs Skipped)" #~ msgstr "S'ha iniciat la descร rrega (s'ha omรจs {} canรงons)" #~ msgid "Download Started (1 Song Skipped)" #~ msgstr "S'ha iniciat la descร rrega (s'ha omรจs 1 canรงรณ)" #~ msgid "Open Pop-out Window" #~ msgstr "Obre la finestra emergent" #~ msgid "Close Pop-out Window" #~ msgstr "Tanca la finestra emergent" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "Lletres proporcionades per LRCLIB" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "Difumina el fons al reproductor (experimental)" #~ msgid "Gradient generated based on the album art of the current song" #~ msgstr "Degradat generat a partir de la carร tula de la canรงรณ actual" #~ msgid "Player in Pop-out Window" #~ msgstr "Reproductor en una finestra emergent" #~ msgid "1 Album" #~ msgstr "1 ร lbum" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} ร lbums" #~ msgid "1 Song" #~ msgstr "1 canรงรณ" Jeffser-Nocturne-b442fee/po/de.po000066400000000000000000001153451523240030000167320ustar00rootroot00000000000000# German translations for Nocturne. # Copyright (C) 2026 Jeffser # This file is distributed under the same license as the Nocturne package. # Martin Prokoph , 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-05 18:30-0600\n" "PO-Revision-Date: 2026-04-02 14:11+0100\n" "Last-Translator: Martin Prokoph \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Fรผhre deine Musikbibliothek zusammen" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "Ein moderner Client fรผr Navidrome / Jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Funktionen" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "Erkunden von Titeln, Artists, Alben, Radiosendern und Playlists" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "Playlist-Verwaltung" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Mpris-Integration" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Eingebaute Navidrome Instanzverwaltung" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Automatische Liedtext-Abfrage" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Coole Oberflรคche" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Startseite" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Warteschlange" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:170 #: src/ui/window.blp:222 msgid "Lyrics" msgstr "Liedtexte" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Titelliste" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Albenรผbersicht" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "music" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:211 src/ui/song/row.blp:69 msgid "External File" msgstr "Externe Datei" #: src/actions.py:212 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Dieser Titel wurde aus einer externen Datei geladen, weshalb weniger " "Features verglichen mit einem Titel aus der Bibliothek verfรผgbar sind." #: src/actions.py:214 src/ui/song/queue.blp:40 msgid "Close" msgstr "SchlieรŸen" #: src/actions.py:232 msgid "Navidrome server deleted successfully" msgstr "Navidrome-Server erfolgreich gelรถscht" #: src/actions.py:241 msgid "Delete Navidrome Server" msgstr "Navidrome-Server lรถschen" #: src/actions.py:242 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "" "Bist du sicher, dass du den eingebauten Navidrome-Server lรถschen mรถchtest?" #: src/actions.py:244 src/actions.py:355 src/actions.py:389 src/actions.py:674 #: src/actions.py:790 src/preferences.py:322 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "Abbrechen" #: src/actions.py:245 msgid "Keep Data" msgstr "Daten behalten" #: src/actions.py:246 msgid "Delete Everything" msgstr "Alles lรถschen" #: src/actions.py:301 msgid "No Name" msgstr "" #: src/actions.py:320 msgid "Radio updated successfully" msgstr "Radiostation erfolgreich aktualisiert" #: src/actions.py:320 msgid "Radio added successfully" msgstr "Radiostation erfolgreich hinzugefรผgt" #: src/actions.py:331 msgid "Error updating radio" msgstr "Fehler beim Aktualisieren der Radiostation" #: src/actions.py:331 msgid "Error adding radio" msgstr "Fehler beim Hinzufรผgen der Radiostation" #: src/actions.py:340 src/actions.py:666 msgid "Name" msgstr "Name" #: src/actions.py:345 msgid "Stream Url" msgstr "Stream-URL" #: src/actions.py:352 msgid "Update Radio Station" msgstr "Radiostation aktualisieren" #: src/actions.py:352 msgid "Add Radio Station" msgstr "Radiostation hinzufรผgen" #: src/actions.py:356 src/preferences.py:323 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Speichern" #: src/actions.py:372 msgid "Radio deleted successfully" msgstr "Radiostation erfolgreich gelรถscht" #: src/actions.py:380 msgid "Error deleting radio" msgstr "Fehler beim Lรถschen der Radiostation" #: src/actions.py:386 msgid "Delete Radio Station" msgstr "Radiostation lรถschen" #: src/actions.py:387 src/actions.py:788 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Bist du sicher, dass du '{}' lรถschen mรถchtest?" #: src/actions.py:390 src/actions.py:791 src/constants.py:343 #: src/constants.py:396 msgid "Delete" msgstr "Lรถschen" #: src/actions.py:420 src/actions.py:447 src/actions.py:452 src/actions.py:541 #: src/actions.py:600 msgid "Playing Next" msgstr "Wird als nรคchstes wiedergegeben" #: src/actions.py:430 src/actions.py:463 src/actions.py:468 src/actions.py:556 #: src/actions.py:615 msgid "Playing Later" msgstr "Wird spรคter wiedergegeben" #: src/actions.py:447 src/actions.py:463 src/actions.py:905 src/actions.py:1040 #: src/widgets/playlist/button.py:50 src/widgets/playlist/button.py:52 #: src/widgets/playlist/dialog.py:31 src/widgets/playlist/page.py:118 #: src/widgets/playlist/page.py:120 src/widgets/playlist/row.py:49 #: src/widgets/playlist/row.py:51 src/widgets/playlist/selector_row.py:38 #: src/widgets/playlist/selector_row.py:40 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" msgstr[1] "" #: src/actions.py:497 msgid "Lyrics Saved" msgstr "Liedtext gespeichert" #: src/actions.py:647 msgid "Playlist updated successfully" msgstr "Playlist erfolgreich aktualisiert" #: src/actions.py:647 msgid "Playlist created successfully" msgstr "Playlist erfolgreich erstellt" #: src/actions.py:657 msgid "Error updating playlist" msgstr "Fehler beim Aktualisieren der Playlist" #: src/actions.py:657 msgid "Error creating playlist" msgstr "Fehler beim Erstellen der Playlist" #: src/actions.py:671 msgid "Update Playlist" msgstr "Playlist aktualisieren" #: src/actions.py:671 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Playlist erstellen" #: src/actions.py:675 msgid "Update" msgstr "Aktualisieren" #: src/actions.py:675 msgid "Create" msgstr "Erstellen" #: src/actions.py:695 src/actions.py:700 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:732 src/actions.py:734 src/actions.py:754 src/actions.py:756 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:761 src/actions.py:877 src/actions.py:910 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: src/actions.py:775 msgid "Playlist Deleted" msgstr "Playlist gelรถscht" #: src/actions.py:787 msgid "Delete Playlist" msgstr "Playlist lรถschen" #: src/actions.py:836 msgid "No songs found" msgstr "Keine Titel gefunden" #: src/actions.py:879 src/actions.py:906 src/actions.py:943 src/actions.py:979 msgid "Download Started" msgstr "" #: src/actions.py:886 src/actions.py:917 src/actions.py:924 src/actions.py:959 #: src/actions.py:995 msgid "Already Downloaded" msgstr "" #: src/actions.py:911 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:923 msgid "Download Skipped" msgstr "" #: src/actions.py:947 src/actions.py:983 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: src/actions.py:1022 src/actions.py:1041 msgid "Deleted" msgstr "" #: src/constants.py:159 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Hintereinander" #: src/constants.py:163 msgid "Repeat All" msgstr "Alle wiederholen" #: src/constants.py:167 msgid "Repeat One" msgstr "Einen Titel wiederholen" #: src/constants.py:172 #, python-brace-format msgid "Low ({})" msgstr "" #: src/constants.py:173 #, python-brace-format msgid "Medium ({})" msgstr "" #: src/constants.py:174 #, python-brace-format msgid "High ({})" msgstr "" #: src/constants.py:175 #, python-brace-format msgid "Ultra ({})" msgstr "" #: src/constants.py:176 msgid "Original File" msgstr "" #. Item #: src/constants.py:183 src/ui/pages/home.blp:5 msgid "Home" msgstr "Startseite" #. Item #: src/constants.py:188 src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:223 src/widgets/pages/home.py:62 msgid "Artists" msgstr "Kรผnstler" #. Item #. Section #: src/constants.py:193 src/constants.py:255 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:231 #: src/widgets/pages/home.py:74 msgid "Playlists" msgstr "Playlists" #. Section #: src/constants.py:200 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:215 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:50 msgid "Albums" msgstr "Alben" #. Item #: src/constants.py:203 src/constants.py:238 src/window.py:135 msgid "All" msgstr "Alle" #. Item #: src/constants.py:208 msgid "Random" msgstr "Zufรคllig" #. Item #: src/constants.py:213 src/constants.py:243 msgid "Favorites" msgstr "" #. Item #: src/constants.py:218 msgid "Recently Added" msgstr "Zuletzt hinzugefรผgt" #. Item #: src/constants.py:223 msgid "Recently Played" msgstr "Zuletzt gespielt" #. Item #: src/constants.py:228 msgid "Most Played" msgstr "Meistgespielt" #. Section #: src/constants.py:235 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:207 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "Lieder" #. Item #: src/constants.py:248 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Radios" #: src/constants.py:262 src/constants.py:313 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "Abspielen" #: src/constants.py:267 src/constants.py:300 src/constants.py:318 msgid "Shuffle" msgstr "Zufallswiedergabe" #: src/constants.py:272 src/constants.py:323 src/constants.py:356 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "Als nรคchstes wiedergeben" #: src/constants.py:277 src/constants.py:328 src/constants.py:361 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "Spรคter wiedergeben" #: src/constants.py:282 src/constants.py:376 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "Zu Playlist hinzufรผgen" #: src/constants.py:287 src/constants.py:333 src/constants.py:381 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "Herunterladen" #: src/constants.py:292 src/constants.py:391 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "Kรผnstler anzeigen" #: src/constants.py:305 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:108 msgid "Radio" msgstr "Radio" #: src/constants.py:338 src/constants.py:366 msgid "Edit" msgstr "Bearbeiten" #: src/constants.py:352 msgid "Select" msgstr "Auswรคhlen" #: src/constants.py:371 msgid "Edit Lyrics" msgstr "Liedtext bearbeiten" #: src/constants.py:386 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "Album anzeigen" #: src/constants.py:402 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Entfernen" #: src/constants.py:407 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "" #: src/constants.py:416 msgid "Visit Webpage" msgstr "" #: src/constants.py:422 msgid "Update Server" msgstr "Server aktualisieren" #: src/constants.py:427 msgid "Delete Server" msgstr "Server lรถschen" #: src/integrations/jellyfin.py:15 msgid "Connect to a Jellyfin server." msgstr "" #: src/integrations/jellyfin.py:19 msgid "Jellyfin" msgstr "" #: src/integrations/jellyfin.py:20 msgid "Use an existing Jellyfin instance" msgstr "Eine bestehende Jellyfin-Instanz nutzen" #: src/integrations/local.py:17 src/integrations/local.py:23 #: src/integrations/local.py:509 msgid "Local Files" msgstr "Lokale Dateien" #: src/integrations/local.py:18 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" #: src/integrations/local.py:20 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Weiter" #: src/integrations/local.py:24 msgid "Limited functionality" msgstr "Beschrรคnkte Funktionalitรคt" #: src/integrations/local.py:36 msgid "Loading Songs" msgstr "" #: src/integrations/local.py:215 msgid "No Album" msgstr "" #: src/integrations/local.py:525 src/integrations/local.py:534 msgid "Offline Mode" msgstr "" #: src/integrations/local.py:526 msgid "Access your downloads" msgstr "" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "" #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "Externer Server" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "" #: src/integrations/navidrome.py:550 src/integrations/navidrome.py:559 msgid "Managed Server" msgstr "Verwalteter Server" #: src/integrations/navidrome.py:551 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" #: src/integrations/navidrome.py:554 msgid "Manage Server" msgstr "Server verwalten" #: src/integrations/navidrome.py:560 msgid "Create and use a Navidrome instance" msgstr "" #: src/main.py:69 msgid "Music is Playing" msgstr "" #: src/main.py:116 msgid "Login Failed" msgstr "Anmeldung fehlgeschlagen" #: src/preferences.py:311 msgid "Settings Page" msgstr "" #: src/preferences.py:314 msgid "User Token" msgstr "" #: src/preferences.py:318 msgid "Link ListenBrainz" msgstr "" #: src/preferences.py:319 msgid "Connect your ListenBrainz account with a user token" msgstr "" #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "Album abspielen" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "Favorit an/aus" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Album-Coverbild" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "Albumseite anzeigen" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "Kรผnstlerseite anzeigen" #: src/ui/album/page.blp:5 src/widgets/album/page.py:131 msgid "Album" msgstr "Album" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "Optionen" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Kรผnstler" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Biografie erweitern" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Liste" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Liedtext-Editor" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "Pausieren" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "Fortschrittsleiste des aktuellen Titels" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Setze diesen Zeitpunkt als Zeitstempel der nรคchsten Zeile" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Zeile bei Zeitstempel hinzufรผgen" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Kein Liedtext" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "" "Fรผr diesen Titel wurde kein Liedtext gefunden โ€“ um anzufangen, fรผge eine " "neue Zeile hinzu" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Zeitstempel" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Gehe zu Zeitstempel" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Setze aktuellen Zeitstempel" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Zeile entfernen" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Raster" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Alben durchsuchen" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Ende der Liste erreicht" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "Keine Alben gefunden" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Kรผnstler durchsuchen" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "Keine Kรผnstler gefunden" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "Keine Elemente gefunden" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "Anmelden" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Navidrome-Server verwalten" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "Mehr Optionen" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Server-Zertifikaten vertrauen" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Bibliotheksverzeichnis" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Nutzername" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Passwort" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Playlists durchsuchen" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "Keine Playlists gefunden" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Radiosender durchsuchen" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Radiostation hinzufรผgen" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "Keine Radiostationen gefunden" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Einrichtung" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Herunterladen von Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne wird Navidrome automatisch herunterladen" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Herunterladen (~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "Wird heruntergeladen" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Ein Fehler ist aufgetreten" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Versuche es spรคter nochmal" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "Erfolgreich heruntergeladen" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Erstelle ein Admin-Konto bevor du weitermachst und stelle zudem sicher,dass " "sich alle Titel in deinem Musikverzeichnis befinden" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Webseite der Instanz" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Titel durchsuchen" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "Keine Titel gefunden" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Willkommen" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Willkommen in Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Wiedergabe" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "Titelbild" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "Website des aktuellen Radiosenders besuchen" #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "Vorheriger Titel" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "Nรคchster Titel" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "Favorisieren" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "Seitenleiste anzeigen" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Fortschrittsleiste des aktuellen Titels" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "Titelbild des aktuellen Liedes" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "Zurรผckgehen" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "Kein Liedtext gefunden" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "Nicht heruntergeladen" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "Der Liedtext fรผr diesen Titel wurde noch nicht heruntergeladen, er kรถnnte " "zum Herunterladen verfรผgbar sein" #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "Liedtextdatei laden" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Pop-out-Fenster" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:154 msgid "Player" msgstr "Wiedergabesteuerung" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:16 #: src/ui/window.blp:255 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "Vollbild umschalten" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:194 msgid "Song Details" msgstr "Titel-Details" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:160 #: src/ui/window.blp:216 msgid "Queue" msgstr "Warteschlange" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:176 #: src/ui/window.blp:228 msgid "Equalizer" msgstr "" #: src/ui/playing/queue_page.blp:20 msgid "Generating next queue" msgstr "Nรคchste Warteschlange wird generiert" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Automatische Wiedergabe" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "Erstelle eine neue Warteschlange sobald die aktuelle zuende ist" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Lautstรคrke" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Lautstรคrke stumm" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Volle Lautstรคrke" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "Playlist wiedergeben" #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "Playlist anzeigen" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Titelbild der Playlist" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "Playlist-Seite anzeigen" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "Playlist" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Suche (oder erstelle eine neue Playlist)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Musik wird zu Playlist hinzugefรผgt" #: src/ui/playlist/dialog.blp:34 msgid "Create new playlist and add songs" msgstr "Neue Playlist erstellen und Titel hinzufรผgen" #: src/ui/preferences.blp:7 msgid "General" msgstr "Allgemein" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Verhalten" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Beim SchlieรŸen verstecken" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "" #: src/ui/preferences.blp:33 msgid "Session" msgstr "Sitzung" #: src/ui/preferences.blp:36 msgid "ListenBrainz Link" msgstr "" #: src/ui/preferences.blp:37 msgid "Keep track of your playback history" msgstr "" #: src/ui/preferences.blp:43 src/ui/preferences.blp:44 msgid "Link" msgstr "" #: src/ui/preferences.blp:52 src/ui/preferences.blp:53 msgid "Unlink" msgstr "" #: src/ui/preferences.blp:65 msgid "User" msgstr "Benutzer" #: src/ui/preferences.blp:85 msgid "Logout" msgstr "Abmelden" #: src/ui/preferences.blp:95 msgid "Customization" msgstr "" #: src/ui/preferences.blp:98 msgid "Interface" msgstr "Oberflรคche" #: src/ui/preferences.blp:100 msgid "Show Context Button in Rows" msgstr "Kontext-Knopf in Reihen anzeigen" #: src/ui/preferences.blp:101 msgid "The menu can also be shown by right clicking / long pressing" msgstr "Das Menรผ kann auch mit Rechtsklick oder langem Drรผcken geรถffnet werden" #: src/ui/preferences.blp:104 msgid "Show Labels in Buttons of Pages" msgstr "" #: src/ui/preferences.blp:107 msgid "Show Playlists in Sidebar" msgstr "" #: src/ui/preferences.blp:110 msgid "Show Progressbar in Player Footer" msgstr "" #: src/ui/preferences.blp:113 msgid "Make Background Translucent in Player" msgstr "" #: src/ui/preferences.blp:116 msgid "Use Sidebar Player When Possible" msgstr "" #: src/ui/preferences.blp:117 msgid "Sidebar player is only available when the window is wide enough" msgstr "" #: src/ui/preferences.blp:122 msgid "Dynamic Background" msgstr "" #: src/ui/preferences.blp:123 msgid "Background generated based on the album art of the current song" msgstr "" #: src/ui/preferences.blp:126 msgid "Use in Main Window" msgstr "" #: src/ui/preferences.blp:127 msgid "Experimental" msgstr "" #: src/ui/preferences.blp:133 src/ui/preferences.blp:134 #: src/ui/preferences.blp:157 src/ui/preferences.blp:158 #: src/ui/preferences.blp:181 src/ui/preferences.blp:182 msgid "Off" msgstr "" #: src/ui/preferences.blp:138 src/ui/preferences.blp:139 #: src/ui/preferences.blp:162 src/ui/preferences.blp:163 #: src/ui/preferences.blp:186 src/ui/preferences.blp:187 msgid "Gradient" msgstr "" #: src/ui/preferences.blp:143 src/ui/preferences.blp:144 #: src/ui/preferences.blp:167 src/ui/preferences.blp:168 #: src/ui/preferences.blp:191 src/ui/preferences.blp:192 msgid "Blur" msgstr "" #: src/ui/preferences.blp:151 msgid "Use in Player" msgstr "" #: src/ui/preferences.blp:175 msgid "Use in Pop-out Window" msgstr "" #: src/ui/preferences.blp:199 msgid "Use as Accent Color" msgstr "" #: src/ui/preferences.blp:204 msgid "Homepage" msgstr "Startseite" #: src/ui/preferences.blp:205 msgid "Reload homepage to see changes" msgstr "Lade die Startseite neu, um ร„nderungen zu sehen" #: src/ui/preferences.blp:241 msgid "Visualizer" msgstr "" #: src/ui/preferences.blp:245 src/ui/window.blp:245 msgid "Preferences" msgstr "Einstellungen" #: src/ui/preferences.blp:247 msgid "Show Visualizer in Player" msgstr "" #: src/ui/preferences.blp:248 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" #: src/ui/preferences.blp:253 msgid "Appearance" msgstr "" #: src/ui/preferences.blp:257 msgid "Number of Bars" msgstr "" #: src/ui/preferences.blp:268 msgid "Visualizer Type" msgstr "" #: src/ui/preferences.blp:274 src/ui/preferences.blp:275 msgid "Wave" msgstr "" #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Bars" msgstr "" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Particles" msgstr "" #: src/ui/preferences.blp:292 msgid "Fill Mode" msgstr "" #: src/ui/preferences.blp:298 src/ui/preferences.blp:299 msgid "Solid" msgstr "" #: src/ui/preferences.blp:303 src/ui/preferences.blp:304 msgid "Translucent" msgstr "" #: src/ui/preferences.blp:308 src/ui/preferences.blp:309 msgid "Border" msgstr "" #: src/ui/preferences.blp:317 msgid "Color" msgstr "" #: src/ui/preferences.blp:321 msgid "Use Dynamic Color" msgstr "" #: src/ui/preferences.blp:324 msgid "Invert Color" msgstr "" #: src/ui/preferences.blp:328 msgid "Manual Color" msgstr "" #: src/ui/preferences.blp:334 msgid "Visualizer Color" msgstr "" #: src/ui/shortcuts-dialog.blp:6 msgid "Shortcuts" msgstr "Tastenkombinationen" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "Tastenkombinationen anzeigen" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "Einstellungen anzeigen" #: src/ui/shortcuts-dialog.blp:20 src/ui/window.blp:250 msgid "Open Pop-Out Window" msgstr "" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "Beenden" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "Icon ziehen" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "Auswahlbox" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Titelbild des Liedes" #: src/ui/window.blp:74 msgid "Play Random Queue" msgstr "Zufรคllige Warteschlange abspielen" #: src/ui/window.blp:87 msgid "Menu" msgstr "Menรผ" #: src/ui/window.blp:115 msgid "Random Albums" msgstr "" #: src/ui/window.blp:119 msgid "Favorite Albums" msgstr "" #: src/ui/window.blp:123 msgid "Newest Albums" msgstr "" #: src/ui/window.blp:127 msgid "Recent Albums" msgstr "" #: src/ui/window.blp:131 msgid "Frequent Albums" msgstr "" #: src/ui/window.blp:260 msgid "Keyboard Shortcuts" msgstr "Tastenkombinationen" #: src/ui/window.blp:265 msgid "About Nocturne" msgstr "รœber Nocturne" #: src/widgets/album/button.py:63 src/widgets/album/page.py:147 #: src/widgets/artist/page.py:127 src/widgets/playing/control_page.py:217 #: src/widgets/song/row.py:162 msgid "Favorite" msgstr "" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:222 #: src/widgets/song/row.py:167 msgid "Not Favorite" msgstr "" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "" #: src/widgets/artist/button.py:43 src/widgets/artist/row.py:48 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "" msgstr[1] "" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "Verwandte Kรผnstler" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "Autor" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Kein Zeitstempel" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "Extra-Menรผ" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Lokale Musikbibliothek laden" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "" #: src/widgets/pages/setup.py:62 msgid "Installing" msgstr "Wird installiert" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integration" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC-Datei" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Neue Playlist" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add songs to '{}'" msgstr "Fรผge Titel in '{}' hinzu" #: src/widgets/song/row.py:146 src/widgets/song/row.py:151 msgid "Multiple Artists" msgstr "Mehrere Kรผnstler" #, python-brace-format #~ msgid "{} Songs" #~ msgstr "{} Titel" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "{} Lieder entfernt" #~ msgid "Song Removed" #~ msgstr "Lied entfernt" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "{} Lieder hinzugefรผgt" #~ msgid "1 Song Added" #~ msgstr "1 Lied hinzugefรผgt" #~ msgid "1 Song Skipped" #~ msgstr "1 Lied รผbersprungen" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "{} Lieder รผbersprungen" #~ msgid "Starred" #~ msgstr "Favoriten" #~ msgid "Jellyfin (Experimental)" #~ msgstr "Jellyfin (Experimentell)" #~ msgid "Navidrome" #~ msgstr "Navidrome" #~ msgid "Use an existing Navidrome / Subsonic instance" #~ msgstr "Benutze eine bestehende Navidrome-/Subsonic-Instanz" #~ msgid "Navidrome Managed" #~ msgstr "Navidrome (Verwaltet)" #~ msgid "Instance Website" #~ msgstr "Instanz-Website" #~ msgid "Create and use Navidrome instance" #~ msgstr "Erstelle und nutze eine Navidrome-Instanz" #~ msgid "Go to Welcome Page" #~ msgstr "Gehe zur Wilkommensseite" #~ msgid "" #~ "Nocturne is powered by a Subsonic compatible instance, if you do not have " #~ "one Nocturne can automatically set up a Navidrome instance for you!" #~ msgstr "" #~ "Nocturne wird von einer mit Subsonic kompatiblen Instanz angetrieben, " #~ "falls du keinehast, kann Nocturne automatisch eine Navidrome-Instanz fรผr " #~ "dich anlegen!" #~ msgid "Open Pop-out Window" #~ msgstr "Pop-out-Fenster รถffnen" #~ msgid "Close Pop-out Window" #~ msgstr "Pop-out-Fenster schlieรŸen" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "Liedtexte werden von LRCLIB bereitgestellt" #~ msgid "Use Dynamic Background in Player" #~ msgstr "Dynamischen Hintergrund in der Wiedergabesteuerung nutzen" #~ msgid "Generate a gradient background based on the current song cover art" #~ msgstr "" #~ "Generiere einen Hintergrundfarbverlauf basierend auf dem aktuellen " #~ "Titelbild" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "Verschwommener Hintergrund der Wiedergabesteuerung (Experimentell)" #~ msgid "Make background of player slightly transparent and blur the backdrop" #~ msgstr "" #~ "Macht den Hintergrund leicht transparent und stellt darunterliegende " #~ "Elemente verschwommen dar" #~ msgid "Restore Session" #~ msgstr "Sitzung wiederherstellen" #~ msgid "Retrieve previous queue" #~ msgstr "Vorherige Warteschlange wiederherstellen" #~ msgid "Hide Nocturne when closing" #~ msgstr "" #~ "Verstecke Nocturne in der Systemleiste, sobald das Programm geschlossen " #~ "wird" #~ msgid "Sections" #~ msgstr "Abschnitte" #~ msgid "Max amount of songs to be shown" #~ msgstr "Maximale Anzahl angezeigter Titel" #~ msgid "Max amount of albums to be shown" #~ msgstr "Maximale Anzahl angezeigter Alben" #~ msgid "Max amount of artists to be shown" #~ msgstr "Maximale Anzahl angezeigter Kรผnstler" #~ msgid "Max amount of playlists to be shown" #~ msgstr "Maximale Anzahl angezeigter Playlists" #~ msgid "Player in Pop-out Window" #~ msgstr "Wiedergabesteuerung in Pop-out-Fenster" #~ msgid "1 Album" #~ msgstr "1 Album" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} Alben" #~ msgid "1 Song" #~ msgstr "1 Titel" Jeffser-Nocturne-b442fee/po/es.po000066400000000000000000001643451523240030000167550ustar00rootroot00000000000000# Spanish translations for Nocturne package. # Copyright (C) 2026 Jeffser # This file is distributed under the same license as the Nocturne package. # Jeffry Samuel , 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-29 07:28-0600\n" "PO-Revision-Date: 2026-03-25 15:34-0600\n" "Last-Translator: Jeffry Samuel \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Reรบne tu biblioteca musical" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern music client" msgstr "Un cliente de mรบsica moderno" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Caracterรญsticas" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files" msgstr "Compatibilidad con Jellyfin, OpenSubsonic, Bandcamp y archivos locales" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "Exploraciรณn por canciones, artistas, รกlbumes, radios y playlists." #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Playlist management" msgstr "Gestiรณn de Playlists" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Ecualizador de audio y visualizador de audio" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Integraciรณn con Mpris" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Gestiรณn de instancia de Navidrome integrada" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Obtenciรณn automรกtica de letras" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "Descargas y modo offline" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Interface linda" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Pรกgina Principal" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Cola de Canciones" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:93 src/ui/window.blp:192 #: src/ui/window.blp:244 msgid "Lyrics" msgstr "Letras" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Lista de Canciones" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Pรกgina de รlbum" #: data/com.jeffser.Nocturne.metainfo.xml.in:86 msgid "Mini Player" msgstr "Mini Reproductor" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "opensubsonic" msgstr "opensubsonic" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "bandcamp" msgstr "bandcamp" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:95 msgid "music" msgstr "music" #: data/com.jeffser.Nocturne.metainfo.xml.in:96 msgid "gtk" msgstr "gtk" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "Proveedor de Bรบsqueda Nocturne" #: src/actions.py:150 src/widgets/pages/login.py:133 msgid "Error" msgstr "Error" #: src/actions.py:151 msgid "No details provided" msgstr "No se proporcionaron detalles" #: src/actions.py:153 src/actions.py:300 src/preferences.py:594 #: src/ui/song/queue.blp:43 msgid "Close" msgstr "Cerrar" #: src/actions.py:191 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "No hay suficientes datos para el Nocturne Playback ({})" #: src/actions.py:297 src/ui/song/row.blp:72 msgid "External File" msgstr "Archivo Externo" #: src/actions.py:298 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Esta pista se cargรณ desde un archivo externo, lo que significa que tendrรก " "menos funciones en comparaciรณn con una pista dentro de la biblioteca" #: src/actions.py:318 msgid "Navidrome server deleted successfully" msgstr "El servidor Navidrome se eliminรณ correctamente" #: src/actions.py:327 msgid "Delete Navidrome Server" msgstr "Eliminar Servidor Navidrome" #: src/actions.py:328 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "ยฟEstรก seguro de que desea eliminar el servidor Navidrome integrado?" #: src/actions.py:330 src/actions.py:469 src/actions.py:503 src/actions.py:681 #: src/actions.py:759 src/preferences.py:512 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:181 msgid "Cancel" msgstr "Cancelar" #: src/actions.py:331 msgid "Keep Data" msgstr "Mantener Datos" #: src/actions.py:332 msgid "Delete Everything" msgstr "Eliminar Todo" #: src/actions.py:417 msgid "No Name" msgstr "Sin Nombre" #: src/actions.py:436 msgid "Radio updated successfully" msgstr "Radio actualizada correctamente" #: src/actions.py:436 msgid "Radio added successfully" msgstr "Radio agregada correctamente" #: src/actions.py:443 msgid "Error updating radio" msgstr "Error actualizando radio" #: src/actions.py:443 msgid "Error adding radio" msgstr "Error agregando radio" #: src/actions.py:454 src/actions.py:673 msgid "Name" msgstr "Nombre" #: src/actions.py:459 msgid "Stream Url" msgstr "Url de Stream" #: src/actions.py:466 msgid "Update Radio Station" msgstr "Actualizar Estaciรณn de Radio" #: src/actions.py:466 msgid "Add Radio Station" msgstr "Agregar Estaciรณn de Radio" #: src/actions.py:470 src/preferences.py:513 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Guardar" #: src/actions.py:483 msgid "Radio deleted successfully" msgstr "Radio eliminada correctamente" #: src/actions.py:487 msgid "Error deleting radio" msgstr "Error eliminando radio" #: src/actions.py:500 msgid "Delete Radio Station" msgstr "Eliminar Estaciรณn de Radio" #: src/actions.py:501 src/actions.py:757 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "ยฟEstรกs seguro de que quieres eliminar '{}'?" #: src/actions.py:504 src/actions.py:760 src/constants.py:585 #: src/constants.py:633 msgid "Delete" msgstr "Eliminar" #: src/actions.py:523 src/actions.py:535 src/actions.py:537 src/actions.py:579 #: src/actions.py:630 msgid "Playing Next" msgstr "Reproduciendo a Continuaciรณn" #: src/actions.py:527 src/actions.py:542 src/actions.py:544 src/actions.py:586 #: src/actions.py:637 msgid "Playing Later" msgstr "Reproduciendo Mรกs Tarde" #: src/actions.py:535 src/actions.py:542 src/actions.py:870 src/actions.py:995 #: src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} Canciones" #: src/actions.py:655 msgid "Playlist updated successfully" msgstr "Playlist actualizada correctamente" #: src/actions.py:655 msgid "Playlist created successfully" msgstr "Playlist creada correctamente" #: src/actions.py:657 msgid "Error updating playlist" msgstr "Error actualizando playlist" #: src/actions.py:657 msgid "Error creating playlist" msgstr "Error creando playlist" #: src/actions.py:678 msgid "Update Playlist" msgstr "Actualizar Playlist" #: src/actions.py:678 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Crear Playlist" #: src/actions.py:682 msgid "Update" msgstr "Actualizar" #: src/actions.py:682 msgid "Create" msgstr "Crear" #: src/actions.py:695 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:720 src/actions.py:734 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:738 src/actions.py:875 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: src/actions.py:749 msgid "Playlist Deleted" msgstr "Playlist Eliminada" #: src/actions.py:756 msgid "Delete Playlist" msgstr "Eliminar Playlist" #: src/actions.py:806 msgid "No songs found" msgstr "No se encontraron canciones" #: src/actions.py:847 src/actions.py:871 src/actions.py:906 src/actions.py:939 msgid "Download Started" msgstr "Descarga Iniciada" #: src/actions.py:854 src/actions.py:882 src/actions.py:889 src/actions.py:922 #: src/actions.py:955 msgid "Already Downloaded" msgstr "Ya Fue Descargado" #: src/actions.py:876 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:888 msgid "Download Skipped" msgstr "Descarga Saltada" #: src/actions.py:910 src/actions.py:943 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: src/actions.py:979 src/actions.py:996 msgid "Deleted" msgstr "Eliminado" #: src/constants.py:372 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Consecutivo" #: src/constants.py:376 msgid "Repeat All" msgstr "Repetir Todo" #: src/constants.py:380 msgid "Repeat One" msgstr "Repetir Una" #: src/constants.py:385 #, python-brace-format msgid "Low ({})" msgstr "Bajo ({})" #: src/constants.py:386 #, python-brace-format msgid "Medium ({})" msgstr "Medio ({})" #: src/constants.py:387 #, python-brace-format msgid "High ({})" msgstr "Alto ({})" #: src/constants.py:388 #, python-brace-format msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:389 msgid "Original File" msgstr "Archivo Original" #. Item #: src/constants.py:397 src/ui/pages/home.blp:5 msgid "Home" msgstr "Hogar" #. Section #: src/constants.py:404 src/ui/artist/page.blp:164 #: src/ui/pages/albums_all.blp:5 src/ui/pages/albums_all.blp:28 #: src/ui/pages/home.blp:103 src/ui/preferences.blp:312 msgid "Albums" msgstr "รlbumes" #. Item #: src/constants.py:409 src/constants.py:446 src/constants.py:463 #: src/constants.py:485 msgid "All" msgstr "Todos" #. Item #: src/constants.py:414 msgid "Random" msgstr "Aleatorio" #. Item #: src/constants.py:419 src/constants.py:451 src/constants.py:468 msgid "Favorites" msgstr "Favoritos" #. Item #: src/constants.py:424 msgid "Recently Added" msgstr "Agregado Recientemente" #. Item #: src/constants.py:429 msgid "Recently Played" msgstr "Reproducido Recientemente" #. Item #: src/constants.py:434 msgid "Most Played" msgstr "Mรกs reproducida" #. Section #. list #: src/constants.py:441 src/integrations/models.py:163 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/pages/home.blp:109 src/ui/preferences.blp:320 msgid "Artists" msgstr "Artistas" #. Section #: src/constants.py:458 src/ui/album/page.blp:160 src/ui/pages/home.blp:97 #: src/ui/pages/songs_all.blp:5 src/ui/pages/songs_all.blp:28 #: src/ui/preferences.blp:304 msgid "Songs" msgstr "Canciones" #. Item #: src/constants.py:473 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Radios" #. Section #. Item (special) #: src/constants.py:480 src/constants.py:490 src/ui/pages/home.blp:115 #: src/ui/pages/playlists.blp:5 src/ui/pages/playlists.blp:28 #: src/ui/preferences.blp:328 msgid "Playlists" msgstr "Playlists" #: src/constants.py:499 src/constants.py:550 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:114 #: src/ui/song/queue.blp:58 src/ui/song/queue.blp:60 msgid "Play" msgstr "Reproducir" #: src/constants.py:504 src/constants.py:537 src/constants.py:560 msgid "Shuffle" msgstr "Mezclar" #: src/constants.py:509 src/constants.py:565 src/constants.py:598 #: src/ui/song/queue.blp:66 src/ui/song/queue.blp:68 msgid "Play Next" msgstr "Reproducir a Continuaciรณn" #: src/constants.py:514 src/constants.py:570 src/constants.py:603 #: src/ui/song/queue.blp:74 src/ui/song/queue.blp:76 msgid "Play Later" msgstr "Reproducir Mรกs Tarde" #: src/constants.py:519 src/constants.py:613 src/ui/song/queue.blp:82 #: src/ui/song/queue.blp:84 msgid "Add To Playlist" msgstr "Agregar a Playlist" #: src/constants.py:524 src/constants.py:575 src/constants.py:618 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:90 #: src/ui/song/queue.blp:92 msgid "Download" msgstr "Descargar" #: src/constants.py:529 src/constants.py:628 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Mostrar Artista" #: src/constants.py:542 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Radio" #: src/constants.py:555 msgid "Resume" msgstr "Reanudar" #: src/constants.py:580 src/constants.py:608 msgid "Edit" msgstr "Editar" #: src/constants.py:594 msgid "Select" msgstr "Seleccionar" #: src/constants.py:623 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Mostrar รlbum" #: src/constants.py:639 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Remove" msgstr "Eliminar" #: src/constants.py:644 src/ui/song/queue.blp:112 src/ui/song/queue.blp:114 msgid "Delete Download" msgstr "Eliminar Descarga" #: src/constants.py:650 src/main.py:158 msgid "Show Details" msgstr "Mostar Detalles" #: src/constants.py:661 msgid "Visit Webpage" msgstr "Visitar Pรกgina Web" #: src/constants.py:667 msgid "Update Server" msgstr "Actualizar Server" #: src/constants.py:672 msgid "Delete Server" msgstr "Eliminar Server" #: src/constants.py:681 msgid "Edit Lyrics" msgstr "Editar Letra" #: src/constants.py:686 msgid "Remove Lyrics" msgstr "Remover Letra" #: src/integrations/base.py:306 msgid "Could not generate SQL database" msgstr "No se pudo generar la base de datos SQL" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "Navegando" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "Escuchando Mรบsica" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "Conรฉctate a un servidor Jellyfin." #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "Usa una instancia de Jellyfin existente" #: src/integrations/jellyfin.py:297 msgid "Could not log in" msgstr "No se pudo iniciar sesiรณn" #: src/integrations/jellyfin.py:1162 msgid "Unknown" msgstr "Desconocido" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:692 msgid "Local Files" msgstr "Archivos Locales" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Deja que Nocturne cargue tus archivos locales directamente; para bibliotecas " "grandes, se recomienda usar un servidor dedicado." #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Continuar" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "Funcionalidad Limitada" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "Cargando Canciones" #: src/integrations/local.py:138 src/integrations/navidrome.py:747 msgid "Could not locate path, try again" msgstr "No se pudo encontrar la ruta, intรฉntelo de nuevo" #: src/integrations/local.py:318 msgid "No Album" msgstr "Sin รlbum" #: src/integrations/local.py:704 src/integrations/local.py:713 msgid "Offline Mode" msgstr "Modo Offline" #: src/integrations/local.py:705 msgid "Access your downloads" msgstr "Accede a tus descargas" #: src/integrations/models.py:142 src/ui/album/page.blp:5 #: src/widgets/album/page.py:128 msgid "Album" msgstr "รlbum" #: src/integrations/models.py:144 msgid "Display Artist" msgstr "Artista Principal" #: src/integrations/models.py:146 msgid "MusicBrainz ID" msgstr "MusicBrainz ID" #: src/integrations/models.py:147 msgid "Track Number" msgstr "Nรบmero de Pieza" #: src/integrations/models.py:148 msgid "Year" msgstr "Aรฑo" #: src/integrations/models.py:149 msgid "Size" msgstr "Tamaรฑo" #: src/integrations/models.py:150 msgid "Suffix" msgstr "Sufijo" #: src/integrations/models.py:151 src/widgets/album/button.py:94 #: src/widgets/album/page.py:144 src/widgets/artist/page.py:109 #: src/widgets/playing/control_page.py:192 src/widgets/song/row.py:183 msgid "Favorite" msgstr "Favorito" #: src/integrations/models.py:152 msgid "Duration" msgstr "Duraciรณn" #: src/integrations/models.py:154 msgid "Bit Rate" msgstr "Tasa de Bits" #: src/integrations/models.py:155 msgid "Bit Depth" msgstr "Profundidad de Bits" #: src/integrations/models.py:156 msgid "Sampling Rate" msgstr "Frecuencia de Muestreo" #: src/integrations/models.py:157 msgid "Channel Count" msgstr "Cantidad de Canales" #: src/integrations/models.py:159 msgid "Path" msgstr "Direcciรณn" #: src/integrations/models.py:160 msgid "Disc Number" msgstr "Nรบmero de Disco" #: src/integrations/models.py:161 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:162 msgid "Genres" msgstr "Generos" #. list #: src/integrations/models.py:164 msgid "Track Gain" msgstr "Ganancia de Pista" #: src/integrations/models.py:165 msgid "Album Gain" msgstr "Ganancia de รlbum" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Conรฉctate a un servidor OpenSubsonic como Navidrome." #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "Servidor Externo" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic instance" msgstr "Utilice una instancia de OpenSubsonic existente" #: src/integrations/navidrome.py:173 msgid "Server returned an error" msgstr "El servidor devolviรณ un error" #: src/integrations/navidrome.py:173 msgid "Unknown Error" msgstr "Error desconocido" #: src/integrations/navidrome.py:722 src/integrations/navidrome.py:731 msgid "Managed Server" msgstr "Servidor Gestionado" #: src/integrations/navidrome.py:723 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "Conรฉctate a una instancia de Navidrome gestionada directamente por Nocturne." #: src/integrations/navidrome.py:726 msgid "Manage Server" msgstr "Gestiona Servidor" #: src/integrations/navidrome.py:732 msgid "Create and use a Navidrome instance" msgstr "Crea y utiliza una instancia de Navidrome" #: src/integrations/navidrome.py:791 msgid "Important: Use your Subsonic login information." msgstr "Importante: Utilice su informaciรณn de inicio de sesiรณn de Subsonic." #: src/integrations/navidrome.py:794 msgid "Bandcamp User Settings" msgstr "Configuraciรณn de usuario de Bandcamp" #: src/integrations/navidrome.py:800 msgid "Explore your online library" msgstr "Explora tu librerรญa en lรญnea" #: src/main.py:96 msgid "Music is Playing" msgstr "Mรบsica en Reproducciรณn" #: src/main.py:109 msgid "Fullscreen Player Active" msgstr "Reproductor de Pantalla Completa Activo" #: src/main.py:155 msgid "Login Failed" msgstr "Inicio de Sesiรณn Fallido" #: src/nocturne_search_provider.py.in:12 src/ui/album/button.blp:21 msgid "Play Album" msgstr "Reproduce รlbum" #: src/nocturne_search_provider.py.in:13 src/ui/song/button.blp:16 msgid "Play Song" msgstr "Reproducir Canciรณn" #: src/nocturne_search_provider.py.in:14 src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "Reproduce Playlist" #: src/nocturne_search_provider.py.in:113 msgid "Open" msgstr "Abrir" #: src/preferences.py:20 src/preferences.py:22 msgid "Item" msgstr "Item" #: src/preferences.py:27 src/preferences.py:29 msgid "Section" msgstr "Secciรณn" #: src/preferences.py:501 msgid "Settings Page" msgstr "Pรกgina de Ajustes" #: src/preferences.py:504 msgid "User Token" msgstr "Token de Usuario" #: src/preferences.py:508 msgid "Link ListenBrainz" msgstr "Vincula ListenBrainz" #: src/preferences.py:509 msgid "Connect your ListenBrainz account with a user token" msgstr "Conecta tu cuenta de ListenBrainz con un token de usuario" #: src/preferences.py:584 msgid "Flatpak Sandbox Warning" msgstr "Advertencia de Flatpak Sandbox" #: src/preferences.py:585 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "Para conectarse a Discord, se requiere un permiso adicional. Una vez que " "ejecute el siguiente comando, reinicie Nocturne" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:115 msgid "Toggle star" msgstr "Alternar estrella" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Portada del รกlbum" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "Entrar en la pรกgina del รกlbum" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "Entrar en la pรกgina del artista" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:228 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "1 Estrella" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:237 msgid "2 Stars" msgstr "2 Estrellas" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:246 msgid "3 Stars" msgstr "3 Estrellas" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:255 msgid "4 Stars" msgstr "4 Estrellas" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:264 msgid "5 Stars" msgstr "5 Estrellas" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:130 msgid "Options" msgstr "Opciones" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Artista" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Ampliar Biografรญa" #: src/ui/artist/page.blp:159 msgid "Top Songs" msgstr "Canciones Top" #: src/ui/artist/page.blp:169 msgid "Related Artists" msgstr "Artistas Relacionados" #: src/ui/containers/carousel.blp:44 msgid "Pan to the Left" msgstr "Desplazar Hacia la Izquierda" #: src/ui/containers/carousel.blp:50 msgid "Pan to the Right" msgstr "Desplazar Hacia la Derecha" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "Descargado" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Remover de File" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "Fila de Descargas" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "Las canciones descargadas estรกn disponibles en modo offline" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Borrar Descargas Realizadas" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Editor de Letras" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:187 #: src/ui/playing/footer.blp:129 msgid "Pause" msgstr "Pausar" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:122 msgid "Current song progress bar" msgstr "Barra de progreso de la canciรณn actual" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Establecer la Marca de Tiempo de la Siguiente Lรญnea a Ahora" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Agregar Lรญnea Ahora" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Sin Letras" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "" "No se encontraron letras para esta canciรณn. Para empezar, aรฑade una nueva " "lรญnea" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Marca de Tiempo" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Ir a la Marca de Tiempo" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Establecer Marca de Tiempo" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Eliminar Lรญnea" #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/starred.blp:13 msgid "List" msgstr "Lista" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/starred.blp:18 msgid "Grid" msgstr "Cuadricula" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Buscar รlbumes" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Se ha Llegado al Final de la Lista" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "No se Encontraron รlbumes" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Buscar artistas" #: src/ui/pages/artists.blp:114 src/widgets/pages/starred.py:44 msgid "No Artists Found" msgstr "No se Encontraron Artistas" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "Alternar Busqueda" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "Buscar" #: src/ui/pages/home.blp:81 msgid "Welcome back" msgstr "Bienvenido" #: src/ui/pages/home.blp:91 msgid "On Replay" msgstr "En repeticiรณn" #: src/ui/pages/home.blp:128 msgid "No Elements Found" msgstr "No se Encontraron Elementos" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 #: src/widgets/pages/login.py:38 src/widgets/pages/login.py:78 msgid "Login" msgstr "Inicio de Sesiรณn" #: src/ui/pages/login.blp:36 msgid "Server Status" msgstr "Estado del Servidor" #: src/ui/pages/login.blp:37 src/widgets/pages/login.py:52 msgid "Not Running" msgstr "No en Funcionamiento" #: src/ui/pages/login.blp:38 msgid "Restart Instance" msgstr "Reiniciar Instancia" #: src/ui/pages/login.blp:51 msgid "Manage Navidrome Server" msgstr "Gestionar Servidor Navidrome" #: src/ui/pages/login.blp:61 msgid "Url" msgstr "Url" #: src/ui/pages/login.blp:69 msgid "More Options" msgstr "Mรกs Opciones" #: src/ui/pages/login.blp:75 msgid "Trust Server Certificates" msgstr "Confiar en Certificados del Servidor" #: src/ui/pages/login.blp:83 msgid "Library Directory" msgstr "Directorio de Librerรญa" #: src/ui/pages/login.blp:96 msgid "Username" msgstr "Usuario" #: src/ui/pages/login.blp:101 msgid "Password" msgstr "Contraseรฑa" #: src/ui/pages/login.blp:122 src/ui/pages/login.blp:123 msgid "Use Quick Connect" msgstr "Usar Conexiรณn Rรกpida" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:29 msgid "Playback" msgstr "Reproducciรณn" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "Ir a Pรกgina Principal" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "Alternar Visualizador" #: src/ui/pages/playback.blp:54 src/window.py:228 msgid "Nocturne Playback" msgstr "Nocturne Playback" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "Estas son las canciones que mรกs has escuchado del mes pasado" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "Guardar Playlist" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "Playlist Agregada" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "Gracias por usar Nocturne!" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Buscar Playlists" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "No se Encontraron Playlists" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Buscar radios" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Agregar Radio" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "No se Encontraron Radios" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Configuraciรณn Inicial" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Descarga de Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne descargarรก Navidrome automรกticamente" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Descargar (~20โ€ฏmb)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "Descargando" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Se ha Producido un Error" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Por favor, intรฉntelo de nuevo mรกs tarde" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "Descarga Exitosa" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Antes de continuar, asegรบrese de crear una cuenta de administrador y tambiรฉn " "de que todas sus pistas estรฉn dentro de su directorio de mรบsica" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Pรกgina Web de Instancia" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Buscar canciones" #: src/ui/pages/songs_all.blp:116 src/ui/pages/starred.blp:94 #: src/ui/song/queue.blp:140 msgid "No Songs Found" msgstr "No se Encontraron Canciones" #: src/ui/pages/starred.blp:29 msgid "Search favorite songs" msgstr "Buscar canciones favoritas" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Bienvenido" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Bienvenido a Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "Seleccione un tipo de instancia para comenzar" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Reproduciendo" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "Visitar la pรกgina de la radio actual" #: src/ui/playing/control_page.blp:157 src/ui/playing/footer.blp:99 msgid "Previous" msgstr "Previo" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:143 msgid "Next" msgstr "Siguiente" #: src/ui/playing/control_page.blp:221 msgid "Rate" msgstr "Calificar" #: src/ui/playing/control_page.blp:277 msgid "Star" msgstr "Dar Estrella" #: src/ui/playing/control_page.blp:284 msgid "Toggle Sidebar" msgstr "Alternar Barra Lateral" #: src/ui/playing/cover_art.blp:35 msgid "Audio" msgstr "Audio" #: src/ui/playing/cover_art.blp:43 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Portada de Canciรณn" #: src/ui/playing/cover_art.blp:49 src/ui/playing/cover_art.blp:57 msgid "Video" msgstr "Video" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "Preajustes" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Plano" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Claridad Vocal" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "Jazz Suave" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rock" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Clรกsico" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Acรบstico" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Indicador de progreso de la canciรณn actual" #: src/ui/playing/footer.blp:45 msgid "Current song cover art" msgstr "Portada de la canciรณn actual" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "Volver" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "Opciones de Letra" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "No se Encontraron Letras" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "No Descargado" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "La letra de esta canciรณn aรบn no se ha descargado, es posible que estรฉ " "disponible para su descarga" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "Cargar Archivo de Letras" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Ventana Desplegable" #: src/ui/playing/popout_window.blp:67 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:216 msgid "Song Details" msgstr "Detalles de Canciรณn" #: src/ui/playing/popout_window.blp:87 src/ui/window.blp:182 #: src/ui/window.blp:238 msgid "Queue" msgstr "Cola" #: src/ui/playing/popout_window.blp:99 src/ui/window.blp:198 #: src/ui/window.blp:250 msgid "Equalizer" msgstr "Ecualizador" #: src/ui/playing/queue_page.blp:19 msgid "Generating Next Queue" msgstr "Generando Siguiente Cola" #: src/ui/playing/queue_page.blp:23 msgid "Autoplay" msgstr "Reproducciรณn Automรกtica" #: src/ui/playing/queue_page.blp:24 msgid "Generate a new queue when the current one ends" msgstr "Generar una nueva cola cuando finalice la actual" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Volumen" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Altavoz Silenciado" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Altavoz a Todo Volumen" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "Muestra Playlist" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Portada de playlist" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "Entrar pรกgina de playlist" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:83 msgid "Playlist" msgstr "Playlist" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Buscar (o crear nueva playlist)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Agregando Mรบsica a Playlist" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "Crear una Nueva Playlist y Agrega Canciones" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "General" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Comportamiento" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "Restaurar la cola al iniciar" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Esconder al Cerrar" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "Simular Letras Estilo Palabra por Palabra" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "Usado cuando solo se dispone de letras estilo lรญnea por lรญnea" #: src/ui/preferences.blp:22 msgid "Auto Download Lyrics" msgstr "Descarga Automรกtica de Letras" #: src/ui/preferences.blp:23 msgid "Used when the current instance does not provide lyrics" msgstr "Se utiliza cuando la instancia actual no proporciona letras" #: src/ui/preferences.blp:26 msgid "Use Track and Album Gain" msgstr "Utilizar Ganancia de Canciรณn y รlbum" #: src/ui/preferences.blp:27 msgid "Adjust the volume based on gain metadata" msgstr "Ajusta el volumen segรบn los metadatos de ganancia" #: src/ui/preferences.blp:30 msgid "Default Page on Login" msgstr "Pรกgina por Defecto en Inicio" #: src/ui/preferences.blp:35 msgid "Maximum Bitrate" msgstr "Bitrate Mรกximo" #: src/ui/preferences.blp:41 msgid "Gnome Search" msgstr "Bรบsqueda Gnome" #: src/ui/preferences.blp:42 msgid "" "Search for your music directly from the system search bar while Nocturne is " "running. You can enable or disable this feature completely in System Settings" msgstr "" "Busca tu mรบsica directamente desde la barra de bรบsqueda del sistema mientras " "Nocturne estรฉ en ejecuciรณn. Puedes activar o desactivar esta funciรณn por " "completo en la configuraciรณn del sistema" #: src/ui/preferences.blp:45 msgid "Include Artists" msgstr "Incluir Artistas" #: src/ui/preferences.blp:48 msgid "Include Albums" msgstr "Incluir Albumes" #: src/ui/preferences.blp:51 msgid "Include Songs" msgstr "Incluir Canciones" #: src/ui/preferences.blp:54 msgid "Include Playlists" msgstr "Incluir Playlists" #: src/ui/preferences.blp:58 msgid "Session" msgstr "Sesiรณn" #: src/ui/preferences.blp:61 msgid "Discord Rich Presence" msgstr "Discord Rich Presence" #: src/ui/preferences.blp:62 msgid "Share the currently playing song on Discord" msgstr "Comparte la canciรณn que se estรก reproduciendo en Discord" #: src/ui/preferences.blp:66 msgid "Share Cover Art With Discord" msgstr "Compartir Arte con Discord" #: src/ui/preferences.blp:67 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "Si la instancia no es accesible mediante los servidores de Discord, se " "utilizarรก CoverArtArchive en su lugar" #: src/ui/preferences.blp:73 msgid "ListenBrainz Link" msgstr "Vincula ListenBrainz" #: src/ui/preferences.blp:74 msgid "Keep track of your playback history" msgstr "Mantรฉn un registro de tu historial de reproducciรณn" #: src/ui/preferences.blp:80 src/ui/preferences.blp:81 msgid "Link" msgstr "Conectar" #: src/ui/preferences.blp:89 src/ui/preferences.blp:90 msgid "Unlink" msgstr "Desconectar" #: src/ui/preferences.blp:102 msgid "User" msgstr "Usuario" #: src/ui/preferences.blp:122 msgid "Delete Image Cache" msgstr "Eliminar Cachรฉ de Imรกgenes" #: src/ui/preferences.blp:129 msgid "Logout" msgstr "Cerrar Sesiรณn" #: src/ui/preferences.blp:139 msgid "Customization" msgstr "Personalizaciรณn" #: src/ui/preferences.blp:142 msgid "Interface" msgstr "Interfaz" #: src/ui/preferences.blp:144 msgid "Show Context Button in Rows" msgstr "Mostrar Botรณn de Contexto en las Filas" #: src/ui/preferences.blp:145 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" "El menรบ tambiรฉn se puede mostrar haciendo clic con el botรณn derecho / " "manteniendo pulsado" #: src/ui/preferences.blp:148 msgid "Show Labels in Buttons of Pages" msgstr "Mostrar Etiquetas en Botones de Pรกginas" #: src/ui/preferences.blp:151 msgid "Show Progressbar in Player Footer" msgstr "Mostrar Barra de Progreso en el Pie de Reproductor" #: src/ui/preferences.blp:154 msgid "Make Background Translucent in Player" msgstr "Hacer Fondo Translรบcido en el Reproductor" #: src/ui/preferences.blp:155 msgid "Does not affect sidebar player" msgstr "No afecta el reproductor de barra lateral" #: src/ui/preferences.blp:158 msgid "Use Sidebar Player When Possible" msgstr "Usar el Reproductor de Barra Lateral Cuando es Posible" #: src/ui/preferences.blp:159 msgid "Sidebar player is only available when the window is wide enough" msgstr "" "El Reproductor de Barra Lateral solo estรก disponible cuando la ventana es lo " "suficientemente ancha" #: src/ui/preferences.blp:162 msgid "Filter Singles" msgstr "Filtrar Sencillos" #: src/ui/preferences.blp:163 msgid "Hides albums with only one song (does not affect homepage)" msgstr "" "Oculta los รกlbumes con una sola canciรณn (no afecta a la pรกgina principal)" #: src/ui/preferences.blp:166 msgid "Button Size" msgstr "Tamaรฑo de Botones" #: src/ui/preferences.blp:167 msgid "Affects albums, artists, playlists and songs" msgstr "Afecta albumes, artistas, playlists y canciones" #: src/ui/preferences.blp:173 src/ui/preferences.blp:174 msgid "Small" msgstr "Pequeรฑo" #: src/ui/preferences.blp:178 src/ui/preferences.blp:179 msgid "Big" msgstr "Grande" #: src/ui/preferences.blp:187 msgid "Dynamic Background" msgstr "Fondo Dinรกmico" #: src/ui/preferences.blp:188 msgid "Background generated based on the album art of the current song" msgstr "Fondo generado a partir de la carรกtula del รกlbum de la canciรณn actual" #: src/ui/preferences.blp:191 msgid "Use in Main Window" msgstr "Usar en la Ventana Principal" #: src/ui/preferences.blp:197 src/ui/preferences.blp:198 #: src/ui/preferences.blp:221 src/ui/preferences.blp:222 #: src/ui/preferences.blp:245 src/ui/preferences.blp:246 #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Off" msgstr "Apagado" #: src/ui/preferences.blp:202 src/ui/preferences.blp:203 #: src/ui/preferences.blp:226 src/ui/preferences.blp:227 #: src/ui/preferences.blp:250 src/ui/preferences.blp:251 msgid "Gradient" msgstr "Gradiante" #: src/ui/preferences.blp:207 src/ui/preferences.blp:208 #: src/ui/preferences.blp:231 src/ui/preferences.blp:232 #: src/ui/preferences.blp:255 src/ui/preferences.blp:256 msgid "Blur" msgstr "Blur" #: src/ui/preferences.blp:215 msgid "Use in Player" msgstr "Usar en Reproductor" #: src/ui/preferences.blp:239 msgid "Use in Pop-out Window" msgstr "Usar en Ventana Emergente" #: src/ui/preferences.blp:263 msgid "Use as Accent Color" msgstr "Usar Como Color de Acento" #: src/ui/preferences.blp:268 msgid "Homepage" msgstr "Hogar" #: src/ui/preferences.blp:269 msgid "Reload homepage to see changes" msgstr "Actualiza la pรกgina del hogar para ver cambios" #: src/ui/preferences.blp:271 msgid "Welcome Message" msgstr "Mensaje de Bienvenida" #: src/ui/preferences.blp:272 msgid "Where to get user information" msgstr "Dรณnde obtener informaciรณn del usuario" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Instance" msgstr "Instancia" #: src/ui/preferences.blp:289 src/ui/preferences.blp:290 msgid "System" msgstr "Sistema" #: src/ui/preferences.blp:296 src/ui/window.blp:153 msgid "Frequent Albums" msgstr "รlbumes Frecuentes" #: src/ui/preferences.blp:338 msgid "Sidebar" msgstr "Barra Lateral" #: src/ui/preferences.blp:342 msgid "Visualizer" msgstr "Visualizador" #: src/ui/preferences.blp:346 src/ui/window.blp:267 msgid "Preferences" msgstr "Preferencias" #: src/ui/preferences.blp:348 msgid "Show Visualizer in Player" msgstr "Mostrar Visualizador en el Reproductor" #: src/ui/preferences.blp:349 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "Muestra un visualizador de audio sobre la portada en el reproductor" #: src/ui/preferences.blp:354 msgid "Appearance" msgstr "Apariencia" #: src/ui/preferences.blp:358 msgid "Number of Bars" msgstr "Nรบmero de Barras" #: src/ui/preferences.blp:369 msgid "Visualizer Type" msgstr "Tipo de Visualizador" #: src/ui/preferences.blp:375 src/ui/preferences.blp:376 msgid "Wave" msgstr "Ola" #: src/ui/preferences.blp:380 src/ui/preferences.blp:381 msgid "Bars" msgstr "Barras" #: src/ui/preferences.blp:385 src/ui/preferences.blp:386 msgid "Particles" msgstr "Particulas" #: src/ui/preferences.blp:393 msgid "Fill Mode" msgstr "Modo de Relleno" #: src/ui/preferences.blp:399 src/ui/preferences.blp:400 msgid "Solid" msgstr "Solido" #: src/ui/preferences.blp:404 src/ui/preferences.blp:405 msgid "Translucent" msgstr "Translรบcido" #: src/ui/preferences.blp:409 src/ui/preferences.blp:410 msgid "Border" msgstr "Borde" #: src/ui/preferences.blp:418 msgid "Color" msgstr "Color" #: src/ui/preferences.blp:422 msgid "Use Dynamic Color" msgstr "Utilizar Color Dinรกmico" #: src/ui/preferences.blp:425 msgid "Invert Color" msgstr "Invertir Color" #: src/ui/preferences.blp:429 msgid "Manual Color" msgstr "Color Manual" #: src/ui/preferences.blp:435 msgid "Visualizer Color" msgstr "Color de Visualizador" #: src/ui/shortcuts-dialog.blp:12 msgid "Reload Page" msgstr "Recargar Pรกgina" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Shortcuts" msgstr "Mostrar Atajos" #: src/ui/shortcuts-dialog.blp:20 msgid "Show Preferences" msgstr "Mostrar Preferencias" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "Cerrar" #: src/ui/shortcuts-dialog.blp:31 msgid "Toggle Playback" msgstr "Alternar Reproducciรณn" #: src/ui/shortcuts-dialog.blp:35 msgid "Next Song" msgstr "Siguiente Canciรณn" #: src/ui/shortcuts-dialog.blp:39 msgid "Previous Song" msgstr "Canciรณn Previa" #: src/ui/shortcuts-dialog.blp:43 msgid "Raise Volume" msgstr "Aumentar Volumen" #: src/ui/shortcuts-dialog.blp:47 msgid "Lower Volume" msgstr "Restar Volumen" #: src/ui/shortcuts-dialog.blp:52 msgid "Window" msgstr "Ventana" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:277 #: src/widgets/playing/popout_window.py:48 msgid "Toggle Fullscreen" msgstr "Alternar Pantalla Completa" #: src/ui/shortcuts-dialog.blp:58 src/ui/window.blp:272 msgid "Open Pop-Out Window" msgstr "Abrir Ventana Emergente" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Portada de canciรณn" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "Indicador de Reproducciรณn" #: src/ui/song/queue.blp:50 src/ui/song/queue.blp:52 msgid "Select All" msgstr "Seleccionar Todo" #: src/ui/song/row.blp:44 msgid "Drag icon" msgstr "Icono de arrastre" #: src/ui/song/row.blp:142 msgid "Selection box" msgstr "Caja de selecciรณn" #: src/ui/window.blp:94 msgid "Play Random Queue" msgstr "Reproducir Cola Aleatoria" #: src/ui/window.blp:102 msgid "Menu" msgstr "Menรบ" #: src/ui/window.blp:125 msgid "Favorite Artists" msgstr "Artistas Favoritos" #: src/ui/window.blp:131 msgid "Favorite Songs" msgstr "Canciones Favoritas" #: src/ui/window.blp:137 msgid "Random Albums" msgstr "รlbumes Aleatorios" #: src/ui/window.blp:141 msgid "Favorite Albums" msgstr "รlbumes Favoritos" #: src/ui/window.blp:145 msgid "Newest Albums" msgstr "รlbumes Nuevos" #: src/ui/window.blp:149 msgid "Recent Albums" msgstr "รlbumes Recientes" #: src/ui/window.blp:176 msgid "Player" msgstr "Reproductor" #: src/ui/window.blp:282 msgid "See Last Nocturne Playback" msgstr "Mostrar el รšltimo Nocturne Playback" #: src/ui/window.blp:287 msgid "Keyboard Shortcuts" msgstr "Atajos de Teclado" #: src/ui/window.blp:292 msgid "About Nocturne" msgstr "Sobre Nocturne" #: src/widgets/album/button.py:99 src/widgets/album/page.py:149 #: src/widgets/artist/page.py:114 src/widgets/playing/control_page.py:197 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "No Favorito" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "Disco {}" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "" msgstr[1] "" #: src/widgets/artist/page.py:97 msgid "Author" msgstr "Autor" #: src/widgets/containers/context.py:50 #, python-brace-format msgid "{} Stars" msgstr "{} Estrellas" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Sin Marca de Tiempo" #: src/widgets/lyrics/dialog.py:195 msgid "Lyrics Saved" msgstr "Letras Guardadas" #: src/widgets/pages/login.py:52 src/widgets/pages/login.py:131 msgid "Running" msgstr "En Funcionamiento" #: src/widgets/pages/login.py:85 msgid "Extra Menu" msgstr "Menรบ Extra" #: src/widgets/pages/login.py:120 src/widgets/pages/setup.py:83 msgid "Music Library" msgstr "Librerรญa de Mรบsica" #: src/widgets/pages/login.py:130 msgid "Restarted" msgstr "Reiniciado" #: src/widgets/pages/login.py:172 msgid "Quick Connect" msgstr "Conexiรณn Rรกpida" #: src/widgets/pages/login.py:173 msgid "Error getting code" msgstr "Error obteniendo cรณdigo" #: src/widgets/pages/login.py:175 msgid "Quick Connect Page" msgstr "Pรกgina de Conexiรณn Rรกpida" #: src/widgets/pages/playback.py:29 #, python-brace-format msgid "Nocturne Playback ~ {}" msgstr "Nocturne Playback ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 #, python-brace-format msgid "{} Plays" msgstr "{} Reproducciones" #: src/widgets/pages/setup.py:70 msgid "Installing" msgstr "Instalando" #: src/widgets/pages/starred.py:42 msgid "Search favorite artists" msgstr "Buscar artistas favoritos" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integraciรณn" #: src/widgets/playing/lyrics_page.py:222 msgid "LRC File" msgstr "Archivo LRC" #: src/widgets/playing/player.py:638 msgid "Warning: Song changed but volume is set to 0" msgstr "Advertencia: La canciรณn ha cambiado, pero el volumen estรก en 0" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:115 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" msgstr[1] "" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Nueva Playlist" #: src/widgets/playlist/selector_row.py:33 #, python-brace-format msgid "Add Songs to '{}'" msgstr "Agregar Canciones a '{}'" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "Si" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "No" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "No se ha Encontrado" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "Multiples Artistas" #: src/window.py:229 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "ยกTu cola de reproducciรณn para {} estรก disponible!" #: src/window.py:231 msgid "Show" msgstr "Mostar" #: src/window.py:232 msgid "Later" msgstr "Mรกs Tarde" #~ msgid "Bandcamp" #~ msgstr "Bandcamp" #~ msgid "Bandcamp (Beta)" #~ msgstr "Bandcamp (Beta)" #~ msgid "Show Pan Buttons on Carousels" #~ msgstr "Mostrar Botones de Desplazamiento en Carruseles" #~ msgid "Only visible when a carousel has 5 or more items" #~ msgstr "Solo visible cuando un carrusel tiene 5 o mรกs elementos" #~ msgid "Autoplay is not available in this instance" #~ msgstr "La reproducciรณn automรกtica no estรก disponible en esta instancia" #~ msgid "A modern Navidrome / Jellyfin client" #~ msgstr "Un cliente moderno de Navidrome / Jellyfin" #~ msgid "Compatibility with Jellyfin, OpenSubsonic and local files" #~ msgstr "Compatibilidad con Jellyfin, OpenSubsonic y archivos locales" #~ msgid "Use an existing OpenSubsonic / Navidrome instance" #~ msgstr "Utilice una instancia existente de OpenSubsonic/Navidrome" #~ msgid "Local Music Library" #~ msgstr "Libreria de Mรบsica Local" #~ msgid "Experimental" #~ msgstr "Experimental" #~ msgid "Show Sidebar" #~ msgstr "Mostrar Barra Lateral" #, python-brace-format #~ msgid "Not enough songs found for Playback ({})" #~ msgstr "No se encontraron suficientes canciones para el Playback ({})" #~ msgid "Generating next queue" #~ msgstr "Generando la siguiente cola" #~ msgid "Create new playlist and add songs" #~ msgstr "Crear nueva playlist y agrega canciones" #, python-brace-format #~ msgid "Add songs to '{}'" #~ msgstr "Agregar canciones a '{}'" #~ msgid "bit Rate" #~ msgstr "Bit Rate" #~ msgid "Show Playlists in Sidebar" #~ msgstr "Mostrar Playlists en Barra Lateral" #~ msgid "Shortcuts" #~ msgstr "Atajos" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "{} Canciones Eliminadas" #~ msgid "Song Removed" #~ msgstr "Canciรณn Eliminada" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "{} Canciones Agregadas" #~ msgid "1 Song Added" #~ msgstr "1 Canciรณn Agregada" #~ msgid "1 Song Skipped" #~ msgstr "1 Canciรณn Saltada" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "{} Canciones Saltada" #, python-brace-format #~ msgid "{} Songs ({})" #~ msgstr "{} Canciones ({})" #, python-brace-format #~ msgid "1 Song ({})" #~ msgstr "1 Canciรณn ({})" #, python-brace-format #~ msgid "Download Started ({} Songs Skipped)" #~ msgstr "Descarga Iniciada ({} Canciones Saltadas)" #~ msgid "Download Started (1 Song Skipped)" #~ msgstr "Descarga Iniciada (1 Canciรณn Saltada)" #~ msgid "1 Album" #~ msgstr "1 รlbum" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} รlbumes" #~ msgid "1 Song" #~ msgstr "1 Canciรณn" #~ msgid "Open Pop-out Window" #~ msgstr "Abrir Ventana Desplegable" #~ msgid "Close Pop-out Window" #~ msgstr "Cerrar Ventana Desplegable" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "La letra es proporcionada por LRCLIB" #~ msgid "Player in Pop-out Window" #~ msgstr "Reproductor en Ventana Desplegable" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "Difuminar el Fondo en el Reproductor (Experimental)" #~ msgid "Gradient generated based on the album art of the current song" #~ msgstr "" #~ "Gradiente generado en base a la portada del รกlbum de la canciรณn actual" #~ msgid "Use in Popout Window" #~ msgstr "Usar en Ventana Emergente" #~ msgid "Instance Website" #~ msgstr "Sitio Web de Instancia" #~ msgid "Go to Welcome Page" #~ msgstr "Ir a la Pรกgina de Bienvenida" #~ msgid "Whether to show some playlists in the main sidebar" #~ msgstr "Si se deben mostrar unas playlists en la barra lateral principal" #~ msgid "Use Dynamic Background in Player" #~ msgstr "Usar Fondo Dinรกmico en el Reproductor" #~ msgid "Generate a gradient background based on the current song cover art" #~ msgstr "Genera un fondo degradado basado en la portada de la canciรณn actual" #~ msgid "Make background of player slightly transparent and blur the backdrop" #~ msgstr "" #~ "Haz que el fondo del reproductor sea ligeramente transparente y difuminado" #~ msgid "Which page is shown by default" #~ msgstr "Cual pรกgina es mostrada por defecto" #~ msgid "Restore Session" #~ msgstr "Restaurar Sesiรณn" #~ msgid "Retrieve previous queue" #~ msgstr "Recuperar la cola anterior" #~ msgid "Hide Nocturne when closing" #~ msgstr "Ocultar Nocturne al cerrar" #~ msgid "Sections" #~ msgstr "Secciones" #~ msgid "Max amount of songs to be shown" #~ msgstr "Cantidad mรกxima de canciones que se mostrarรกn" #~ msgid "Max amount of albums to be shown" #~ msgstr "Cantidad mรกxima de รกlbumes que se mostrarรกn" #~ msgid "Max amount of artists to be shown" #~ msgstr "Cantidad mรกxima de artistas que se mostrarรกn" #~ msgid "Max amount of playlists to be shown" #~ msgstr "Cantidad mรกxima de playlists que se mostrarรกn" #~ msgid "Automatically Set Color" #~ msgstr "Asignar Color Automรกticamente" #~ msgid "Jellyfin (Experimental)" #~ msgstr "Jellyfin (Experimental)" #~ msgid "Navidrome" #~ msgstr "Navidrome" #~ msgid "Use an existing Navidrome / Subsonic instance" #~ msgstr "Usar instancia existente de Navidrome / Subsonic" #~ msgid "Navidrome Managed" #~ msgstr "Navidrome Gestionado" #~ msgid "Create and use Navidrome instance" #~ msgstr "Crea y usa una instancia de Navidrome" #~ msgid "" #~ "Nocturne is powered by a Subsonic compatible instance, if you do not have " #~ "one Nocturne can automatically set up a Navidrome instance for you!" #~ msgstr "" #~ "Nocturne funciona con una instancia compatible con Subsonic; si no " #~ "dispone de una, ยกNocturne puede configurar automรกticamente una instancia " #~ "de Navidrome!" #~ msgid "Show Visualizer in Player (Experimental)" #~ msgstr "Mostrar Visualizador en el Reproductor (Experimental)" #~ msgid "Starred" #~ msgstr "Estrellas" #~ msgid "GTK;" #~ msgstr "GTK;" #~ msgid "A modern Navidrome / Subsonic client" #~ msgstr "Un cliente moderno de Navidrome / Subsonic" #~ msgid "Play icon" #~ msgstr "Icono de reproducciรณn" #~ msgid "New" #~ msgstr "Nuevo" #~ msgid "Added screen to indicate when no lyrics are found inside editor" #~ msgstr "" #~ "Se agregรณ una pantalla para indicar cuรกndo no se encuentran letras dentro " #~ "del editor" #~ msgid "Added transitions when logging in" #~ msgstr "Se agregaron transiciones al iniciar sesiรณn" #~ msgid "Added option to update / delete Navidrome server" #~ msgstr "Se agregรณ la opciรณn de actualizar / eliminar el servidor Navidrome" #~ msgid "Added newest, recent and frequent album filtering for local instance" #~ msgstr "" #~ "Se agregรณ el filtrado de รกlbumes mรกs recientes, nuevos y frecuentes para " #~ "la instancia local" #~ msgid "Better shadows for album buttons" #~ msgstr "Mejores sombras para los botones del รกlbum" #~ msgid "Fixes" #~ msgstr "Arreglos" #~ msgid "Remove dynamic background if song does not have a cover art" #~ msgstr "Eliminar el fondo dinรกmico si la canciรณn no tiene una carรกtula" #~ msgid "Only change song if current row is deleted" #~ msgstr "Solo se puede cambiar de canciรณn si se elimina la fila actual" #~ msgid "Fixed lyric editor margins" #~ msgstr "Se arreglaron los margenes del editor de letras" #~ msgid "Items in sidebar are selected when changing pages" #~ msgstr "" #~ "Los elementos de la barra lateral se seleccionan al cambiar de pรกgina" #~ msgid "Fixed show sidebar button being shown in wide mode" #~ msgstr "" #~ "Se solucionรณ el problema de que el botรณn de la barra lateral se mostraba " #~ "en modo ancho" #~ msgid "Save lyrics as lrc from editor even when not fully synced" #~ msgstr "" #~ "Guarda las letras como archivos lrc desde el editor, incluso cuando no " #~ "estรฉn completamente sincronizadas." #~ msgid "Fixed external files being saved in restore queue" #~ msgstr "" #~ "Se solucionรณ el problema de que los archivos externos se guardaban en la " #~ "cola de restauraciรณn" #~ msgid "" #~ "Fixed wrong cover art being used when loading external music into " #~ "Navidrome instance" #~ msgstr "" #~ "Se corrigiรณ el error que provocaba que se utilizara una carรกtula " #~ "incorrecta al cargar mรบsica externa en una instancia de Navidrome" #~ msgid "" #~ "First release of Nocturne, please report any bugs or features you might " #~ "want to see in the next version" #~ msgstr "" #~ "Primera versiรณn de Nocturne. Por favor, informen de cualquier error o " #~ "caracterรญstica que les gustarรญa ver en la prรณxima versiรณn." #~ msgid "Homepage Url" #~ msgstr "Url de Pรกgina Principal" #~ msgid "Offline" #~ msgstr "Offline" #~ msgid "Connect to Server" #~ msgstr "Conectarse a Servidor" #~ msgid "Use existing instance" #~ msgstr "Utilizar una instancia existente" #~ msgid "Use Navidrome" #~ msgstr "Usa Navidrome" #~ msgid "Use integrated instance" #~ msgstr "Usa instancia integrada" #~ msgid "Only Local Files" #~ msgstr "Solo Archivos Locales" #~ msgid "Limited functionallity" #~ msgstr "Funcionalidad limitada" #~ msgid "No User Created" #~ msgstr "No se ha Creado un Usuario" #~ msgid "You need to create a user before using the integrated instance" #~ msgstr "Debes crear un usuario antes de usar la instancia integrada" #~ msgid "An Error Ocurred" #~ msgstr "Se ha producido un error" #~ msgid "Integrated instance" #~ msgstr "Instancia integrada" #~ msgid "Only Use Local Files" #~ msgstr "Utilizar Solo Archivos Locales" #~ msgid "Max ammount of songs to be shown" #~ msgstr "Cantidad mรกxima de canciones que se mostrarรกn" #~ msgid "Max ammount of albums to be shown" #~ msgstr "Cantidad mรกxima de รกlbumes que se mostrarรกn" #~ msgid "Max ammount of artists to be shown" #~ msgstr "Cantidad mรกxima de artistas que se mostrarรกn" #~ msgid "Max ammount of playlists to be shown" #~ msgstr "Cantidad mรกxima de playlists que se mostrarรกn" Jeffser-Nocturne-b442fee/po/eu.po000066400000000000000000001434441523240030000167540ustar00rootroot00000000000000# Basque translations for PACKAGE package. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Ibai Oihanguren Sala , 2026. # msgid "" msgstr "" "Project-Id-Version: Nocturne\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-20 18:36+0200\n" "PO-Revision-Date: 2026-07-20 20:27+0200\n" "Last-Translator: Ibai Oihanguren Sala \n" "Language-Team: Basque\n" "Language: eu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 50.0\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Musika;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Bateratu zure musika liburutegia" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern music client" msgstr "Musika bezero modernoa" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Eginbideak" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files" msgstr "Bateragarria Jellyfin, OpenSubsonic, Bandcamp eta fitxategi lokalekin" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "" "Arakatu abestiak, artistak, albumak, irratiak eta erreprodukzio-zerrendak" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Playlist management" msgstr "Kudeatu erreprodukzio-zerrendak" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Audio ekualizadorea eta audio bistaratzailea" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "MPRIS integrazioa" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Navidrome instantzien kudeaketa integratua" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Hitzak automatikoki eskuratzeko aukera" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "Deskargak eta offline modua" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Interfaze itxurosoa" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Webgune nagusia" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Abestien ilara" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:93 src/ui/window.blp:182 #: src/ui/window.blp:234 msgid "Lyrics" msgstr "Hitzak" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Abestien zerrenda" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Albumaren orria" #: data/com.jeffser.Nocturne.metainfo.xml.in:86 msgid "Mini Player" msgstr "Mini-erreproduzitzailea" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "opensubsonic" msgstr "opensubsonic" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "bandcamp" msgstr "bandcamp" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:95 msgid "music" msgstr "musika" #: data/com.jeffser.Nocturne.metainfo.xml.in:96 msgid "gtk" msgstr "gtk" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "Nocturne bilaketa-hornitzailea" #: src/actions.py:141 src/widgets/pages/login.py:117 msgid "Error" msgstr "Errorea" #: src/actions.py:142 msgid "No details provided" msgstr "Ez da xehetasunik eman" #: src/actions.py:144 src/actions.py:295 src/preferences.py:478 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "Itxi" #: src/actions.py:185 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "Ez da behar beste datu aurkitu Nocturne entzunaldirako ({})" #: src/actions.py:292 src/ui/song/row.blp:72 msgid "External File" msgstr "Kanpoko fitxategia" #: src/actions.py:293 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Pista hau kanpoko fitxategi batetik kargatu da, eta beraz, liburutegiko " "pista batek baino eginbide gutxiago izango ditu" #: src/actions.py:313 msgid "Navidrome server deleted successfully" msgstr "Navidrome zerbitzaria behar bezala ezabatu da" #: src/actions.py:322 msgid "Delete Navidrome Server" msgstr "Ezabatu Navidrome zerbitzaria" #: src/actions.py:323 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Ziur Navidrome zerbitzaria ezabatu nahi duzula?" #: src/actions.py:325 src/actions.py:475 src/actions.py:511 src/actions.py:792 #: src/actions.py:884 src/preferences.py:431 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:162 msgid "Cancel" msgstr "Utzi" #: src/actions.py:326 msgid "Keep Data" msgstr "Mantendu datuak" #: src/actions.py:327 msgid "Delete Everything" msgstr "Ezabatu dena" #: src/actions.py:417 msgid "No Name" msgstr "Izenik ez" #: src/actions.py:436 msgid "Radio updated successfully" msgstr "Irratia behar bezala eguneratu da" #: src/actions.py:436 msgid "Radio added successfully" msgstr "Irratia behar bezala gehitu da" #: src/actions.py:447 msgid "Error updating radio" msgstr "Errorea irratia eguneratzean" #: src/actions.py:447 msgid "Error adding radio" msgstr "Errorea irratia gehitzean" #: src/actions.py:460 src/actions.py:784 msgid "Name" msgstr "Izena" #: src/actions.py:465 msgid "Stream Url" msgstr "Jarioaren URLa" #: src/actions.py:472 msgid "Update Radio Station" msgstr "Eguneratu irrati-estazioa" #: src/actions.py:472 msgid "Add Radio Station" msgstr "Gehitu irrati-estazioa" #: src/actions.py:476 src/preferences.py:432 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Gorde" #: src/actions.py:490 msgid "Radio deleted successfully" msgstr "Irratia behar bezala ezabatu da" #: src/actions.py:498 msgid "Error deleting radio" msgstr "Errorea irratia ezabatzean" #: src/actions.py:508 msgid "Delete Radio Station" msgstr "Ezabatu irrati-estazioa" #: src/actions.py:509 src/actions.py:882 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Ziur '{}' ezabatu nahi duzula?" #: src/actions.py:512 src/actions.py:885 src/constants.py:533 #: src/constants.py:581 msgid "Delete" msgstr "Ezabatu" #: src/actions.py:546 src/actions.py:578 src/actions.py:584 src/actions.py:672 #: src/actions.py:733 msgid "Playing Next" msgstr "Hurrena entzungai" #: src/actions.py:558 src/actions.py:597 src/actions.py:603 src/actions.py:681 #: src/actions.py:742 msgid "Playing Later" msgstr "Geroago entzungai" #: src/actions.py:578 src/actions.py:597 src/actions.py:1005 #: src/actions.py:1140 src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} abesti" #: src/actions.py:633 msgid "Lyrics Saved" msgstr "Hitzak gorde dira" #: src/actions.py:764 msgid "Playlist updated successfully" msgstr "Erreprodukzio-zerrenda behar bezala eguneratu da" #: src/actions.py:764 msgid "Playlist created successfully" msgstr "Erreprodukzio-zerrenda behar bezala sortu da" #: src/actions.py:770 msgid "Error updating playlist" msgstr "Errorea erreprodukzio-zerrenda eguneratzean" #: src/actions.py:770 msgid "Error creating playlist" msgstr "Errorea erreprodukzio-zerrenda sortzean" #: src/actions.py:789 msgid "Update Playlist" msgstr "Eguneratu erreprodukzio-zerrenda" #: src/actions.py:789 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Sortu erreprodukzio-zerrenda" #: src/actions.py:793 msgid "Update" msgstr "Eguneratu" #: src/actions.py:793 msgid "Create" msgstr "Sortu" #: src/actions.py:807 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "Abesti {} kendu da" msgstr[1] "{} abesti kendu dira" #: src/actions.py:841 src/actions.py:855 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "Abesti {} gehitu da" msgstr[1] "{} abesti gehitu dira" #: src/actions.py:859 src/actions.py:1010 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "Abesti {} saltatu da" msgstr[1] "{} abesti saltatu dira" #: src/actions.py:869 msgid "Playlist Deleted" msgstr "Erreprodukzio-zerrenda ezabatu da" #: src/actions.py:881 msgid "Delete Playlist" msgstr "Ezabatu erreprodukzio-zerrenda" #: src/actions.py:936 msgid "No songs found" msgstr "Ez da abestirik aurkitu" #: src/actions.py:979 src/actions.py:1006 src/actions.py:1043 #: src/actions.py:1079 msgid "Download Started" msgstr "Deskarga hasi da" #: src/actions.py:986 src/actions.py:1017 src/actions.py:1024 #: src/actions.py:1059 src/actions.py:1095 msgid "Already Downloaded" msgstr "Dagoeneko deskargatua" #: src/actions.py:1011 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "Abesti {} ({})" msgstr[1] "{} abesti ({})" #: src/actions.py:1023 msgid "Download Skipped" msgstr "Deskarga saltatu da" #: src/actions.py:1047 src/actions.py:1083 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "Deskarga hasita (abesti {} saltatu da)" msgstr[1] "Deskarga hasita ({} abesti saltatu dira)" #: src/actions.py:1122 src/actions.py:1141 msgid "Deleted" msgstr "Ezabatuta" #: src/constants.py:343 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Jarraian" #: src/constants.py:347 msgid "Repeat All" msgstr "Errepikatu dena" #: src/constants.py:351 msgid "Repeat One" msgstr "Errepikatu bat" #: src/constants.py:356 #, python-brace-format msgid "Low ({})" msgstr "Baxua ({})" #: src/constants.py:357 #, python-brace-format msgid "Medium ({})" msgstr "Tartekoa ({})" #: src/constants.py:358 #, python-brace-format msgid "High ({})" msgstr "Altua ({})" #: src/constants.py:359 #, python-brace-format msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:360 msgid "Original File" msgstr "Fitxateg. jatorrizkoa" #. Item #: src/constants.py:367 src/ui/pages/home.blp:5 msgid "Home" msgstr "Hasiera" #. Item #. list #: src/constants.py:372 src/integrations/models.py:163 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:279 src/widgets/pages/home.py:54 msgid "Artists" msgstr "Artistak" #. Section #: src/constants.py:379 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:271 #: src/widgets/artist/page.py:53 src/widgets/pages/home.py:49 msgid "Albums" msgstr "Albumak" #. Item #: src/constants.py:382 src/constants.py:417 src/constants.py:437 #: src/window.py:149 msgid "All" msgstr "Guztiak" #. Item #: src/constants.py:387 msgid "Random" msgstr "Ausaz" #. Item #: src/constants.py:392 src/constants.py:422 msgid "Favorites" msgstr "Gogokoak" #. Item #: src/constants.py:397 msgid "Recently Added" msgstr "Gehitu berriak" #. Item #: src/constants.py:402 msgid "Recently Played" msgstr "Entzun berriak" #. Item #: src/constants.py:407 msgid "Most Played" msgstr "Entzunenak" #. Section #: src/constants.py:414 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:263 #: src/widgets/album/page.py:45 src/widgets/pages/home.py:38 #: src/widgets/playlist/page.py:29 msgid "Songs" msgstr "Abestiak" #. Item #: src/constants.py:427 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Irratiak" #. Section #: src/constants.py:434 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:287 #: src/widgets/pages/home.py:59 msgid "Playlists" msgstr "Erreprodukzio-zerrendak" #: src/constants.py:447 src/constants.py:498 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:114 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play" msgstr "Erreproduzitu" #: src/constants.py:452 src/constants.py:485 src/constants.py:508 msgid "Shuffle" msgstr "Nahastu" #: src/constants.py:457 src/constants.py:513 src/constants.py:546 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Next" msgstr "Erreproduzitu hurrena" #: src/constants.py:462 src/constants.py:518 src/constants.py:551 #: src/ui/song/queue.blp:71 src/ui/song/queue.blp:73 msgid "Play Later" msgstr "Erreproduzitu geroago" #: src/constants.py:467 src/constants.py:561 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Add To Playlist" msgstr "Gehitu erreprodukzio-zerrendara" #: src/constants.py:472 src/constants.py:523 src/constants.py:566 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:87 #: src/ui/song/queue.blp:89 msgid "Download" msgstr "Deskargatu" #: src/constants.py:477 src/constants.py:576 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Erakutsi artista" #: src/constants.py:490 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Irratia" #: src/constants.py:503 msgid "Resume" msgstr "Berrekin" #: src/constants.py:528 src/constants.py:556 msgid "Edit" msgstr "Editatu" #: src/constants.py:542 msgid "Select" msgstr "Hautatu" #: src/constants.py:571 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Erakutsi albuma" #: src/constants.py:587 src/ui/song/queue.blp:98 src/ui/song/queue.blp:100 msgid "Remove" msgstr "Kendu" #: src/constants.py:592 src/ui/song/queue.blp:109 src/ui/song/queue.blp:111 msgid "Delete Download" msgstr "Ezabatu deskarga" #: src/constants.py:598 src/main.py:156 msgid "Show Details" msgstr "Erakutsi xehetasunak" #: src/constants.py:609 msgid "Visit Webpage" msgstr "Bisitatu webgunea" #: src/constants.py:615 msgid "Update Server" msgstr "Eguneratu zerbitzaria" #: src/constants.py:620 msgid "Delete Server" msgstr "Ezabatu zerbitzaria" #: src/constants.py:629 msgid "Edit Lyrics" msgstr "Editatu hitzak" #: src/constants.py:634 msgid "Remove Lyrics" msgstr "Ezabatu hitzak" #: src/integrations/base.py:269 msgid "Could not generate SQL database" msgstr "Ezin izan da SQL datu-basea sortu" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "Arakatzen" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "Musika entzuten" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "Konektatu Jellyfin zerbitzari batera." #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "Erabili existitzen den Jellyfin zerbitzari bat" #: src/integrations/jellyfin.py:254 msgid "Could not log in" msgstr "Ezin izan da saioa hasi" #: src/integrations/jellyfin.py:1037 msgid "Unknown" msgstr "Ezezaguna" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:667 msgid "Local Files" msgstr "Fitxategi lokalak" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Utzi Nocturneri fitxategi lokalak zuzenean kargatzen. Liburutegi " "handientzako zerbitzari dedikatu bat erabiltzea komeni da." #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Jarraitu" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "Funtzionalitate mugatua" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "Abestiak kargatzen" #: src/integrations/local.py:138 src/integrations/navidrome.py:759 msgid "Could not locate path, try again" msgstr "Ezin izan da bidea aurkitu, saiatu berriz" #: src/integrations/local.py:316 msgid "No Album" msgstr "Albumik ez" #: src/integrations/local.py:685 src/integrations/local.py:694 msgid "Offline Mode" msgstr "Offline modua" #: src/integrations/local.py:686 msgid "Access your downloads" msgstr "Atzitu zure deskargak" #: src/integrations/models.py:142 src/ui/album/page.blp:5 #: src/widgets/album/page.py:129 msgid "Album" msgstr "Albuma" #: src/integrations/models.py:144 msgid "Display Artist" msgstr "Artistaren izena" #: src/integrations/models.py:146 msgid "MusicBrainz ID" msgstr "MusicBrainz IDa" #: src/integrations/models.py:147 msgid "Track Number" msgstr "Pista zenbakia" #: src/integrations/models.py:148 msgid "Year" msgstr "Urtea" #: src/integrations/models.py:149 msgid "Size" msgstr "Tamaina" #: src/integrations/models.py:150 msgid "Suffix" msgstr "Atzizkia" #: src/integrations/models.py:151 src/widgets/album/button.py:90 #: src/widgets/album/page.py:145 src/widgets/artist/page.py:125 #: src/widgets/playing/control_page.py:189 src/widgets/song/row.py:183 msgid "Favorite" msgstr "Gogokoa" #: src/integrations/models.py:152 msgid "Duration" msgstr "Iraupena" #: src/integrations/models.py:154 msgid "Bit Rate" msgstr "Bit-tasa" #: src/integrations/models.py:155 msgid "Bit Depth" msgstr "Bit sakonera" #: src/integrations/models.py:156 msgid "Sampling Rate" msgstr "Laginketa-tasa" #: src/integrations/models.py:157 msgid "Channel Count" msgstr "Kanal kopurua" #: src/integrations/models.py:159 msgid "Path" msgstr "Bidea" #: src/integrations/models.py:160 msgid "Disc Number" msgstr "Disko zenbakia" #: src/integrations/models.py:161 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:162 msgid "Genres" msgstr "Generoak" #. list #: src/integrations/models.py:164 msgid "Track Gain" msgstr "Pistaren irabazia" #: src/integrations/models.py:165 msgid "Album Gain" msgstr "Albumaren irabazia" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Konektatu OpenSubsonic zerbitzari batera (adb. Navidrome)." #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "Kanpoko zerbitzaria" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic instance" msgstr "Erabili OpenSubsonic instantzia bat" #: src/integrations/navidrome.py:171 msgid "Server returned an error" msgstr "Zerbitzariak errorea itzuli du" #: src/integrations/navidrome.py:171 msgid "Unknown Error" msgstr "Errore ezezaguna" #: src/integrations/navidrome.py:734 src/integrations/navidrome.py:743 msgid "Managed Server" msgstr "Zerbitzari kudeatua" #: src/integrations/navidrome.py:735 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "Konektatu Nocturnek zuzenean kudea dezakeen Navidrome instantzia batera." #: src/integrations/navidrome.py:738 msgid "Manage Server" msgstr "Kudeatu zerbitzaria" #: src/integrations/navidrome.py:744 msgid "Create and use a Navidrome instance" msgstr "Sortu eta erabili Navidrome instantzia" #: src/integrations/navidrome.py:803 msgid "Important: Use your Subsonic login information." msgstr "Garrantzitsua: Erabili zure Subsonic saioa hasteko informazioa" #: src/integrations/navidrome.py:806 msgid "Bandcamp User Settings" msgstr "Bandcampeko erabiltzaile-ezarpenak" #: src/integrations/navidrome.py:812 msgid "Explore your online library" msgstr "Arakatu lineako liburutegia" #: src/main.py:94 msgid "Music is Playing" msgstr "Musika erreproduzitzen" #: src/main.py:107 msgid "Fullscreen Player Active" msgstr "Pantaila osoko erreproduzitzailea aktibo" #: src/main.py:153 msgid "Login Failed" msgstr "Saio hasierak huts egin du" #: src/nocturne_search_provider.py.in:12 src/ui/album/button.blp:21 msgid "Play Album" msgstr "Erreproduzitu albuma" #: src/nocturne_search_provider.py.in:13 src/ui/song/button.blp:16 msgid "Play Song" msgstr "Erreproduzitu abestia" #: src/nocturne_search_provider.py.in:14 src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "Erreproduzitu zerrenda" #: src/nocturne_search_provider.py.in:113 msgid "Open" msgstr "Ireki" #: src/preferences.py:420 msgid "Settings Page" msgstr "Ezarpenen orria" #: src/preferences.py:423 msgid "User Token" msgstr "Erabiltzaile-tokena" #: src/preferences.py:427 msgid "Link ListenBrainz" msgstr "Lotu ListenBrainz" #: src/preferences.py:428 msgid "Connect your ListenBrainz account with a user token" msgstr "Konektatu zure ListenBrainz kontua erabiltzaile-token bidez" #: src/preferences.py:468 msgid "Flatpak Sandbox Warning" msgstr "Flatpak ingurune isolatuaren abisua" #: src/preferences.py:469 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "Discordera konektatzeko baimen gehigarri bat behar da. Ondorengo komandoa " "exekutatu ondoren berrabiarazi Nocturne" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:115 msgid "Toggle star" msgstr "Eman/kendu izarra" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Albumaren azala" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "Sartu albumaren orrira" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "Sartu artistaren orrira" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:228 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "Izar 1" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:237 msgid "2 Stars" msgstr "2 izar" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:246 msgid "3 Stars" msgstr "3 izar" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:255 msgid "4 Stars" msgstr "4 izar" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:264 msgid "5 Stars" msgstr "5 izar" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:130 msgid "Options" msgstr "Aukerak" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Artista" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Biografia hedatua" #: src/ui/containers/carousel.blp:24 src/ui/containers/carousel.blp:26 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Zerrenda" #: src/ui/containers/carousel.blp:41 msgid "Pan to the Left" msgstr "Korritu ezkerrera" #: src/ui/containers/carousel.blp:47 msgid "Pan to the Right" msgstr "Korritu eskumara" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "Deskargatuta" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Kendu ilaratik" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "Deskargen ilara" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "Deskargatutako abestiak erabilgarri daude offline moduan" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Garbitu amaitutako deskargak" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Hitzen editorea" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:187 #: src/ui/playing/footer.blp:129 msgid "Pause" msgstr "Pausatu" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:122 msgid "Current song progress bar" msgstr "Uneko abestiaren aurrerapen-barra" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Ezarri une hau hurrengo lerroaren denbora-markan" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Gehitu lerroa denbora-marka honetan" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Hitzik ez" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "Ez da hitzik aurkitu pista honentzako, gehitu lerro berria hasteko" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Denbora-marka" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Joan denbora-markara" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Ezarri uneko denbora-marka" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Kendu lerroa" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Sareta" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Bilatu albumak" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Zerrendaren amaierara iritsi zara" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "Ez da albumik aurkitu" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Bilatu artistak" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "Ez da artistarik aurkitu" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "Erakutsi / ezkutatu bilatzailea" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "Bilatu" #: src/ui/pages/home.blp:68 msgid "No Elements Found" msgstr "Ez da elementurik aurkitu" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:107 src/ui/pages/login.blp:108 #: src/widgets/pages/login.py:38 src/widgets/pages/login.py:78 msgid "Login" msgstr "Hasi saioa" #: src/ui/pages/login.blp:36 msgid "Server Status" msgstr "Zerbitzariaren egoera" #: src/ui/pages/login.blp:37 src/widgets/pages/login.py:52 msgid "Not Running" msgstr "Ez dago martxan" #: src/ui/pages/login.blp:38 msgid "Restart Instance" msgstr "Berrabiarazi instantzia" #: src/ui/pages/login.blp:50 msgid "Manage Navidrome Server" msgstr "Kudeatu Navidrome zerbitzaria" #: src/ui/pages/login.blp:60 msgid "Url" msgstr "URLa" #: src/ui/pages/login.blp:67 msgid "More Options" msgstr "Aukera gehiago" #: src/ui/pages/login.blp:73 msgid "Trust Server Certificates" msgstr "Fidatu zerbitzariaren ziurtagiriez" #: src/ui/pages/login.blp:81 msgid "Library Directory" msgstr "Liburutegiaren direktorioa" #: src/ui/pages/login.blp:93 msgid "Username" msgstr "Erabiltzaile-izena" #: src/ui/pages/login.blp:97 msgid "Password" msgstr "Pasahitza" #: src/ui/pages/login.blp:117 src/ui/pages/login.blp:118 msgid "Use Quick Connect" msgstr "Erabili konexio bizkorra" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:25 msgid "Playback" msgstr "Erreprodukzioa" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "Joan Hasierara" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "Erakutsi / ezkutatu bistaratzailea" #: src/ui/pages/playback.blp:54 src/window.py:207 msgid "Nocturne Playback" msgstr "Nocturne entzunaldia" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "Hauek dira azken hilean gehien entzun dituzun abestiak" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "Gorde erreprodukzio-zerrenda" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "Erreprodukzio-zerrenda gehitu da" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "Milesker Nocturne erabiltzeagatik!" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Bilatu erreprodukzio-zerrendak" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "Ez da erreprodukzio-zerrendarik aurkitu" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Bilatu irratiak" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Gehitu irratia" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "Ez da irratirik aurkitu" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Konfiguratu" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Navidromeren deskarga" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturnek Navidrome automatikoki deskargatuko du" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Deskargatu (~20 mb)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "Deskargatzen" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Errore bat gertatu da" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Saiatu berriro geroago" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "Behar bezala deskargatu da" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Jarraitu aurretik, ziurtatu administratzaile kontu bat sortu duzula eta " "pista guztiak musika direktorio barruan daudela" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Instantziaren webgunea" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Bilatu abestiak" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:96 #: src/ui/song/queue.blp:136 msgid "No Songs Found" msgstr "Ez da abestirik aurkitu" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Gogoko abestiak" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "Bilatu gogoko abestiak" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Ongi etorri" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Ongi etorri Nocturnera" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "Hautatu instantzia mota hasteko" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Erreproduzitzen" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "Bisitatu uneko irratiaren webgunea" #: src/ui/playing/control_page.blp:157 src/ui/playing/footer.blp:99 msgid "Previous" msgstr "Aurrekoa" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:143 msgid "Next" msgstr "Hurrengoa" #: src/ui/playing/control_page.blp:221 msgid "Rate" msgstr "Puntuatu" #: src/ui/playing/control_page.blp:277 msgid "Star" msgstr "Eman izarra" #: src/ui/playing/control_page.blp:284 msgid "Toggle Sidebar" msgstr "Erakutsi / ezkutatu alboko barra" #: src/ui/playing/cover_art.blp:28 msgid "Audio" msgstr "Audioa" #: src/ui/playing/cover_art.blp:36 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Abestiaren azala" #: src/ui/playing/cover_art.blp:42 src/ui/playing/cover_art.blp:50 msgid "Video" msgstr "Bideoa" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "Aurrezarpenak" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Laua" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Ahots garbia" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "Jazz leuna" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rocka" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Klasikoa" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Akustikoa" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Uneko abestiaren aurrerapen adierazlea" #: src/ui/playing/footer.blp:45 msgid "Current song cover art" msgstr "Uneko abestiaren azala" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "Itzuli" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "Hitzen aukerak" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "Ez da hitzik aurkitu" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "Instrumentala" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "Ez da deskargatu" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "Abesti honen hitzak oraindik ez dira deskargatu, baliteke deskargagarri " "egotea" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "Kargatu hitzen fitxategia" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Leiho gainerakorra" #: src/ui/playing/popout_window.blp:67 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:206 msgid "Song Details" msgstr "Abestiaren xehetasunak" #: src/ui/playing/popout_window.blp:87 src/ui/window.blp:172 #: src/ui/window.blp:228 msgid "Queue" msgstr "Ilara" #: src/ui/playing/popout_window.blp:99 src/ui/window.blp:188 #: src/ui/window.blp:240 msgid "Equalizer" msgstr "Ekualizadorea" #: src/ui/playing/queue_page.blp:20 msgid "Generating Next Queue" msgstr "Hurrengoen ilara sortzen" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Erreprodukzio automatikoa" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "Sortu ilara berria unekoa amaitzean" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Bolumena" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Mututu bozgorailua" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Bozgorailua bolumen osoan" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "Erakutsi erreprodukzio-zerrendak" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Erreprodukzio-zerrendaren azala" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "Sartu erreprodukzio-zerrendaren orrira" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:84 msgid "Playlist" msgstr "Erreprodukzio-zerrenda" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Bilatu (edo sortu erreprodukzio-zerrenda berria)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Musika erreprodukzio-zerrendara gehitzen" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "Sortu zerrenda berria eta gehitu abestiak" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "Orokorra" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Portaera" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "Berreskuratu ilara abioan" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Ezkutatu ixtean" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "Simulatu hitzez hitzeko karaokea" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "Lerroz lerroko hitzak soilik daudenean erabilia" #: src/ui/preferences.blp:22 msgid "Auto Download Lyrics" msgstr "Deskargatu hitzak automatikoki" #: src/ui/preferences.blp:23 msgid "Used when the current instance does not provide lyrics" msgstr "Uneko instantziak hitzak ez dakartzanean erabilia" #: src/ui/preferences.blp:26 msgid "Use Track and Album Gain" msgstr "Erabili pista eta albumen irabazia" #: src/ui/preferences.blp:27 msgid "Adjust the volume based on gain metadata" msgstr "Doitu bolumena irabaziaren metadatua kontuan hartuta" #: src/ui/preferences.blp:30 msgid "Default Page on Login" msgstr "Hasierako orri lehenetsia" #: src/ui/preferences.blp:35 msgid "Maximum Bitrate" msgstr "Gehienezko bit-tasa" #: src/ui/preferences.blp:41 msgid "Gnome Search" msgstr "Gnome bilaketa" #: src/ui/preferences.blp:42 msgid "" "Search for your music directly from the system search bar while Nocturne is " "running. You can enable or disable this feature completely in System Settings" msgstr "" "Bilatu musika zuzenean sistemaren bilaketa-barratik, Nocturne martxan " "dagoenean. Eginbide hau erabat gaitu edo ezgaitu dezakezu sistemaren " "ezarpenetan" #: src/ui/preferences.blp:45 msgid "Include Artists" msgstr "Gehitu artistak" #: src/ui/preferences.blp:48 msgid "Include Albums" msgstr "Gehitu albumak" #: src/ui/preferences.blp:51 msgid "Include Songs" msgstr "Gehitu abestiak" #: src/ui/preferences.blp:54 msgid "Include Playlists" msgstr "Gehitu erreprodukzio-zerrendak" #: src/ui/preferences.blp:58 msgid "Session" msgstr "Saioa" #: src/ui/preferences.blp:61 msgid "Discord Rich Presence" msgstr "Discorden presentzia aberastua" #: src/ui/preferences.blp:62 msgid "Share the currently playing song on Discord" msgstr "Partekatu uneko abestia Discorden" #: src/ui/preferences.blp:66 msgid "Share Cover Art With Discord" msgstr "Partekatu azala Discorden" #: src/ui/preferences.blp:67 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "Discord zerbitzariek ezin badute instantzia atzitu, CoverArtArchive " "erabiliko da trukean" #: src/ui/preferences.blp:73 msgid "ListenBrainz Link" msgstr "ListenBrainz esteka" #: src/ui/preferences.blp:74 msgid "Keep track of your playback history" msgstr "Erregistratu zure erreprodukzioen historia" #: src/ui/preferences.blp:80 src/ui/preferences.blp:81 msgid "Link" msgstr "Lotu" #: src/ui/preferences.blp:89 src/ui/preferences.blp:90 msgid "Unlink" msgstr "Askatu" #: src/ui/preferences.blp:102 msgid "User" msgstr "Erabiltzailea" #: src/ui/preferences.blp:122 msgid "Logout" msgstr "Amaitu saioa" #: src/ui/preferences.blp:132 msgid "Customization" msgstr "Pertsonalizazioa" #: src/ui/preferences.blp:135 msgid "Interface" msgstr "Interfazea" #: src/ui/preferences.blp:137 msgid "Show Context Button in Rows" msgstr "Erakutsi kontestu-botoia lerroetan" #: src/ui/preferences.blp:138 msgid "The menu can also be shown by right clicking / long pressing" msgstr "Menua erakusteko eskuin-klika erabili edo luze sakatu" #: src/ui/preferences.blp:141 msgid "Show Labels in Buttons of Pages" msgstr "Erakutsi orrietako botoien etiketak" #: src/ui/preferences.blp:144 msgid "Show Progressbar in Player Footer" msgstr "Erakutsi aurrerapen-barra erreproduzitzailearen oinean" #: src/ui/preferences.blp:147 msgid "Make Background Translucent in Player" msgstr "Erreproduzitzailearen atzeko plano zeharrargia" #: src/ui/preferences.blp:150 msgid "Use Sidebar Player When Possible" msgstr "Erakutsi erreproduzitzailea alboko barran ahal dela" #: src/ui/preferences.blp:151 msgid "Sidebar player is only available when the window is wide enough" msgstr "" "Alboko barrako erreproduzitzailea leihoa behar bezain zabala denean soilik " "erakuts daiteke" #: src/ui/preferences.blp:154 msgid "Show Pan Buttons on Carousels" msgstr "Erakutsi ezker / eskuin botoiak karruseletan" #: src/ui/preferences.blp:155 msgid "Only visible when a carousel has 5 or more items" msgstr "Karruselak 5 elementutik gora dituenean soilik ikusgai" #: src/ui/preferences.blp:158 msgid "Button Size" msgstr "Botoien tamaina" #: src/ui/preferences.blp:159 msgid "Affects albums, artists, playlists and songs" msgstr "Album, artista, erreprodukzio-zerrenda eta abestiei eragiten die" #: src/ui/preferences.blp:165 src/ui/preferences.blp:166 msgid "Small" msgstr "Txikia" #: src/ui/preferences.blp:170 src/ui/preferences.blp:171 msgid "Big" msgstr "Handia" #: src/ui/preferences.blp:179 msgid "Dynamic Background" msgstr "Atzeko plano dinamikoa" #: src/ui/preferences.blp:180 msgid "Background generated based on the album art of the current song" msgstr "Atzeko planoa uneko abestiaren album azalean oinarrituz sortzen da" #: src/ui/preferences.blp:183 msgid "Use in Main Window" msgstr "Erabili leiho nagusian" #: src/ui/preferences.blp:189 src/ui/preferences.blp:190 #: src/ui/preferences.blp:213 src/ui/preferences.blp:214 #: src/ui/preferences.blp:237 src/ui/preferences.blp:238 msgid "Off" msgstr "Ez" #: src/ui/preferences.blp:194 src/ui/preferences.blp:195 #: src/ui/preferences.blp:218 src/ui/preferences.blp:219 #: src/ui/preferences.blp:242 src/ui/preferences.blp:243 msgid "Gradient" msgstr "Gradientea" #: src/ui/preferences.blp:199 src/ui/preferences.blp:200 #: src/ui/preferences.blp:223 src/ui/preferences.blp:224 #: src/ui/preferences.blp:247 src/ui/preferences.blp:248 msgid "Blur" msgstr "Lausoa" #: src/ui/preferences.blp:207 msgid "Use in Player" msgstr "Erabili erreproduzitzailean" #: src/ui/preferences.blp:231 msgid "Use in Pop-out Window" msgstr "Erabili leiho gainerakorrean" #: src/ui/preferences.blp:255 msgid "Use as Accent Color" msgstr "Erabili nabarmentze kolore gisa" #: src/ui/preferences.blp:260 msgid "Homepage" msgstr "Hasiera orria" #: src/ui/preferences.blp:261 msgid "Reload homepage to see changes" msgstr "Birkargatu hasiera orria aldaketak ikusteko" #: src/ui/preferences.blp:297 msgid "Sidebar" msgstr "Alboko barra" #: src/ui/preferences.blp:301 msgid "Visualizer" msgstr "Bistaratzailea" #: src/ui/preferences.blp:305 src/ui/window.blp:257 msgid "Preferences" msgstr "Hobespenak" #: src/ui/preferences.blp:307 msgid "Show Visualizer in Player" msgstr "Erakutsi espektroa erreproduzitzailean" #: src/ui/preferences.blp:308 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" "Erakutsi audio-espektroaren bistaratzea album azalaren gainean, " "erreproduzitzailean" #: src/ui/preferences.blp:313 msgid "Appearance" msgstr "Itxura" #: src/ui/preferences.blp:317 msgid "Number of Bars" msgstr "Barra kopurua" #: src/ui/preferences.blp:328 msgid "Visualizer Type" msgstr "Bistaratzaile mota" #: src/ui/preferences.blp:334 src/ui/preferences.blp:335 msgid "Wave" msgstr "Uhina" #: src/ui/preferences.blp:339 src/ui/preferences.blp:340 msgid "Bars" msgstr "Barrak" #: src/ui/preferences.blp:344 src/ui/preferences.blp:345 msgid "Particles" msgstr "Partikulak" #: src/ui/preferences.blp:352 msgid "Fill Mode" msgstr "Betetze modua" #: src/ui/preferences.blp:358 src/ui/preferences.blp:359 msgid "Solid" msgstr "Solidoa" #: src/ui/preferences.blp:363 src/ui/preferences.blp:364 msgid "Translucent" msgstr "Zeharrargia" #: src/ui/preferences.blp:368 src/ui/preferences.blp:369 msgid "Border" msgstr "Ertzak" #: src/ui/preferences.blp:377 msgid "Color" msgstr "Kolorea" #: src/ui/preferences.blp:381 msgid "Use Dynamic Color" msgstr "Erabili kolore dinamikoa" #: src/ui/preferences.blp:384 msgid "Invert Color" msgstr "Alderantzi kolorea" #: src/ui/preferences.blp:388 msgid "Manual Color" msgstr "Kolore zehatza" #: src/ui/preferences.blp:394 msgid "Visualizer Color" msgstr "Bistaratzailearen kolorea" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Shortcuts" msgstr "Erakutsi laster-teklak" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Preferences" msgstr "Erakutsi hobespenak" #: src/ui/shortcuts-dialog.blp:20 msgid "Quit" msgstr "Irten" #: src/ui/shortcuts-dialog.blp:27 msgid "Toggle Playback" msgstr "Erreproduzitu / Gelditu" #: src/ui/shortcuts-dialog.blp:31 msgid "Next Song" msgstr "Hurrengo abestia" #: src/ui/shortcuts-dialog.blp:35 msgid "Previous Song" msgstr "Aurreko abestia" #: src/ui/shortcuts-dialog.blp:39 msgid "Raise Volume" msgstr "Igo bolumena" #: src/ui/shortcuts-dialog.blp:43 msgid "Lower Volume" msgstr "Jaitsi bolumena" #: src/ui/shortcuts-dialog.blp:48 msgid "Window" msgstr "Leihoa" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:267 #: src/widgets/playing/popout_window.py:48 msgid "Toggle Fullscreen" msgstr "Pantaila osoa gaitu / ezgaitu" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:262 msgid "Open Pop-Out Window" msgstr "Ireki leiho gainerakorra" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Abestiaren azala" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "Erreprodukzio-adierazlea" #: src/ui/song/queue.blp:47 src/ui/song/queue.blp:49 msgid "Select All" msgstr "Hautatu dena" #: src/ui/song/row.blp:44 msgid "Drag icon" msgstr "Arrastatze ikonoa" #: src/ui/song/row.blp:142 msgid "Selection box" msgstr "Hautapen kutxa" #: src/ui/window.blp:91 msgid "Play Random Queue" msgstr "Erreproduzitu ausazko ilara" #: src/ui/window.blp:99 msgid "Menu" msgstr "Menua" #: src/ui/window.blp:127 msgid "Random Albums" msgstr "Albumak ausaz" #: src/ui/window.blp:131 msgid "Favorite Albums" msgstr "Gogoko albumak" #: src/ui/window.blp:135 msgid "Newest Albums" msgstr "Album berrienak" #: src/ui/window.blp:139 msgid "Recent Albums" msgstr "Oraintsuko albumak" #: src/ui/window.blp:143 msgid "Frequent Albums" msgstr "Ohiko albumak" #: src/ui/window.blp:166 msgid "Player" msgstr "Erreproduzitzailea" #: src/ui/window.blp:272 msgid "See Last Nocturne Playback" msgstr "Ikusi Nocturneko azken entzunaldia" #: src/ui/window.blp:277 msgid "Keyboard Shortcuts" msgstr "Teklatuko lasterbideak" #: src/ui/window.blp:282 msgid "About Nocturne" msgstr "Nocturneri buruz" #: src/widgets/album/button.py:95 src/widgets/album/page.py:150 #: src/widgets/artist/page.py:130 src/widgets/playing/control_page.py:194 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "Ez gogokoa" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "{} diskoa" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "Album {}" msgstr[1] "{} album" #: src/widgets/artist/page.py:48 msgid "Top Songs" msgstr "Abesti entzunenak" #: src/widgets/artist/page.py:58 msgid "Related Artists" msgstr "Artista erlazionatuak" #: src/widgets/artist/page.py:113 msgid "Author" msgstr "Egilea" #: src/widgets/containers/context.py:50 #, python-brace-format msgid "{} Stars" msgstr "{} izar" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Denbora-markarik ez" #: src/widgets/pages/login.py:52 src/widgets/pages/login.py:115 msgid "Running" msgstr "Martxan" #: src/widgets/pages/login.py:86 msgid "Extra Menu" msgstr "Menu estra" #: src/widgets/pages/login.py:104 src/widgets/pages/setup.py:83 msgid "Music Library" msgstr "Musika-liburutegia" #: src/widgets/pages/login.py:114 msgid "Restarted" msgstr "Berrabiarazia" #: src/widgets/pages/login.py:153 msgid "Quick Connect" msgstr "Konexio bizkorra" #: src/widgets/pages/login.py:154 msgid "Error getting code" msgstr "Errorea kodea lortzean" #: src/widgets/pages/login.py:156 msgid "Quick Connect Page" msgstr "Konexio bizkorrerako orria" #: src/widgets/pages/playback.py:29 #, python-brace-format msgid "Nocturne Playback ~ {}" msgstr "Nocturne erreprodukzioa ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 #, python-brace-format msgid "{} Plays" msgstr "{} erreprodukzio" #: src/widgets/pages/setup.py:70 msgid "Installing" msgstr "Instalatzen" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integrazioa" #: src/widgets/playing/lyrics_page.py:246 msgid "LRC File" msgstr "LRC fitxategia" #: src/widgets/playing/player.py:621 msgid "Warning: Song changed but volume is set to 0" msgstr "Abisua: Abestia aldatu da, baina bolumena zeroan dago" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:116 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "Abesti {}" msgstr[1] "{} abesti" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Erreprodukzio-zerrenda berria" #: src/widgets/playlist/selector_row.py:33 #, python-brace-format msgid "Add Songs to '{}'" msgstr "Gehitu abestiak hona: '{}'" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "Bai" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "Ez" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "Ez da aurkitu" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "Hainbat artista" #: src/window.py:208 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "{}(e)rako erreprodukzio-ilara prest dago!" #: src/window.py:210 msgid "Show" msgstr "Erakutsi" #: src/window.py:211 msgid "Later" msgstr "Geroago" #~ msgid "A modern Navidrome / Jellyfin client" #~ msgstr "Navidrome / Jellyfin bezero modernoa" #~ msgid "Compatibility with Jellyfin, OpenSubsonic and local files" #~ msgstr "Jellyfin, OpenSubsonic eta fitxategi lokalekin bateragarria" #~ msgid "Use an existing OpenSubsonic / Navidrome instance" #~ msgstr "Erabili existitzen den OpenSubsonic / Navidrome instantzia bat" #~ msgid "Show Sidebar" #~ msgstr "Erakutsi alboko barra" #~ msgid "Experimental" #~ msgstr "Esperimentala" #~ msgid "Local Music Library" #~ msgstr "Musika liburutegi lokala" #~ msgid "Not enough songs found for Playback ({})" #~ msgstr "Ez dira erreprodukziorako behar beste abesti aurkitu ({})" #~ msgid "Generating next queue" #~ msgstr "Hurrengo ilara sortzen" #~ msgid "Create new playlist and add songs" #~ msgstr "Sortu zerrenda berria eta gehitu abestiak" #~ msgid "Add songs to '{}'" #~ msgstr "Gehitu abestiak hona: '{}'" #~ msgid "{} Songs Removed" #~ msgstr "{} abesti kendu dira" #~ msgid "Song Removed" #~ msgstr "Abestia kendu da" #~ msgid "{} Songs Added" #~ msgstr "{} abesti gehitu dira" #~ msgid "1 Song Added" #~ msgstr "Abesti 1 gehitu da" #~ msgid "1 Song Skipped" #~ msgstr "Abesti 1 saltatu da" #~ msgid "{} Songs Skipped" #~ msgstr "{} abesti saltatu dira" #~ msgid "{} Songs ({})" #~ msgstr "{} abesti ({})" #~ msgid "1 Song ({})" #~ msgstr "Abesti 1 ({})" #~ msgid "Download Started ({} Songs Skipped)" #~ msgstr "Deskarga hasi da ({} abesti saltatu dira)" #~ msgid "Download Started (1 Song Skipped)" #~ msgstr "Deskarga hasi da (abesti 1 saltatu da)" #~ msgid "bit Rate" #~ msgstr "bit-tasa" #~ msgid "bit Depth" #~ msgstr "bit-sakonera" #~ msgid "sampling Rate" #~ msgstr "laginketa-tasa" #~ msgid "Show Playlists in Sidebar" #~ msgstr "Erakutsi erreprodukzio-zerrendak alboko barran" #~ msgid "1 Album" #~ msgstr "Album 1" #~ msgid "{} Albums" #~ msgstr "{} album" #~ msgid "1 Song" #~ msgstr "Abesti 1" #~ msgid "Shortcuts" #~ msgstr "Laster-teklak" Jeffser-Nocturne-b442fee/po/hr.po000066400000000000000000001144401523240030000167460ustar00rootroot00000000000000# Croatian translation for Nocturne. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the Nocturne package. # Milo Ivir , 2026. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-05 18:30-0600\n" "PO-Revision-Date: 2026-05-16 22:33+0200\n" "Last-Translator: Milo Ivir \n" "Language-Team: \n" "Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 3.9\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Glazba;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Objedini svz svoju glazbu u jednu fonoteku" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "Moderan Navidrome / Jellyfin klijent" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Znaฤajke" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "Istraลพivanje po pjesmama, izvoฤ‘aฤima, albumima, radijima i playlistama" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "Upravljanje playlistama" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "Kompatibilnost s Jellyfin, OpenSubsonic i lokalnim datotekama" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Audio ekvilajzer i audio vizualizator" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Mpris integracija" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Integrirano upravljanje Navidrome instancama" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Automatsko dohvaฤ‡anje tekstova pjesama" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "Preuzimanja i offline modus" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Super suฤelje" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Poฤetna web-stranica" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Red pjesama" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:170 #: src/ui/window.blp:222 msgid "Lyrics" msgstr "Tekstovi" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Popis pjesama" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Stranica albuma" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "glazba" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:211 src/ui/song/row.blp:69 msgid "External File" msgstr "Eksterna datoteka" #: src/actions.py:212 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Ova je pjesma uฤitana iz eksterne datoteke, ลกto znaฤi da ฤ‡e imati manje " "funkcija u usporedbi s pjesmom unutar fonoteke" #: src/actions.py:214 src/ui/song/queue.blp:40 msgid "Close" msgstr "Zatvori" #: src/actions.py:232 msgid "Navidrome server deleted successfully" msgstr "Navidrome server je uspjeลกno izbrisan" #: src/actions.py:241 msgid "Delete Navidrome Server" msgstr "Izbriลกi Navidrome server" #: src/actions.py:242 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Stvarno ลพeliลก izbrisati integrirani Navidrome server?" #: src/actions.py:244 src/actions.py:355 src/actions.py:389 src/actions.py:674 #: src/actions.py:790 src/preferences.py:322 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "Odustani" #: src/actions.py:245 msgid "Keep Data" msgstr "Zadrลพi podatke" #: src/actions.py:246 msgid "Delete Everything" msgstr "Izbriลกi sve" #: src/actions.py:301 msgid "No Name" msgstr "Bez imena" #: src/actions.py:320 msgid "Radio updated successfully" msgstr "Radio je uspjeลกno aktualiziran" #: src/actions.py:320 msgid "Radio added successfully" msgstr "Radio je uspjeลกno dodan" #: src/actions.py:331 msgid "Error updating radio" msgstr "Greลกka prilikom aktuliziranja radija" #: src/actions.py:331 msgid "Error adding radio" msgstr "Greลกka prilikom dodavanja radija" #: src/actions.py:340 src/actions.py:666 msgid "Name" msgstr "Ime" #: src/actions.py:345 msgid "Stream Url" msgstr "URL streama" #: src/actions.py:352 msgid "Update Radio Station" msgstr "Aktulaiziraj radio stanicu" #: src/actions.py:352 msgid "Add Radio Station" msgstr "Dodaj radio stanicu" #: src/actions.py:356 src/preferences.py:323 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Spremi" #: src/actions.py:372 msgid "Radio deleted successfully" msgstr "Radio je uspjeลกno izbrisan" #: src/actions.py:380 msgid "Error deleting radio" msgstr "Greลกka prilikom brisanja radija" #: src/actions.py:386 msgid "Delete Radio Station" msgstr "Izbriลกi radio stanicu" #: src/actions.py:387 src/actions.py:788 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Stvarno ลพeliลก izbrisati โ€ž{}โ€?" #: src/actions.py:390 src/actions.py:791 src/constants.py:343 #: src/constants.py:396 msgid "Delete" msgstr "Izbriลกi" #: src/actions.py:420 src/actions.py:447 src/actions.py:452 src/actions.py:541 #: src/actions.py:600 msgid "Playing Next" msgstr "Svira se kao sljedeฤ‡a" #: src/actions.py:430 src/actions.py:463 src/actions.py:468 src/actions.py:556 #: src/actions.py:615 msgid "Playing Later" msgstr "Svira se kasnije" #: src/actions.py:447 src/actions.py:463 src/actions.py:905 src/actions.py:1040 #: src/widgets/playlist/button.py:50 src/widgets/playlist/button.py:52 #: src/widgets/playlist/dialog.py:31 src/widgets/playlist/page.py:118 #: src/widgets/playlist/page.py:120 src/widgets/playlist/row.py:49 #: src/widgets/playlist/row.py:51 src/widgets/playlist/selector_row.py:38 #: src/widgets/playlist/selector_row.py:40 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "{} pjesma" msgstr[1] "{} pjesme" msgstr[2] "{} pjesama" #: src/actions.py:497 msgid "Lyrics Saved" msgstr "Tekstovi pjesama su spremnljeni" #: src/actions.py:647 msgid "Playlist updated successfully" msgstr "Playlista je uspjeลกno aktulizirana" #: src/actions.py:647 msgid "Playlist created successfully" msgstr "Playlista je uspjeลกno stvoren" #: src/actions.py:657 msgid "Error updating playlist" msgstr "Greลกka prilikom aktuliziranja playliste" #: src/actions.py:657 msgid "Error creating playlist" msgstr "Greลกka prilikom stvarnja playliste" #: src/actions.py:671 msgid "Update Playlist" msgstr "Aktuliziraj playlistu" #: src/actions.py:671 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Stvori playlistu" #: src/actions.py:675 msgid "Update" msgstr "Aktualiziraj" #: src/actions.py:675 msgid "Create" msgstr "Stvori" #: src/actions.py:695 src/actions.py:700 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "Uklonjena je {} pjesma" msgstr[1] "Uklonjene su {} pjesme" msgstr[2] "Uklonjeno je {} pjesama" #: src/actions.py:732 src/actions.py:734 src/actions.py:754 src/actions.py:756 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "Dodana je {} pjesma" msgstr[1] "Dodane su {} pjesme" msgstr[2] "Dodano je {} pjesama" #: src/actions.py:761 src/actions.py:877 src/actions.py:910 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "Preskoฤena je {} pjesma" msgstr[1] "Preskoฤene su {} pjesme" msgstr[2] "Preskoฤeno je {} pjesama" #: src/actions.py:775 msgid "Playlist Deleted" msgstr "Playlista je izbrisana" #: src/actions.py:787 msgid "Delete Playlist" msgstr "Izbriลกi playlistu" #: src/actions.py:836 msgid "No songs found" msgstr "Nema pjesama" #: src/actions.py:879 src/actions.py:906 src/actions.py:943 src/actions.py:979 msgid "Download Started" msgstr "Preuzimanje je poฤelo" #: src/actions.py:886 src/actions.py:917 src/actions.py:924 src/actions.py:959 #: src/actions.py:995 msgid "Already Downloaded" msgstr "Veฤ‡ je preuzeto" #: src/actions.py:911 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} pjesma ({})" msgstr[1] "{} pjesme ({})" msgstr[2] "{} pjesama ({})" #: src/actions.py:923 msgid "Download Skipped" msgstr "Preuzimanje je preskoฤeno" #: src/actions.py:947 src/actions.py:983 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "Preuzimanje je zapoฤelo ({} pjesma je preskoฤena)" msgstr[1] "Preuzimanje je zapoฤelo ({} pjesme su preskoฤene)" msgstr[2] "Preuzimanje je zapoฤelo ({} pjesama je preskoฤeno)" #: src/actions.py:1022 src/actions.py:1041 msgid "Deleted" msgstr "Izbrisano" #: src/constants.py:159 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Uzastopno" #: src/constants.py:163 msgid "Repeat All" msgstr "Ponovi sve" #: src/constants.py:167 msgid "Repeat One" msgstr "Ponovi jednu" #: src/constants.py:172 #, python-brace-format msgid "Low ({})" msgstr "Nisko ({})" #: src/constants.py:173 #, python-brace-format msgid "Medium ({})" msgstr "Srednje ({})" #: src/constants.py:174 #, python-brace-format msgid "High ({})" msgstr "Visoko ({})" #: src/constants.py:175 #, python-brace-format msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:176 msgid "Original File" msgstr "Izvorna datoteka" #. Item #: src/constants.py:183 src/ui/pages/home.blp:5 msgid "Home" msgstr "Poฤetna" #. Item #: src/constants.py:188 src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:223 src/widgets/pages/home.py:62 msgid "Artists" msgstr "Izvoฤ‘aฤi" #. Item #. Section #: src/constants.py:193 src/constants.py:255 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:231 #: src/widgets/pages/home.py:74 msgid "Playlists" msgstr "Playliste" #. Section #: src/constants.py:200 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:215 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:50 msgid "Albums" msgstr "Albumi" #. Item #: src/constants.py:203 src/constants.py:238 src/window.py:135 msgid "All" msgstr "Sve" #. Item #: src/constants.py:208 msgid "Random" msgstr "Sluฤajno" #. Item #: src/constants.py:213 src/constants.py:243 msgid "Favorites" msgstr "Favoriti" #. Item #: src/constants.py:218 msgid "Recently Added" msgstr "Nedavno dodano" #. Item #: src/constants.py:223 msgid "Recently Played" msgstr "Nedavno svirano" #. Item #: src/constants.py:228 msgid "Most Played" msgstr "Najsluลกanije" #. Section #: src/constants.py:235 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:207 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "Pjesme" #. Item #: src/constants.py:248 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Radio stanice" #: src/constants.py:262 src/constants.py:313 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "Sviraj" #: src/constants.py:267 src/constants.py:300 src/constants.py:318 msgid "Shuffle" msgstr "Promijeลกaj" #: src/constants.py:272 src/constants.py:323 src/constants.py:356 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "Sviraj sljedeฤ‡u" #: src/constants.py:277 src/constants.py:328 src/constants.py:361 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "Sviraj kasnije" #: src/constants.py:282 src/constants.py:376 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "Dodaj u playlistu" #: src/constants.py:287 src/constants.py:333 src/constants.py:381 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "Preuzmi" #: src/constants.py:292 src/constants.py:391 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "Prikaลพi izvoฤ‘aฤa" #: src/constants.py:305 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:108 msgid "Radio" msgstr "Radio" #: src/constants.py:338 src/constants.py:366 msgid "Edit" msgstr "Uredi" #: src/constants.py:352 msgid "Select" msgstr "Odaberi" #: src/constants.py:371 msgid "Edit Lyrics" msgstr "Uredi tekstove pjesama" #: src/constants.py:386 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "Prikaลพi album" #: src/constants.py:402 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Ukloni" #: src/constants.py:407 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "Izbriลกi preuzimanje" #: src/constants.py:416 msgid "Visit Webpage" msgstr "Posjeti web stranicu" #: src/constants.py:422 msgid "Update Server" msgstr "Aktualiziraj server" #: src/constants.py:427 msgid "Delete Server" msgstr "Izbriลกi server" #: src/integrations/jellyfin.py:15 msgid "Connect to a Jellyfin server." msgstr "Poveลพi se na jedan Jellyfin server." #: src/integrations/jellyfin.py:19 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:20 msgid "Use an existing Jellyfin instance" msgstr "Koristi jednu postojeฤ‡u Jellyfin instancu" #: src/integrations/local.py:17 src/integrations/local.py:23 #: src/integrations/local.py:509 msgid "Local Files" msgstr "Lokalne datoteke" #: src/integrations/local.py:18 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Neka Nocturne uฤita tvoje lokalne datoteke izravno, za velike fonoteke " "preporuฤuje se koriลกtenje namjenskog servera." #: src/integrations/local.py:20 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Nastavi" #: src/integrations/local.py:24 msgid "Limited functionality" msgstr "Ograniฤena funkcionalnost" #: src/integrations/local.py:36 msgid "Loading Songs" msgstr "Uฤitavanje pjesama" #: src/integrations/local.py:215 msgid "No Album" msgstr "Nema albuma" #: src/integrations/local.py:525 src/integrations/local.py:534 msgid "Offline Mode" msgstr "Offline modus" #: src/integrations/local.py:526 msgid "Access your downloads" msgstr "Pristupi svojim preuzimanjima" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Poveลพi se na jedan OpenSubsonic server poput Navidromea." #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "Eksterni server" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "Koristi jednu postojeฤ‡u OpenSubsonic / Navidrome instancu" #: src/integrations/navidrome.py:550 src/integrations/navidrome.py:559 msgid "Managed Server" msgstr "Upravljani server" #: src/integrations/navidrome.py:551 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "Poveลพi se na jednu Navidrome instancu kojom izravno upravlja Nocturne." #: src/integrations/navidrome.py:554 msgid "Manage Server" msgstr "Upravljaj serverom" #: src/integrations/navidrome.py:560 msgid "Create and use a Navidrome instance" msgstr "Stvori i koristi jednu Navidrome instancu" #: src/main.py:69 msgid "Music is Playing" msgstr "Glazba svira" #: src/main.py:116 msgid "Login Failed" msgstr "Prijava nije uspjela" #: src/preferences.py:311 msgid "Settings Page" msgstr "Stranica s postavkama" #: src/preferences.py:314 msgid "User Token" msgstr "Korisniฤki token" #: src/preferences.py:318 msgid "Link ListenBrainz" msgstr "Poveลพi ListenBrainz" #: src/preferences.py:319 msgid "Connect your ListenBrainz account with a user token" msgstr "Poveลพi svoj ListenBrainz raฤun s korisniฤkim tokenom" #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "Sviraj album" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "Uklj./Isklj. zvijezde" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Omot albuma" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "Upiลกi stranicu albuma" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "Upiลกi stranicu izvoฤ‘aฤa" #: src/ui/album/page.blp:5 src/widgets/album/page.py:131 msgid "Album" msgstr "Album" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "1 zvjezda" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "2 zvjezde" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "3 zvjezde" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "4 zvjezde" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "5 zvjezdq" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "Opcije" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Umjetnik" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Proลกiri biografiju" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Popis" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "Preuzeto" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Ukloni iz reda" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "Red preuizamanja" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "Preuzete pjesme su dostupne u offline modusu" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Izbriลกi zavrลกena preuzimanja" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Ureฤ‘ivaฤ tekstova pjesama" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "Pauza" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "Traka napretka trenutaฤne pjesme" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Postavi vremensku oznaku sljedeฤ‡eg retka na Sada" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Dodaj redak u vremensku oznaku" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Nema tekstova pjesama" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "Nema teksta za ovu pjesmu. Zapoฤni dodavanjem novog retka" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Vremenska oznaka" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Idi na vremensku oznaku" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Postavi te trenutalnu vremensku oznaku" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Ukloni redak" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Mreลพa" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Pretraลพi albume" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Dosegnut kraj popisa" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "Nema albuma" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Pretraลพi izvoฤ‘aฤe" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "Nema izvoฤ‘aฤa" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "Nema elemenata" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "Prijava" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "Stanje servera" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "Ne pokreฤ‡e se" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "Ponovno pokretanje instancw" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Upravljanje Navidrome serverom" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "Url" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "Viลกe opcija" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Certifikati povjerljivih servera" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Direktorij fonoteke" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Korisniฤko ime" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Lozinka" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "Koristi bro povezivanje" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Pretraลพi playliste" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "Nema playlisti" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Pretraลพi radija" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Dodaj radio" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "Nema radija" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Postavi" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Navidrome preuzimanje" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne ฤ‡e automatski preuzeti Navidrome" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Preuzimanje (~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "Preuzimanje" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Dogodila se greลกka" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Pokuลกaj ponovo kasnije" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "Preuzimanje je uspjelo" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Prije nego ลกto nastaviลก, pobrini se da stvoriลก administratorski raฤun i " "provjeri jesu li sve tvoje pjesme u mapi s glazbom" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Instance web-stranice" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Pretraลพi pjesme" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "Nema pjesama" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Favoritne pjesme" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "Pretraลพi favoritne pjesme" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Pozdrav" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Pozdrav u Nocrturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "Zapoฤni s biranjem vrste instance" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Trenutaฤno svira" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "Omot pjesme" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "Posjeti poฤetnu web-stranicu aktualnog radija" #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "Prethodna" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "Sljedeฤ‡e" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "Zvijezda" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "Prikaลพi boฤnu traku" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "Predodreฤ‘ene postavke" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Flat" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Vokalna glazba" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "Lagani Jazz" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rock" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Klasiฤna" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Akustiฤna" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Indikator trenutaฤnog napretka pjesme" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "Naslovnica trenutaฤne pjesme" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "Idi natrag" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "Nema tekstova pjesama" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Instrumentalna" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "Nije preuzeto" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "Tekstovi pjesama ove pjesme joลก nisu preuzeti, moลพda su dostupni za " "preuzimanje" #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "Uฤitaj datoteku tekstova pjesama" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Iskoฤni prozor" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:154 msgid "Player" msgstr "Player" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:16 #: src/ui/window.blp:255 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "Uklj./Isklj. cjeloekranski prikaz" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:194 msgid "Song Details" msgstr "Detalji pjesme" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:160 #: src/ui/window.blp:216 msgid "Queue" msgstr "Red" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:176 #: src/ui/window.blp:228 msgid "Equalizer" msgstr "Ekvilajzer" #: src/ui/playing/queue_page.blp:20 msgid "Generating next queue" msgstr "Generiranje daljnjeg reda" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Automatska reprodukcija" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "Generiraj novi red kada trenutaฤni zavrลกi" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Glasnoฤ‡a" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Zvuฤnik je iskljuฤen" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Zvuฤnik je na punoj glasnoฤ‡i" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "Sviraj playlistu" #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "Prujaลพi playlistu" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Naslovnica playliste" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "Upiลกi stranicu playliste" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "Playlista" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Traลพi (ili stvori novu playlistu)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Dodavanje glazbe u playlistu" #: src/ui/playlist/dialog.blp:34 msgid "Create new playlist and add songs" msgstr "Stvori novu playlistu i dodaj pjesme" #: src/ui/preferences.blp:7 msgid "General" msgstr "Opฤ‡e" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Ponaลกanje" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "Obnovi red prilikom pokretanja" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Sakrij pri zatvaranju" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "Simuliraj tekstove pjesama rijeฤ po rijeฤ" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "Koristi se samo kada su dostupni tekstovi pjesama redak po redak" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "Standardna stranica za prijavu" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "Maksimalna brzina prijenosa" #: src/ui/preferences.blp:33 msgid "Session" msgstr "Sesija" #: src/ui/preferences.blp:36 msgid "ListenBrainz Link" msgstr "ListenBrainz poveznica" #: src/ui/preferences.blp:37 msgid "Keep track of your playback history" msgstr "Prati svoju povijest reprodukcija" #: src/ui/preferences.blp:43 src/ui/preferences.blp:44 msgid "Link" msgstr "Poveลพi" #: src/ui/preferences.blp:52 src/ui/preferences.blp:53 msgid "Unlink" msgstr "Odspoji" #: src/ui/preferences.blp:65 msgid "User" msgstr "Korisnik" #: src/ui/preferences.blp:85 msgid "Logout" msgstr "Odjava" #: src/ui/preferences.blp:95 msgid "Customization" msgstr "Prilagoฤ‘avanje" #: src/ui/preferences.blp:98 msgid "Interface" msgstr "Suฤelje" #: src/ui/preferences.blp:100 msgid "Show Context Button in Rows" msgstr "Prikaลพi gumb za kontekst u redovima" #: src/ui/preferences.blp:101 msgid "The menu can also be shown by right clicking / long pressing" msgstr "Izbornik se takoฤ‘er moลพe prikazati desnim klikom / dugim pritiskom" #: src/ui/preferences.blp:104 msgid "Show Labels in Buttons of Pages" msgstr "Prikaลพi oznake u gumbima stranica" #: src/ui/preferences.blp:107 msgid "Show Playlists in Sidebar" msgstr "Prikaลพi playliste u boฤnoj traci" #: src/ui/preferences.blp:110 msgid "Show Progressbar in Player Footer" msgstr "Prikaลพi traku napretka u podnoลพju playera" #: src/ui/preferences.blp:113 msgid "Make Background Translucent in Player" msgstr "Postavi pozadinu providnom u playeru" #: src/ui/preferences.blp:116 msgid "Use Sidebar Player When Possible" msgstr "Koristi player u boฤnoj traci kad god je moguฤ‡e" #: src/ui/preferences.blp:117 msgid "Sidebar player is only available when the window is wide enough" msgstr "Player u boฤnoj traci je dostupan samo kada je prozor dovoljno ลกirok" #: src/ui/preferences.blp:122 msgid "Dynamic Background" msgstr "Dinamiฤna pozadina" #: src/ui/preferences.blp:123 msgid "Background generated based on the album art of the current song" msgstr "Pozadina je generirana na temelju omota albuma trenutaฤne pjesme" #: src/ui/preferences.blp:126 msgid "Use in Main Window" msgstr "Koristi u glavnom prozoru" #: src/ui/preferences.blp:127 msgid "Experimental" msgstr "Eksperimentalno" #: src/ui/preferences.blp:133 src/ui/preferences.blp:134 #: src/ui/preferences.blp:157 src/ui/preferences.blp:158 #: src/ui/preferences.blp:181 src/ui/preferences.blp:182 msgid "Off" msgstr "Iskljuฤeno" #: src/ui/preferences.blp:138 src/ui/preferences.blp:139 #: src/ui/preferences.blp:162 src/ui/preferences.blp:163 #: src/ui/preferences.blp:186 src/ui/preferences.blp:187 msgid "Gradient" msgstr "Gradijent" #: src/ui/preferences.blp:143 src/ui/preferences.blp:144 #: src/ui/preferences.blp:167 src/ui/preferences.blp:168 #: src/ui/preferences.blp:191 src/ui/preferences.blp:192 msgid "Blur" msgstr "Zamuฤ‡eno" #: src/ui/preferences.blp:151 msgid "Use in Player" msgstr "Koristi u playeru" #: src/ui/preferences.blp:175 msgid "Use in Pop-out Window" msgstr "Koristi u iskoฤnom prozoru" #: src/ui/preferences.blp:199 msgid "Use as Accent Color" msgstr "Koristi boju za isticanje" #: src/ui/preferences.blp:204 msgid "Homepage" msgstr "Poฤetna web-stranica" #: src/ui/preferences.blp:205 msgid "Reload homepage to see changes" msgstr "Za prikaz promjena ponovo uฤitaj web-stranicu" #: src/ui/preferences.blp:241 msgid "Visualizer" msgstr "Vizualizator" #: src/ui/preferences.blp:245 src/ui/window.blp:245 msgid "Preferences" msgstr "Postavke" #: src/ui/preferences.blp:247 msgid "Show Visualizer in Player" msgstr "Prikaลพi vizualizator u playeru" #: src/ui/preferences.blp:248 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "Prikaลพi vizualizator audio spektra iznad omota albuma u playeru" #: src/ui/preferences.blp:253 msgid "Appearance" msgstr "Izgled" #: src/ui/preferences.blp:257 msgid "Number of Bars" msgstr "Broj traka" #: src/ui/preferences.blp:268 msgid "Visualizer Type" msgstr "Vrsta vizualizatora" #: src/ui/preferences.blp:274 src/ui/preferences.blp:275 msgid "Wave" msgstr "Valovi" #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Bars" msgstr "Trake" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Particles" msgstr "ฤŒestice" #: src/ui/preferences.blp:292 msgid "Fill Mode" msgstr "Modes ispune" #: src/ui/preferences.blp:298 src/ui/preferences.blp:299 msgid "Solid" msgstr "Puna boja" #: src/ui/preferences.blp:303 src/ui/preferences.blp:304 msgid "Translucent" msgstr "Providno" #: src/ui/preferences.blp:308 src/ui/preferences.blp:309 msgid "Border" msgstr "Rub" #: src/ui/preferences.blp:317 msgid "Color" msgstr "Boja" #: src/ui/preferences.blp:321 msgid "Use Dynamic Color" msgstr "Koristiu dinaminฤnu boju" #: src/ui/preferences.blp:324 msgid "Invert Color" msgstr "Invertiraj boju" #: src/ui/preferences.blp:328 msgid "Manual Color" msgstr "Ruฤna boja" #: src/ui/preferences.blp:334 msgid "Visualizer Color" msgstr "Vizualizator" #: src/ui/shortcuts-dialog.blp:6 msgid "Shortcuts" msgstr "Preฤaci" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "Prikaลพi preฤace" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "Prikaลพi postavke" #: src/ui/shortcuts-dialog.blp:20 src/ui/window.blp:250 msgid "Open Pop-Out Window" msgstr "Otvori iskoฤni prozor" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "Zatvori aplikaciju" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "Povuci ikonu" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "Izbornik" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Naslovnica pjesme" #: src/ui/window.blp:74 msgid "Play Random Queue" msgstr "Sviraj red sluฤajnim redoslijedom" #: src/ui/window.blp:87 msgid "Menu" msgstr "Izbornik" #: src/ui/window.blp:115 msgid "Random Albums" msgstr "Sluฤajni albumi" #: src/ui/window.blp:119 msgid "Favorite Albums" msgstr "Favoritni albumi" #: src/ui/window.blp:123 msgid "Newest Albums" msgstr "Najnoviji albumi" #: src/ui/window.blp:127 msgid "Recent Albums" msgstr "Nedavni albumi" #: src/ui/window.blp:131 msgid "Frequent Albums" msgstr "ฤŒesti albumi" #: src/ui/window.blp:260 msgid "Keyboard Shortcuts" msgstr "Tipkovni preฤaci" #: src/ui/window.blp:265 msgid "About Nocturne" msgstr "O aplikaciji Nocturne" #: src/widgets/album/button.py:63 src/widgets/album/page.py:147 #: src/widgets/artist/page.py:127 src/widgets/playing/control_page.py:217 #: src/widgets/song/row.py:162 msgid "Favorite" msgstr "Favorit" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:222 #: src/widgets/song/row.py:167 msgid "Not Favorite" msgstr "Nije favorit" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "Disk {}" #: src/widgets/artist/button.py:43 src/widgets/artist/row.py:48 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} album" msgstr[1] "{} albuma" msgstr[2] "{} albuma" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "Najbolje pjesme" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "Povezani izvoฤ‘aฤi" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "Autor" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Nema vremenske oznake" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "Pokreฤ‡e se" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "Dodatni imenik" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Lokalna fonoteka" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "Ponovo pokrenuto" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "Greลกka" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "Brzo povezivanje" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "Greลกka prilikom ohvaฤ‡anja koda" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "Stranica za brzo povezivanje" #: src/widgets/pages/setup.py:62 msgid "Installing" msgstr "Instaliranje" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integracija" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC datoteka" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Nova playlista" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add songs to '{}'" msgstr "Dodaj pjesme u โ€ž{}โ€" #: src/widgets/song/row.py:146 src/widgets/song/row.py:151 msgid "Multiple Artists" msgstr "Viลกe izvoฤ‘aฤa" Jeffser-Nocturne-b442fee/po/meson.build000066400000000000000000000000511523240030000201270ustar00rootroot00000000000000i18n.gettext('nocturne', preset: 'glib') Jeffser-Nocturne-b442fee/po/nocturne.pot000066400000000000000000001157621523240030000203660ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-29 07:28-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern music client" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Playlist management" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:93 src/ui/window.blp:192 #: src/ui/window.blp:244 msgid "Lyrics" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:86 msgid "Mini Player" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "opensubsonic" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "bandcamp" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "navidrome" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "jellyfin" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:95 msgid "music" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:96 msgid "gtk" msgstr "" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "" #: src/actions.py:150 src/widgets/pages/login.py:133 msgid "Error" msgstr "" #: src/actions.py:151 msgid "No details provided" msgstr "" #: src/actions.py:153 src/actions.py:300 src/preferences.py:594 #: src/ui/song/queue.blp:43 msgid "Close" msgstr "" #: src/actions.py:191 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "" #: src/actions.py:297 src/ui/song/row.blp:72 msgid "External File" msgstr "" #: src/actions.py:298 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" #: src/actions.py:318 msgid "Navidrome server deleted successfully" msgstr "" #: src/actions.py:327 msgid "Delete Navidrome Server" msgstr "" #: src/actions.py:328 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "" #: src/actions.py:330 src/actions.py:469 src/actions.py:503 src/actions.py:681 #: src/actions.py:759 src/preferences.py:512 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:181 msgid "Cancel" msgstr "" #: src/actions.py:331 msgid "Keep Data" msgstr "" #: src/actions.py:332 msgid "Delete Everything" msgstr "" #: src/actions.py:417 msgid "No Name" msgstr "" #: src/actions.py:436 msgid "Radio updated successfully" msgstr "" #: src/actions.py:436 msgid "Radio added successfully" msgstr "" #: src/actions.py:443 msgid "Error updating radio" msgstr "" #: src/actions.py:443 msgid "Error adding radio" msgstr "" #: src/actions.py:454 src/actions.py:673 msgid "Name" msgstr "" #: src/actions.py:459 msgid "Stream Url" msgstr "" #: src/actions.py:466 msgid "Update Radio Station" msgstr "" #: src/actions.py:466 msgid "Add Radio Station" msgstr "" #: src/actions.py:470 src/preferences.py:513 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "" #: src/actions.py:483 msgid "Radio deleted successfully" msgstr "" #: src/actions.py:487 msgid "Error deleting radio" msgstr "" #: src/actions.py:500 msgid "Delete Radio Station" msgstr "" #: src/actions.py:501 src/actions.py:757 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "" #: src/actions.py:504 src/actions.py:760 src/constants.py:585 #: src/constants.py:633 msgid "Delete" msgstr "" #: src/actions.py:523 src/actions.py:535 src/actions.py:537 src/actions.py:579 #: src/actions.py:630 msgid "Playing Next" msgstr "" #: src/actions.py:527 src/actions.py:542 src/actions.py:544 src/actions.py:586 #: src/actions.py:637 msgid "Playing Later" msgstr "" #: src/actions.py:535 src/actions.py:542 src/actions.py:870 src/actions.py:995 #: src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "" #: src/actions.py:655 msgid "Playlist updated successfully" msgstr "" #: src/actions.py:655 msgid "Playlist created successfully" msgstr "" #: src/actions.py:657 msgid "Error updating playlist" msgstr "" #: src/actions.py:657 msgid "Error creating playlist" msgstr "" #: src/actions.py:678 msgid "Update Playlist" msgstr "" #: src/actions.py:678 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "" #: src/actions.py:682 msgid "Update" msgstr "" #: src/actions.py:682 msgid "Create" msgstr "" #: src/actions.py:695 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:720 src/actions.py:734 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:738 src/actions.py:875 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: src/actions.py:749 msgid "Playlist Deleted" msgstr "" #: src/actions.py:756 msgid "Delete Playlist" msgstr "" #: src/actions.py:806 msgid "No songs found" msgstr "" #: src/actions.py:847 src/actions.py:871 src/actions.py:906 src/actions.py:939 msgid "Download Started" msgstr "" #: src/actions.py:854 src/actions.py:882 src/actions.py:889 src/actions.py:922 #: src/actions.py:955 msgid "Already Downloaded" msgstr "" #: src/actions.py:876 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:888 msgid "Download Skipped" msgstr "" #: src/actions.py:910 src/actions.py:943 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: src/actions.py:979 src/actions.py:996 msgid "Deleted" msgstr "" #: src/constants.py:372 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "" #: src/constants.py:376 msgid "Repeat All" msgstr "" #: src/constants.py:380 msgid "Repeat One" msgstr "" #: src/constants.py:385 #, python-brace-format msgid "Low ({})" msgstr "" #: src/constants.py:386 #, python-brace-format msgid "Medium ({})" msgstr "" #: src/constants.py:387 #, python-brace-format msgid "High ({})" msgstr "" #: src/constants.py:388 #, python-brace-format msgid "Ultra ({})" msgstr "" #: src/constants.py:389 msgid "Original File" msgstr "" #. Item #: src/constants.py:397 src/ui/pages/home.blp:5 msgid "Home" msgstr "" #. Section #: src/constants.py:404 src/ui/artist/page.blp:164 #: src/ui/pages/albums_all.blp:5 src/ui/pages/albums_all.blp:28 #: src/ui/pages/home.blp:103 src/ui/preferences.blp:312 msgid "Albums" msgstr "" #. Item #: src/constants.py:409 src/constants.py:446 src/constants.py:463 #: src/constants.py:485 msgid "All" msgstr "" #. Item #: src/constants.py:414 msgid "Random" msgstr "" #. Item #: src/constants.py:419 src/constants.py:451 src/constants.py:468 msgid "Favorites" msgstr "" #. Item #: src/constants.py:424 msgid "Recently Added" msgstr "" #. Item #: src/constants.py:429 msgid "Recently Played" msgstr "" #. Item #: src/constants.py:434 msgid "Most Played" msgstr "" #. Section #. list #: src/constants.py:441 src/integrations/models.py:163 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/pages/home.blp:109 src/ui/preferences.blp:320 msgid "Artists" msgstr "" #. Section #: src/constants.py:458 src/ui/album/page.blp:160 src/ui/pages/home.blp:97 #: src/ui/pages/songs_all.blp:5 src/ui/pages/songs_all.blp:28 #: src/ui/preferences.blp:304 msgid "Songs" msgstr "" #. Item #: src/constants.py:473 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "" #. Section #. Item (special) #: src/constants.py:480 src/constants.py:490 src/ui/pages/home.blp:115 #: src/ui/pages/playlists.blp:5 src/ui/pages/playlists.blp:28 #: src/ui/preferences.blp:328 msgid "Playlists" msgstr "" #: src/constants.py:499 src/constants.py:550 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:114 #: src/ui/song/queue.blp:58 src/ui/song/queue.blp:60 msgid "Play" msgstr "" #: src/constants.py:504 src/constants.py:537 src/constants.py:560 msgid "Shuffle" msgstr "" #: src/constants.py:509 src/constants.py:565 src/constants.py:598 #: src/ui/song/queue.blp:66 src/ui/song/queue.blp:68 msgid "Play Next" msgstr "" #: src/constants.py:514 src/constants.py:570 src/constants.py:603 #: src/ui/song/queue.blp:74 src/ui/song/queue.blp:76 msgid "Play Later" msgstr "" #: src/constants.py:519 src/constants.py:613 src/ui/song/queue.blp:82 #: src/ui/song/queue.blp:84 msgid "Add To Playlist" msgstr "" #: src/constants.py:524 src/constants.py:575 src/constants.py:618 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:90 #: src/ui/song/queue.blp:92 msgid "Download" msgstr "" #: src/constants.py:529 src/constants.py:628 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "" #: src/constants.py:542 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "" #: src/constants.py:555 msgid "Resume" msgstr "" #: src/constants.py:580 src/constants.py:608 msgid "Edit" msgstr "" #: src/constants.py:594 msgid "Select" msgstr "" #: src/constants.py:623 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "" #: src/constants.py:639 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Remove" msgstr "" #: src/constants.py:644 src/ui/song/queue.blp:112 src/ui/song/queue.blp:114 msgid "Delete Download" msgstr "" #: src/constants.py:650 src/main.py:158 msgid "Show Details" msgstr "" #: src/constants.py:661 msgid "Visit Webpage" msgstr "" #: src/constants.py:667 msgid "Update Server" msgstr "" #: src/constants.py:672 msgid "Delete Server" msgstr "" #: src/constants.py:681 msgid "Edit Lyrics" msgstr "" #: src/constants.py:686 msgid "Remove Lyrics" msgstr "" #: src/integrations/base.py:306 msgid "Could not generate SQL database" msgstr "" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "" #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "" #: src/integrations/jellyfin.py:297 msgid "Could not log in" msgstr "" #: src/integrations/jellyfin.py:1162 msgid "Unknown" msgstr "" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:692 msgid "Local Files" msgstr "" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "" #: src/integrations/local.py:138 src/integrations/navidrome.py:747 msgid "Could not locate path, try again" msgstr "" #: src/integrations/local.py:318 msgid "No Album" msgstr "" #: src/integrations/local.py:704 src/integrations/local.py:713 msgid "Offline Mode" msgstr "" #: src/integrations/local.py:705 msgid "Access your downloads" msgstr "" #: src/integrations/models.py:142 src/ui/album/page.blp:5 #: src/widgets/album/page.py:128 msgid "Album" msgstr "" #: src/integrations/models.py:144 msgid "Display Artist" msgstr "" #: src/integrations/models.py:146 msgid "MusicBrainz ID" msgstr "" #: src/integrations/models.py:147 msgid "Track Number" msgstr "" #: src/integrations/models.py:148 msgid "Year" msgstr "" #: src/integrations/models.py:149 msgid "Size" msgstr "" #: src/integrations/models.py:150 msgid "Suffix" msgstr "" #: src/integrations/models.py:151 src/widgets/album/button.py:94 #: src/widgets/album/page.py:144 src/widgets/artist/page.py:109 #: src/widgets/playing/control_page.py:192 src/widgets/song/row.py:183 msgid "Favorite" msgstr "" #: src/integrations/models.py:152 msgid "Duration" msgstr "" #: src/integrations/models.py:154 msgid "Bit Rate" msgstr "" #: src/integrations/models.py:155 msgid "Bit Depth" msgstr "" #: src/integrations/models.py:156 msgid "Sampling Rate" msgstr "" #: src/integrations/models.py:157 msgid "Channel Count" msgstr "" #: src/integrations/models.py:159 msgid "Path" msgstr "" #: src/integrations/models.py:160 msgid "Disc Number" msgstr "" #: src/integrations/models.py:161 msgid "BPM" msgstr "" #: src/integrations/models.py:162 msgid "Genres" msgstr "" #. list #: src/integrations/models.py:164 msgid "Track Gain" msgstr "" #: src/integrations/models.py:165 msgid "Album Gain" msgstr "" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "" #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic instance" msgstr "" #: src/integrations/navidrome.py:173 msgid "Server returned an error" msgstr "" #: src/integrations/navidrome.py:173 msgid "Unknown Error" msgstr "" #: src/integrations/navidrome.py:722 src/integrations/navidrome.py:731 msgid "Managed Server" msgstr "" #: src/integrations/navidrome.py:723 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" #: src/integrations/navidrome.py:726 msgid "Manage Server" msgstr "" #: src/integrations/navidrome.py:732 msgid "Create and use a Navidrome instance" msgstr "" #: src/integrations/navidrome.py:791 msgid "Important: Use your Subsonic login information." msgstr "" #: src/integrations/navidrome.py:794 msgid "Bandcamp User Settings" msgstr "" #: src/integrations/navidrome.py:800 msgid "Explore your online library" msgstr "" #: src/main.py:96 msgid "Music is Playing" msgstr "" #: src/main.py:109 msgid "Fullscreen Player Active" msgstr "" #: src/main.py:155 msgid "Login Failed" msgstr "" #: src/nocturne_search_provider.py.in:12 src/ui/album/button.blp:21 msgid "Play Album" msgstr "" #: src/nocturne_search_provider.py.in:13 src/ui/song/button.blp:16 msgid "Play Song" msgstr "" #: src/nocturne_search_provider.py.in:14 src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "" #: src/nocturne_search_provider.py.in:113 msgid "Open" msgstr "" #: src/preferences.py:20 src/preferences.py:22 msgid "Item" msgstr "" #: src/preferences.py:27 src/preferences.py:29 msgid "Section" msgstr "" #: src/preferences.py:501 msgid "Settings Page" msgstr "" #: src/preferences.py:504 msgid "User Token" msgstr "" #: src/preferences.py:508 msgid "Link ListenBrainz" msgstr "" #: src/preferences.py:509 msgid "Connect your ListenBrainz account with a user token" msgstr "" #: src/preferences.py:584 msgid "Flatpak Sandbox Warning" msgstr "" #: src/preferences.py:585 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:115 msgid "Toggle star" msgstr "" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:228 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:237 msgid "2 Stars" msgstr "" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:246 msgid "3 Stars" msgstr "" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:255 msgid "4 Stars" msgstr "" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:264 msgid "5 Stars" msgstr "" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:130 msgid "Options" msgstr "" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "" #: src/ui/artist/page.blp:159 msgid "Top Songs" msgstr "" #: src/ui/artist/page.blp:169 msgid "Related Artists" msgstr "" #: src/ui/containers/carousel.blp:44 msgid "Pan to the Left" msgstr "" #: src/ui/containers/carousel.blp:50 msgid "Pan to the Right" msgstr "" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:187 #: src/ui/playing/footer.blp:129 msgid "Pause" msgstr "" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:122 msgid "Current song progress bar" msgstr "" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "" #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/starred.blp:13 msgid "List" msgstr "" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/starred.blp:18 msgid "Grid" msgstr "" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "" #: src/ui/pages/artists.blp:114 src/widgets/pages/starred.py:44 msgid "No Artists Found" msgstr "" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "" #: src/ui/pages/home.blp:81 msgid "Welcome back" msgstr "" #: src/ui/pages/home.blp:91 msgid "On Replay" msgstr "" #: src/ui/pages/home.blp:128 msgid "No Elements Found" msgstr "" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 #: src/widgets/pages/login.py:38 src/widgets/pages/login.py:78 msgid "Login" msgstr "" #: src/ui/pages/login.blp:36 msgid "Server Status" msgstr "" #: src/ui/pages/login.blp:37 src/widgets/pages/login.py:52 msgid "Not Running" msgstr "" #: src/ui/pages/login.blp:38 msgid "Restart Instance" msgstr "" #: src/ui/pages/login.blp:51 msgid "Manage Navidrome Server" msgstr "" #: src/ui/pages/login.blp:61 msgid "Url" msgstr "" #: src/ui/pages/login.blp:69 msgid "More Options" msgstr "" #: src/ui/pages/login.blp:75 msgid "Trust Server Certificates" msgstr "" #: src/ui/pages/login.blp:83 msgid "Library Directory" msgstr "" #: src/ui/pages/login.blp:96 msgid "Username" msgstr "" #: src/ui/pages/login.blp:101 msgid "Password" msgstr "" #: src/ui/pages/login.blp:122 src/ui/pages/login.blp:123 msgid "Use Quick Connect" msgstr "" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:29 msgid "Playback" msgstr "" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "" #: src/ui/pages/playback.blp:54 src/window.py:228 msgid "Nocturne Playback" msgstr "" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "" #: src/ui/pages/songs_all.blp:116 src/ui/pages/starred.blp:94 #: src/ui/song/queue.blp:140 msgid "No Songs Found" msgstr "" #: src/ui/pages/starred.blp:29 msgid "Search favorite songs" msgstr "" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "" #: src/ui/playing/control_page.blp:157 src/ui/playing/footer.blp:99 msgid "Previous" msgstr "" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:143 msgid "Next" msgstr "" #: src/ui/playing/control_page.blp:221 msgid "Rate" msgstr "" #: src/ui/playing/control_page.blp:277 msgid "Star" msgstr "" #: src/ui/playing/control_page.blp:284 msgid "Toggle Sidebar" msgstr "" #: src/ui/playing/cover_art.blp:35 msgid "Audio" msgstr "" #: src/ui/playing/cover_art.blp:43 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "" #: src/ui/playing/cover_art.blp:49 src/ui/playing/cover_art.blp:57 msgid "Video" msgstr "" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "" #: src/ui/playing/footer.blp:45 msgid "Current song cover art" msgstr "" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "" #: src/ui/playing/popout_window.blp:67 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:216 msgid "Song Details" msgstr "" #: src/ui/playing/popout_window.blp:87 src/ui/window.blp:182 #: src/ui/window.blp:238 msgid "Queue" msgstr "" #: src/ui/playing/popout_window.blp:99 src/ui/window.blp:198 #: src/ui/window.blp:250 msgid "Equalizer" msgstr "" #: src/ui/playing/queue_page.blp:19 msgid "Generating Next Queue" msgstr "" #: src/ui/playing/queue_page.blp:23 msgid "Autoplay" msgstr "" #: src/ui/playing/queue_page.blp:24 msgid "Generate a new queue when the current one ends" msgstr "" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:83 msgid "Playlist" msgstr "" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "" #: src/ui/preferences.blp:22 msgid "Auto Download Lyrics" msgstr "" #: src/ui/preferences.blp:23 msgid "Used when the current instance does not provide lyrics" msgstr "" #: src/ui/preferences.blp:26 msgid "Use Track and Album Gain" msgstr "" #: src/ui/preferences.blp:27 msgid "Adjust the volume based on gain metadata" msgstr "" #: src/ui/preferences.blp:30 msgid "Default Page on Login" msgstr "" #: src/ui/preferences.blp:35 msgid "Maximum Bitrate" msgstr "" #: src/ui/preferences.blp:41 msgid "Gnome Search" msgstr "" #: src/ui/preferences.blp:42 msgid "" "Search for your music directly from the system search bar while Nocturne is " "running. You can enable or disable this feature completely in System Settings" msgstr "" #: src/ui/preferences.blp:45 msgid "Include Artists" msgstr "" #: src/ui/preferences.blp:48 msgid "Include Albums" msgstr "" #: src/ui/preferences.blp:51 msgid "Include Songs" msgstr "" #: src/ui/preferences.blp:54 msgid "Include Playlists" msgstr "" #: src/ui/preferences.blp:58 msgid "Session" msgstr "" #: src/ui/preferences.blp:61 msgid "Discord Rich Presence" msgstr "" #: src/ui/preferences.blp:62 msgid "Share the currently playing song on Discord" msgstr "" #: src/ui/preferences.blp:66 msgid "Share Cover Art With Discord" msgstr "" #: src/ui/preferences.blp:67 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" #: src/ui/preferences.blp:73 msgid "ListenBrainz Link" msgstr "" #: src/ui/preferences.blp:74 msgid "Keep track of your playback history" msgstr "" #: src/ui/preferences.blp:80 src/ui/preferences.blp:81 msgid "Link" msgstr "" #: src/ui/preferences.blp:89 src/ui/preferences.blp:90 msgid "Unlink" msgstr "" #: src/ui/preferences.blp:102 msgid "User" msgstr "" #: src/ui/preferences.blp:122 msgid "Delete Image Cache" msgstr "" #: src/ui/preferences.blp:129 msgid "Logout" msgstr "" #: src/ui/preferences.blp:139 msgid "Customization" msgstr "" #: src/ui/preferences.blp:142 msgid "Interface" msgstr "" #: src/ui/preferences.blp:144 msgid "Show Context Button in Rows" msgstr "" #: src/ui/preferences.blp:145 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" #: src/ui/preferences.blp:148 msgid "Show Labels in Buttons of Pages" msgstr "" #: src/ui/preferences.blp:151 msgid "Show Progressbar in Player Footer" msgstr "" #: src/ui/preferences.blp:154 msgid "Make Background Translucent in Player" msgstr "" #: src/ui/preferences.blp:155 msgid "Does not affect sidebar player" msgstr "" #: src/ui/preferences.blp:158 msgid "Use Sidebar Player When Possible" msgstr "" #: src/ui/preferences.blp:159 msgid "Sidebar player is only available when the window is wide enough" msgstr "" #: src/ui/preferences.blp:162 msgid "Filter Singles" msgstr "" #: src/ui/preferences.blp:163 msgid "Hides albums with only one song (does not affect homepage)" msgstr "" #: src/ui/preferences.blp:166 msgid "Button Size" msgstr "" #: src/ui/preferences.blp:167 msgid "Affects albums, artists, playlists and songs" msgstr "" #: src/ui/preferences.blp:173 src/ui/preferences.blp:174 msgid "Small" msgstr "" #: src/ui/preferences.blp:178 src/ui/preferences.blp:179 msgid "Big" msgstr "" #: src/ui/preferences.blp:187 msgid "Dynamic Background" msgstr "" #: src/ui/preferences.blp:188 msgid "Background generated based on the album art of the current song" msgstr "" #: src/ui/preferences.blp:191 msgid "Use in Main Window" msgstr "" #: src/ui/preferences.blp:197 src/ui/preferences.blp:198 #: src/ui/preferences.blp:221 src/ui/preferences.blp:222 #: src/ui/preferences.blp:245 src/ui/preferences.blp:246 #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Off" msgstr "" #: src/ui/preferences.blp:202 src/ui/preferences.blp:203 #: src/ui/preferences.blp:226 src/ui/preferences.blp:227 #: src/ui/preferences.blp:250 src/ui/preferences.blp:251 msgid "Gradient" msgstr "" #: src/ui/preferences.blp:207 src/ui/preferences.blp:208 #: src/ui/preferences.blp:231 src/ui/preferences.blp:232 #: src/ui/preferences.blp:255 src/ui/preferences.blp:256 msgid "Blur" msgstr "" #: src/ui/preferences.blp:215 msgid "Use in Player" msgstr "" #: src/ui/preferences.blp:239 msgid "Use in Pop-out Window" msgstr "" #: src/ui/preferences.blp:263 msgid "Use as Accent Color" msgstr "" #: src/ui/preferences.blp:268 msgid "Homepage" msgstr "" #: src/ui/preferences.blp:269 msgid "Reload homepage to see changes" msgstr "" #: src/ui/preferences.blp:271 msgid "Welcome Message" msgstr "" #: src/ui/preferences.blp:272 msgid "Where to get user information" msgstr "" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Instance" msgstr "" #: src/ui/preferences.blp:289 src/ui/preferences.blp:290 msgid "System" msgstr "" #: src/ui/preferences.blp:296 src/ui/window.blp:153 msgid "Frequent Albums" msgstr "" #: src/ui/preferences.blp:338 msgid "Sidebar" msgstr "" #: src/ui/preferences.blp:342 msgid "Visualizer" msgstr "" #: src/ui/preferences.blp:346 src/ui/window.blp:267 msgid "Preferences" msgstr "" #: src/ui/preferences.blp:348 msgid "Show Visualizer in Player" msgstr "" #: src/ui/preferences.blp:349 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" #: src/ui/preferences.blp:354 msgid "Appearance" msgstr "" #: src/ui/preferences.blp:358 msgid "Number of Bars" msgstr "" #: src/ui/preferences.blp:369 msgid "Visualizer Type" msgstr "" #: src/ui/preferences.blp:375 src/ui/preferences.blp:376 msgid "Wave" msgstr "" #: src/ui/preferences.blp:380 src/ui/preferences.blp:381 msgid "Bars" msgstr "" #: src/ui/preferences.blp:385 src/ui/preferences.blp:386 msgid "Particles" msgstr "" #: src/ui/preferences.blp:393 msgid "Fill Mode" msgstr "" #: src/ui/preferences.blp:399 src/ui/preferences.blp:400 msgid "Solid" msgstr "" #: src/ui/preferences.blp:404 src/ui/preferences.blp:405 msgid "Translucent" msgstr "" #: src/ui/preferences.blp:409 src/ui/preferences.blp:410 msgid "Border" msgstr "" #: src/ui/preferences.blp:418 msgid "Color" msgstr "" #: src/ui/preferences.blp:422 msgid "Use Dynamic Color" msgstr "" #: src/ui/preferences.blp:425 msgid "Invert Color" msgstr "" #: src/ui/preferences.blp:429 msgid "Manual Color" msgstr "" #: src/ui/preferences.blp:435 msgid "Visualizer Color" msgstr "" #: src/ui/shortcuts-dialog.blp:12 msgid "Reload Page" msgstr "" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Shortcuts" msgstr "" #: src/ui/shortcuts-dialog.blp:20 msgid "Show Preferences" msgstr "" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "" #: src/ui/shortcuts-dialog.blp:31 msgid "Toggle Playback" msgstr "" #: src/ui/shortcuts-dialog.blp:35 msgid "Next Song" msgstr "" #: src/ui/shortcuts-dialog.blp:39 msgid "Previous Song" msgstr "" #: src/ui/shortcuts-dialog.blp:43 msgid "Raise Volume" msgstr "" #: src/ui/shortcuts-dialog.blp:47 msgid "Lower Volume" msgstr "" #: src/ui/shortcuts-dialog.blp:52 msgid "Window" msgstr "" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:277 #: src/widgets/playing/popout_window.py:48 msgid "Toggle Fullscreen" msgstr "" #: src/ui/shortcuts-dialog.blp:58 src/ui/window.blp:272 msgid "Open Pop-Out Window" msgstr "" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "" #: src/ui/song/queue.blp:50 src/ui/song/queue.blp:52 msgid "Select All" msgstr "" #: src/ui/song/row.blp:44 msgid "Drag icon" msgstr "" #: src/ui/song/row.blp:142 msgid "Selection box" msgstr "" #: src/ui/window.blp:94 msgid "Play Random Queue" msgstr "" #: src/ui/window.blp:102 msgid "Menu" msgstr "" #: src/ui/window.blp:125 msgid "Favorite Artists" msgstr "" #: src/ui/window.blp:131 msgid "Favorite Songs" msgstr "" #: src/ui/window.blp:137 msgid "Random Albums" msgstr "" #: src/ui/window.blp:141 msgid "Favorite Albums" msgstr "" #: src/ui/window.blp:145 msgid "Newest Albums" msgstr "" #: src/ui/window.blp:149 msgid "Recent Albums" msgstr "" #: src/ui/window.blp:176 msgid "Player" msgstr "" #: src/ui/window.blp:282 msgid "See Last Nocturne Playback" msgstr "" #: src/ui/window.blp:287 msgid "Keyboard Shortcuts" msgstr "" #: src/ui/window.blp:292 msgid "About Nocturne" msgstr "" #: src/widgets/album/button.py:99 src/widgets/album/page.py:149 #: src/widgets/artist/page.py:114 src/widgets/playing/control_page.py:197 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "" msgstr[1] "" #: src/widgets/artist/page.py:97 msgid "Author" msgstr "" #: src/widgets/containers/context.py:50 #, python-brace-format msgid "{} Stars" msgstr "" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "" #: src/widgets/lyrics/dialog.py:195 msgid "Lyrics Saved" msgstr "" #: src/widgets/pages/login.py:52 src/widgets/pages/login.py:131 msgid "Running" msgstr "" #: src/widgets/pages/login.py:85 msgid "Extra Menu" msgstr "" #: src/widgets/pages/login.py:120 src/widgets/pages/setup.py:83 msgid "Music Library" msgstr "" #: src/widgets/pages/login.py:130 msgid "Restarted" msgstr "" #: src/widgets/pages/login.py:172 msgid "Quick Connect" msgstr "" #: src/widgets/pages/login.py:173 msgid "Error getting code" msgstr "" #: src/widgets/pages/login.py:175 msgid "Quick Connect Page" msgstr "" #: src/widgets/pages/playback.py:29 #, python-brace-format msgid "Nocturne Playback ~ {}" msgstr "" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 #, python-brace-format msgid "{} Plays" msgstr "" #: src/widgets/pages/setup.py:70 msgid "Installing" msgstr "" #: src/widgets/pages/starred.py:42 msgid "Search favorite artists" msgstr "" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "" #: src/widgets/playing/lyrics_page.py:222 msgid "LRC File" msgstr "" #: src/widgets/playing/player.py:638 msgid "Warning: Song changed but volume is set to 0" msgstr "" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:115 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" msgstr[1] "" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "" #: src/widgets/playlist/selector_row.py:33 #, python-brace-format msgid "Add Songs to '{}'" msgstr "" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "" #: src/window.py:229 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "" #: src/window.py:231 msgid "Show" msgstr "" #: src/window.py:232 msgid "Later" msgstr "" Jeffser-Nocturne-b442fee/po/pt_BR.po000066400000000000000000001403421523240030000173430ustar00rootroot00000000000000# Portuguese translations for PACKAGE package # Traduรงรตes em portuguรชs brasileiro para o pacote PACKAGE. # Copyright (C) 2026 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Rikelry <153693133+Rikelry@users.noreply.github.com>, 2026. # msgid "" msgstr "" "Project-Id-Version: Nocturne\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-07-22 22:14-0300\n" "PO-Revision-Date: 2026-07-21 10:50-0300\n" "Last-Translator: Rikelry <153693133+Rikelry@users.noreply.github.com>\n" "Language-Team: Brazilian Portuguese\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Mรบsica;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Reรบna toda a sua biblioteca de mรบsicas" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern music client" msgstr "Um cliente de mรบsica moderno" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Recursos" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Compatibility with Jellyfin, OpenSubsonic, Bandcamp and local files" msgstr "Compatรญvel com Jellyfin, OpenSubsonic, Bandcamp e arquivos locais" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "Explore mรบsicas, artistas, รกlbuns, rรกdios e playlists" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Playlist management" msgstr "Gerenciamento de playlists" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Equalizador e visualizador de รกudio" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Integraรงรฃo com MPRIS" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Gerenciamento integrado de instรขncias do Navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Obtenรงรฃo automรกtica de letras" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "Downloads e modo offline" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "Interface moderna" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Pรกgina inicial" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "Fila de reproduรงรฃo" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:93 src/ui/window.blp:185 #: src/ui/window.blp:237 msgid "Lyrics" msgstr "Letras" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "Lista de mรบsicas" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Pรกgina do รกlbum" #: data/com.jeffser.Nocturne.metainfo.xml.in:86 msgid "Mini Player" msgstr "Mini player" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "opensubsonic" msgstr "opensubsonic" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "bandcamp" msgstr "bandcamp" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:95 msgid "music" msgstr "mรบsica" #: data/com.jeffser.Nocturne.metainfo.xml.in:96 msgid "gtk" msgstr "gtk" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "Provedor de pesquisa do Nocturne" #: src/actions.py:140 src/widgets/pages/login.py:133 msgid "Error" msgstr "Erro" #: src/actions.py:141 msgid "No details provided" msgstr "Nenhum detalhe fornecido" #: src/actions.py:143 src/actions.py:284 src/preferences.py:486 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "Fechar" #: src/actions.py:181 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "Dados insuficientes para o Nocturne Playback ({})" #: src/actions.py:281 src/ui/song/row.blp:72 msgid "External File" msgstr "Arquivo externo" #: src/actions.py:282 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Esta faixa foi carregada de um arquivo externo, portanto terรก menos recursos " "do que uma faixa da biblioteca." #: src/actions.py:302 msgid "Navidrome server deleted successfully" msgstr "Servidor do Navidrome removido com sucesso" #: src/actions.py:311 msgid "Delete Navidrome Server" msgstr "Remover servidor do Navidrome" #: src/actions.py:312 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Tem certeza de que deseja remover o servidor integrado do Navidrome?" #: src/actions.py:314 src/actions.py:453 src/actions.py:487 src/actions.py:665 #: src/actions.py:743 src/preferences.py:431 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:181 msgid "Cancel" msgstr "Cancelar" #: src/actions.py:315 msgid "Keep Data" msgstr "Manter dados" #: src/actions.py:316 msgid "Delete Everything" msgstr "Excluir tudo" #: src/actions.py:401 msgid "No Name" msgstr "Sem nome" #: src/actions.py:420 msgid "Radio updated successfully" msgstr "Rรกdio atualizada com sucesso" #: src/actions.py:420 msgid "Radio added successfully" msgstr "Rรกdio adicionada com sucesso" #: src/actions.py:427 msgid "Error updating radio" msgstr "Erro ao atualizar a rรกdio" #: src/actions.py:427 msgid "Error adding radio" msgstr "Erro ao adicionar a rรกdio" #: src/actions.py:438 src/actions.py:657 msgid "Name" msgstr "Nome" #: src/actions.py:443 msgid "Stream Url" msgstr "URL do stream" #: src/actions.py:450 msgid "Update Radio Station" msgstr "Atualizar estaรงรฃo de rรกdio" #: src/actions.py:450 msgid "Add Radio Station" msgstr "Adicionar estaรงรฃo de rรกdio" #: src/actions.py:454 src/preferences.py:432 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Salvar" #: src/actions.py:467 msgid "Radio deleted successfully" msgstr "Rรกdio removida com sucesso" #: src/actions.py:471 msgid "Error deleting radio" msgstr "Erro ao remover a rรกdio" #: src/actions.py:484 msgid "Delete Radio Station" msgstr "Remover estaรงรฃo de rรกdio" #: src/actions.py:485 src/actions.py:741 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Tem certeza de que deseja remover '{}'?" #: src/actions.py:488 src/actions.py:744 src/constants.py:534 #: src/constants.py:582 msgid "Delete" msgstr "Excluir" #: src/actions.py:507 src/actions.py:519 src/actions.py:521 src/actions.py:563 #: src/actions.py:614 msgid "Playing Next" msgstr "Reproduzindo em seguida" #: src/actions.py:511 src/actions.py:526 src/actions.py:528 src/actions.py:570 #: src/actions.py:621 msgid "Playing Later" msgstr "Reproduzir mais tarde" #: src/actions.py:519 src/actions.py:526 src/actions.py:854 src/actions.py:979 #: src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} mรบsicas" #: src/actions.py:639 msgid "Playlist updated successfully" msgstr "Playlist atualizada com sucesso" #: src/actions.py:639 msgid "Playlist created successfully" msgstr "Playlist criada com sucesso" #: src/actions.py:641 msgid "Error updating playlist" msgstr "Erro ao atualizar a playlist" #: src/actions.py:641 msgid "Error creating playlist" msgstr "Erro ao criar a playlist" #: src/actions.py:662 msgid "Update Playlist" msgstr "Atualizar playlist" #: src/actions.py:662 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Criar playlist" #: src/actions.py:666 msgid "Update" msgstr "Atualizar" #: src/actions.py:666 msgid "Create" msgstr "Criar" #: src/actions.py:679 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "{} mรบsica removida" msgstr[1] "{} mรบsicas removidas" #: src/actions.py:704 src/actions.py:718 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "{} mรบsica adicionada" msgstr[1] "{} mรบsicas adicionadas" #: src/actions.py:722 src/actions.py:859 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "{} mรบsica ignorada" msgstr[1] "{} mรบsicas ignoradas" #: src/actions.py:733 msgid "Playlist Deleted" msgstr "Playlist removida" #: src/actions.py:740 msgid "Delete Playlist" msgstr "Remover playlist" #: src/actions.py:790 msgid "No songs found" msgstr "Nenhuma mรบsica encontrada" #: src/actions.py:831 src/actions.py:855 src/actions.py:890 src/actions.py:923 msgid "Download Started" msgstr "Download iniciado" #: src/actions.py:838 src/actions.py:866 src/actions.py:873 src/actions.py:906 #: src/actions.py:939 msgid "Already Downloaded" msgstr "Jรก baixado" #: src/actions.py:860 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} mรบsica ({})" msgstr[1] "{} mรบsicas ({})" #: src/actions.py:872 msgid "Download Skipped" msgstr "Download ignorado" #: src/actions.py:894 src/actions.py:927 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "Download iniciado ({} mรบsica ignorada)" msgstr[1] "Download iniciado ({} mรบsicas ignoradas)" #: src/actions.py:963 src/actions.py:980 msgid "Deleted" msgstr "Removido" #: src/constants.py:344 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Sequencial" #: src/constants.py:348 msgid "Repeat All" msgstr "Repetir todas" #: src/constants.py:352 msgid "Repeat One" msgstr "Repetir uma" #: src/constants.py:357 #, python-brace-format msgid "Low ({})" msgstr "Baixa ({})" #: src/constants.py:358 #, python-brace-format msgid "Medium ({})" msgstr "Mรฉdia ({})" #: src/constants.py:359 #, python-brace-format msgid "High ({})" msgstr "Alta ({})" #: src/constants.py:360 #, python-brace-format msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:361 msgid "Original File" msgstr "Arquivo original" #. Item #: src/constants.py:368 src/ui/pages/home.blp:5 msgid "Home" msgstr "Inรญcio" #. Item #. list #: src/constants.py:373 src/integrations/models.py:163 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/pages/home.blp:65 src/ui/preferences.blp:287 msgid "Artists" msgstr "Artistas" #. Section #: src/constants.py:380 src/ui/artist/page.blp:164 #: src/ui/pages/albums_all.blp:5 src/ui/pages/albums_all.blp:28 #: src/ui/pages/home.blp:59 src/ui/preferences.blp:279 msgid "Albums" msgstr "รlbuns" #. Item #: src/constants.py:383 src/constants.py:418 src/constants.py:438 #: src/window.py:155 msgid "All" msgstr "Todos" #. Item #: src/constants.py:388 msgid "Random" msgstr "Aleatรณrio" #. Item #: src/constants.py:393 src/constants.py:423 msgid "Favorites" msgstr "Favoritos" #. Item #: src/constants.py:398 msgid "Recently Added" msgstr "Recรฉm adicionados" #. Item #: src/constants.py:403 msgid "Recently Played" msgstr "Recรฉm reproduzidos" #. Item #: src/constants.py:408 msgid "Most Played" msgstr "Mais reproduzidos" #. Section #: src/constants.py:415 src/ui/album/page.blp:160 src/ui/pages/home.blp:53 #: src/ui/pages/songs_all.blp:5 src/ui/pages/songs_all.blp:28 #: src/ui/preferences.blp:271 msgid "Songs" msgstr "Mรบsicas" #. Item #: src/constants.py:428 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Rรกdios" #. Section #: src/constants.py:435 src/ui/pages/home.blp:71 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:295 msgid "Playlists" msgstr "Playlists" #: src/constants.py:448 src/constants.py:499 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:114 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play" msgstr "Reproduzir" #: src/constants.py:453 src/constants.py:486 src/constants.py:509 msgid "Shuffle" msgstr "Embaralhar" #: src/constants.py:458 src/constants.py:514 src/constants.py:547 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Next" msgstr "Reproduzir em seguida" #: src/constants.py:463 src/constants.py:519 src/constants.py:552 #: src/ui/song/queue.blp:71 src/ui/song/queue.blp:73 msgid "Play Later" msgstr "Reproduzir mais tarde" #: src/constants.py:468 src/constants.py:562 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Add To Playlist" msgstr "Adicionar ร  playlist" #: src/constants.py:473 src/constants.py:524 src/constants.py:567 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:87 #: src/ui/song/queue.blp:89 msgid "Download" msgstr "Baixar" #: src/constants.py:478 src/constants.py:577 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Mostrar artista" #: src/constants.py:491 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Rรกdio" #: src/constants.py:504 msgid "Resume" msgstr "Retomar" #: src/constants.py:529 src/constants.py:557 msgid "Edit" msgstr "Editar" #: src/constants.py:543 msgid "Select" msgstr "Selecionar" #: src/constants.py:572 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Mostrar รกlbum" #: src/constants.py:588 src/ui/song/queue.blp:98 src/ui/song/queue.blp:100 msgid "Remove" msgstr "Remover" #: src/constants.py:593 src/ui/song/queue.blp:109 src/ui/song/queue.blp:111 msgid "Delete Download" msgstr "Excluir download" #: src/constants.py:599 src/main.py:156 msgid "Show Details" msgstr "Mostrar detalhes" #: src/constants.py:610 msgid "Visit Webpage" msgstr "Visitar pรกgina" #: src/constants.py:616 msgid "Update Server" msgstr "Atualizar servidor" #: src/constants.py:621 msgid "Delete Server" msgstr "Remover servidor" #: src/constants.py:630 msgid "Edit Lyrics" msgstr "Editar letra" #: src/constants.py:635 msgid "Remove Lyrics" msgstr "Remover letra" #: src/integrations/base.py:306 msgid "Could not generate SQL database" msgstr "Nรฃo foi possรญvel gerar o banco de dados SQL" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "Navegando" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "Ouvindo mรบsica" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "Conecte-se a um servidor Jellyfin." #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "Use uma instรขncia Jellyfin existente" #: src/integrations/jellyfin.py:295 msgid "Could not log in" msgstr "Nรฃo foi possรญvel fazer login" #: src/integrations/jellyfin.py:1135 msgid "Unknown" msgstr "Desconhecido" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:691 msgid "Local Files" msgstr "Arquivos locais" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Permita que o Nocturne carregue seus arquivos locais diretamente. Para " "bibliotecas grandes, รฉ recomendado usar um servidor dedicado." #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Continuar" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "Funcionalidade limitada" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "Carregando mรบsicas" #: src/integrations/local.py:138 src/integrations/navidrome.py:749 msgid "Could not locate path, try again" msgstr "Nรฃo foi possรญvel localizar o caminho, tente novamente" #: src/integrations/local.py:317 msgid "No Album" msgstr "Sem รกlbum" #: src/integrations/local.py:709 src/integrations/local.py:718 msgid "Offline Mode" msgstr "Modo offline" #: src/integrations/local.py:710 msgid "Access your downloads" msgstr "Acesse seus downloads" #: src/integrations/models.py:142 src/ui/album/page.blp:5 #: src/widgets/album/page.py:128 msgid "Album" msgstr "รlbum" #: src/integrations/models.py:144 msgid "Display Artist" msgstr "Exibir artista" #: src/integrations/models.py:146 msgid "MusicBrainz ID" msgstr "ID do MusicBrainz" #: src/integrations/models.py:147 msgid "Track Number" msgstr "Nรบmero da faixa" #: src/integrations/models.py:148 msgid "Year" msgstr "Ano" #: src/integrations/models.py:149 msgid "Size" msgstr "Tamanho" #: src/integrations/models.py:150 msgid "Suffix" msgstr "Extensรฃo" #: src/integrations/models.py:151 src/widgets/album/button.py:94 #: src/widgets/album/page.py:144 src/widgets/artist/page.py:109 #: src/widgets/playing/control_page.py:191 src/widgets/song/row.py:183 msgid "Favorite" msgstr "Favorito" #: src/integrations/models.py:152 msgid "Duration" msgstr "Duraรงรฃo" #: src/integrations/models.py:154 msgid "Bit Rate" msgstr "Taxa de bits" #: src/integrations/models.py:155 msgid "Bit Depth" msgstr "Profundidade de bits" #: src/integrations/models.py:156 msgid "Sampling Rate" msgstr "Taxa de amostragem" #: src/integrations/models.py:157 msgid "Channel Count" msgstr "Quantidade de canais" #: src/integrations/models.py:159 msgid "Path" msgstr "Caminho" #: src/integrations/models.py:160 msgid "Disc Number" msgstr "Nรบmero do disco" #: src/integrations/models.py:161 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:162 msgid "Genres" msgstr "Gรชneros" #. list #: src/integrations/models.py:164 msgid "Track Gain" msgstr "Ganho da faixa" #: src/integrations/models.py:165 msgid "Album Gain" msgstr "Ganho do รกlbum" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Conecte-se a um servidor OpenSubsonic como o Navidrome." #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "Servidor externo" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic instance" msgstr "Use uma instรขncia OpenSubsonic existente" #: src/integrations/navidrome.py:173 msgid "Server returned an error" msgstr "O servidor retornou um erro" #: src/integrations/navidrome.py:173 msgid "Unknown Error" msgstr "Erro desconhecido" #: src/integrations/navidrome.py:724 src/integrations/navidrome.py:733 msgid "Managed Server" msgstr "Servidor gerenciado" #: src/integrations/navidrome.py:725 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "Conecte-se a uma instรขncia do Navidrome gerenciada diretamente pelo Nocturne." #: src/integrations/navidrome.py:728 msgid "Manage Server" msgstr "Gerenciar servidor" #: src/integrations/navidrome.py:734 msgid "Create and use a Navidrome instance" msgstr "Crie e use uma instรขncia do Navidrome" #: src/integrations/navidrome.py:793 msgid "Important: Use your Subsonic login information." msgstr "Importante: use suas informaรงรตes de login do Subsonic." #: src/integrations/navidrome.py:796 msgid "Bandcamp User Settings" msgstr "Configuraรงรตes de usuรกrio do Bandcamp" #: src/integrations/navidrome.py:802 msgid "Explore your online library" msgstr "Explore sua biblioteca online" #: src/main.py:94 msgid "Music is Playing" msgstr "Mรบsica em reproduรงรฃo" #: src/main.py:107 msgid "Fullscreen Player Active" msgstr "Reprodutor em tela cheia ativo" #: src/main.py:153 msgid "Login Failed" msgstr "Falha no login" #: src/nocturne_search_provider.py.in:12 src/ui/album/button.blp:21 msgid "Play Album" msgstr "Reproduzir รกlbum" #: src/nocturne_search_provider.py.in:13 src/ui/song/button.blp:16 msgid "Play Song" msgstr "Reproduzir mรบsica" #: src/nocturne_search_provider.py.in:14 src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "Reproduzir playlist" #: src/nocturne_search_provider.py.in:113 msgid "Open" msgstr "Abrir" #: src/preferences.py:420 msgid "Settings Page" msgstr "Pรกgina de configuraรงรตes" #: src/preferences.py:423 msgid "User Token" msgstr "Token de usuรกrio" #: src/preferences.py:427 msgid "Link ListenBrainz" msgstr "Vincular ListenBrainz" #: src/preferences.py:428 msgid "Connect your ListenBrainz account with a user token" msgstr "Conecte sua conta do ListenBrainz usando um token de usuรกrio" #: src/preferences.py:476 msgid "Flatpak Sandbox Warning" msgstr "Aviso da sandbox do Flatpak" #: src/preferences.py:477 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "Para conectar-se ao Discord, รฉ necessรกria uma permissรฃo adicional. Apรณs " "executar o comando a seguir, reinicie o Nocturne" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:115 msgid "Toggle star" msgstr "Alternar estrela" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Capa do รกlbum" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "Entrar na pรกgina do รกlbum" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "Entrar na pรกgina do artista" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:228 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "1 estrela" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:237 msgid "2 Stars" msgstr "2 estrelas" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:246 msgid "3 Stars" msgstr "3 estrelas" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:255 msgid "4 Stars" msgstr "4 estrelas" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:264 msgid "5 Stars" msgstr "5 estrelas" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:130 msgid "Options" msgstr "Opรงรตes" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Artista" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Expandir biografia" #: src/ui/artist/page.blp:159 msgid "Top Songs" msgstr "Mรบsicas mais populares" #: src/ui/artist/page.blp:169 msgid "Related Artists" msgstr "Artistas relacionados" #: src/ui/containers/carousel.blp:24 src/ui/containers/carousel.blp:26 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Lista" #: src/ui/containers/carousel.blp:41 msgid "Pan to the Left" msgstr "Deslocar para a esquerda" #: src/ui/containers/carousel.blp:47 msgid "Pan to the Right" msgstr "Deslocar para a direita" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "Baixado" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Remover da fila" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "Fila de downloads" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "As mรบsicas baixadas estรฃo disponรญveis no modo offline" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Limpar downloads concluรญdos" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "Editor de letras" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:187 #: src/ui/playing/footer.blp:129 msgid "Pause" msgstr "Pausar" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:122 msgid "Current song progress bar" msgstr "Barra de progresso da mรบsica atual" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Definir o tempo da prรณxima linha para agora" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Adicionar linha no tempo" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "Sem letras" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "" "Nenhuma letra encontrada para esta faixa. Para comeรงar, adicione uma nova " "linha" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Marca de tempo" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Ir para a marca de tempo" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Definir marca de tempo atual" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Remover linha" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Grade" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Pesquisar รกlbuns" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Fim da lista alcanรงado" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "Nenhum รกlbum encontrado" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Pesquisar artistas" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "Nenhum artista encontrado" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "Alternar pesquisa" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "Pesquisar" #: src/ui/pages/home.blp:84 msgid "No Elements Found" msgstr "Nenhum elemento encontrado" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 #: src/widgets/pages/login.py:38 src/widgets/pages/login.py:78 msgid "Login" msgstr "Entrar" #: src/ui/pages/login.blp:36 msgid "Server Status" msgstr "Status do servidor" #: src/ui/pages/login.blp:37 src/widgets/pages/login.py:52 msgid "Not Running" msgstr "Nรฃo estรก em execuรงรฃo" #: src/ui/pages/login.blp:38 msgid "Restart Instance" msgstr "Reiniciar instรขncia" #: src/ui/pages/login.blp:51 msgid "Manage Navidrome Server" msgstr "Gerenciar servidor Navidrome" #: src/ui/pages/login.blp:61 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:69 msgid "More Options" msgstr "Mais opรงรตes" #: src/ui/pages/login.blp:75 msgid "Trust Server Certificates" msgstr "Confiar nos certificados do servidor" #: src/ui/pages/login.blp:83 msgid "Library Directory" msgstr "Diretรณrio da biblioteca" #: src/ui/pages/login.blp:96 msgid "Username" msgstr "Nome de usuรกrio" #: src/ui/pages/login.blp:101 msgid "Password" msgstr "Senha" #: src/ui/pages/login.blp:122 src/ui/pages/login.blp:123 msgid "Use Quick Connect" msgstr "Usar conexรฃo rรกpida" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:25 msgid "Playback" msgstr "Reproduรงรฃo" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "Ir para inรญcio" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "Alternar visualizador" #: src/ui/pages/playback.blp:54 src/window.py:213 msgid "Nocturne Playback" msgstr "Reproduรงรฃo do Nocturne" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "Estas sรฃo suas mรบsicas mais reproduzidas no รบltimo mรชs" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "Salvar playlist" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "Playlist adicionada" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "Obrigado por usar o Nocturne!" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "Pesquisar playlists" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "Nenhuma playlist encontrada" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Pesquisar rรกdios" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Adicionar rรกdio" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "Nenhuma rรกdio encontrada" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Configuraรงรฃo" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Download do Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "O Nocturne baixarรก o Navidrome automaticamente" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "Baixar (~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "Baixando" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Ocorreu um erro" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Tente novamente mais tarde" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "Download concluรญdo com sucesso" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Antes de continuar, certifique-se de criar uma conta de administrador e " "tambรฉm de que todas as suas faixas estejam dentro do diretรณrio de mรบsicas" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Pรกgina web da instรขncia" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "Pesquisar mรบsicas" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:96 #: src/ui/song/queue.blp:137 msgid "No Songs Found" msgstr "Nenhuma mรบsica encontrada" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Mรบsicas favoritas" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "Pesquisar mรบsicas favoritas" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "Boas-vindas" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Boas-vindas ao Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "Selecione um tipo de instรขncia para comeรงar" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Reproduzindo" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "Visitar pรกgina inicial da rรกdio atual" #: src/ui/playing/control_page.blp:157 src/ui/playing/footer.blp:99 msgid "Previous" msgstr "Anterior" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:143 msgid "Next" msgstr "Prรณxima" #: src/ui/playing/control_page.blp:221 msgid "Rate" msgstr "Avaliar" #: src/ui/playing/control_page.blp:277 msgid "Star" msgstr "Estrela" #: src/ui/playing/control_page.blp:284 msgid "Toggle Sidebar" msgstr "Alternar barra lateral" #: src/ui/playing/cover_art.blp:35 msgid "Audio" msgstr "รudio" #: src/ui/playing/cover_art.blp:43 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Capa da mรบsica" #: src/ui/playing/cover_art.blp:49 src/ui/playing/cover_art.blp:57 msgid "Video" msgstr "Vรญdeo" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "Predefiniรงรตes" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Plano" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Clareza vocal" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "Jazz suave" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rock" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Clรกssico" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Acรบstico" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Indicador de progresso da mรบsica atual" #: src/ui/playing/footer.blp:45 msgid "Current song cover art" msgstr "Capa da mรบsica atual" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "Voltar" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "Opรงรตes de letras" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "Nenhuma letra encontrada" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "Nรฃo baixado" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "A letra desta mรบsica ainda nรฃo foi baixada, ela pode estar disponรญvel para " "download" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "Carregar arquivo de letra" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Janela destacada" #: src/ui/playing/popout_window.blp:67 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:209 msgid "Song Details" msgstr "Detalhes da mรบsica" #: src/ui/playing/popout_window.blp:87 src/ui/window.blp:175 #: src/ui/window.blp:231 msgid "Queue" msgstr "Fila" #: src/ui/playing/popout_window.blp:99 src/ui/window.blp:191 #: src/ui/window.blp:243 msgid "Equalizer" msgstr "Equalizador" #: src/ui/playing/queue_page.blp:19 msgid "Generating Next Queue" msgstr "Gerando prรณxima fila" #: src/ui/playing/queue_page.blp:23 msgid "Autoplay" msgstr "Reproduรงรฃo automรกtica" #: src/ui/playing/queue_page.blp:24 msgid "Generate a new queue when the current one ends" msgstr "Gerar uma nova fila quando a atual terminar" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Volume" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Alto-falante sem som" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Alto-falante no volume mรกximo" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "Mostrar playlist" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "Capa da playlist" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "Entrar na pรกgina da playlist" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:83 msgid "Playlist" msgstr "Playlist" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Pesquisar (ou criar nova playlist)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Adicionando mรบsica ร  playlist" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "Criar nova playlist e adicionar mรบsicas" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "Geral" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "Comportamento" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "Restaurar fila ao iniciar" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "Ocultar ao fechar" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "Simular letras palavra por palavra" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "Usado quando apenas letras linha por linha estรฃo disponรญveis" #: src/ui/preferences.blp:22 msgid "Auto Download Lyrics" msgstr "Baixar letras automaticamente" #: src/ui/preferences.blp:23 msgid "Used when the current instance does not provide lyrics" msgstr "Usado quando a instรขncia atual nรฃo fornece letras" #: src/ui/preferences.blp:26 msgid "Use Track and Album Gain" msgstr "Usar ganho da faixa e do รกlbum" #: src/ui/preferences.blp:27 msgid "Adjust the volume based on gain metadata" msgstr "Ajustar o volume com base nos metadados de ganho" #: src/ui/preferences.blp:30 msgid "Default Page on Login" msgstr "Pรกgina padrรฃo ao entrar" #: src/ui/preferences.blp:35 msgid "Maximum Bitrate" msgstr "Taxa de bits mรกxima" #: src/ui/preferences.blp:41 msgid "Gnome Search" msgstr "Pesquisa do GNOME" #: src/ui/preferences.blp:42 msgid "" "Search for your music directly from the system search bar while Nocturne is " "running. You can enable or disable this feature completely in System Settings" msgstr "" "Pesquise suas mรบsicas diretamente pela barra de pesquisa do sistema enquanto " "o Nocturne estiver em execuรงรฃo. Vocรช pode ativar ou desativar completamente " "esse recurso nas Configuraรงรตes do Sistema" #: src/ui/preferences.blp:45 msgid "Include Artists" msgstr "Incluir artistas" #: src/ui/preferences.blp:48 msgid "Include Albums" msgstr "Incluir รกlbuns" #: src/ui/preferences.blp:51 msgid "Include Songs" msgstr "Incluir mรบsicas" #: src/ui/preferences.blp:54 msgid "Include Playlists" msgstr "Incluir playlists" #: src/ui/preferences.blp:58 msgid "Session" msgstr "Sessรฃo" #: src/ui/preferences.blp:61 msgid "Discord Rich Presence" msgstr "Rich Presence do Discord" #: src/ui/preferences.blp:62 msgid "Share the currently playing song on Discord" msgstr "Compartilhar a mรบsica atual no Discord" #: src/ui/preferences.blp:66 msgid "Share Cover Art With Discord" msgstr "Compartilhar capa com o Discord" #: src/ui/preferences.blp:67 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "Se a instรขncia nรฃo puder ser acessada pelos servidores do Discord, o " "CoverArtArchive serรก usado em seu lugar" #: src/ui/preferences.blp:73 msgid "ListenBrainz Link" msgstr "Link do ListenBrainz" #: src/ui/preferences.blp:74 msgid "Keep track of your playback history" msgstr "Mantenha um registro do seu histรณrico de reproduรงรฃo" #: src/ui/preferences.blp:80 src/ui/preferences.blp:81 msgid "Link" msgstr "Vincular" #: src/ui/preferences.blp:89 src/ui/preferences.blp:90 msgid "Unlink" msgstr "Desvincular" #: src/ui/preferences.blp:102 msgid "User" msgstr "Usuรกrio" #: src/ui/preferences.blp:122 msgid "Delete Image Cache" msgstr "Excluir cache de imagens" #: src/ui/preferences.blp:129 msgid "Logout" msgstr "Sair" #: src/ui/preferences.blp:139 msgid "Customization" msgstr "Personalizaรงรฃo" #: src/ui/preferences.blp:142 msgid "Interface" msgstr "Interface" #: src/ui/preferences.blp:144 msgid "Show Context Button in Rows" msgstr "Mostrar botรฃo de contexto nas linhas" #: src/ui/preferences.blp:145 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" "O menu tambรฉm pode ser exibido clicando com o botรฃo direito ou mantendo " "pressionado" #: src/ui/preferences.blp:148 msgid "Show Labels in Buttons of Pages" msgstr "Mostrar rรณtulos nos botรตes das pรกginas" #: src/ui/preferences.blp:151 msgid "Show Progressbar in Player Footer" msgstr "Mostrar barra de progresso no rodapรฉ do player" #: src/ui/preferences.blp:154 msgid "Make Background Translucent in Player" msgstr "Tornar o plano de fundo translรบcido no player" #: src/ui/preferences.blp:155 msgid "Does not affect sidebar player" msgstr "Nรฃo afeta o reprodutor da barra lateral" #: src/ui/preferences.blp:158 msgid "Use Sidebar Player When Possible" msgstr "Usar player lateral quando possรญvel" #: src/ui/preferences.blp:159 msgid "Sidebar player is only available when the window is wide enough" msgstr "" "O player lateral sรณ estรก disponรญvel quando a janela รฉ larga o suficiente" #: src/ui/preferences.blp:162 msgid "Filter Singles" msgstr "Filtrar singles" #: src/ui/preferences.blp:163 msgid "Hides albums with only one song (does not affect homepage)" msgstr "Oculta รกlbuns com apenas uma mรบsica (nรฃo afeta a pรกgina inicial)" #: src/ui/preferences.blp:166 msgid "Button Size" msgstr "Tamanho dos botรตes" #: src/ui/preferences.blp:167 msgid "Affects albums, artists, playlists and songs" msgstr "Afeta รกlbuns, artistas, playlists e mรบsicas" #: src/ui/preferences.blp:173 src/ui/preferences.blp:174 msgid "Small" msgstr "Pequeno" #: src/ui/preferences.blp:178 src/ui/preferences.blp:179 msgid "Big" msgstr "Grande" #: src/ui/preferences.blp:187 msgid "Dynamic Background" msgstr "Plano de fundo dinรขmico" #: src/ui/preferences.blp:188 msgid "Background generated based on the album art of the current song" msgstr "Plano de fundo gerado com base na arte da capa da mรบsica atual" #: src/ui/preferences.blp:191 msgid "Use in Main Window" msgstr "Usar na janela principal" #: src/ui/preferences.blp:197 src/ui/preferences.blp:198 #: src/ui/preferences.blp:221 src/ui/preferences.blp:222 #: src/ui/preferences.blp:245 src/ui/preferences.blp:246 msgid "Off" msgstr "Desativado" #: src/ui/preferences.blp:202 src/ui/preferences.blp:203 #: src/ui/preferences.blp:226 src/ui/preferences.blp:227 #: src/ui/preferences.blp:250 src/ui/preferences.blp:251 msgid "Gradient" msgstr "Gradiente" #: src/ui/preferences.blp:207 src/ui/preferences.blp:208 #: src/ui/preferences.blp:231 src/ui/preferences.blp:232 #: src/ui/preferences.blp:255 src/ui/preferences.blp:256 msgid "Blur" msgstr "Desfoque" #: src/ui/preferences.blp:215 msgid "Use in Player" msgstr "Usar no player" #: src/ui/preferences.blp:239 msgid "Use in Pop-out Window" msgstr "Usar na janela destacada" #: src/ui/preferences.blp:263 msgid "Use as Accent Color" msgstr "Usar como cor de destaque" #: src/ui/preferences.blp:268 msgid "Homepage" msgstr "Pรกgina inicial" #: src/ui/preferences.blp:269 msgid "Reload homepage to see changes" msgstr "Recarregue a pรกgina inicial para ver as alteraรงรตes" #: src/ui/preferences.blp:305 msgid "Sidebar" msgstr "Barra lateral" #: src/ui/preferences.blp:309 msgid "Visualizer" msgstr "Visualizador" #: src/ui/preferences.blp:313 src/ui/window.blp:260 msgid "Preferences" msgstr "Preferรชncias" #: src/ui/preferences.blp:315 msgid "Show Visualizer in Player" msgstr "Mostrar visualizador no player" #: src/ui/preferences.blp:316 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" "Mostrar um visualizador de espectro de รกudio sobre a arte da capa no player" #: src/ui/preferences.blp:321 msgid "Appearance" msgstr "Aparรชncia" #: src/ui/preferences.blp:325 msgid "Number of Bars" msgstr "Nรบmero de barras" #: src/ui/preferences.blp:336 msgid "Visualizer Type" msgstr "Tipo de visualizador" #: src/ui/preferences.blp:342 src/ui/preferences.blp:343 msgid "Wave" msgstr "Onda" #: src/ui/preferences.blp:347 src/ui/preferences.blp:348 msgid "Bars" msgstr "Barras" #: src/ui/preferences.blp:352 src/ui/preferences.blp:353 msgid "Particles" msgstr "Partรญculas" #: src/ui/preferences.blp:360 msgid "Fill Mode" msgstr "Modo de preenchimento" #: src/ui/preferences.blp:366 src/ui/preferences.blp:367 msgid "Solid" msgstr "Sรณlido" #: src/ui/preferences.blp:371 src/ui/preferences.blp:372 msgid "Translucent" msgstr "Translรบcido" #: src/ui/preferences.blp:376 src/ui/preferences.blp:377 msgid "Border" msgstr "Borda" #: src/ui/preferences.blp:385 msgid "Color" msgstr "Cor" #: src/ui/preferences.blp:389 msgid "Use Dynamic Color" msgstr "Usar cor dinรขmica" #: src/ui/preferences.blp:392 msgid "Invert Color" msgstr "Inverter cor" #: src/ui/preferences.blp:396 msgid "Manual Color" msgstr "Cor manual" #: src/ui/preferences.blp:402 msgid "Visualizer Color" msgstr "Cor do visualizador" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Shortcuts" msgstr "Mostrar atalhos" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Preferences" msgstr "Mostrar preferรชncias" #: src/ui/shortcuts-dialog.blp:20 msgid "Quit" msgstr "Sair" #: src/ui/shortcuts-dialog.blp:27 msgid "Toggle Playback" msgstr "Alternar reproduรงรฃo" #: src/ui/shortcuts-dialog.blp:31 msgid "Next Song" msgstr "Prรณxima mรบsica" #: src/ui/shortcuts-dialog.blp:35 msgid "Previous Song" msgstr "Mรบsica anterior" #: src/ui/shortcuts-dialog.blp:39 msgid "Raise Volume" msgstr "Aumentar volume" #: src/ui/shortcuts-dialog.blp:43 msgid "Lower Volume" msgstr "Diminuir volume" #: src/ui/shortcuts-dialog.blp:48 msgid "Window" msgstr "Janela" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:270 #: src/widgets/playing/popout_window.py:48 msgid "Toggle Fullscreen" msgstr "Alternar tela cheia" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:265 msgid "Open Pop-Out Window" msgstr "Abrir janela destacada" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "Arte da capa da mรบsica" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "Indicador de reproduรงรฃo" #: src/ui/song/queue.blp:47 src/ui/song/queue.blp:49 msgid "Select All" msgstr "Selecionar tudo" #: src/ui/song/row.blp:44 msgid "Drag icon" msgstr "รcone de arrastar" #: src/ui/song/row.blp:142 msgid "Selection box" msgstr "Caixa de seleรงรฃo" #: src/ui/window.blp:94 msgid "Play Random Queue" msgstr "Reproduzir fila aleatรณria" #: src/ui/window.blp:102 msgid "Menu" msgstr "Menu" #: src/ui/window.blp:130 msgid "Random Albums" msgstr "รlbuns aleatรณrios" #: src/ui/window.blp:134 msgid "Favorite Albums" msgstr "รlbuns favoritos" #: src/ui/window.blp:138 msgid "Newest Albums" msgstr "รlbuns mais recentes" #: src/ui/window.blp:142 msgid "Recent Albums" msgstr "รlbuns recentes" #: src/ui/window.blp:146 msgid "Frequent Albums" msgstr "รlbuns frequentes" #: src/ui/window.blp:169 msgid "Player" msgstr "Player" #: src/ui/window.blp:275 msgid "See Last Nocturne Playback" msgstr "Ver a รบltima retrospectiva do Nocturne" #: src/ui/window.blp:280 msgid "Keyboard Shortcuts" msgstr "Atalhos de teclado" #: src/ui/window.blp:285 msgid "About Nocturne" msgstr "Sobre o Nocturne" #: src/widgets/album/button.py:99 src/widgets/album/page.py:149 #: src/widgets/artist/page.py:114 src/widgets/playing/control_page.py:196 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "Nรฃo รฉ favorito" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "Disco {}" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} รกlbum" msgstr[1] "{} รกlbuns" #: src/widgets/artist/page.py:97 msgid "Author" msgstr "Autor" #: src/widgets/containers/context.py:50 #, python-brace-format msgid "{} Stars" msgstr "{} estrelas" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Sem marca de tempo" #: src/widgets/lyrics/dialog.py:195 msgid "Lyrics Saved" msgstr "Letra salva" #: src/widgets/pages/login.py:52 src/widgets/pages/login.py:131 msgid "Running" msgstr "Em execuรงรฃo" #: src/widgets/pages/login.py:85 msgid "Extra Menu" msgstr "Menu adicional" #: src/widgets/pages/login.py:120 src/widgets/pages/setup.py:83 msgid "Music Library" msgstr "Biblioteca de mรบsicas" #: src/widgets/pages/login.py:130 msgid "Restarted" msgstr "Reiniciado" #: src/widgets/pages/login.py:172 msgid "Quick Connect" msgstr "Conexรฃo rรกpida" #: src/widgets/pages/login.py:173 msgid "Error getting code" msgstr "Erro ao obter o cรณdigo" #: src/widgets/pages/login.py:175 msgid "Quick Connect Page" msgstr "Pรกgina de conexรฃo rรกpida" #: src/widgets/pages/playback.py:29 #, python-brace-format msgid "Nocturne Playback ~ {}" msgstr "Retrospectiva do Nocturne ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 #, python-brace-format msgid "{} Plays" msgstr "{} reproduรงรตes" #: src/widgets/pages/setup.py:70 msgid "Installing" msgstr "Instalando" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integraรงรฃo" #: src/widgets/playing/lyrics_page.py:220 msgid "LRC File" msgstr "Arquivo LRC" #: src/widgets/playing/player.py:625 msgid "Warning: Song changed but volume is set to 0" msgstr "Aviso: a mรบsica mudou, mas o volume estรก definido como 0" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:115 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "{} mรบsica" msgstr[1] "{} mรบsicas" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Nova playlist" #: src/widgets/playlist/selector_row.py:33 #, python-brace-format msgid "Add Songs to '{}'" msgstr "Adicionar mรบsicas ร  playlist '{}'" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "Sim" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "Nรฃo" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "Nรฃo encontrado" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "Vรกrios artistas" #: src/window.py:214 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "Sua fila de reproduรงรฃo de {} estรก disponรญvel!" #: src/window.py:216 msgid "Show" msgstr "Mostrar" #: src/window.py:217 msgid "Later" msgstr "Mais tarde" #~ msgid "Show Pan Buttons on Carousels" #~ msgstr "Mostrar botรตes de navegaรงรฃo nos carrossรฉis" #~ msgid "Only visible when a carousel has 5 or more items" #~ msgstr "Visรญvel apenas quando um carrossel possui 5 ou mais itens" Jeffser-Nocturne-b442fee/po/ru.po000066400000000000000000001622271523240030000167710ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Aleksandr Shamaraev , 2026. # gttn-84 <4youtome@rambler.ru>, 2026. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-19 20:14+1100\n" "PO-Revision-Date: 2026-06-19 20:56+1100\n" "Last-Translator: Aleksandr Shamaraev \n" "Language-Team: \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 3.9\n" #: data/com.jeffser.Nocturne.desktop.in:3 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:11 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;ะœัƒะทั‹ะบะฐ;ะัƒะดะธะพ;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "ะžะฑัŠะตะดะธะฝะธั‚ะต ัะฒะพัŽ ะผัƒะทั‹ะบะฐะปัŒะฝัƒัŽ ะฑะธะฑะปะธะพั‚ะตะบัƒ" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "ะกะพะฒั€ะตะผะตะฝะฝั‹ะน ะบะปะธะตะฝั‚ Navidrome / Jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "ะคัƒะฝะบั†ะธะธ" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "ะŸะพะธัะบ ะฟะพ ะฟะตัะฝัะผ, ะธัะฟะพะปะฝะธั‚ะตะปัะผ, ะฐะปัŒะฑะพะผะฐะผ, ั€ะฐะดะธะพัั‚ะฐะฝั†ะธัะผ ะธ ะฟะปะตะนะปะธัั‚ะฐะผ" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "ะฃะฟั€ะฐะฒะปะตะฝะธะต ะฟะปะตะนะปะธัั‚ะฐะผะธ" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "ะกะพะฒะผะตัั‚ะธะผะพัั‚ัŒ ั Jellyfin, OpenSubsonic ะธ ะปะพะบะฐะปัŒะฝั‹ะผะธ ั„ะฐะนะปะฐะผะธ" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "ะัƒะดะธะพัะบะฒะฐะปะฐะนะทะตั€ ะธ ะฐัƒะดะธะพะฒะธะทัƒะฐะปะธะทะฐั‚ะพั€" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "ะ˜ะฝั‚ะตะณั€ะฐั†ะธั Mpris" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "ะ˜ะฝั‚ะตะณั€ะธั€ะพะฒะฐะฝะฝะพะต ัƒะฟั€ะฐะฒะปะตะฝะธะต ัะบะทะตะผะฟะปัั€ะฐะผะธ Navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะพะต ะฟะพะปัƒั‡ะตะฝะธะต ั‚ะตะบัั‚ะฐ ะฟะตัะตะฝ" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "ะ—ะฐะณั€ัƒะทะบะธ ะธ ะฐะฒั‚ะพะฝะพะผะฝั‹ะน ั€ะตะถะธะผ" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "ะšะปะฐััะฝั‹ะน ะธะฝั‚ะตั€ั„ะตะนั" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "ะ”ะถะตั„ั„ั€ะธ ะกะฐะผัƒัะปัŒ ะญะดัƒะฐั€ั‚ะต ะ ะพั…ะฐั" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "ะ“ะปะฐะฒะฝะฐั ัั‚ั€ะฐะฝะธั†ะฐ" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "ะžั‡ะตั€ะตะดัŒ ะฟะตัะตะฝ" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:91 src/ui/window.blp:190 #: src/ui/window.blp:242 msgid "Lyrics" msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "ะกะฟะธัะพะบ ะฟะตัะตะฝ" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "ะกั‚ั€ะฐะฝะธั†ะฐ ะฐะปัŒะฑะพะผะฐ" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "ะผัƒะทั‹ะบะฐ" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:138 src/widgets/pages/login.py:107 msgid "Error" msgstr "ะžัˆะธะฑะบะฐ" #: src/actions.py:139 msgid "No details provided" msgstr "ะŸะพะดั€ะพะฑะฝะพัั‚ะธ ะฝะต ะฟั€ะตะดะพัั‚ะฐะฒะปะตะฝั‹" #: src/actions.py:141 src/actions.py:290 src/preferences.py:433 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "ะ—ะฐะบั€ั‹ั‚ัŒ" #: src/actions.py:182 msgid "Not enough data found for Nocturne Playback ({})" msgstr "ะะฐะนะดะตะฝะพ ะฝะตะดะพัั‚ะฐั‚ะพั‡ะฝะพ ะดะฐะฝะฝั‹ั… ะดะปั ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั Nocturne ({})" #: src/actions.py:287 src/ui/song/row.blp:69 msgid "External File" msgstr "ะ’ะฝะตัˆะฝะธะน ั„ะฐะนะป" #: src/actions.py:288 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "ะญั‚ะพั‚ ั‚ั€ะตะบ ะฑั‹ะป ะทะฐะณั€ัƒะถะตะฝ ะธะท ะฒะฝะตัˆะฝะตะณะพ ั„ะฐะนะปะฐ, ะฐ ัั‚ะพ ะทะฝะฐั‡ะธั‚, ั‡ั‚ะพ ะพะฝ ะฑัƒะดะตั‚ ะธะผะตั‚ัŒ " "ะผะตะฝัŒัˆะต ั„ัƒะฝะบั†ะธะน ะฟะพ ัั€ะฐะฒะฝะตะฝะธัŽ ั ั‚ั€ะตะบะพะผ ะธะท ะฑะธะฑะปะธะพั‚ะตะบะธ" #: src/actions.py:308 msgid "Navidrome server deleted successfully" msgstr "ะกะตั€ะฒะตั€ Navidrome ัƒัะฟะตัˆะฝะพ ัƒะดะฐะปะตะฝ" #: src/actions.py:317 msgid "Delete Navidrome Server" msgstr "ะฃะดะฐะปะธั‚ัŒ ัะตั€ะฒะตั€ Navidrome" #: src/actions.py:318 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "ะ’ั‹ ัƒะฒะตั€ะตะฝั‹, ั‡ั‚ะพ ั…ะพั‚ะธั‚ะต ัƒะดะฐะปะธั‚ัŒ ะฒัั‚ั€ะพะตะฝะฝั‹ะน ัะตั€ะฒะตั€ Navidrome?" #: src/actions.py:320 src/actions.py:465 src/actions.py:499 src/actions.py:841 #: src/actions.py:944 src/preferences.py:386 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "ะžั‚ะผะตะฝะฐ" #: src/actions.py:321 msgid "Keep Data" msgstr "ะžัั‚ะฐะฒะธั‚ัŒ ะดะฐะฝะฝั‹ะต" #: src/actions.py:322 msgid "Delete Everything" msgstr "ะฃะดะฐะปะธั‚ะต ะฒัั‘" #: src/actions.py:411 msgid "No Name" msgstr "ะ‘ะตะท ะธะผะตะฝะธ" #: src/actions.py:430 msgid "Radio updated successfully" msgstr "ะ ะฐะดะธะพ ัƒัะฟะตัˆะฝะพ ะพะฑะฝะพะฒะปะตะฝะพ" #: src/actions.py:430 msgid "Radio added successfully" msgstr "ะ ะฐะดะธะพ ัƒัะฟะตัˆะฝะพ ะดะพะฑะฐะฒะปะตะฝะพ" #: src/actions.py:441 msgid "Error updating radio" msgstr "ะžัˆะธะฑะบะฐ ะพะฑะฝะพะฒะปะตะฝะธั ั€ะฐะดะธะพ" #: src/actions.py:441 msgid "Error adding radio" msgstr "ะžัˆะธะฑะบะฐ ะดะพะฑะฐะฒะปะตะฝะธั ั€ะฐะดะธะพ" #: src/actions.py:450 src/actions.py:833 msgid "Name" msgstr "ะ˜ะผั" #: src/actions.py:455 msgid "Stream Url" msgstr "URL ะฟะพั‚ะพะบะฐ" #: src/actions.py:462 msgid "Update Radio Station" msgstr "ะžะฑะฝะพะฒะปะตะฝะธะต ั€ะฐะดะธะพัั‚ะฐะฝั†ะธะธ" #: src/actions.py:462 msgid "Add Radio Station" msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ั€ะฐะดะธะพัั‚ะฐะฝั†ะธัŽ" #: src/actions.py:466 src/preferences.py:387 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "ะกะพั…ั€ะฐะฝะธั‚ัŒ" #: src/actions.py:482 msgid "Radio deleted successfully" msgstr "ะ ะฐะดะธะพ ัƒัะฟะตัˆะฝะพ ัƒะดะฐะปะตะฝะพ" #: src/actions.py:490 msgid "Error deleting radio" msgstr "ะžัˆะธะฑะบะฐ ัƒะดะฐะปะตะฝะธั ั€ะฐะดะธะพ" #: src/actions.py:496 msgid "Delete Radio Station" msgstr "ะฃะดะฐะปะธั‚ัŒ ั€ะฐะดะธะพัั‚ะฐะฝั†ะธัŽ" #: src/actions.py:497 src/actions.py:942 msgid "Are you sure you want to delete '{}'?" msgstr "ะ’ั‹ ัƒะฒะตั€ะตะฝั‹, ั‡ั‚ะพ ั…ะพั‚ะธั‚ะต ัƒะดะฐะปะธั‚ัŒ '{}'?" #: src/actions.py:500 src/actions.py:945 src/constants.py:375 #: src/constants.py:423 msgid "Delete" msgstr "ะฃะดะฐะปะธั‚ัŒ" #: src/actions.py:534 src/actions.py:566 src/actions.py:572 src/actions.py:674 #: src/actions.py:762 msgid "Playing Next" msgstr "ะ˜ะณั€ะฐั‚ัŒ ะดะฐะปัŒัˆะต" #: src/actions.py:546 src/actions.py:585 src/actions.py:591 src/actions.py:691 #: src/actions.py:779 msgid "Playing Later" msgstr "ะ˜ะณั€ะฐั‚ัŒ ะฟะพะทะถะต" #: src/actions.py:566 src/actions.py:585 src/actions.py:1059 #: src/actions.py:1194 src/widgets/playlist/dialog.py:31 msgid "{} Songs" msgstr "{} ะŸะตัะฝะธ" #: src/actions.py:621 msgid "Lyrics Saved" msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ ัะพั…ั€ะฐะฝะตะฝ" #: src/actions.py:814 msgid "Playlist updated successfully" msgstr "ะŸะปะตะนะปะธัั‚ ัƒัะฟะตัˆะฝะพ ะพะฑะฝะพะฒะปะตะฝ" #: src/actions.py:814 msgid "Playlist created successfully" msgstr "ะŸะปะตะนะปะธัั‚ ัƒัะฟะตัˆะฝะพ ัะพะทะดะฐะฝ" #: src/actions.py:824 msgid "Error updating playlist" msgstr "ะžัˆะธะฑะบะฐ ะพะฑะฝะพะฒะปะตะฝะธั ะฟะปะตะนะปะธัั‚ะฐ" #: src/actions.py:824 msgid "Error creating playlist" msgstr "ะžัˆะธะฑะบะฐ ัะพะทะดะฐะฝะธั ะฟะปะตะนะปะธัั‚ะฐ" #: src/actions.py:838 msgid "Update Playlist" msgstr "ะžะฑะฝะพะฒะธั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/actions.py:838 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "ะกะพะทะดะฐั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/actions.py:842 msgid "Update" msgstr "ะžะฑะฝะพะฒะธั‚ัŒ" #: src/actions.py:842 msgid "Create" msgstr "ะกะพะทะดะฐั‚ัŒ" #: src/actions.py:861 msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "{} ะŸะตัะฝั ัƒะดะฐะปะตะฝะฐ" msgstr[1] "{} ะŸะตัะฝะธ ัƒะดะฐะปะตะฝั‹" msgstr[2] "{} ะŸะตัะตะฝ ัƒะดะฐะปะตะฝะพ" #: src/actions.py:892 src/actions.py:912 msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "{} ะŸะตัะฝั ะดะพะฑะฐะฒะปะตะฝะฐ" msgstr[1] "{} ะŸะตัะฝะธ ะดะพะฑะฐะฒะปะตะฝั‹" msgstr[2] "{} ะŸะตัะตะฝ ะดะพะฑะฐะฒะปะตะฝะพ" #: src/actions.py:916 src/actions.py:1064 msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "{} ะŸะตัะฝั ะฟั€ะพะฟัƒั‰ะตะฝะฐ" msgstr[1] "{} ะŸะตัะฝะธ ะฟั€ะพะฟัƒั‰ะตะฝั‹" msgstr[2] "{} ะŸะตัะตะฝ ะฟั€ะพะฟัƒั‰ะตะฝะพ" #: src/actions.py:929 msgid "Playlist Deleted" msgstr "ะŸะปะตะนะปะธัั‚ ัƒะดะฐะปะตะฝ" #: src/actions.py:941 msgid "Delete Playlist" msgstr "ะฃะดะฐะปะธั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/actions.py:990 msgid "No songs found" msgstr "ะŸะตัะฝะธ ะฝะต ะฝะฐะนะดะตะฝั‹" #: src/actions.py:1033 src/actions.py:1060 src/actions.py:1097 #: src/actions.py:1133 msgid "Download Started" msgstr "ะ—ะฐะณั€ัƒะทะบะฐ ะฝะฐั‡ะฐะปะฐััŒ" #: src/actions.py:1040 src/actions.py:1071 src/actions.py:1078 #: src/actions.py:1113 src/actions.py:1149 msgid "Already Downloaded" msgstr "ะฃะถะต ะทะฐะณั€ัƒะถะตะฝะพ" #: src/actions.py:1065 msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} ะŸะตัะฝั ({})" msgstr[1] "{} ะŸะตัะฝะธ ({})" msgstr[2] "{} ะŸะตัะตะฝ ({})" #: src/actions.py:1077 msgid "Download Skipped" msgstr "ะ—ะฐะณั€ัƒะทะบะฐ ะฟั€ะพะฟัƒั‰ะตะฝะฐ" #: src/actions.py:1101 src/actions.py:1137 msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "ะ—ะฐะณั€ัƒะทะบะฐ ะฝะฐั‡ะฐะปะฐััŒ ({} ะŸะตัะฝั ะฟั€ะพะฟัƒั‰ะตะฝะฐ)" msgstr[1] "ะ—ะฐะณั€ัƒะทะบะฐ ะฝะฐั‡ะฐะปะฐััŒ ({} ะŸะตัะฝะธ ะฟั€ะพะฟัƒั‰ะตะฝั‹)" msgstr[2] "ะ—ะฐะณั€ัƒะทะบะฐ ะฝะฐั‡ะฐะปะฐััŒ ({} ะŸะตัะตะฝ ะฟั€ะพะฟัƒั‰ะตะฝะพ)" #: src/actions.py:1176 src/actions.py:1195 msgid "Deleted" msgstr "ะฃะดะฐะปะตะฝะพ" #: src/constants.py:185 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "ะŸะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝะพ" #: src/constants.py:189 msgid "Repeat All" msgstr "ะŸะพะฒั‚ะพั€ะธั‚ัŒ ะฒัะต" #: src/constants.py:193 msgid "Repeat One" msgstr "ะŸะพะฒั‚ะพั€ะธั‚ัŒ ะพะดะฝัƒ" #: src/constants.py:198 msgid "Low ({})" msgstr "ะะธะทะบะธะน ({})" #: src/constants.py:199 msgid "Medium ({})" msgstr "ะกั€ะตะดะฝะธะน ({})" #: src/constants.py:200 msgid "High ({})" msgstr "ะ’ั‹ัะพะบะธะน ({})" #: src/constants.py:201 msgid "Ultra ({})" msgstr "ะฃะปัŒั‚ั€ะฐ ({})" #: src/constants.py:202 msgid "Original File" msgstr "ะ˜ัั…ะพะดะฝั‹ะน ั„ะฐะนะป" #. Item #: src/constants.py:209 src/ui/pages/home.blp:5 msgid "Home" msgstr "ะ”ะพะผะพะน" #. Item #. list #: src/constants.py:214 src/integrations/models.py:159 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:259 src/widgets/pages/home.py:54 msgid "Artists" msgstr "ะั€ั‚ะธัั‚ั‹" #. Section #: src/constants.py:221 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:251 #: src/widgets/artist/page.py:53 src/widgets/pages/home.py:49 msgid "Albums" msgstr "ะะปัŒะฑะพะผั‹" #. Item #: src/constants.py:224 src/constants.py:259 src/constants.py:279 #: src/window.py:149 msgid "All" msgstr "ะ’ัะต" #. Item #: src/constants.py:229 msgid "Random" msgstr "ะกะปัƒั‡ะฐะนะฝะพ" #. Item #: src/constants.py:234 src/constants.py:264 msgid "Favorites" msgstr "ะ˜ะทะฑั€ะฐะฝะฝะพะต" #. Item #: src/constants.py:239 msgid "Recently Added" msgstr "ะะตะดะฐะฒะฝะพ ะดะพะฑะฐะฒะปะตะฝะฝั‹ะต" #. Item #: src/constants.py:244 msgid "Recently Played" msgstr "ะะตะดะฐะฒะฝะพ ัั‹ะณั€ะฐะฝะฝั‹ะต" #. Item #: src/constants.py:249 msgid "Most Played" msgstr "ะกะฐะผั‹ะต ะฟะพะฟัƒะปัั€ะฝั‹ะต" #. Section #: src/constants.py:256 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:243 #: src/widgets/album/page.py:45 src/widgets/pages/home.py:38 #: src/widgets/playlist/page.py:29 msgid "Songs" msgstr "ะŸะตัะฝะธ" #. Item #: src/constants.py:269 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "ะ ะฐะดะธะพ" #. Section #: src/constants.py:276 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:267 #: src/widgets/pages/home.py:59 msgid "Playlists" msgstr "ะŸะปะตะนะปะธัั‚ั‹" #: src/constants.py:289 src/constants.py:340 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:225 src/ui/playing/footer.blp:103 #: src/ui/song/queue.blp:47 src/ui/song/queue.blp:49 msgid "Play" msgstr "ะ˜ะณั€ะฐั‚ัŒ" #: src/constants.py:294 src/constants.py:327 src/constants.py:350 msgid "Shuffle" msgstr "ะŸะตั€ะตะผะตัˆะฐั‚ัŒ" #: src/constants.py:299 src/constants.py:355 src/constants.py:388 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตัั‚ะธ ะดะฐะปะตะต" #: src/constants.py:304 src/constants.py:360 src/constants.py:393 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตัั‚ะธ ะฟะพะทะถะต" #: src/constants.py:309 src/constants.py:403 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฒ ะฟะปะตะนะปะธัั‚" #: src/constants.py:314 src/constants.py:365 src/constants.py:408 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:167 #: src/ui/playing/lyrics_page.blp:170 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "ะกะบะฐั‡ะฐั‚ัŒ" #: src/constants.py:319 src/constants.py:418 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะั€ั‚ะธัั‚ะฐ" #: src/constants.py:332 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "ะ ะฐะดะธะพ" #: src/constants.py:345 msgid "Resume" msgstr "ะ’ะพะทะพะฑะฝะพะฒะธั‚ัŒ" #: src/constants.py:370 src/constants.py:398 msgid "Edit" msgstr "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ" #: src/constants.py:384 msgid "Select" msgstr "ะ’ั‹ะฑั€ะฐั‚ัŒ" #: src/constants.py:413 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฐะปัŒะฑะพะผ" #: src/constants.py:429 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "ะฃะดะฐะปะธั‚ัŒ" #: src/constants.py:434 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "ะฃะดะฐะปะธั‚ัŒ ะทะฐะณั€ัƒะทะบัƒ" #: src/constants.py:440 src/main.py:134 msgid "Show Details" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฟะพะดั€ะพะฑะฝะพัั‚ะธ" #: src/constants.py:451 msgid "Visit Webpage" msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ะฒะตะฑ-ัั‚ั€ะฐะฝะธั†ัƒ" #: src/constants.py:457 msgid "Update Server" msgstr "ะžะฑะฝะพะฒะธั‚ัŒ ะกะตั€ะฒะตั€" #: src/constants.py:462 msgid "Delete Server" msgstr "ะฃะดะฐะปะธั‚ัŒ ะกะตั€ะฒะตั€" #: src/constants.py:471 msgid "Edit Lyrics" msgstr "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ั‚ะตะบัั‚ ะฟะตัะฝะธ" #: src/constants.py:476 msgid "Remove Lyrics" msgstr "ะฃะดะฐะปะธั‚ัŒ ั‚ะตะบัั‚ ะฟะตัะฝะธ" #: src/integrations/base.py:148 msgid "Could not generate SQL database" msgstr "ะะต ัƒะดะฐะปะพััŒ ัะณะตะฝะตั€ะธั€ะพะฒะฐั‚ัŒ ะฑะฐะทัƒ ะดะฐะฝะฝั‹ั… SQL" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "ะŸั€ะพัะผะพั‚ั€" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "ะŸั€ะพัะปัƒัˆะธะฒะฐะฝะธะต ะผัƒะทั‹ะบะธ" #: src/integrations/jellyfin.py:18 msgid "Connect to a Jellyfin server." msgstr "ะŸะพะดะบะปัŽั‡ะธั‚ัั ะบ ัะตั€ะฒะตั€ัƒ Jellyfin." #: src/integrations/jellyfin.py:22 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:23 msgid "Use an existing Jellyfin instance" msgstr "ะ˜ัะฟะพะปัŒะทัƒะนั‚ะต ััƒั‰ะตัั‚ะฒัƒัŽั‰ะธะน Jellyfin" #: src/integrations/jellyfin.py:232 msgid "Could not log in" msgstr "ะะต ัƒะดะฐะปะพััŒ ะฒะพะนั‚ะธ ะฒ ัะธัั‚ะตะผัƒ" #: src/integrations/jellyfin.py:968 msgid "Unknown" msgstr "ะะตะธะทะฒะตัั‚ะฝั‹ะน" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:586 msgid "Local Files" msgstr "ะ›ะพะบะฐะปัŒะฝั‹ะต ั„ะฐะนะปั‹" #: src/integrations/local.py:21 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "ะŸัƒัั‚ัŒ Nocturne ะทะฐะณั€ัƒะถะฐะตั‚ ะฒะฐัˆะธ ะปะพะบะฐะปัŒะฝั‹ะต ั„ะฐะนะปั‹ ะฝะฐะฟั€ัะผัƒัŽ; ะดะปั ะฑะพะปัŒัˆะธั… " "ะฑะธะฑะปะธะพั‚ะตะบ ั€ะตะบะพะผะตะฝะดัƒะตั‚ัั ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒั‹ะดะตะปะตะฝะฝั‹ะน ัะตั€ะฒะตั€." #: src/integrations/local.py:23 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "ะŸั€ะพะดะพะปะถะธั‚ัŒ" #: src/integrations/local.py:27 msgid "Limited functionality" msgstr "ะžะณั€ะฐะฝะธั‡ะตะฝะฝะฐั ั„ัƒะฝะบั†ะธะพะฝะฐะปัŒะฝะพัั‚ัŒ" #. load songs, albums, artists #: src/integrations/local.py:68 msgid "Loading Songs" msgstr "ะ—ะฐะณั€ัƒะทะบะฐ ะฟะตัะตะฝ" #: src/integrations/local.py:280 msgid "No Album" msgstr "ะะตั‚ ะฐะปัŒะฑะพะผะฐ" #: src/integrations/local.py:602 src/integrations/local.py:611 msgid "Offline Mode" msgstr "ะะฒั‚ะพะฝะพะผะฝั‹ะน ั€ะตะถะธะผ" #: src/integrations/local.py:603 msgid "Access your downloads" msgstr "ะŸะพะปัƒั‡ั‚ัŒ ะดะพัั‚ัƒะฟ ะบ ัะฒะพะธะผ ะทะฐะณั€ัƒะทะบะฐะผ" #: src/integrations/models.py:138 src/ui/album/page.blp:5 #: src/widgets/album/page.py:129 msgid "Album" msgstr "ะะปัŒะฑะพะผ" #: src/integrations/models.py:140 msgid "Display Artist" msgstr "ะฅัƒะดะพะถะฝะธะบ-ะพั„ะพั€ะผะธั‚ะตะปัŒ" #: src/integrations/models.py:142 msgid "MusicBrainz ID" msgstr "ะ˜ะดะตะฝั‚ะธั„ะธะบะฐั‚ะพั€ MusicBrainz" #: src/integrations/models.py:143 msgid "Track Number" msgstr "ะะพะผะตั€ ั‚ั€ะตะบะฐ" #: src/integrations/models.py:144 msgid "Year" msgstr "ะ“ะพะด" #: src/integrations/models.py:145 msgid "Size" msgstr "ะ ะฐะทะผะตั€" #: src/integrations/models.py:146 msgid "Suffix" msgstr "ะŸั€ะธัั‚ะฐะฒะบะฐ" #: src/integrations/models.py:147 src/widgets/album/button.py:90 #: src/widgets/album/page.py:145 src/widgets/artist/page.py:125 #: src/widgets/playing/control_page.py:178 src/widgets/song/row.py:183 msgid "Favorite" msgstr "ะ˜ะทะฑั€ะฐะฝะฝะพะต" #: src/integrations/models.py:148 msgid "Duration" msgstr "ะŸั€ะพะดะพะปะถะธั‚ะตะปัŒะฝะพัั‚ัŒ" #: src/integrations/models.py:150 msgid "Bit Rate" msgstr "ะ‘ะธั‚ั€ะตะนั‚" #: src/integrations/models.py:151 msgid "Bit Depth" msgstr "ะ“ะปัƒะฑะธะฝะฐ ะฑะธั‚ะฐ" #: src/integrations/models.py:152 msgid "Sampling Rate" msgstr "ะงะฐัั‚ะพั‚ะฐ ะดะธัะบั€ะตั‚ะธะทะฐั†ะธะธ" #: src/integrations/models.py:153 msgid "Channel Count" msgstr "ะšะพะปะธั‡ะตัั‚ะฒะพ ะบะฐะฝะฐะปะพะฒ" #: src/integrations/models.py:155 msgid "Path" msgstr "ะŸัƒั‚ัŒ" #: src/integrations/models.py:156 msgid "Disc Number" msgstr "ะะพะผะตั€ ะดะธัะบะฐ" #: src/integrations/models.py:157 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:158 msgid "Genres" msgstr "ะ–ะฐะฝั€ั‹" #. list #: src/integrations/models.py:160 msgid "Track Gain" msgstr "ะฃัะธะปะตะฝะธะต ั‚ั€ะตะบะฐ" #: src/integrations/models.py:161 msgid "Album Gain" msgstr "ะฃะฒะตะปะธั‡ะตะฝะธะต ะฐะปัŒะฑะพะผะฐ" #: src/integrations/navidrome.py:19 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "ะŸะพะดะบะปัŽั‡ะธั‚ะตััŒ ะบ ัะตั€ะฒะตั€ัƒ OpenSubsonic, ะบะฐะบ ะบ Navidrome." #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "ะ’ะฝะตัˆะฝะธะน ัะตั€ะฒะตั€" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ััƒั‰ะตัั‚ะฒัƒัŽั‰ะธะน ัะบะทะตะผะฟะปัั€ OpenSubsonic / Navidrome" #: src/integrations/navidrome.py:141 msgid "Server returned an error" msgstr "ะกะตั€ะฒะตั€ ะฒะตั€ะฝัƒะป ะพัˆะธะฑะบัƒ" #: src/integrations/navidrome.py:141 msgid "Unknown Error" msgstr "ะะตะธะทะฒะตัั‚ะฝะฐั ะพัˆะธะฑะบะฐ" #: src/integrations/navidrome.py:591 src/integrations/navidrome.py:600 msgid "Managed Server" msgstr "ะฃะฟั€ะฐะฒะปัะตะผั‹ะน ัะตั€ะฒะตั€" #: src/integrations/navidrome.py:592 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "ะŸะพะดะบะปัŽั‡ะธั‚ะตััŒ ะบ ัะบะทะตะผะฟะปัั€ัƒ Navidrome, ัƒะฟั€ะฐะฒะปัะตะผะพะผัƒ ะฝะตะฟะพัั€ะตะดัั‚ะฒะตะฝะฝะพ Nocturne." #: src/integrations/navidrome.py:595 msgid "Manage Server" msgstr "ะฃะฟั€ะฐะฒะปะตะฝะธะต ัะตั€ะฒะตั€ะพะผ" #: src/integrations/navidrome.py:601 msgid "Create and use a Navidrome instance" msgstr "ะกะพะทะดะฐั‚ัŒ ะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ัะบะทะตะผะฟะปัั€ Navidrome" #: src/main.py:70 msgid "Music is Playing" msgstr "ะ˜ะณั€ะฐะตั‚ ะผัƒะทั‹ะบะฐ" #: src/main.py:83 msgid "Fullscreen Player Active" msgstr "ะะบั‚ะธะฒะตะฝ ะฟะพะปะฝะพัะบั€ะฐะฝะฝั‹ะน ะฟั€ะพะธะณั€ั‹ะฒะฐั‚ะตะปัŒ" #: src/main.py:131 msgid "Login Failed" msgstr "ะžัˆะธะฑะบะฐ ะฒั…ะพะดะฐ" #: src/preferences.py:375 msgid "Settings Page" msgstr "ะกั‚ั€ะฐะฝะธั†ะฐ ะฝะฐัั‚ั€ะพะตะบ" #: src/preferences.py:378 msgid "User Token" msgstr "ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะน ั‚ะพะบะตะฝ" #: src/preferences.py:382 msgid "Link ListenBrainz" msgstr "ะกัั‹ะปะบะฐ ListenBrainz" #: src/preferences.py:383 msgid "Connect your ListenBrainz account with a user token" msgstr "" "ะŸะพะดะบะปัŽั‡ะธั‚ะต ัะฒะพัŽ ัƒั‡ะตั‚ะฝัƒัŽ ะทะฐะฟะธััŒ ListenBrainz ั ะฟะพะผะพั‰ัŒัŽ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะพะณะพ " "ั‚ะพะบะตะฝะฐ" #: src/preferences.py:423 msgid "Flatpak Sandbox Warning" msgstr "ะŸั€ะตะดัƒะฟั€ะตะถะดะตะฝะธะต ะพ ะฟะตัะพั‡ะฝะธั†ะต Flatpak" #: src/preferences.py:424 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "ะ”ะปั ะฟะพะดะบะปัŽั‡ะตะฝะธั ะบ Discord ั‚ั€ะตะฑัƒะตั‚ัั ะดะพะฟะพะปะฝะธั‚ะตะปัŒะฝะพะต ั€ะฐะทั€ะตัˆะตะฝะธะต. ะŸะพัะปะต " "ะฒั‹ะฟะพะปะฝะตะฝะธั ัะปะตะดัƒัŽั‰ะตะน ะบะพะผะฐะฝะดั‹ ะฟะตั€ะตะทะฐะฟัƒัั‚ะธั‚ะต Nocturne" #: src/ui/album/button.blp:21 msgid "Play Album" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตัั‚ะธ ะฐะปัŒะฑะพะผ" #: src/ui/album/button.blp:36 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "ะŸะตั€ะตะบะปัŽั‡ะธั‚ัŒ ะทะฒะตะทะดะพั‡ะบัƒ" #: src/ui/album/button.blp:61 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "ะžะฑะปะพะถะบะฐ ะฐะปัŒะฑะพะผะฐ" #: src/ui/album/button.blp:87 msgid "Enter album page" msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ัƒ ะฐะปัŒะฑะพะผะฐ" #: src/ui/album/button.blp:128 src/ui/album/page.blp:85 #: src/ui/song/button.blp:81 msgid "Enter artist page" msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ัƒ ะธัะฟะพะปะฝะธั‚ะตะปั" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:117 src/widgets/containers/context.py:50 msgid "1 Star" msgstr "1 ะทะฒะตะทะดะฐ" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:126 msgid "2 Stars" msgstr "2 ะทะฒะตะทะดั‹" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:135 msgid "3 Stars" msgstr "3 ะทะฒะตะทะดั‹" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:144 msgid "4 Stars" msgstr "4 ะทะฒะตะทะดั‹" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:153 msgid "5 Stars" msgstr "5 ะทะฒะตะทะด" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "ะŸะฐั€ะฐะผะตั‚ั€ั‹" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "ะั€ั‚ะธัั‚" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "ะ ะฐะทะฒะตั€ะฝัƒั‚ัŒ ะฑะธะพะณั€ะฐั„ะธัŽ" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "ะกะฟะธัะพะบ" #: src/ui/containers/carousel.blp:38 msgid "Pan to the Left" msgstr "ะŸะพะฒะตั€ะฝะธั‚ะต ะฒะปะตะฒะพ" #: src/ui/containers/carousel.blp:64 msgid "Pan to the Right" msgstr "ะŸะพะฒะตั€ะฝะธั‚ะต ะฒะฟั€ะฐะฒะพ" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "ะ—ะฐะณั€ัƒะถะตะฝะพ" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "ะฃะดะฐะปะธั‚ัŒ ะธะท ะพั‡ะตั€ะตะดะธ" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "ะžั‡ะตั€ะตะดัŒ ะทะฐะณั€ัƒะทะพะบ" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "ะ—ะฐะณั€ัƒะถะตะฝะฝั‹ะต ะฟะตัะฝะธ ะดะพัั‚ัƒะฟะฝั‹ ะฒ ะฐะฒั‚ะพะฝะพะผะฝะพะผ ั€ะตะถะธะผะต" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "ะžั‡ะธัั‚ะธั‚ัŒ ะทะฐะฒะตั€ัˆะตะฝะฝั‹ะต ะ—ะฐะณั€ัƒะทะบะธ" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "ะ ะตะดะฐะบั‚ะพั€ ั‚ะตะบัั‚ะพะฒ ะฟะตัะตะฝ" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:240 #: src/ui/playing/footer.blp:118 msgid "Pause" msgstr "ะŸะฐัƒะทะฐ" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:175 msgid "Current song progress bar" msgstr "ะ˜ะฝะดะธะบะฐั‚ะพั€ ะฒั‹ะฟะพะปะฝะตะฝะธั ั‚ะตะบัƒั‰ะตะน ะฟะตัะฝะธ" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "ะฃัั‚ะฐะฝะพะฒะธั‚ัŒ ะผะตั‚ะบัƒ ะฒั€ะตะผะตะฝะธ ัะปะตะดัƒัŽั‰ะตะน ัั‚ั€ะพะบะธ ะฝะฐ ั‚ะตะบัƒั‰ะตะต ะฒั€ะตะผั" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ัั‚ั€ะพะบัƒ ะฒ ะพั‚ะผะตั‚ะบะต ะฒั€ะตะผะตะฝะธ" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ ะพั‚ััƒั‚ัั‚ะฒัƒะตั‚" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ ะฝะต ะฝะฐะนะดะตะฝ. ะงั‚ะพะฑั‹ ะฝะฐั‡ะฐั‚ัŒ, ะดะพะฑะฐะฒัŒั‚ะต ะฝะพะฒัƒัŽ ัั‚ั€ะพะบัƒ" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "ะžั‚ะผะตั‚ะบะฐ ะฒั€ะตะผะตะฝะธ" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "ะŸะตั€ะตะนั‚ะธ ะบ ะฒั€ะตะผะตะฝะฝะพะน ะผะตั‚ะบะต" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "ะฃัั‚ะฐะฝะพะฒะธั‚ัŒ ั‚ะตะบัƒั‰ัƒัŽ ะผะตั‚ะบัƒ ะฒั€ะตะผะตะฝะธ" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "ะฃะดะฐะปะธั‚ัŒ ัั‚ั€ะพะบัƒ" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "ะกะตั‚ะบะฐ" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "ะŸะพะธัะบ ะฐะปัŒะฑะพะผะพะฒ" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/playlists.blp:111 #: src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "ะ”ะพัั‚ะธะณะฝัƒั‚ ะบะพะฝะตั† ัะฟะธัะบะฐ" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "ะะปัŒะฑะพะผั‹ ะฝะต ะฝะฐะนะดะตะฝั‹" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "ะŸะพะธัะบ ะฐั€ั‚ะธัั‚ะพะฒ" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "ะั€ั‚ะธัั‚ะพะฒ ะฝะต ะฝะฐะนะดะตะฝะพ" #: src/ui/pages/home.blp:15 msgid "Toggle Search" msgstr "ะŸะตั€ะตะบะปัŽั‡ะธั‚ัŒ ะฟะพะธัะบ" #: src/ui/pages/home.blp:24 src/ui/shortcuts-dialog.blp:8 msgid "Search" msgstr "ะŸะพะธัะบ" #: src/ui/pages/home.blp:68 msgid "No Elements Found" msgstr "ะญะปะตะผะตะฝั‚ั‹ ะฝะต ะฝะฐะนะดะตะฝั‹" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "ะ’ั…ะพะด" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "ะกั‚ะฐั‚ัƒั ัะตั€ะฒะตั€ะฐ" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "ะะต ะทะฐะฟัƒั‰ะตะฝ" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "ะŸะตั€ะตะทะฐะฟัƒัั‚ะธั‚ัŒ ัะบะทะตะผะฟะปัั€" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "ะฃะฟั€ะฐะฒะปะตะฝะธะต ัะตั€ะฒะตั€ะพะผ Navidrome" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL-ะฐะดั€ะตั" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "ะ”ะพะฟะพะปะฝะธั‚ะตะปัŒะฝั‹ะต ะฟะฐั€ะฐะผะตั‚ั€ั‹" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "ะ”ะพะฒะตั€ัั‚ัŒ ัะตั€ั‚ะธั„ะธะบะฐั‚ะฐะผ ัะตั€ะฒะตั€ะฐ" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "ะ”ะธั€ะตะบั‚ะพั€ะธั ะฑะธะฑะปะธะพั‚ะตะบะธ" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "ะ˜ะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "ะŸะฐั€ะพะปัŒ" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฑั‹ัั‚ั€ะพะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:25 msgid "Playback" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต" #: src/ui/pages/playback.blp:17 msgid "Go Home" msgstr "ะ”ะพะผะพะน" #: src/ui/pages/playback.blp:24 msgid "Toggle Visualizer" msgstr "ะŸะตั€ะตะบะปัŽั‡ะตะฝะธะต ะฒะธะทัƒะฐะปะธะทะฐั‚ะพั€ะฐ" #: src/ui/pages/playback.blp:54 src/window.py:207 msgid "Nocturne Playback" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต Nocturne" #: src/ui/pages/playback.blp:64 msgid "These are your most played songs from last month" msgstr "ะญั‚ะพ ะฒะฐัˆะธ ัะฐะผั‹ะต ั‡ะฐัั‚ะพ ะฟั€ะพะธะณั€ั‹ะฒะฐะตะผั‹ะต ะฟะตัะฝะธ ะทะฐ ะฟั€ะพัˆะปั‹ะน ะผะตััั†" #: src/ui/pages/playback.blp:83 src/ui/pages/playback.blp:85 msgid "Save Playlist" msgstr "ะกะพั…ั€ะฐะฝะธั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/ui/pages/playback.blp:98 src/ui/pages/playback.blp:100 msgid "Playlist Added" msgstr "ะŸะปะตะนะปะธัั‚ ะดะพะฑะฐะฒะปะตะฝ" #: src/ui/pages/playback.blp:123 msgid "Thank you for using Nocturne!" msgstr "ะกะฟะฐัะธะฑะพ ะทะฐ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะต Nocturne!" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "ะŸะพะธัะบ ะฟะปะตะนะปะธัั‚ะพะฒ" #: src/ui/pages/playlists.blp:127 msgid "No Playlists Found" msgstr "ะŸะปะตะนะปะธัั‚ั‹ ะฝะต ะฝะฐะนะดะตะฝั‹" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "ะŸะพะธัะบ ั€ะฐะดะธะพัั‚ะฐะฝั†ะธะน" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ั€ะฐะดะธะพ" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "ะ ะฐะดะธะพัั‚ะฐะฝั†ะธะธ ะฝะต ะฝะฐะนะดะตะฝั‹" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "ะะฐัั‚ั€ะพะนะบะฐ" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "ะกะบะฐั‡ะฐั‚ัŒ Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะทะฐะณั€ัƒะทะธั‚ Navidrome" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "ะกะบะฐั‡ะฐั‚ัŒ (~20 ะœะ‘)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "ะ—ะฐะณั€ัƒะทะบะฐ" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "ะŸั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะฟะพะทะถะต" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "ะ—ะฐะณั€ัƒะทะบะฐ ะฟั€ะพัˆะปะฐ ัƒัะฟะตัˆะฝะพ" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "ะŸั€ะตะถะดะต ั‡ะตะผ ะฟั€ะพะดะพะปะถะธั‚ัŒ, ัƒะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะฒั‹ ัะพะทะดะฐะปะธ ัƒั‡ะตั‚ะฝัƒัŽ ะทะฐะฟะธััŒ " "ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ะฐ, ะฐ ั‚ะฐะบะถะต ั‡ั‚ะพ ะฒัะต ะฒะฐัˆะธ ั‚ั€ะตะบะธ ะฝะฐั…ะพะดัั‚ัั ะฒ ะฟะฐะฟะบะต ั ะผัƒะทั‹ะบะพะน" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "ะŸั€ะธะผะตั€ Webpage" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "ะŸะพะธัะบ ะฟะตัะตะฝ" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:96 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "ะŸะตัะฝะธ ะฝะต ะฝะฐะนะดะตะฝั‹" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "ะ›ัŽะฑะธะผั‹ะต ะฟะตัะฝะธ" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "ะŸะพะธัะบ ะปัŽะฑะธะผั‹ั… ะฟะตัะตะฝ" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "ะ”ะพะฑั€ะพ ะฟะพะถะฐะปะพะฒะฐั‚ัŒ" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "ะ”ะพะฑั€ะพ ะฟะพะถะฐะปะพะฒะฐั‚ัŒ ะฒ Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "ะ’ั‹ะฑะตั€ะธั‚ะต ั‚ะธะฟ ัะบะทะตะผะฟะปัั€ะฐ, ั‡ั‚ะพะฑั‹ ะฝะฐั‡ะฐั‚ัŒ ั€ะฐะฑะพั‚ัƒ" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "ะ˜ะณั€ะฐะตั‚" #: src/ui/playing/control_page.blp:75 msgid "Visit current radio homepage" msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ะณะปะฐะฒะฝัƒัŽ ัั‚ั€ะฐะฝะธั†ัƒ ั‚ะตะบัƒั‰ะตะณะพ ั€ะฐะดะธะพ" #: src/ui/playing/control_page.blp:210 src/ui/playing/footer.blp:88 msgid "Previous" msgstr "ะŸั€ะตะดั‹ะดัƒั‰ะฐั" #: src/ui/playing/control_page.blp:254 src/ui/playing/footer.blp:132 msgid "Next" msgstr "ะกะปะตะดัƒัŽั‰ะฐั" #: src/ui/playing/control_page.blp:274 msgid "Star" msgstr "ะ—ะฒะตะทะดะฐ" #: src/ui/playing/control_page.blp:281 msgid "Toggle Sidebar" msgstr "ะŸะตั€ะตะบะปัŽั‡ะธั‚ัŒ ะฑะพะบะพะฒัƒัŽ ะฟะฐะฝะตะปัŒ" #: src/ui/playing/cover_art.blp:28 msgid "Audio" msgstr "ะัƒะดะธะพ" #: src/ui/playing/cover_art.blp:36 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "ะžะฑะปะพะถะบะฐ ะฟะตัะฝะธ" #: src/ui/playing/cover_art.blp:42 src/ui/playing/cover_art.blp:50 msgid "Video" msgstr "ะ’ะธะดะตะพ" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "ะŸั€ะตะดัƒัั‚ะฐะฝะพะฒะบะธ" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "ะ ะพะฒะฝะพ" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "ะงั‘ั‚ะบะพัั‚ัŒ ะฒะพะบะฐะปะฐ" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "ะŸะปะฐะฒะฝั‹ะน Jazz" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "ะ ะพะบ" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "ะšะปะฐััะธะบะฐ" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "ะะบัƒัั‚ะธะบะฐ" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "ะ˜ะฝะดะธะบะฐั‚ะพั€ ั‚ะตะบัƒั‰ะตะณะพ ะฟั€ะพะณั€ะตััะฐ ะฟะตัะฝะธ" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "ะขะตะบัƒั‰ะฐั ะพะฑะปะพะถะบะฐ ะฟะตัะฝะธ" #: src/ui/playing/lyrics_page.blp:25 src/ui/playing/lyrics_page.blp:85 #: src/ui/playing/lyrics_page.blp:115 src/ui/playing/lyrics_page.blp:119 #: src/ui/playing/lyrics_page.blp:132 src/ui/playing/lyrics_page.blp:136 msgid "Go Back" msgstr "ะ’ะตั€ะฝัƒั‚ัŒัั" #: src/ui/playing/lyrics_page.blp:67 msgid "Lyrics Options" msgstr "ะ’ะฐั€ะธะฐะฝั‚ั‹ ั‚ะตะบัั‚ะฐ ะฟะตัะฝะธ" #: src/ui/playing/lyrics_page.blp:112 msgid "No Lyrics Found" msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ ะฝะต ะฝะฐะนะดะตะฝ" #: src/ui/playing/lyrics_page.blp:129 msgid "Instrumental" msgstr "ะ˜ะฝัั‚ั€ัƒะผะตะฝั‚ะฐะปัŒะฝั‹ะน" #: src/ui/playing/lyrics_page.blp:154 msgid "Not Downloaded" msgstr "ะะต ะทะฐะณั€ัƒะถะตะฝ" #: src/ui/playing/lyrics_page.blp:156 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "" "ะขะตะบัั‚ ัั‚ะพะน ะฟะตัะฝะธ ะฟะพะบะฐ ะฝะต ัะบะฐั‡ะฐะฝ, ะฒะพะทะผะพะถะฝะพ, ะพะฝ ัƒะถะต ะดะพัั‚ัƒะฟะตะฝ ะดะปั ัะบะฐั‡ะธะฒะฐะฝะธั" #: src/ui/playing/lyrics_page.blp:180 src/ui/playing/lyrics_page.blp:183 msgid "Load Lyrics File" msgstr "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ั„ะฐะนะป ั ั‚ะตะบัั‚ะพะผ ะฟะตัะฝะธ" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "ะ’ัะฟะปั‹ะฒะฐัŽั‰ะตะต ะพะบะฝะพ" #: src/ui/playing/popout_window.blp:65 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:214 msgid "Song Details" msgstr "ะŸะพะดั€ะพะฑะฝะพัั‚ะธ ะพ ะฟะตัะฝะต" #: src/ui/playing/popout_window.blp:85 src/ui/window.blp:180 #: src/ui/window.blp:236 msgid "Queue" msgstr "ะžั‡ะตั€ะตะดัŒ" #: src/ui/playing/popout_window.blp:97 src/ui/window.blp:196 #: src/ui/window.blp:248 msgid "Equalizer" msgstr "ะญะบะฒะฐะปะฐะนะทะตั€" #: src/ui/playing/queue_page.blp:20 msgid "Generating Next Queue" msgstr "ะกะพะทะดะฐะฝะธะต ัะปะตะดัƒัŽั‰ะตะน ะพั‡ะตั€ะตะดะธ" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "ะะฒั‚ะพะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "ะกะพะทะดะฐะฝะธะต ะฝะพะฒะพะน ะพั‡ะตั€ะตะดะธ, ะฟะพ ะทะฐะฒะตั€ัˆะตะฝะธะธ ั‚ะตะบัƒั‰ะตะน" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "ะ“ั€ะพะผะบะพัั‚ัŒ" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "ะžั‚ะบะปัŽั‡ะตะฝะธะต ะทะฒัƒะบะฐ ะดะธะฝะฐะผะธะบะฐ" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "ะ”ะธะฝะฐะผะธะบ ะฝะฐ ะฟะพะปะฝัƒัŽ ะณั€ะพะผะบะพัั‚ัŒ" #: src/ui/playlist/button.blp:28 msgid "Play Playlist" msgstr "ะ˜ะณั€ะฐั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/ui/playlist/button.blp:41 msgid "Show Playlist" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฟะปะตะนะปะธัั‚" #: src/ui/playlist/button.blp:53 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "ะžะฑะปะพะถะบะฐ ะฟะปะตะนะปะธัั‚ะฐ" #: src/ui/playlist/button.blp:65 msgid "Enter playlist page" msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ัƒ ะฟะปะตะนะปะธัั‚ะฐ" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:84 msgid "Playlist" msgstr "ะŸะปะตะนะปะธัั‚" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "ะŸะพะธัะบ (ะธะปะธ ัะพะทะดะฐะฝะธะต ะฝะพะฒะพะณะพ ะฟะปะตะนะปะธัั‚ะฐ)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "ะ”ะพะฑะฐะฒะปะตะฝะธะต ะผัƒะทั‹ะบะธ ะฒ ะฟะปะตะนะปะธัั‚" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "ะกะพะทะดะฐั‚ัŒ ะฝะพะฒั‹ะน ะฟะปะตะนะปะธัั‚ ะธ ะดะพะฑะฐะฒะธั‚ัŒ ะฟะตัะฝะธ" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "ะžะฑั‰ะธะต" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "ะŸะพะฒะตะดะตะฝะธะต" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ ะพั‡ะตั€ะตะดัŒ ะฟั€ะธ ะทะฐะฟัƒัะบะต" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "ะกะบั€ั‹ั‚ัŒ ะฟั€ะธ ะทะฐะบั€ั‹ั‚ะธะธ" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "ะ˜ะผะธั‚ะฐั†ะธั ั‚ะตะบัั‚ะฐ ะฟะตัะฝะธ ัะปะพะฒะพ ะฒ ัะปะพะฒะพ" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "ะ˜ัะฟะพะปัŒะทัƒะตั‚ัั, ะบะพะณะดะฐ ะดะพัั‚ัƒะฟะฝั‹ ั‚ะพะปัŒะบะพ ั‚ะตะบัั‚ั‹ ะฟะตัะตะฝ ะฟะพัั‚ั€ะพั‡ะฝะพ" #: src/ui/preferences.blp:22 msgid "Use Track and Album Gain" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ัƒัะธะปะตะฝะธะต ั‚ั€ะตะบะฐ ะธ ะฐะปัŒะฑะพะผะฐ" #: src/ui/preferences.blp:23 msgid "Adjust the volume based on gain metadata" msgstr "ะžั‚ั€ะตะณัƒะปะธั€ะพะฒะฐั‚ัŒ ะณั€ะพะผะบะพัั‚ัŒ ะฝะฐ ะพัะฝะพะฒะต ะผะตั‚ะฐะดะฐะฝะฝั‹ั… ัƒัะธะปะตะฝะธั" #: src/ui/preferences.blp:26 msgid "Default Page on Login" msgstr "ะกั‚ั€ะฐะฝะธั†ะฐ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ ะฟั€ะธ ะฒั…ะพะดะต ะฒ ัะธัั‚ะตะผัƒ" #: src/ui/preferences.blp:31 msgid "Maximum Bitrate" msgstr "ะœะฐะบัะธะผะฐะปัŒะฝะฐั ัะบะพั€ะพัั‚ัŒ ะฟะตั€ะตะดะฐั‡ะธ ะดะฐะฝะฝั‹ั…" #: src/ui/preferences.blp:37 msgid "Session" msgstr "ะกะตััะธั" #: src/ui/preferences.blp:40 msgid "Discord Rich Presence" msgstr "ะ‘ะพะณะฐั‚ะพะต ะฟั€ะธััƒั‚ัั‚ะฒะธะต ะฒ Discord" #: src/ui/preferences.blp:41 msgid "Share the currently playing song on Discord" msgstr "ะŸะพะดะตะปะธั‚ะตััŒ ั‚ะตะบัƒั‰ะตะน ะฒะพัะฟั€ะพะธะทะฒะพะดะธะผะพะน ะฟะตัะฝะตะน ะฒ Discord" #: src/ui/preferences.blp:45 msgid "Share Cover Art With Discord" msgstr "ะŸะพะดะตะปะธั‚ัั ะพะฑะปะพะถะบะพะน ั Discord" #: src/ui/preferences.blp:46 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "ะ•ัะปะธ ัะบะทะตะผะฟะปัั€ ะฝะตะดะพัั‚ัƒะฟะตะฝ ะดะปั ัะตั€ะฒะตั€ะพะฒ Discord, ะฒะผะตัั‚ะพ ะฝะตะณะพ ะฑัƒะดะตั‚ " "ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒัั CoverArtArchive" #: src/ui/preferences.blp:52 msgid "ListenBrainz Link" msgstr "ะกัั‹ะปะบะฐ ListenBrainz" #: src/ui/preferences.blp:53 msgid "Keep track of your playback history" msgstr "ะžั‚ัะปะตะถะธะฒะฐั‚ัŒ ะธัั‚ะพั€ะธัŽ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั" #: src/ui/preferences.blp:59 src/ui/preferences.blp:60 msgid "Link" msgstr "ะกัั‹ะปะบะฐ" #: src/ui/preferences.blp:68 src/ui/preferences.blp:69 msgid "Unlink" msgstr "ะžั‚ะผะตะฝะธั‚ัŒ ััั‹ะปะบัƒ" #: src/ui/preferences.blp:81 msgid "User" msgstr "ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒ" #: src/ui/preferences.blp:101 msgid "Logout" msgstr "ะ’ั‹ะนั‚ะธ" #: src/ui/preferences.blp:111 msgid "Customization" msgstr "ะะฐัั‚ั€ะพะนะบะฐ" #: src/ui/preferences.blp:114 msgid "Interface" msgstr "ะ˜ะฝั‚ะตั€ั„ะตะนั" #: src/ui/preferences.blp:116 msgid "Show Context Button in Rows" msgstr "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะบะพะฝั‚ะตะบัั‚ะฝัƒัŽ ะบะฝะพะฟะบัƒ ะฒ ัั‚ั€ะพะบะฐั…" #: src/ui/preferences.blp:117 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" "ะœะตะฝัŽ ั‚ะฐะบะถะต ะผะพะถะฝะพ ะพั‚ะพะฑั€ะฐะทะธั‚ัŒ ั‰ะตะปั‡ะบะพะผ ะฟั€ะฐะฒะพะน ะบะฝะพะฟะบะพะน ะผั‹ัˆะธ ะธะปะธ ะดะปะธั‚ะตะปัŒะฝั‹ะผ " "ะฝะฐะถะฐั‚ะธะตะผ" #: src/ui/preferences.blp:120 msgid "Show Labels in Buttons of Pages" msgstr "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะฟะพะดะฟะธัะธ ะฝะฐ ะบะฝะพะฟะบะฐั… ัั‚ั€ะฐะฝะธั†" #: src/ui/preferences.blp:123 msgid "Show Progressbar in Player Footer" msgstr "ะžั‚ะพะฑั€ะฐะทะธั‚ัŒ ะธะฝะดะธะบะฐั‚ะพั€ ะฒั‹ะฟะพะปะฝะตะฝะธั ะฒ ะฝะธะถะฝะตะผ ะบะพะปะพะฝั‚ะธั‚ัƒะปะต ะฟั€ะพะธะณั€ั‹ะฒะฐั‚ะตะปั" #: src/ui/preferences.blp:126 msgid "Make Background Translucent in Player" msgstr "ะกะดะตะปะฐั‚ัŒ ั„ะพะฝ ะฒ ะฟะปะตะตั€ะต ะฟะพะปัƒะฟั€ะพะทั€ะฐั‡ะฝั‹ะผ" #: src/ui/preferences.blp:129 msgid "Use Sidebar Player When Possible" msgstr "ะŸะพ ะฒะพะทะผะพะถะฝะพัั‚ะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฑะพะบะพะฒะพะน ะฟะปะตะตั€" #: src/ui/preferences.blp:130 msgid "Sidebar player is only available when the window is wide enough" msgstr "ะ‘ะพะบะพะฒะพะน ะฟะปะตะตั€ ะดะพัั‚ัƒะฟะตะฝ ั‚ะพะปัŒะบะพ ั‚ะพะณะดะฐ, ะบะพะณะดะฐ ะพะบะฝะพ ะดะพัั‚ะฐั‚ะพั‡ะฝะพ ัˆะธั€ะพะบะพะต" #: src/ui/preferences.blp:133 msgid "Show Pan Buttons on Carousels" msgstr "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะบะฝะพะฟะบะธ ะฟะฐะฝะพั€ะฐะผะธั€ะพะฒะฐะฝะธั ะฝะฐ ะบะฐั€ัƒัะตะปัั…" #: src/ui/preferences.blp:134 msgid "Only visible when a carousel has 5 or more items" msgstr "" "ะžั‚ะพะฑั€ะฐะถะฐะตั‚ัั ั‚ะพะปัŒะบะพ ะฒ ั‚ะพะผ ัะปัƒั‡ะฐะต, ะตัะปะธ ะฒ ะบะฐั€ัƒัะตะปะธ 5 ะธะปะธ ะฑะพะปะตะต ัะปะตะผะตะฝั‚ะพะฒ" #: src/ui/preferences.blp:137 msgid "Button Size" msgstr "ะ ะฐะทะผะตั€ ะบะฝะพะฟะบะธ" #: src/ui/preferences.blp:138 msgid "Affects albums, artists, playlists and songs" msgstr "ะ’ะปะธัะตั‚ ะฝะฐ ะฐะปัŒะฑะพะผั‹, ะธัะฟะพะปะฝะธั‚ะตะปะตะน, ะฟะปะตะนะปะธัั‚ั‹ ะธ ะฟะตัะฝะธ" #: src/ui/preferences.blp:144 src/ui/preferences.blp:145 msgid "Small" msgstr "ะœะฐะปะตะฝัŒะบะธะน" #: src/ui/preferences.blp:149 src/ui/preferences.blp:150 msgid "Big" msgstr "ะ‘ะพะปัŒัˆะพะน" #: src/ui/preferences.blp:158 msgid "Dynamic Background" msgstr "ะ”ะธะฝะฐะผะธั‡ะตัะบะธะน ั„ะพะฝ" #: src/ui/preferences.blp:159 msgid "Background generated based on the album art of the current song" msgstr "ะคะพะฝะพะฒะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต ัะพะทะดะฐะฝะพ ะฝะฐ ะพัะฝะพะฒะต ะพะฑะปะพะถะบะธ ะฐะปัŒะฑะพะผะฐ ั‚ะตะบัƒั‰ะตะน ะฟะตัะฝะธ" #: src/ui/preferences.blp:162 msgid "Use in Main Window" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒ ะณะปะฐะฒะฝะพะผ ะพะบะฝะต" #: src/ui/preferences.blp:163 msgid "Experimental" msgstr "ะญะบัะฟะตั€ะธะผะตะฝั‚ะฐะปัŒะฝะพ" #: src/ui/preferences.blp:169 src/ui/preferences.blp:170 #: src/ui/preferences.blp:193 src/ui/preferences.blp:194 #: src/ui/preferences.blp:217 src/ui/preferences.blp:218 msgid "Off" msgstr "ะ’ั‹ะบะปัŽั‡ะตะฝะพ" #: src/ui/preferences.blp:174 src/ui/preferences.blp:175 #: src/ui/preferences.blp:198 src/ui/preferences.blp:199 #: src/ui/preferences.blp:222 src/ui/preferences.blp:223 msgid "Gradient" msgstr "ะ“ั€ะฐะดะธะตะฝั‚" #: src/ui/preferences.blp:179 src/ui/preferences.blp:180 #: src/ui/preferences.blp:203 src/ui/preferences.blp:204 #: src/ui/preferences.blp:227 src/ui/preferences.blp:228 msgid "Blur" msgstr "ะ ะฐะทะผั‹ั‚ะธะต" #: src/ui/preferences.blp:187 msgid "Use in Player" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒ ะฟะปะตะตั€ะต" #: src/ui/preferences.blp:211 msgid "Use in Pop-out Window" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒะพ ะฒัะฟะปั‹ะฒะฐัŽั‰ะตะผ ะพะบะฝะต" #: src/ui/preferences.blp:235 msgid "Use as Accent Color" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒ ะบะฐั‡ะตัั‚ะฒะต ะฐะบั†ะตะฝั‚ะฝะพะณะพ ั†ะฒะตั‚ะฐ" #: src/ui/preferences.blp:240 msgid "Homepage" msgstr "ะ”ะพะผะฐัˆะฝัั ัั‚ั€ะฐะฝะธั†ะฐ" #: src/ui/preferences.blp:241 msgid "Reload homepage to see changes" msgstr "ะžะฑะฝะพะฒะธั‚ะต ะณะปะฐะฒะฝัƒัŽ ัั‚ั€ะฐะฝะธั†ัƒ, ั‡ั‚ะพะฑั‹ ัƒะฒะธะดะตั‚ัŒ ะธะทะผะตะฝะตะฝะธั" #: src/ui/preferences.blp:277 msgid "Sidebar" msgstr "ะ‘ะพะบะพะฒะฐั ะฟะฐะฝะตะปัŒ" #: src/ui/preferences.blp:281 msgid "Visualizer" msgstr "ะ’ะธะทัƒะฐะปะธะทะฐั‚ะพั€" #: src/ui/preferences.blp:285 src/ui/window.blp:265 msgid "Preferences" msgstr "ะะฐัั‚ั€ะพะนะบะธ" #: src/ui/preferences.blp:287 msgid "Show Visualizer in Player" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฒะธะทัƒะฐะปะธะทะฐั‚ะพั€ ะฒ ะฟะปะตะตั€ะต" #: src/ui/preferences.blp:288 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" "ะžั‚ะพะฑั€ะฐะทะธั‚ัŒ ะฒะธะทัƒะฐะปะธะทะฐั‚ะพั€ ะทะฒัƒะบะพะฒะพะณะพ ัะฟะตะบั‚ั€ะฐ ะฟะพะฒะตั€ั… ะพะฑะปะพะถะบะธ ะฐะปัŒะฑะพะผะฐ ะฒ ะฟะปะตะตั€ะต" #: src/ui/preferences.blp:293 msgid "Appearance" msgstr "ะ’ะฝะตัˆะฝะธะน ะฒะธะด" #: src/ui/preferences.blp:297 msgid "Number of Bars" msgstr "ะšะพะปะธั‡ะตัั‚ะฒะพ ะฑั€ัƒััŒะตะฒ" #: src/ui/preferences.blp:308 msgid "Visualizer Type" msgstr "ะขะธะฟ ะฒะธะทัƒะฐะปะธะทะฐั‚ะพั€ะฐ" #: src/ui/preferences.blp:314 src/ui/preferences.blp:315 msgid "Wave" msgstr "ะ’ะพะปะฝะฐ" #: src/ui/preferences.blp:319 src/ui/preferences.blp:320 msgid "Bars" msgstr "ะ‘ั€ัƒััŒั" #: src/ui/preferences.blp:324 src/ui/preferences.blp:325 msgid "Particles" msgstr "ะงะฐัั‚ะธั†ั‹" #: src/ui/preferences.blp:332 msgid "Fill Mode" msgstr "ะ ะตะถะธะผ ะทะฐะฟะพะปะฝะตะฝะธั" #: src/ui/preferences.blp:338 src/ui/preferences.blp:339 msgid "Solid" msgstr "ะกะฟะปะพัˆะฝะพะน" #: src/ui/preferences.blp:343 src/ui/preferences.blp:344 msgid "Translucent" msgstr "ะŸะพะปัƒะฟั€ะพะทั€ะฐั‡ะฝั‹ะน" #: src/ui/preferences.blp:348 src/ui/preferences.blp:349 msgid "Border" msgstr "ะ“ั€ะฐะฝะธั†ะฐ" #: src/ui/preferences.blp:357 msgid "Color" msgstr "ะฆะฒะตั‚" #: src/ui/preferences.blp:361 msgid "Use Dynamic Color" msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะดะธะฝะฐะผะธั‡ะตัะบะธะน ั†ะฒะตั‚" #: src/ui/preferences.blp:364 msgid "Invert Color" msgstr "ะ˜ะฝะฒะตั€ั‚ะธั€ะพะฒะฐั‚ัŒ ั†ะฒะตั‚" #: src/ui/preferences.blp:368 msgid "Manual Color" msgstr "ะ ัƒั‡ะฝะพะน ั†ะฒะตั‚" #: src/ui/preferences.blp:374 msgid "Visualizer Color" msgstr "ะ’ะธะทัƒะฐะปะธะทะฐั‚ะพั€ ั†ะฒะตั‚ะฐ" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Shortcuts" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ัะพั‡ะตั‚ะฐะฝะธั ะบะปะฐะฒะธัˆ" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Preferences" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฝะฐัั‚ั€ะพะนะบะธ" #: src/ui/shortcuts-dialog.blp:20 msgid "Quit" msgstr "ะ’ั‹ั…ะพะด" #: src/ui/shortcuts-dialog.blp:27 msgid "Toggle Playback" msgstr "ะŸะตั€ะตะบะปัŽั‡ะธั‚ัŒ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต" #: src/ui/shortcuts-dialog.blp:31 msgid "Next Song" msgstr "ะกะปะตะดัƒัŽั‰ะฐั ะฟะตัะฝั" #: src/ui/shortcuts-dialog.blp:35 msgid "Previous Song" msgstr "ะŸั€ะตะดั‹ะดัƒั‰ะฐั ะฟะตัะฝั" #: src/ui/shortcuts-dialog.blp:39 msgid "Raise Volume" msgstr "ะฃะฒะตะปะธั‡ะตั‚ัŒ ะณั€ะพะผะบะพัั‚ัŒ" #: src/ui/shortcuts-dialog.blp:43 msgid "Lower Volume" msgstr "ะฃะผะตะฝัŒัˆะธั‚ัŒ ะณั€ะพะผะบะพัั‚ัŒ" #: src/ui/shortcuts-dialog.blp:48 msgid "Window" msgstr "ะžะบะฝะพ" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:275 #: src/widgets/playing/popout_window.py:53 msgid "Toggle Fullscreen" msgstr "ะŸะตั€ะตะบะปัŽั‡ะตะฝะธะต ะฟะพะปะฝะพัะบั€ะฐะฝะฝะพะณะพ ั€ะตะถะธะผะฐ" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:270 msgid "Open Pop-Out Window" msgstr "ะžั‚ะบั€ั‹ั‚ัŒ ะฒัะฟะปั‹ะฒะฐัŽั‰ะตะต ะพะบะฝะพ" #: src/ui/song/button.blp:16 msgid "Play Song" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตัั‚ะธ ะฟะตัะฝัŽ" #: src/ui/song/button.blp:31 src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "ะžะฑะปะพะถะบะฐ ะฟะตัะฝะธ" #: src/ui/song/button.blp:45 msgid "Play Indicator" msgstr "ะ˜ะฝะดะธะบะฐั‚ะพั€ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "ะŸะตั€ะตั‚ะฐั‰ะธั‚ัŒ ะทะฝะฐั‡ะพะบ" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "ะŸะพะปะต ะฒั‹ะฑะพั€ะฐ" #: src/ui/window.blp:99 msgid "Play Random Queue" msgstr "ะ˜ะณั€ะฐั‚ัŒ ัะปัƒั‡ะฐะนะฝัƒัŽ ะพั‡ะตั€ะตะดัŒ" #: src/ui/window.blp:107 msgid "Menu" msgstr "ะœะตะฝัŽ" #: src/ui/window.blp:135 msgid "Random Albums" msgstr "ะกะปัƒั‡ะฐะนะฝั‹ะต ะฐะปัŒะฑะพะผั‹" #: src/ui/window.blp:139 msgid "Favorite Albums" msgstr "ะ›ัŽะฑะธะผั‹ะต ะฐะปัŒะฑะพะผั‹" #: src/ui/window.blp:143 msgid "Newest Albums" msgstr "ะะพะฒะตะนัˆะธะต ะฐะปัŒะฑะพะผั‹" #: src/ui/window.blp:147 msgid "Recent Albums" msgstr "ะะตะดะฐะฒะฝะธะต ะฐะปัŒะฑะพะผั‹" #: src/ui/window.blp:151 msgid "Frequent Albums" msgstr "ะงะฐัั‚ั‹ะต ะฐะปัŒะฑะพะผะพะฒ" #: src/ui/window.blp:174 msgid "Player" msgstr "ะŸะปะตะตั€" #: src/ui/window.blp:280 msgid "See Last Nocturne Playback" msgstr "ะกะผะพั‚ั€ะธั‚ะต ะŸะพัะปะตะดะฝะตะต ะ’ะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต Nocturne" #: src/ui/window.blp:285 msgid "Keyboard Shortcuts" msgstr "ะกะพั‡ะตั‚ะฐะฝะธั ะบะปะฐะฒะธัˆ" #: src/ui/window.blp:290 msgid "About Nocturne" msgstr "ะž Nocturne" #: src/widgets/album/button.py:95 src/widgets/album/page.py:150 #: src/widgets/artist/page.py:130 src/widgets/playing/control_page.py:183 #: src/widgets/song/row.py:188 msgid "Not Favorite" msgstr "ะะต ะธะทะฑั€ะฐะฝะฝะพะต" #: src/widgets/album/page.py:18 msgid "Disc {}" msgstr "ะ”ะธัะบ {}" #: src/widgets/artist/button.py:56 src/widgets/artist/row.py:46 msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} ะะปัŒะฑะพะผ" msgstr[1] "{} ะะปัŒะฑะพะผะฐ" msgstr[2] "{} ะะปัŒะฑะพะผะพะฒ" #: src/widgets/artist/page.py:48 msgid "Top Songs" msgstr "ะ›ัƒั‡ัˆะธะต ะฟะตัะฝะธ" #: src/widgets/artist/page.py:58 msgid "Related Artists" msgstr "ะกะฒัะทะฐะฝะฝั‹ะต ะฐั€ั‚ะธัั‚ั‹" #: src/widgets/artist/page.py:113 msgid "Author" msgstr "ะะฒั‚ะพั€" #: src/widgets/containers/context.py:50 msgid "{} Stars" msgstr "{} ะ—ะฒะตะทะดั‹" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "ะžั‚ะผะตั‚ะบะฐ ะฒั€ะตะผะตะฝะธ ะพั‚ััƒั‚ัั‚ะฒัƒะตั‚" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "ะ ะฐะฑะพั‚ะฐัŽั‰ะธะน" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "ะ”ะพะฟะพะปะฝะธั‚ะตะปัŒะฝะพะต ะผะตะฝัŽ" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "ะ›ะพะบะฐะปัŒะฝะฐั ะผัƒะทั‹ะบะฐะปัŒะฝะฐั ะฑะธะฑะปะธะพั‚ะตะบะฐ" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "ะŸะตั€ะตะทะฐะฟัƒั‰ะตะฝะพ" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "ะ‘ั‹ัั‚ั€ะพะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฟะพะปัƒั‡ะตะฝะธะธ ะบะพะดะฐ" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "ะกั‚ั€ะฐะฝะธั†ะฐ ะฑั‹ัั‚ั€ะพะณะพ ะฟะพะดะบะปัŽั‡ะตะฝะธั" #: src/widgets/pages/playback.py:29 msgid "Nocturne Playback ~ {}" msgstr "ะ’ะพัะฟั€ะพะธะทะฒะตะดะตะฝะธะต Nocturne ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 msgid "{} Plays" msgstr "{} ะ˜ะณั€ะฐะตั‚" #: src/widgets/pages/setup.py:63 msgid "Installing" msgstr "ะฃัั‚ะฐะฝะพะฒะบะฐ" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "ะ˜ะฝั‚ะตะณั€ะฐั†ะธั" #: src/widgets/playing/lyrics_page.py:234 msgid "LRC File" msgstr "LRC ะคะฐะนะป" #: src/widgets/playing/player.py:634 msgid "Warning: Song changed but volume is set to 0" msgstr "ะ’ะฝะธะผะฐะฝะธะต: ะฟะตัะฝั ัะผะตะฝะธะปะฐััŒ, ะฝะพ ะณั€ะพะผะบะพัั‚ัŒ ัƒัั‚ะฐะฝะพะฒะปะตะฝะฐ โ€‹โ€‹ะฝะฐ 0" #: src/widgets/playlist/button.py:63 src/widgets/playlist/page.py:116 #: src/widgets/playlist/row.py:47 src/widgets/playlist/selector_row.py:36 msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "{} ะŸะตัะฝั" msgstr[1] "{} ะŸะตัะฝะธ" msgstr[2] "{} ะŸะตัะตะฝ" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "ะะพะฒั‹ะน ะฟะปะตะนะปะธัั‚" #: src/widgets/playlist/selector_row.py:33 msgid "Add Songs to '{}'" msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฟะตัะฝะธ ะฒ '{}'" #: src/widgets/song/details_dialog.py:22 msgid "Yes" msgstr "ะ”ะฐ" #: src/widgets/song/details_dialog.py:22 msgid "No" msgstr "ะะตั‚" #: src/widgets/song/details_dialog.py:27 msgid "Not Found" msgstr "ะะต ะฝะฐะนะดะตะฝะพ" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "ะœะฝะพะถะตัั‚ะฒะพ ะั€ั‚ะธัั‚ะพะฒ" #: src/window.py:208 msgid "Your Playback queue for {} is available!" msgstr "ะ’ะฐัˆะฐ ะพั‡ะตั€ะตะดัŒ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั ะดะปั {} ะดะพัั‚ัƒะฟะฝะฐ!" #: src/window.py:210 msgid "Show" msgstr "ะŸะพะบะฐะทะฐั‚ัŒ" #: src/window.py:211 msgid "Later" msgstr "ะŸะพะทะถะต" #~ msgid "Show Sidebar" #~ msgstr "ะŸะพะบะฐะทะฐั‚ัŒ ะฑะพะบะพะฒัƒัŽ ะฟะฐะฝะตะปัŒ" #~ msgid "Generating next queue" #~ msgstr "ะกะพะทะดะฐะฝะธะต ัะปะตะดัƒัŽั‰ะตะน ะพั‡ะตั€ะตะดะธ" #~ msgid "Create new playlist and add songs" #~ msgstr "ะกะพะทะดะฐั‚ัŒ ะฝะพะฒั‹ะน ะฟะปะตะนะปะธัั‚ ะธ ะดะพะฑะฐะฒะธั‚ัŒ ะฟะตัะฝะธ" #~ msgid "Show Playlists in Sidebar" #~ msgstr "ะžั‚ะพะฑั€ะฐะถะตะฝะธะต ะฟะปะตะนะปะธัั‚ะพะฒ ะฒ ะฑะพะบะพะฒะพะน ะฟะฐะฝะตะปะธ" #~ msgid "Shortcuts" #~ msgstr "ะกะพั‡ะตั‚ะฐะฝะธั ะบะปะฐะฒะธัˆ" #, python-brace-format #~ msgid "Add songs to '{}'" #~ msgstr "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฟะตัะฝะธ ะฒ '{}'" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "{} ะŸะตัะฝะธ ัƒะดะฐะปะตะฝั‹" #~ msgid "Song Removed" #~ msgstr "ะŸะตัะฝั ัƒะดะฐะปะตะฝะฐ" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "{} ะŸะตัะฝะธ ะดะพะฑะฐะฒะปะตะฝั‹" #~ msgid "1 Song Added" #~ msgstr "ะ”ะพะฑะฐะฒะปะตะฝะฐ โ€‹โ€‹1 ะฟะตัะฝั" #~ msgid "1 Song Skipped" #~ msgstr "ะŸั€ะพะฟัƒั‰ะตะฝะฐ 1 ะฟะตัะฝั" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "{} ะŸะตัะฝะธ ะฟั€ะพะฟัƒั‰ะตะฝั‹" #~ msgid "Jellyfin (Experimental)" #~ msgstr "Jellyfin (ะญะบัะฟะตั€ะธะผะตะฝั‚ะฐะปัŒะฝะพ)" #~ msgid "Use an existing Navidrome / Subsonic instance" #~ msgstr "ะ˜ัะฟะพะปัŒะทัƒะนั‚ะต ััƒั‰ะตัั‚ะฒัƒัŽั‰ะธะน Navidrome / Subsonic ัะบะทะตะผะฟะปัั€" #~ msgid "Navidrome Managed" #~ msgstr "ะฃะฟั€ะฐะฒะปัะตะผั‹ะน Navidrome" #~ msgid "Instance Website" #~ msgstr "ะญะบะทะตะผะฟะปัั€ ะฒะตะฑ-ัะฐะนั‚ะฐ" #~ msgid "Create and use Navidrome instance" #~ msgstr "ะกะพะทะดะฐะฝะธะต ะธ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะต ัะบะทะตะผะฟะปัั€ะฐ Navidrome" #~ msgid "Go to Welcome Page" #~ msgstr "ะŸะตั€ะตะนั‚ะธ ะฝะฐ ัั‚ั€ะฐะฝะธั†ัƒ ะฟั€ะธะฒะตั‚ัั‚ะฒะธั" #~ msgid "" #~ "Nocturne is powered by a Subsonic compatible instance, if you do not have " #~ "one Nocturne can automatically set up a Navidrome instance for you!" #~ msgstr "" #~ "Nocturne ั€ะฐะฑะพั‚ะฐะตั‚ ะฝะฐ ะพัะฝะพะฒะต ัะพะฒะผะตัั‚ะธะผะพะณะพ ั Subsonic ัะบะทะตะผะฟะปัั€ะฐ, ะตัะปะธ ัƒ " #~ "ะฒะฐั ะตะณะพ ะฝะตั‚, Nocturne ะผะพะถะตั‚ ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะฝะฐัั‚ั€ะพะธั‚ัŒ ะดะปั ะฒะฐั ัะบะทะตะผะฟะปัั€ " #~ "Navidrome!" #~ msgid "Open Pop-out Window" #~ msgstr "ะžั‚ะบั€ั‹ั‚ัŒ ะฒัะฟะปั‹ะฒะฐัŽั‰ะตะต ะพะบะฝะพ" #~ msgid "Close Pop-out Window" #~ msgstr "ะ—ะฐะบั€ั‹ั‚ัŒ ะฒัะฟะปั‹ะฒะฐัŽั‰ะตะต ะพะบะฝะพ" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "ะขะตะบัั‚ ะฟะตัะฝะธ ะฟั€ะตะดะพัั‚ะฐะฒะปะตะฝ LRCLIB" #~ msgid "Whether to show some playlists in the main sidebar" #~ msgstr "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะปะธ ะฝะตะบะพั‚ะพั€ั‹ะต ะฟะปะตะนะปะธัั‚ั‹ ะฝะฐ ะณะปะฐะฒะฝะพะน ะฑะพะบะพะฒะพะน ะฟะฐะฝะตะปะธ" #~ msgid "Use Dynamic Background in Player" #~ msgstr "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะดะธะฝะฐะผะธั‡ะตัะบะธะน ั„ะพะฝ ะฒ ะฟะปะตะตั€ะต" #~ msgid "Generate a gradient background based on the current song cover art" #~ msgstr "ะกะพะทะดะฐั‚ัŒ ะณั€ะฐะดะธะตะฝั‚ะฝั‹ะน ั„ะพะฝ ะฝะฐ ะพัะฝะพะฒะต ะพะฑะปะพะถะบะธ ั‚ะตะบัƒั‰ะตะน ะฟะตัะฝะธ" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "ะ ะฐะทะผั‹ั‚ะธะต ั„ะพะฝะฐ ะฒ ะฟะปะตะตั€ะต (ัะบัะฟะตั€ะธะผะตะฝั‚ะฐะปัŒะฝะพ)" #~ msgid "Make background of player slightly transparent and blur the backdrop" #~ msgstr "ะกะดะตะปะฐะนั‚ะต ั„ะพะฝ ะฟะปะตะตั€ะฐ ัะปะตะณะบะฐ ะฟั€ะพะทั€ะฐั‡ะฝั‹ะผ ะธ ั€ะฐะทะผั‹ั‚ัŒ ะตะณะพ" #~ msgid "Restore Session" #~ msgstr "ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ ัะตััะธัŽ" #~ msgid "Retrieve previous queue" #~ msgstr "ะ˜ะทะฒะปะตั‡ัŒ ะฟั€ะตะดั‹ะดัƒั‰ัƒัŽ ะพั‡ะตั€ะตะดัŒ" #~ msgid "Hide Nocturne when closing" #~ msgstr "ะกะบั€ั‹ั‚ัŒ Nocturne ะฟั€ะธ ะทะฐะบั€ั‹ั‚ะธะธ" #~ msgid "Sections" #~ msgstr "ะ ะฐะทะดะตะปั‹" #~ msgid "Max amount of songs to be shown" #~ msgstr "ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฟะตัะตะฝ ะดะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั" #~ msgid "Max amount of albums to be shown" #~ msgstr "ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฐะปัŒะฑะพะผะพะฒ ะดะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั" #~ msgid "Max amount of artists to be shown" #~ msgstr "ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฐั€ั‚ะธัั‚ะพะฒ ะดะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั" #~ msgid "Max amount of playlists to be shown" #~ msgstr "ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฟะปะตะนะปะธัั‚ะพะฒ ะดะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั" #~ msgid "Player in Pop-out Window" #~ msgstr "ะŸะปะตะตั€ ะฒะพ ะฒัะฟะปั‹ะฒะฐัŽั‰ะตะผ ะพะบะฝะต" #~ msgid "1 Album" #~ msgstr "1 ะะปัŒะฑะพะผ" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} ะะปัŒะฑะพะผั‹" #~ msgid "1 Song" #~ msgstr "1 ะŸะตัะฝั" Jeffser-Nocturne-b442fee/po/te.po000066400000000000000000001622351523240030000167520ustar00rootroot00000000000000# Telugu Translation for Nocturne # Copyright (C) 2026 # This file is distributed under the same license as the PACKAGE package. # Please report any bugs at https://gitlab.gnome.org/Teams/Translation/te # Aryan Karamtoth , 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-18 19:03-0600\n" "PO-Revision-Date: 2026-05-23 08:59+0530\n" "Last-Translator: Aryan Karamtoth \n" "Language-Team: GNOME Telugu Translation Team \n" "Language: te\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 50.0\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "เฐฎเฑ€ เฐธเฐ‚เฐ—เฑ€เฐค เฐฒเฑˆเฐฌเฑเฐฐเฐฐเฑ€เฐจเฐฟ เฐ’เฐ•เฐšเฑ‹เฐŸ เฐšเฑ‡เฐฐเฑเฐšเฐ‚เฐกเฐฟ" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "เฐ†เฐงเฑเฐจเฐฟเฐ• เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ / เฐœเฑ†เฐฒเฑเฐฒเฑ€เฐซเฐฟเฐจเฑ เฐ•เฑเฐฒเฐฏเฐฟเฐ‚เฐŸเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "เฐซเฑ€เฐšเฐฐเฑเฐฒเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "เฐชเฐพเฐŸเฐฒเฑ, เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฑ, เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ, เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐฒเฑ เฐฎเฐฐเฐฟเฐฏเฑ เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐฒ เฐฆเฑเฐตเฐพเฐฐเฐพ เฐ…เฐจเฑเฐตเฑ‡เฐทเฐฃ" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ เฐจเฐฟเฐฐเฑเฐตเฐนเฐฃ" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "เฐœเฑ†เฐฒเฑเฐฒเฑ€เฐซเฐฟเฐจเฑ, เฐ“เฐชเฑ†เฐจเฑโ€Œเฐธเฐฌเฑโ€Œเฐธเฑ‹เฐจเฐฟเฐ•เฑ เฐฎเฐฐเฐฟเฐฏเฑ เฐธเฑเฐฅเฐพเฐจเฐฟเฐ• เฐซเฑˆเฐฒเฑโ€Œเฐฒเฐคเฑ‹ เฐ…เฐจเฑเฐ•เฑ‚เฐฒเฐค" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "เฐ†เฐกเฐฟเฐฏเฑ‹ เฐˆเฐ•เฑเฐตเฐฒเฑˆเฐœเฐฐเฑ เฐฎเฐฐเฐฟเฐฏเฑ เฐ†เฐกเฐฟเฐฏเฑ‹ เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Mpris เฐ‡เฐ‚เฐŸเฐฟเฐ—เฑเฐฐเฑ‡เฐทเฐจเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "เฐธเฐฎเฑ€เฐ•เฑƒเฐค เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐ‡เฐจเฑเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑ เฐจเฐฟเฐฐเฑเฐตเฐนเฐฃ" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "เฐ†เฐŸเฑ‹เฐฎเฑ‡เฐŸเฐฟเฐ•เฑ เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐซเฑ†เฐšเฐฟเฐ‚เฐ—เฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑโ€Œเฐฒเฑ เฐฎเฐฐเฐฟเฐฏเฑ เฐ†เฐซเฑโ€Œเฐฒเฑˆเฐจเฑ เฐฎเฑ‹เฐกเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "เฐ•เฑ‚เฐฒเฑ เฐ‡เฐ‚เฐŸเฐฐเฑเฐซเฑ‡เฐธเฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "เฐชเฑเฐฐเฐงเฐพเฐจ เฐนเฑ‹เฐฎเฑโ€Œเฐชเฑ‡เฐœเฑ€" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "เฐชเฐพเฐŸเฐฒ เฐ•เฑเฐฏเฑ‚" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:174 #: src/ui/window.blp:226 msgid "Lyrics" msgstr "เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "เฐชเฐพเฐŸเฐฒ เฐœเฐพเฐฌเฐฟเฐคเฐพ" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐชเฑ‡เฐœเฑ€" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "music" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:154 #, python-brace-format msgid "Not enough songs found for Playback ({})" msgstr "เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑ เฐ•เฑ‹เฐธเฐ‚ เฐคเฐ—เฐฟเฐจเฐจเฑเฐจเฐฟ เฐชเฐพเฐŸเฐฒเฑ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ ({})" #: src/actions.py:259 src/ui/song/row.blp:69 msgid "External File" msgstr "เฐฌเฐพเฐนเฑเฐฏ เฐซเฑˆเฐฒเฑ" #: src/actions.py:260 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "เฐˆ เฐŸเฑเฐฐเฐพเฐ•เฑ เฐฌเฐพเฐนเฑเฐฏ เฐซเฑˆเฐฒเฑ เฐจเฑเฐ‚เฐกเฐฟ เฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ, เฐ…เฐ‚เฐฆเฑเฐตเฐฒเฑเฐฒ เฐฒเฑˆเฐฌเฑเฐฐเฐฐเฑ€เฐฒเฑ‹เฐจเฐฟ เฐŸเฑเฐฐเฐพเฐ•เฑโ€Œเฐคเฑ‹ เฐชเฑ‹เฐฒเฐฟเฐธเฑเฐคเฑ‡ เฐฆเฑ€เฐจเฐฟเฐฒเฑ‹ เฐซเฑ€เฐšเฐฐเฑเฐฒเฑ เฐคเฐ•เฑเฐ•เฑเฐตเฐ—เฐพ เฐ‰เฐ‚เฐŸเฐพเฐฏเฐฟ." #: src/actions.py:262 src/preferences.py:419 src/ui/song/queue.blp:40 msgid "Close" msgstr "เฐฎเฑ‚เฐธเฐฟเฐตเฑ‡เฐฏเฐฟ" #: src/actions.py:280 msgid "Navidrome server deleted successfully" msgstr "เฐจเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐธเฐฐเฑเฐตเฐฐเฑ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:289 msgid "Delete Navidrome Server" msgstr "เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:290 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "เฐฎเฑ€เฐฐเฑ เฐ‡เฐ‚เฐŸเฐฟเฐ—เฑเฐฐเฑ‡เฐŸเฑ†เฐกเฑ เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐพเฐฒเฐจเฑเฐ•เฑเฐ‚เฐŸเฑเฐจเฑเฐจเฐพเฐฐเฐจเฐฟ เฐ–เฐšเฑเฐšเฐฟเฐคเฐ‚เฐ—เฐพ เฐ…เฐจเฑเฐ•เฑเฐ‚เฐŸเฑเฐจเฑเฐจเฐพเฐฐเฐพ?" #: src/actions.py:292 src/actions.py:426 src/actions.py:460 src/actions.py:802 #: src/actions.py:905 src/preferences.py:372 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "เฐฐเฐฆเฑเฐฆเฑ เฐšเฑ‡เฐฏเฐฟ" #: src/actions.py:293 msgid "Keep Data" msgstr "เฐกเฑ‡เฐŸเฐพเฐจเฑ เฐ‰เฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:294 msgid "Delete Everything" msgstr "เฐชเฑเฐฐเฐคเฐฟเฐฆเฑ€ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:372 msgid "No Name" msgstr "เฐชเฑ‡เฐฐเฑ เฐฒเฑ‡เฐฆเฑ" #: src/actions.py:391 msgid "Radio updated successfully" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:391 msgid "Radio added successfully" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:402 msgid "Error updating radio" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐจเฑ เฐ…เฐชเฑโ€Œเฐกเฑ‡เฐŸเฑ เฐšเฑ‡เฐฏเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚" #: src/actions.py:402 msgid "Error adding radio" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐจเฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚ เฐเฐฐเฑเฐชเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:411 src/actions.py:794 msgid "Name" msgstr "เฐชเฑ‡เฐฐเฑ" #: src/actions.py:416 msgid "Stream Url" msgstr "เฐธเฑเฐŸเฑเฐฐเฑ€เฐฎเฑ Url" #: src/actions.py:423 msgid "Update Radio Station" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐธเฑเฐŸเฑ‡เฐทเฐจเฑโ€Œเฐจเฐฟ เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:423 msgid "Add Radio Station" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐธเฑเฐŸเฑ‡เฐทเฐจเฑโ€Œเฐจเฐฟ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:427 src/preferences.py:373 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "เฐธเฑ‡เฐตเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/actions.py:443 msgid "Radio deleted successfully" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:451 msgid "Error deleting radio" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚" #: src/actions.py:457 msgid "Delete Radio Station" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐธเฑเฐŸเฑ‡เฐทเฐจเฑโ€Œเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:458 src/actions.py:903 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "เฐฎเฑ€เฐฐเฑ เฐจเฐฟเฐœเฐ‚เฐ—เฐพ '{}' เฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐพเฐฒเฐจเฑเฐ•เฑเฐ‚เฐŸเฑเฐจเฑเฐจเฐพเฐฐเฐพ?" #: src/actions.py:461 src/actions.py:906 src/constants.py:368 #: src/constants.py:421 msgid "Delete" msgstr "เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฑ" #: src/actions.py:495 src/actions.py:527 src/actions.py:533 src/actions.py:635 #: src/actions.py:723 msgid "Playing Next" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐฌเฐกเฑ‡เฐฆเฐฟ " #: src/actions.py:507 src/actions.py:546 src/actions.py:552 src/actions.py:652 #: src/actions.py:740 msgid "Playing Later" msgstr "เฐคเฐฐเฑเฐตเฐพเฐค เฐชเฑเฐฒเฑ‡ เฐ…เฐตเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/actions.py:527 src/actions.py:546 src/actions.py:1020 #: src/actions.py:1155 src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} เฐชเฐพเฐŸเฐฒเฑ" #: src/actions.py:582 msgid "Lyrics Saved" msgstr "เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐธเฑ‡เฐตเฑ เฐšเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:775 msgid "Playlist updated successfully" msgstr "เฐชเฑเฐฒเฑ‡เฐœเฐพเฐฌเฐฟเฐคเฐพ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:775 msgid "Playlist created successfully" msgstr "เฐชเฑเฐฒเฑ‡เฐœเฐพเฐฌเฐฟเฐคเฐพ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐ‚เฐ—เฐพ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:785 msgid "Error updating playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐ…เฐชเฑโ€Œเฐกเฑ‡เฐŸเฑ เฐšเฑ‡เฐฏเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚" #: src/actions.py:785 msgid "Error creating playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚" #: src/actions.py:799 msgid "Update Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:799 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:803 msgid "Update" msgstr "เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฑ" #: src/actions.py:803 msgid "Create" msgstr "เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฑ" #: src/actions.py:822 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "{} เฐชเฐพเฐŸ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" msgstr[1] "{} เฐชเฐพเฐŸเฐฒเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฑเฐกเฐพเฐฏเฐฟ" #: src/actions.py:853 src/actions.py:873 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "{} เฐชเฐพเฐŸ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" msgstr[1] "{} เฐชเฐพเฐŸเฐฒเฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐฌเฐกเฑเฐกเฐพเฐฏเฐฟ" #: src/actions.py:877 src/actions.py:1025 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "{} เฐชเฐพเฐŸ เฐฆเฐพเฐŸเฐตเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" msgstr[1] "{} เฐฆเฐพเฐŸเฐตเฑ‡เฐฏเฐฌเฐกเฐฟเฐจ เฐชเฐพเฐŸเฐฒเฑ" #: src/actions.py:890 msgid "Playlist Deleted" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:902 msgid "Delete Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/actions.py:951 msgid "No songs found" msgstr "เฐชเฐพเฐŸเฐฒเฑ เฐเฐตเฑ€ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/actions.py:994 src/actions.py:1021 src/actions.py:1058 #: src/actions.py:1094 msgid "Download Started" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:1001 src/actions.py:1032 src/actions.py:1039 #: src/actions.py:1074 src/actions.py:1110 msgid "Already Downloaded" msgstr "เฐ‡เฐชเฑเฐชเฐŸเฐฟเฐ•เฑ‡ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:1026 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} เฐชเฐพเฐŸ ({})" msgstr[1] "{} เฐชเฐพเฐŸเฐฒเฑ ({})" #: src/actions.py:1038 msgid "Download Skipped" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐฆเฐพเฐŸเฐตเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/actions.py:1062 src/actions.py:1098 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฎเฑˆเฐ‚เฐฆเฐฟ ({} เฐชเฐพเฐŸ เฐฆเฐพเฐŸเฐตเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ)" msgstr[1] "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฎเฑˆเฐ‚เฐฆเฐฟ ({} เฐชเฐพเฐŸเฐฒเฑ เฐฆเฐพเฐŸเฐตเฑ‡เฐฏเฐฌเฐกเฑเฐกเฐพเฐฏเฐฟ)" #: src/actions.py:1137 src/actions.py:1156 msgid "Deleted" msgstr "เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/constants.py:178 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "เฐตเฐฐเฑเฐธเฐ—เฐพ" #: src/constants.py:182 msgid "Repeat All" msgstr "เฐ…เฐจเฑเฐจเฑ€ เฐฎเฐฒเฑเฐฒเฐฟ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:186 msgid "Repeat One" msgstr "เฐ’เฐ•เฐŸเฐฟ เฐฎเฐฒเฑเฐฒเฐฟ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:191 #, python-brace-format msgid "Low ({})" msgstr "เฐคเฐ•เฑเฐ•เฑเฐต ({})" #: src/constants.py:192 #, python-brace-format msgid "Medium ({})" msgstr "เฐฎเฐงเฑเฐฏเฐธเฑเฐฅเฐ‚ ({})" #: src/constants.py:193 #, python-brace-format msgid "High ({})" msgstr "เฐ…เฐงเฐฟเฐ• ({})" #: src/constants.py:194 #, python-brace-format msgid "Ultra ({})" msgstr "เฐ…เฐฒเฑเฐŸเฑเฐฐเฐพ ({})" #: src/constants.py:195 msgid "Original File" msgstr "เฐ…เฐธเฐฒเฑ เฐซเฑˆเฐฒเฑ" #. Item #: src/constants.py:202 src/ui/pages/home.blp:5 msgid "Home" msgstr "เฐนเฑ‹เฐฎเฑ" #. Item #. list #: src/constants.py:207 src/integrations/models.py:162 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:236 src/widgets/pages/home.py:64 msgid "Artists" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฑ" #. Section #: src/constants.py:214 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:228 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:51 msgid "Albums" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ" #. Item #: src/constants.py:217 src/constants.py:252 src/constants.py:272 #: src/window.py:145 msgid "All" msgstr "เฐ…เฐจเฑเฐจเฑ€" #. Item #: src/constants.py:222 msgid "Random" msgstr "เฐฏเฐพเฐฆเฑƒเฐšเฑเฐ›เฐฟเฐ•เฐ‚เฐ—เฐพ" #. Item #: src/constants.py:227 src/constants.py:257 msgid "Favorites" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจเฐตเฐฟ" #. Item #: src/constants.py:232 msgid "Recently Added" msgstr "เฐ‡เฐŸเฑ€เฐตเฐฒ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #. Item #: src/constants.py:237 msgid "Recently Played" msgstr "เฐ‡เฐŸเฑ€เฐตเฐฒ เฐ†เฐกเฐฟเฐ‚เฐฆเฐฟ" #. Item #: src/constants.py:242 msgid "Most Played" msgstr "เฐŽเฐ•เฑเฐ•เฑเฐตเฐ—เฐพ เฐ†เฐกเฐฟเฐจเฐตเฐฟ" #. Section #: src/constants.py:249 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:220 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "เฐชเฐพเฐŸเฐฒเฑ" #. Item #: src/constants.py:262 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐฒเฑ" #. Section #: src/constants.py:269 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:244 #: src/widgets/pages/home.py:77 msgid "Playlists" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑเฐฒเฑ " #: src/constants.py:282 src/constants.py:333 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ " #: src/constants.py:287 src/constants.py:320 src/constants.py:343 msgid "Shuffle" msgstr "เฐทเฐซเฑเฐฒเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:292 src/constants.py:348 src/constants.py:381 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:297 src/constants.py:353 src/constants.py:386 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "เฐคเฐฐเฑเฐตเฐพเฐค เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:302 src/constants.py:401 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ เฐ•เฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:307 src/constants.py:358 src/constants.py:406 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/constants.py:312 src/constants.py:416 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐกเฐฟเฐจเฐฟ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/constants.py:325 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹" #: src/constants.py:338 msgid "Resume" msgstr "เฐชเฑเฐจเฐƒเฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:363 src/constants.py:391 msgid "Edit" msgstr "เฐธเฐตเฐฐเฐฟเฐ‚เฐšเฑ" #: src/constants.py:377 msgid "Select" msgstr "เฐŽเฐ‚เฐšเฑเฐ•เฑ‹เฐ‚เฐกเฐฟ" #: src/constants.py:396 msgid "Edit Lyrics" msgstr "เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐพเฐจเฑเฐจเฐฟ เฐธเฐตเฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:411 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/constants.py:427 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฑ" #: src/constants.py:432 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑโ€Œเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:438 msgid "Show Details" msgstr "เฐตเฐฟเฐตเฐฐเฐพเฐฒเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/constants.py:446 msgid "Visit Webpage" msgstr "เฐตเฑ†เฐฌเฑโ€Œเฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐธเฐ‚เฐฆเฐฐเฑเฐถเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:452 msgid "Update Server" msgstr "เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฐฟ เฐจเฐตเฑ€เฐ•เฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/constants.py:457 msgid "Delete Server" msgstr "เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฐฟ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/integrations/discord_rpc.py:171 msgid "Browsing" msgstr "เฐฌเฑเฐฐเฑŒเฐœเฐฟเฐ‚เฐ—เฑ" #: src/integrations/discord_rpc.py:189 msgid "Listening to music" msgstr "เฐธเฐ‚เฐ—เฑ€เฐคเฐ‚ เฐตเฐฟเฐจเฐกเฐ‚" #: src/integrations/jellyfin.py:16 msgid "Connect to a Jellyfin server." msgstr "เฐœเฑ†เฐฒเฑเฐฒเฑ€เฐซเฐฟเฐจเฑ เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐ•เฑ เฐ•เฐจเฑ†เฐ•เฑเฐŸเฑ เฐ…เฐตเฑเฐตเฐ‚เฐกเฐฟ." #: src/integrations/jellyfin.py:20 msgid "Jellyfin" msgstr "เฐœเฑ†เฐฒเฑเฐฒเฑ€ เฐซเฐฟเฐทเฑ" #: src/integrations/jellyfin.py:21 msgid "Use an existing Jellyfin instance" msgstr "เฐ‡เฐชเฑเฐชเฐŸเฐฟเฐ•เฑ‡ เฐ‰เฐจเฑเฐจ เฐœเฑ†เฐฒเฑเฐฒเฑ€เฐซเฐฟเฐจเฑ เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐจเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/integrations/jellyfin.py:958 msgid "Unknown" msgstr "เฐคเฑ†เฐฒเฐฟเฐฏเฐจเฐฟ" #: src/integrations/local.py:22 src/integrations/local.py:28 #: src/integrations/local.py:586 msgid "Local Files" msgstr "เฐธเฑเฐฅเฐพเฐจเฐฟเฐ• เฐซเฑˆเฐฒเฑโ€Œเฐฒเฑ" #: src/integrations/local.py:23 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "เฐฎเฑ€ เฐธเฑเฐฅเฐพเฐจเฐฟเฐ• เฐซเฑˆเฐณเฑเฐฒเฐจเฑ เฐจเฑ‡เฐฐเฑเฐ—เฐพ เฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐกเฐพเฐจเฐฟเฐ•เฐฟ เฐจเฐพเฐ•เฑเฐŸเฐฐเฑเฐจเฑโ€Œเฐจเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ, เฐชเฑ†เฐฆเฑเฐฆ เฐฒเฑˆเฐฌเฑเฐฐเฐฐเฑ€เฐฒ เฐ•เฑ‹เฐธเฐ‚ เฐชเฑเฐฐเฐคเฑเฐฏเฑ‡เฐ• เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฑ " "เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐกเฐ‚ เฐฎเฐ‚เฐšเฐฟเฐฆเฐฟ." #: src/integrations/local.py:25 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "เฐ•เฑŠเฐจเฐธเฐพเฐ—เฐฟเฐ‚เฐšเฑ" #: src/integrations/local.py:29 msgid "Limited functionality" msgstr "เฐชเฐฐเฐฟเฐฎเฐฟเฐค เฐ•เฐพเฐฐเฑเฐฏเฐพเฐšเฐฐเฐฃ" #. load songs, albums, artists #: src/integrations/local.py:70 msgid "Loading Songs" msgstr "เฐชเฐพเฐŸเฐฒเฐจเฑ เฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐธเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/integrations/local.py:283 msgid "No Album" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐฒเฑ‡เฐฆเฑ" #: src/integrations/local.py:602 src/integrations/local.py:611 msgid "Offline Mode" msgstr "เฐ†เฐซเฑโ€Œเฐฒเฑˆเฐจเฑ เฐฎเฑ‹เฐกเฑ" #: src/integrations/local.py:603 msgid "Access your downloads" msgstr "เฐฎเฑ€ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑโ€Œเฐฒเฐจเฑ เฐฏเฐพเฐ•เฑเฐธเฑ†เฐธเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/integrations/models.py:141 src/ui/album/page.blp:5 #: src/widgets/album/page.py:131 msgid "Album" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ" #: src/integrations/models.py:143 msgid "Display Artist" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐกเฐฟเฐจเฐฟ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/integrations/models.py:145 msgid "MusicBrainz ID" msgstr "MusicBrainz ID" #: src/integrations/models.py:146 msgid "Track Number" msgstr "เฐŸเฑเฐฐเฐพเฐ•เฑ เฐธเฐ‚เฐ–เฑเฐฏ" #: src/integrations/models.py:147 msgid "Year" msgstr "เฐธเฐ‚เฐตเฐคเฑเฐธเฐฐเฐ‚" #: src/integrations/models.py:148 msgid "Size" msgstr "เฐชเฐฐเฐฟเฐฎเฐพเฐฃเฐ‚" #: src/integrations/models.py:149 msgid "Suffix" msgstr "เฐชเฑเฐฐเฐคเฑเฐฏเฐฏเฐ‚" #: src/integrations/models.py:150 src/widgets/album/button.py:63 #: src/widgets/album/page.py:147 src/widgets/artist/page.py:127 #: src/widgets/playing/control_page.py:213 src/widgets/song/row.py:163 msgid "Favorite" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจเฐฆเฐฟ" #: src/integrations/models.py:151 msgid "Duration" msgstr "เฐตเฑเฐฏเฐตเฐงเฐฟ" #: src/integrations/models.py:153 msgid "Bit Rate" msgstr "เฐฌเฐฟเฐŸเฑ เฐฐเฑ‡เฐŸเฑ" #: src/integrations/models.py:154 msgid "Bit Depth" msgstr "เฐฌเฐฟเฐŸเฑ เฐกเฑ†เฐชเฑเฐคเฑ" #: src/integrations/models.py:155 msgid "Sampling Rate" msgstr "เฐจเฐฎเฑ‚เฐจเฐพ เฐฐเฑ‡เฐŸเฑ" #: src/integrations/models.py:156 msgid "Channel Count" msgstr "เฐ›เฐพเฐจเฑ†เฐฒเฑ เฐ•เฑŒเฐ‚เฐŸเฑ" #: src/integrations/models.py:158 msgid "Path" msgstr "เฐฎเฐพเฐฐเฑเฐ—เฐ‚" #: src/integrations/models.py:159 msgid "Disc Number" msgstr "เฐกเฐฟเฐธเฑเฐ•เฑ เฐธเฐ‚เฐ–เฑเฐฏ" #: src/integrations/models.py:160 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:161 msgid "Genres" msgstr "เฐถเฑˆเฐฒเฑเฐฒเฑ" #. list #: src/integrations/models.py:163 msgid "Track Gain" msgstr "เฐŸเฑเฐฐเฐพเฐ•เฑ เฐฒเฐพเฐญเฐ‚" #: src/integrations/models.py:164 msgid "Album Gain" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐฒเฐพเฐญเฐ‚" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐตเฐ‚เฐŸเฐฟ เฐ“เฐชเฑ†เฐจเฑโ€Œเฐธเฐฌเฑโ€Œเฐธเฑ‹เฐจเฐฟเฐ•เฑ เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐ•เฑ เฐ•เฐจเฑ†เฐ•เฑเฐŸเฑ เฐ…เฐตเฑเฐตเฐ‚เฐกเฐฟ." #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "เฐฌเฐพเฐนเฑเฐฏ เฐธเฐฐเฑเฐตเฐฐเฑ" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "เฐ‡เฐชเฑเฐชเฐŸเฐฟเฐ•เฑ‡ เฐ‰เฐจเฑเฐจ OpenSubsonic / Navidrome เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐจเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/integrations/navidrome.py:580 src/integrations/navidrome.py:589 msgid "Managed Server" msgstr "เฐจเฐฟเฐฐเฑเฐตเฐนเฐฟเฐ‚เฐšเฐฌเฐกเฑ‡ เฐธเฐฐเฑเฐตเฐฐเฑ" #: src/integrations/navidrome.py:581 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "เฐจเฐพเฐ•เฑเฐŸเฐฐเฑเฐจเฑ เฐฆเฑเฐตเฐพเฐฐเฐพ เฐจเฑ‡เฐฐเฑเฐ—เฐพ เฐจเฐฟเฐฐเฑเฐตเฐนเฐฟเฐ‚เฐšเฐฌเฐกเฑ‡ เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐ‡เฐจเฑเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐ•เฑ เฐ•เฐจเฑ†เฐ•เฑเฐŸเฑ เฐ…เฐตเฑเฐตเฐ‚เฐกเฐฟ." #: src/integrations/navidrome.py:584 msgid "Manage Server" msgstr "เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฐฟ เฐจเฐฟเฐฐเฑเฐตเฐนเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/integrations/navidrome.py:590 msgid "Create and use a Navidrome instance" msgstr "เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐฟ, เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/main.py:70 msgid "Music is Playing" msgstr "เฐธเฐ‚เฐ—เฑ€เฐคเฐ‚ เฐชเฑเฐฒเฑ‡ เฐ…เฐตเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/main.py:83 msgid "Fullscreen Player Active" msgstr "เฐชเฑ‚เฐฐเฑเฐคเฐฟ เฐธเฑเฐ•เฑเฐฐเฑ€เฐจเฑ เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑ เฐฏเฐพเฐ•เฑเฐŸเฐฟเฐตเฑ" #: src/main.py:129 msgid "Login Failed" msgstr "เฐฒเฐพเฐ—เฐฟเฐจเฑ เฐตเฐฟเฐซเฐฒเฐฎเฑˆเฐ‚เฐฆเฐฟ" #: src/preferences.py:361 msgid "Settings Page" msgstr "เฐธเฑ†เฐŸเฑเฐŸเฐฟเฐ‚เฐ—เฑโ€Œเฐฒ เฐชเฑ‡เฐœเฑ€" #: src/preferences.py:364 msgid "User Token" msgstr "เฐตเฐฟเฐจเฐฟเฐฏเฑ‹เฐ—เฐฆเฐพเฐฐเฑ เฐŸเฑ‹เฐ•เฑ†เฐจเฑ" #: src/preferences.py:368 msgid "Link ListenBrainz" msgstr "เฐฒเฐฟเฐ‚เฐ•เฑ ListenBrainz" #: src/preferences.py:369 msgid "Connect your ListenBrainz account with a user token" msgstr "เฐฎเฑ€ ListenBrainz เฐ–เฐพเฐคเฐพเฐจเฑ เฐฏเฑ‚เฐœเฐฐเฑ เฐŸเฑ‹เฐ•เฑ†เฐจเฑโ€Œเฐคเฑ‹ เฐ…เฐจเฑเฐธเฐ‚เฐงเฐพเฐจเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/preferences.py:409 msgid "Flatpak Sandbox Warning" msgstr "เฐซเฑเฐฒเฐพเฐŸเฑโ€Œเฐชเฐพเฐ•เฑ เฐธเฐพเฐ‚เฐกเฑโ€Œเฐฌเฐพเฐ•เฑเฐธเฑ เฐนเฑ†เฐšเฑเฐšเฐฐเฐฟเฐ•" #: src/preferences.py:410 msgid "" "To connect to Discord, an additional permission is required, once you run " "the following command, please restart Nocturne" msgstr "" "เฐกเฐฟเฐธเฑเฐ•เฐพเฐฐเฑเฐกเฑโ€Œเฐ•เฑ เฐ•เฐจเฑ†เฐ•เฑเฐŸเฑ เฐ…เฐตเฑเฐตเฐกเฐพเฐจเฐฟเฐ•เฐฟ เฐ…เฐฆเฐจเฐชเฑ เฐ…เฐจเฑเฐฎเฐคเฐฟ เฐ…เฐตเฐธเฐฐเฐ‚, เฐฎเฑ€เฐฐเฑ เฐ•เฐฟเฐ‚เฐฆเฐฟ เฐ•เฐฎเฐพเฐ‚เฐกเฑโ€Œเฐจเฑ เฐ…เฐฎเฐฒเฑ เฐšเฑ‡เฐธเฐฟเฐจ เฐคเฐฐเฑเฐตเฐพเฐค, เฐฆเฐฏเฐšเฑ‡เฐธเฐฟ " "เฐจเฐพเฐ•เฑเฐŸเฐฐเฑเฐจเฑโ€Œเฐจเฑ เฐชเฑเฐจเฐƒเฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ." #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "เฐŸเฑ‹เฐ—เฑเฐฒเฑ เฐธเฑเฐŸเฐพเฐฐเฑ" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐ•เฐตเฐฐเฑ เฐ†เฐฐเฑเฐŸเฑ" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐจเฐฎเฑ‹เฐฆเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐกเฐฟ เฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐจเฐฎเฑ‹เฐฆเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "1 เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐ‚" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "2 เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐพเฐฒเฑ" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "3 เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐพเฐฒเฑ" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "4 เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐพเฐฒเฑ" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "5 เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐพเฐฒเฑ" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "เฐŽเฐ‚เฐชเฐฟเฐ•เฐฒเฑ" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐกเฑ" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "เฐœเฑ€เฐตเฐฟเฐค เฐšเฐฐเฐฟเฐคเฑเฐฐเฐจเฑ เฐตเฐฟเฐธเฑเฐคเฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "เฐœเฐพเฐฌเฐฟเฐคเฐพ" #: src/ui/containers/carousel.blp:38 msgid "Pan to the Left" msgstr "เฐŽเฐกเฐฎเฐตเฑˆเฐชเฑเฐ•เฑ เฐชเฐพเฐจเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/containers/carousel.blp:64 msgid "Pan to the Right" msgstr "เฐ•เฑเฐกเฐฟเฐตเฑˆเฐชเฑเฐ•เฐฟ เฐชเฐพเฐจเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "เฐ•เฑเฐฏเฑ‚ เฐจเฑเฐ‚เฐกเฐฟ เฐคเฑ€เฐธเฐฟเฐตเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑโ€Œเฐฒ เฐ•เฑเฐฏเฑ‚" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐธเฐฟเฐจ เฐชเฐพเฐŸเฐฒเฑ เฐ†เฐซเฑโ€Œเฐฒเฑˆเฐจเฑ เฐฎเฑ‹เฐกเฑโ€Œเฐฒเฑ‹ เฐ…เฐ‚เฐฆเฑเฐฌเฐพเฐŸเฑเฐฒเฑ‹ เฐ‰เฐ‚เฐŸเฐพเฐฏเฐฟ" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "เฐ•เฑเฐฒเฐฟเฐฏเฐฐเฑ เฐชเฑ‚เฐฐเฑเฐคเฐฏเฐฟเฐ‚เฐฆเฐฟ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑโ€Œเฐฒเฑ" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "เฐฒเฐฟเฐฐเฐฟเฐ•เฑเฐธเฑ เฐŽเฐกเฐฟเฐŸเฐฐเฑ" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "เฐชเฐพเฐœเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐชเฐพเฐŸ เฐชเฑเฐฐเฑ‹เฐ—เฑเฐฐเฑ†เฐธเฑ เฐฌเฐพเฐฐเฑ" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ เฐฒเฑˆเฐจเฑ เฐŸเฑˆเฐฎเฑโ€ŒเฐธเฑเฐŸเฐพเฐ‚เฐชเฑโ€Œเฐจเฑ เฐ‡เฐชเฑเฐชเฑเฐกเฑเฐ—เฐพ เฐธเฑ†เฐŸเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "เฐŸเฑˆเฐฎเฑโ€ŒเฐธเฑเฐŸเฐพเฐ‚เฐชเฑ เฐตเฐฆเฑเฐฆ เฐฒเฑˆเฐจเฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐฒเฑ‡เฐฆเฑ" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "เฐˆ เฐŸเฑเฐฐเฐพเฐ•เฑโ€Œเฐ•เฐฟ เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ, เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐกเฐพเฐจเฐฟเฐ•เฐฟ เฐ•เฑŠเฐคเฑเฐค เฐฒเฑˆเฐจเฑโ€Œเฐจเฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "เฐธเฐฎเฐฏเฐฎเฑเฐฆเฑเฐฐ" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "เฐŸเฑˆเฐฎเฑโ€ŒเฐธเฑเฐŸเฐพเฐ‚เฐชเฑโ€Œเฐ•เฑ เฐตเฑ†เฐณเฑเฐฒเฐ‚เฐกเฐฟ" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐŸเฑˆเฐฎเฑโ€ŒเฐธเฑเฐŸเฐพเฐ‚เฐชเฑโ€Œเฐจเฑ เฐธเฑ†เฐŸเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "เฐฒเฑˆเฐจเฑ เฐคเฑŠเฐฒเฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "เฐ—เฑเฐฐเฐฟเฐกเฑ" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฐจเฑ เฐตเฑ†เฐคเฐ•เฐ‚เฐกเฐฟ" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "เฐœเฐพเฐฌเฐฟเฐคเฐพ เฐฎเฑเฐ—เฐฟเฐ‚เฐชเฑเฐ•เฑ เฐšเฑ‡เฐฐเฑเฐ•เฑเฐ‚เฐฆเฐฟ" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ เฐเฐตเฑ€ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฐจเฑ เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฑ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "เฐ เฐฎเฑ‚เฐฒเฐ•เฐพเฐฒเฑ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "เฐฒเฐพเฐ—เฐฟเฐจเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "เฐธเฐฐเฑเฐตเฐฐเฑ เฐธเฑเฐฅเฐฟเฐคเฐฟ" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "เฐจเฐกเฐตเฐŸเฑเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐจเฑ เฐชเฑเฐจเฐƒเฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐจเฑ เฐจเฐฟเฐฐเฑเฐตเฐนเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "Url" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "เฐฎเฐฐเฐฟเฐจเฑเฐจเฐฟ เฐŽเฐ‚เฐชเฐฟเฐ•เฐฒเฑ" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "เฐŸเฑเฐฐเฐธเฑเฐŸเฑ เฐธเฐฐเฑเฐตเฐฐเฑ เฐธเฐฐเฑเฐŸเฐฟเฐซเฐฟเฐ•เฑ†เฐŸเฑเฐฒเฑ" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "เฐฒเฑˆเฐฌเฑเฐฐเฐฐเฑ€ เฐกเฑˆเฐฐเฑ†เฐ•เฑเฐŸเฐฐเฑ€" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "เฐตเฐฟเฐจเฐฟเฐฏเฑ‹เฐ—เฐฆเฐพเฐฐเฑ เฐชเฑ‡เฐฐเฑ" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "เฐธเฐพเฐ‚เฐ•เฑ‡เฐคเฐฟเฐ• เฐชเฐฆเฐฎเฑ" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "เฐคเฑเฐตเฐฐเฐฟเฐค เฐ•เฐจเฑ†เฐ•เฑเฐทเฐจเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑเฐฒเฐจเฑ เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐฒเฑ เฐเฐตเฑ€ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐฒเฐจเฑ เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐจเฐฟ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹เฐฒเฑ เฐเฐตเฑ€ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "เฐธเฑ†เฐŸเฐชเฑ" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "เฐจเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "เฐจเฐพเฐ•เฑเฐŸเฐฐเฑเฐจเฑ เฐจเฐพเฐตเฐฟเฐกเฑเฐฐเฑ‹เฐฎเฑโ€Œเฐจเฑ เฐธเฑเฐตเฐฏเฐ‚เฐšเฐพเฐฒเฐ•เฐ‚เฐ—เฐพ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐธเฑเฐคเฑเฐ‚เฐฆเฐฟ" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ (~20mb)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐธเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "เฐ’เฐ• เฐฒเฑ‹เฐชเฐ‚ เฐธเฐ‚เฐญเฐตเฐฟเฐ‚เฐšเฐฟเฐ‚เฐฆเฐฟ" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "เฐฆเฐฏเฐšเฑ‡เฐธเฐฟ เฐคเฐฐเฑเฐตเฐพเฐค เฐฎเฐณเฑเฐฒเฑ€ เฐชเฑเฐฐเฐฏเฐคเฑเฐจเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐตเฐฟเฐœเฐฏเฐตเฐ‚เฐคเฐฎเฑˆเฐ‚เฐฆเฐฟ" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "เฐ•เฑŠเฐจเฐธเฐพเฐ—เฐฟเฐ‚เฐšเฑ‡ เฐฎเฑเฐ‚เฐฆเฑ เฐฎเฑ€เฐฐเฑ เฐ’เฐ• เฐ…เฐกเฑเฐฎเฐฟเฐจเฑ เฐ–เฐพเฐคเฐพเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฑเฐ•เฑเฐจเฑเฐจเฐพเฐฐเฐจเฐฟ เฐฎเฐฐเฐฟเฐฏเฑ เฐฎเฑ€ เฐŸเฑเฐฐเฐพเฐ•เฑโ€Œเฐฒเฐจเฑเฐจเฑ€ เฐฎเฑ€ เฐฎเฑเฐฏเฑ‚เฐœเฐฟเฐ•เฑ เฐกเฑˆเฐฐเฑ†เฐ•เฑเฐŸเฐฐเฑ€เฐฒเฑ‹ " "เฐ‰เฐจเฑเฐจเฐพเฐฏเฐจเฐฟ เฐจเฐฟเฐฐเฑเฐงเฐพเฐฐเฐฟเฐ‚เฐšเฑเฐ•เฑ‹เฐ‚เฐกเฐฟ." #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑ เฐตเฑ†เฐฌเฑโ€Œเฐชเฑ‡เฐœเฑ€" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "เฐชเฐพเฐŸเฐฒเฐจเฑ เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "เฐชเฐพเฐŸเฐฒเฑ เฐเฐตเฑ€ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจ เฐชเฐพเฐŸเฐฒเฑ" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจ เฐชเฐพเฐŸเฐฒเฐจเฑ เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "เฐธเฑเฐตเฐพเฐ—เฐคเฐ‚" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "เฐจเฐพเฐ•เฑเฐŸเฐฐเฑเฐจเฑโ€Œเฐ•เฑ เฐธเฑเฐตเฐพเฐ—เฐคเฐ‚" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐกเฐพเฐจเฐฟเฐ•เฐฟ เฐ’เฐ• เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑ เฐฐเฐ•เฐพเฐจเฑเฐจเฐฟ เฐŽเฐ‚เฐšเฑเฐ•เฑ‹เฐ‚เฐกเฐฟ" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "เฐ†เฐกเฑเฐคเฑเฐจเฑเฐจเฐพเฐฐเฑ" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "เฐชเฐพเฐŸ เฐ•เฐตเฐฐเฑ" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐฐเฑ‡เฐกเฐฟเฐฏเฑ‹ เฐนเฑ‹เฐฎเฑโ€Œเฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐธเฐ‚เฐฆเฐฐเฑเฐถเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "เฐฎเฑเฐจเฑเฐชเฐŸเฐฟ" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "เฐจเฐ•เฑเฐทเฐคเฑเฐฐเฐ‚" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "เฐธเฑˆเฐกเฑโ€Œเฐฌเฐพเฐฐเฑโ€Œเฐจเฐฟ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "เฐชเฑเฐฐเฑ€เฐธเฑ†เฐŸเฑเฐฒเฑ" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "เฐซเฑเฐฒเฐพเฐŸเฑ" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "เฐธเฑเฐตเฐฐ เฐธเฑเฐชเฐทเฑเฐŸเฐค" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "เฐธเฑเฐฎเฑ‚เฐคเฑ เฐœเฐพเฐœเฑ" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "เฐฐเฐพเฐ•เฑ" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "เฐ•เฑเฐฒเฐพเฐธเฐฟเฐ•เฑ" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "เฐ…เฐ•เฑŒเฐธเฑเฐŸเฐฟเฐ•เฑ" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐชเฐพเฐŸ เฐชเฑเฐฐเฑ‹เฐ—เฐคเฐฟ เฐธเฑ‚เฐšเฐฟเฐ•" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐชเฐพเฐŸ เฐ•เฐตเฐฐเฑ เฐ†เฐฐเฑเฐŸเฑ" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "เฐตเฑ†เฐจเฐ•เฑเฐ•เฐฟ เฐตเฑ†เฐณเฑเฐณเฑ" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐ•เฐจเฑเฐ—เฑŠเฐจเฐฌเฐกเฐฒเฑ‡เฐฆเฑ" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "เฐตเฐพเฐฏเฐฟเฐฆเฑเฐฏเฐ‚" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐฒเฑ‡เฐฆเฑ" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "เฐˆ เฐชเฐพเฐŸ เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐ‚ เฐ‡เฐ‚เฐ•เฐพ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐ•เฐพเฐฒเฑ‡เฐฆเฑ, เฐ…เฐฆเฐฟ เฐกเฑŒเฐจเฑโ€Œเฐฒเฑ‹เฐกเฑ เฐ•เฑ‹เฐธเฐ‚ เฐ…เฐ‚เฐฆเฑเฐฌเฐพเฐŸเฑเฐฒเฑ‹ เฐ‰เฐ‚เฐกเฐตเฐšเฑเฐšเฑ." #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "เฐชเฐพเฐŸเฐฒ เฐซเฑˆเฐฒเฑ เฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "เฐชเฐพเฐชเฑ-เฐ…เฐตเฑเฐŸเฑ เฐตเฐฟเฐ‚เฐกเฑ‹" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:158 msgid "Player" msgstr "เฐ†เฐŸเฐ—เฐพเฐกเฑ" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:46 #: src/ui/window.blp:259 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "เฐชเฑ‚เฐฐเฑเฐคเฐฟ เฐธเฑเฐ•เฑเฐฐเฑ€เฐจเฑโ€Œเฐจเฑ เฐŸเฑ‹เฐ—เฑเฐฒเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:198 msgid "Song Details" msgstr "เฐชเฐพเฐŸเฐฒ เฐตเฐฟเฐตเฐฐเฐพเฐฒเฑ" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:164 #: src/ui/window.blp:220 msgid "Queue" msgstr "เฐ•เฑเฐฏเฑ‚" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:180 #: src/ui/window.blp:232 msgid "Equalizer" msgstr "เฐˆเฐ•เฑเฐตเฐฒเฑˆเฐœเฐฐเฑ" #: src/ui/playing/queue_page.blp:20 msgid "Generating Next Queue" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ เฐ•เฑเฐฏเฑ‚เฐจเฑ เฐฐเฑ‚เฐชเฑŠเฐ‚เฐฆเฐฟเฐธเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "เฐ†เฐŸเฑ‹เฐชเฑเฐฒเฑ‡" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐ•เฑเฐฏเฑ‚ เฐฎเฑเฐ—เฐฟเฐธเฐฟเฐจเฐชเฑเฐชเฑเฐกเฑ เฐ•เฑŠเฐคเฑเฐค เฐ•เฑเฐฏเฑ‚เฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "เฐตเฐพเฐฒเฑเฐฏเฑ‚เฐฎเฑ" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "เฐธเฑเฐชเฑ€เฐ•เฐฐเฑ เฐฎเฑเฐฏเฑ‚เฐŸเฑ" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "เฐธเฑเฐชเฑ€เฐ•เฐฐเฑ เฐชเฑ‚เฐฐเฑเฐคเฐฟ เฐตเฐพเฐฒเฑเฐฏเฑ‚เฐฎเฑ" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑเฐฒเฐจเฑ เฐชเฑเฐฒเฑ‡ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ " #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑเฐฒเฐจเฑ เฐšเฑ‚เฐชเฑ" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ เฐ•เฐตเฐฐเฑ เฐ†เฐฐเฑเฐŸเฑ" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ เฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐจเฐฎเฑ‹เฐฆเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "เฐถเฑ‹เฐงเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ (เฐฒเฑ‡เฐฆเฐพ เฐ•เฑŠเฐคเฑเฐค เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐ•เฑ เฐธเฐ‚เฐ—เฑ€เฐคเฐพเฐจเฑเฐจเฐฟ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐกเฐ‚" #: src/ui/playlist/dialog.blp:34 msgid "Create New Playlist and Add Songs" msgstr "เฐ•เฑŠเฐคเฑเฐค เฐชเฑเฐฒเฑ‡เฐฒเฐฟเฐธเฑเฐŸเฑโ€Œเฐจเฑ เฐธเฑƒเฐทเฑเฐŸเฐฟเฐ‚เฐšเฐฟ เฐชเฐพเฐŸเฐฒเฐจเฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:7 src/ui/shortcuts-dialog.blp:6 msgid "General" msgstr "เฐธเฐพเฐงเฐพเฐฐเฐฃ" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "เฐชเฑเฐฐเฐตเฐฐเฑเฐคเฐจ" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "เฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐฟเฐจเฐชเฑเฐชเฑเฐกเฑ เฐ•เฑเฐฏเฑ‚เฐจเฑ เฐชเฑเฐจเฐฐเฑเฐฆเฑเฐงเฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "เฐ•เฑเฐฒเฑ‹เฐœเฑโ€Œเฐฒเฑ‹ เฐฆเฐพเฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "เฐชเฐฆเฐ‚ เฐชเฐฆเฐ‚ เฐตเฐพเฐฐเฑ€เฐ—เฐพ เฐชเฐพเฐŸเฐฒ เฐธเฐพเฐนเฐฟเฐคเฑเฐฏเฐพเฐจเฑเฐจเฐฟ เฐ…เฐจเฑเฐ•เฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "เฐชเฐ‚เฐ•เฑเฐคเฐฟ เฐชเฐ‚เฐ•เฑเฐคเฐฟ เฐชเฐพเฐŸ เฐฎเฐพเฐคเฑเฐฐเฐฎเฑ‡ เฐ…เฐ‚เฐฆเฑเฐฌเฐพเฐŸเฑเฐฒเฑ‹ เฐ‰เฐจเฑเฐจเฐชเฑเฐชเฑเฐกเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฑเฐคเฑเฐ‚เฐฆเฐฟ" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "เฐฒเฐพเฐ—เฐฟเฐจเฑ เฐ…เฐฏเฐฟเฐจเฐชเฑเฐชเฑเฐกเฑ เฐกเฐฟเฐซเฐพเฐฒเฑเฐŸเฑ เฐชเฑ‡เฐœเฑ€" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "เฐ—เฐฐเฐฟเฐทเฑเฐŸ เฐฌเฐฟเฐŸเฑเฐฐเฑ‡เฐŸเฑ" #: src/ui/preferences.blp:33 msgid "Session" msgstr "เฐธเฑ†เฐทเฐจเฑ" #: src/ui/preferences.blp:36 msgid "Discord Rich Presence" msgstr "Discord Rich Presence" #: src/ui/preferences.blp:37 msgid "Share the currently playing song on Discord" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐคเฐ‚ เฐชเฑเฐฒเฑ‡ เฐ…เฐตเฑเฐคเฑเฐจเฑเฐจ เฐชเฐพเฐŸเฐจเฑ Discord เฐทเฑ‡เฐฐเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:41 msgid "Share Cover Art With Discord" msgstr "Discord เฐคเฑ‹ เฐ•เฐตเฐฐเฑ เฐ†เฐฐเฑเฐŸเฑโ€Œเฐจเฑ เฐชเฐ‚เฐšเฑเฐ•เฑ‹เฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:42 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" "Discord เฐธเฐฐเฑเฐตเฐฐเฑโ€Œเฐฒ เฐฆเฑเฐตเฐพเฐฐเฐพ เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐจเฑเฐธเฑโ€Œเฐจเฑ เฐšเฑ‡เฐฐเฑเฐ•เฑ‹เฐฒเฑ‡เฐ•เฐชเฑ‹เฐคเฑ‡, เฐฆเฐพเฐจเฐฟเฐ•เฐฟ เฐฌเฐฆเฑเฐฒเฑเฐ—เฐพ CoverArtArchive " "เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐฌเฐกเฑเฐคเฑเฐ‚เฐฆเฐฟ." #: src/ui/preferences.blp:48 msgid "ListenBrainz Link" msgstr "ListenBrainz เฐฒเฐฟเฐ‚เฐ•เฑ" #: src/ui/preferences.blp:49 msgid "Keep track of your playback history" msgstr "เฐฎเฑ€ เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑ เฐšเฐฐเฐฟเฐคเฑเฐฐเฐจเฑ เฐ—เฐฎเฐจเฐฟเฐธเฑเฐคเฑ‚ เฐ‰เฐ‚เฐกเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:55 src/ui/preferences.blp:56 msgid "Link" msgstr "เฐฒเฐฟเฐ‚เฐ•เฑ" #: src/ui/preferences.blp:64 src/ui/preferences.blp:65 msgid "Unlink" msgstr "เฐ…เฐจเฑโ€Œเฐฒเฐฟเฐ‚เฐ•เฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:77 msgid "User" msgstr "เฐตเฐฟเฐจเฐฟเฐฏเฑ‹เฐ—เฐฆเฐพเฐฐเฑ" #: src/ui/preferences.blp:97 msgid "Logout" msgstr "เฐฒเฐพเฐ—เฑเฐ…เฐตเฑเฐŸเฑ" #: src/ui/preferences.blp:107 msgid "Customization" msgstr "เฐ…เฐจเฑเฐ•เฑ‚เฐฒเฑ€เฐ•เฐฐเฐฃ" #: src/ui/preferences.blp:110 msgid "Interface" msgstr "เฐ‡เฐ‚เฐŸเฐฐเฑเฐซเฑ‡เฐธเฑ" #: src/ui/preferences.blp:112 msgid "Show Context Button in Rows" msgstr "เฐ…เฐกเฑเฐกเฑ เฐตเฐฐเฑเฐธเฐฒเฐฒเฑ‹ เฐ•เฐพเฐ‚เฐŸเฑ†เฐ•เฑเฐธเฑเฐŸเฑ เฐฌเฐŸเฐจเฑโ€Œเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/preferences.blp:113 msgid "The menu can also be shown by right clicking / long pressing" msgstr "เฐ•เฑเฐกเฐฟ เฐ•เฑเฐฒเฐฟเฐ•เฑ เฐšเฑ‡เฐฏเฐกเฐ‚ เฐฒเฑ‡เฐฆเฐพ เฐŽเฐ•เฑเฐ•เฑเฐตเฐธเฑ‡เฐชเฑ เฐจเฑŠเฐ•เฑเฐ•เฐฟ เฐชเฐŸเฑเฐŸเฑเฐ•เฑ‹เฐตเฐกเฐ‚ เฐฆเฑเฐตเฐพเฐฐเฐพ เฐ•เฑ‚เฐกเฐพ เฐฎเฑ†เฐจเฑ‚เฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฐตเฐšเฑเฐšเฑ." #: src/ui/preferences.blp:116 msgid "Show Labels in Buttons of Pages" msgstr "เฐชเฑ‡เฐœเฑ€เฐฒ เฐฌเฐŸเฐจเฑเฐฒเฐฒเฑ‹ เฐฒเฑ‡เฐฌเฑเฐฒเฑโ€Œเฐฒเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/preferences.blp:119 msgid "Show Progressbar in Player Footer" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑ เฐซเฑเฐŸเฐฐเฑโ€Œเฐฒเฑ‹ เฐชเฑเฐฐเฑ‹เฐ—เฑเฐฐเฑ†เฐธเฑโ€Œเฐฌเฐพเฐฐเฑโ€Œเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/preferences.blp:122 msgid "Make Background Translucent in Player" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑโ€Œเฐฒเฑ‹ เฐฌเฑเฐฏเฐพเฐ•เฑโ€Œเฐ—เฑเฐฐเฑŒเฐ‚เฐกเฑโ€Œเฐจเฑ เฐชเฐพเฐฐเฐฆเฐฐเฑเฐถเฐ•เฐ‚เฐ—เฐพ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:125 msgid "Use Sidebar Player When Possible" msgstr "เฐตเฑ€เฐฒเฑˆเฐจเฐชเฑเฐชเฑเฐกเฑ เฐธเฑˆเฐกเฑโ€Œเฐฌเฐพเฐฐเฑ เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑโ€Œเฐจเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:126 msgid "Sidebar player is only available when the window is wide enough" msgstr "เฐตเฐฟเฐ‚เฐกเฑ‹ เฐคเฐ—เฐฟเฐจเฐ‚เฐค เฐตเฑ†เฐกเฐฒเฑเฐชเฑเฐ—เฐพ เฐ‰เฐจเฑเฐจเฐชเฑเฐชเฑเฐกเฑ เฐฎเฐพเฐคเฑเฐฐเฐฎเฑ‡ เฐธเฑˆเฐกเฑโ€Œเฐฌเฐพเฐฐเฑ เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑ เฐ…เฐ‚เฐฆเฑเฐฌเฐพเฐŸเฑเฐฒเฑ‹ เฐ‰เฐ‚เฐŸเฑเฐ‚เฐฆเฐฟ" #: src/ui/preferences.blp:129 msgid "Show Pan Buttons on Carousels" msgstr "เฐ•เฑเฐฏเฐพเฐฐเฑŒเฐธเฑ†เฐฒเฑโ€Œเฐฒเฐชเฑˆ เฐชเฐพเฐจเฑ เฐฌเฐŸเฐจเฑโ€Œเฐฒเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/preferences.blp:130 msgid "Only visible when a carousel has 5 or more items" msgstr "เฐ•เฑเฐฏเฐพเฐฐเฑŒเฐธเฑ†เฐฒเฑโ€Œเฐฒเฑ‹ 5 เฐฒเฑ‡เฐฆเฐพ เฐ…เฐ‚เฐคเฐ•เฐ‚เฐŸเฑ‡ เฐŽเฐ•เฑเฐ•เฑเฐต เฐ…เฐ‚เฐถเฐพเฐฒเฑ เฐ‰เฐจเฑเฐจเฐชเฑเฐชเฑเฐกเฑ เฐฎเฐพเฐคเฑเฐฐเฐฎเฑ‡ เฐ•เฐจเฐฟเฐชเฐฟเฐธเฑเฐคเฑเฐ‚เฐฆเฐฟ" #: src/ui/preferences.blp:135 msgid "Dynamic Background" msgstr "เฐกเฑˆเฐจเฐฎเฐฟเฐ•เฑ เฐจเฑ‡เฐชเฐฅเฑเฐฏเฐ‚" #: src/ui/preferences.blp:136 msgid "Background generated based on the album art of the current song" msgstr "เฐชเฑเฐฐเฐธเฑเฐคเฑเฐค เฐชเฐพเฐŸ เฐฏเฑŠเฐ•เฑเฐ• เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐ†เฐฐเฑเฐŸเฑ เฐ†เฐงเฐพเฐฐเฐ‚เฐ—เฐพ เฐจเฑ‡เฐชเฐฅเฑเฐฏเฐ‚ เฐฐเฑ‚เฐชเฑŠเฐ‚เฐฆเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/ui/preferences.blp:139 msgid "Use in Main Window" msgstr "เฐชเฑเฐฐเฐงเฐพเฐจ เฐตเฐฟเฐ‚เฐกเฑ‹เฐฒเฑ‹ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:140 msgid "Experimental" msgstr "เฐชเฑเฐฐเฐฏเฑ‹เฐ—เฐพเฐคเฑเฐฎเฐ•เฐฎเฑˆเฐจเฐฆเฐฟ" #: src/ui/preferences.blp:146 src/ui/preferences.blp:147 #: src/ui/preferences.blp:170 src/ui/preferences.blp:171 #: src/ui/preferences.blp:194 src/ui/preferences.blp:195 msgid "Off" msgstr "เฐ†เฐซเฑ" #: src/ui/preferences.blp:151 src/ui/preferences.blp:152 #: src/ui/preferences.blp:175 src/ui/preferences.blp:176 #: src/ui/preferences.blp:199 src/ui/preferences.blp:200 msgid "Gradient" msgstr "เฐ—เฑเฐฐเฑ‡เฐกเฐฟเฐฏเฐ‚เฐŸเฑ" #: src/ui/preferences.blp:156 src/ui/preferences.blp:157 #: src/ui/preferences.blp:180 src/ui/preferences.blp:181 #: src/ui/preferences.blp:204 src/ui/preferences.blp:205 msgid "Blur" msgstr "เฐฌเฑเฐฒเฐฐเฑ" #: src/ui/preferences.blp:164 msgid "Use in Player" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑโ€Œเฐฒเฑ‹ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:188 msgid "Use in Pop-out Window" msgstr "เฐชเฐพเฐชเฑ-เฐ…เฐตเฑเฐŸเฑ เฐตเฐฟเฐ‚เฐกเฑ‹เฐฒเฑ‹ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:212 msgid "Use as Accent Color" msgstr "เฐฏเฐพเฐ•เฑเฐธเฑ†เฐ‚เฐŸเฑ เฐ•เฐฒเฐฐเฑโ€Œเฐ—เฐพ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:217 msgid "Homepage" msgstr "เฐนเฑ‹เฐฎเฑโ€Œเฐชเฑ‡เฐœเฑ€" #: src/ui/preferences.blp:218 msgid "Reload homepage to see changes" msgstr "เฐฎเฐพเฐฐเฑเฐชเฑเฐฒเฐจเฑ เฐšเฑ‚เฐกเฐŸเฐพเฐจเฐฟเฐ•เฐฟ เฐนเฑ‹เฐฎเฑโ€Œเฐชเฑ‡เฐœเฑ€เฐจเฐฟ เฐฐเฑ€เฐฒเฑ‹เฐกเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:254 msgid "Sidebar" msgstr "เฐธเฑˆเฐกเฑโ€Œเฐฌเฐพเฐฐเฑ" #: src/ui/preferences.blp:258 msgid "Visualizer" msgstr "เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑ" #: src/ui/preferences.blp:262 src/ui/window.blp:249 msgid "Preferences" msgstr "เฐชเฑเฐฐเฐพเฐงเฐพเฐจเฑเฐฏเฐคเฐฒเฑ" #: src/ui/preferences.blp:264 msgid "Show Visualizer in Player" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑโ€Œเฐฒเฑ‹ เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑโ€Œเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/ui/preferences.blp:265 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "เฐชเฑเฐฒเฑ‡เฐฏเฐฐเฑโ€Œเฐฒเฑ‹เฐจเฐฟ เฐ†เฐฒเฑเฐฌเฐฎเฑ เฐ†เฐฐเฑเฐŸเฑ เฐชเฑˆเฐจ เฐ†เฐกเฐฟเฐฏเฑ‹ เฐธเฑเฐชเฑ†เฐ•เฑเฐŸเฑเฐฐเฐฎเฑ เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑโ€Œเฐจเฑ เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:270 msgid "Appearance" msgstr "เฐธเฑเฐตเฐฐเฑ‚เฐชเฐ‚" #: src/ui/preferences.blp:274 msgid "Number of Bars" msgstr "เฐฌเฐพเฐฐเฑเฐฒ เฐธเฐ‚เฐ–เฑเฐฏ" #: src/ui/preferences.blp:285 msgid "Visualizer Type" msgstr "เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑ เฐฐเฐ•เฐ‚" #: src/ui/preferences.blp:291 src/ui/preferences.blp:292 msgid "Wave" msgstr "เฐ…เฐฒ" #: src/ui/preferences.blp:296 src/ui/preferences.blp:297 msgid "Bars" msgstr "เฐฌเฐพเฐฐเฑเฐฒเฑ" #: src/ui/preferences.blp:301 src/ui/preferences.blp:302 msgid "Particles" msgstr "เฐชเฐพเฐฐเฑเฐŸเฐฟเฐ•เฐฒเฑเฐธเฑ" #: src/ui/preferences.blp:309 msgid "Fill Mode" msgstr "เฐฎเฑ‹เฐกเฑ เฐชเฑ‚เฐฐเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:315 src/ui/preferences.blp:316 msgid "Solid" msgstr "เฐ˜เฐจเฐฎเฑˆเฐจเฐฆเฐฟ" #: src/ui/preferences.blp:320 src/ui/preferences.blp:321 msgid "Translucent" msgstr "เฐ…เฐชเฐพเฐฐเฐฆเฐฐเฑเฐถเฐ•" #: src/ui/preferences.blp:325 src/ui/preferences.blp:326 msgid "Border" msgstr "เฐธเฐฐเฐฟเฐนเฐฆเฑเฐฆเฑ" #: src/ui/preferences.blp:334 msgid "Color" msgstr "เฐฐเฐ‚เฐ—เฑ" #: src/ui/preferences.blp:338 msgid "Use Dynamic Color" msgstr "เฐกเฑˆเฐจเฐฎเฐฟเฐ•เฑ เฐ•เฐฒเฐฐเฑ เฐ‰เฐชเฐฏเฑ‹เฐ—เฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/preferences.blp:341 msgid "Invert Color" msgstr "เฐตเฐฟเฐฒเฑ‹เฐฎ เฐฐเฐ‚เฐ—เฑ" #: src/ui/preferences.blp:345 msgid "Manual Color" msgstr "เฐฎเฐพเฐจเฑเฐฏเฑเฐตเฐฒเฑ เฐฐเฐ‚เฐ—เฑ" #: src/ui/preferences.blp:351 msgid "Visualizer Color" msgstr "เฐตเฐฟเฐœเฑเฐตเฐฒเฑˆเฐœเฐฐเฑ เฐฐเฐ‚เฐ—เฑ" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "เฐธเฐคเฑเฐตเฐฐเฐฎเฐพเฐฐเฑเฐ—เฐพเฐฒเฐจเฑ เฐšเฑ‚เฐชเฑ" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "เฐชเฑเฐฐเฐพเฐงเฐพเฐจเฑเฐฏเฐคเฐฒเฐจเฑ เฐšเฑ‚เฐชเฑ" #: src/ui/shortcuts-dialog.blp:16 msgid "Quit" msgstr "เฐจเฐฟเฐทเฑเฐ•เฑเฐฐเฐฎเฐฟเฐ‚เฐšเฑ" #: src/ui/shortcuts-dialog.blp:21 msgid "Playback" msgstr "เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑ" #: src/ui/shortcuts-dialog.blp:23 msgid "Toggle Playback" msgstr "เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑโ€Œเฐจเฑ เฐŸเฑ‹เฐ—เฑเฐฒเฑ เฐšเฑ‡เฐฏเฐ‚เฐกเฐฟ" #: src/ui/shortcuts-dialog.blp:27 msgid "Next Song" msgstr "เฐคเฐฆเฑเฐชเฐฐเฐฟ เฐชเฐพเฐŸ" #: src/ui/shortcuts-dialog.blp:31 msgid "Previous Song" msgstr "เฐฎเฑเฐจเฑเฐชเฐŸเฐฟ เฐชเฐพเฐŸ" #: src/ui/shortcuts-dialog.blp:35 msgid "Raise Volume" msgstr "เฐตเฐพเฐฒเฑเฐฏเฑ‚เฐฎเฑ เฐชเฑ†เฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/ui/shortcuts-dialog.blp:39 msgid "Lower Volume" msgstr "เฐคเฐ•เฑเฐ•เฑเฐต เฐตเฐพเฐฒเฑเฐฏเฑ‚เฐฎเฑ" #: src/ui/shortcuts-dialog.blp:44 msgid "Window" msgstr "เฐตเฐฟเฐ‚เฐกเฑ‹" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:254 msgid "Open Pop-Out Window" msgstr "เฐชเฐพเฐชเฑ-เฐ…เฐตเฑเฐŸเฑ เฐตเฐฟเฐ‚เฐกเฑ‹เฐจเฑ เฐคเฑ†เฐฐเฐตเฐ‚เฐกเฐฟ" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "เฐšเฐฟเฐนเฑเฐจเฐพเฐจเฑเฐจเฐฟ เฐฒเฐพเฐ—เฐ‚เฐกเฐฟ" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "เฐŽเฐ‚เฐชเฐฟเฐ• เฐชเฑ†เฐŸเฑเฐŸเฑ†" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "เฐชเฐพเฐŸ เฐ•เฐตเฐฐเฑ เฐ†เฐฐเฑเฐŸเฑ" #: src/ui/window.blp:78 msgid "Play Random Queue" msgstr "เฐฐเฐพเฐ‚เฐกเฐฎเฑ เฐ•เฑเฐฏเฑ‚ เฐ†เฐกเฐ‚เฐกเฐฟ" #: src/ui/window.blp:91 msgid "Menu" msgstr "เฐฎเฑ†เฐจเฑ‚" #: src/ui/window.blp:119 msgid "Random Albums" msgstr "เฐฏเฐพเฐฆเฑƒเฐšเฑเฐ›เฐฟเฐ• เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:123 msgid "Favorite Albums" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจ เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:127 msgid "Newest Albums" msgstr "เฐธเฐฐเฐฟเฐ•เฑŠเฐคเฑเฐค เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:131 msgid "Recent Albums" msgstr "เฐ‡เฐŸเฑ€เฐตเฐฒเฐฟ เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:135 msgid "Frequent Albums" msgstr "เฐคเฐฐเฐšเฑเฐ—เฐพ เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:269 msgid "Keyboard Shortcuts" msgstr "เฐ•เฑ€เฐฌเฑ‹เฐฐเฑเฐกเฑ เฐทเฐพเฐฐเฑเฐŸเฑโ€Œเฐ•เฐŸเฑโ€Œเฐฒเฑ" #: src/ui/window.blp:274 msgid "About Nocturne" msgstr "เฐจเฑ‹เฐ•เฑเฐŸเฐฐเฑเฐจเฑ เฐ—เฑเฐฐเฐฟเฐ‚เฐšเฐฟ" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:218 #: src/widgets/song/row.py:168 msgid "Not Favorite" msgstr "เฐ‡เฐทเฑเฐŸเฐฎเฑˆเฐจเฐฆเฐฟ เฐ•เฐพเฐฆเฑ" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "เฐกเฐฟเฐธเฑเฐ•เฑ {}" #: src/widgets/artist/button.py:42 src/widgets/artist/row.py:47 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} เฐ†เฐฒเฑเฐฌเฐฎเฑ" msgstr[1] "{} เฐ†เฐฒเฑเฐฌเฐฎเฑโ€Œเฐฒเฑ" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "เฐ…เฐ—เฑเฐฐ เฐชเฐพเฐŸเฐฒเฑ" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "เฐธเฐ‚เฐฌเฐ‚เฐงเฐฟเฐค เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฑ" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "เฐฐเฐšเฐฏเฐฟเฐค" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "เฐŸเฑˆเฐฎเฑโ€ŒเฐธเฑเฐŸเฐพเฐ‚เฐชเฑ เฐฒเฑ‡เฐฆเฑ" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "เฐจเฐกเฑเฐธเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "เฐ…เฐฆเฐจเฐชเฑ เฐฎเฑ†เฐจเฑ" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "เฐธเฑเฐฅเฐพเฐจเฐฟเฐ• เฐธเฐ‚เฐ—เฑ€เฐค เฐฒเฑˆเฐฌเฑเฐฐเฐฐเฑ€" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "เฐชเฑเฐจเฐƒเฐชเฑเฐฐเฐพเฐฐเฐ‚เฐญเฐฟเฐ‚เฐšเฐฌเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "เฐฒเฑ‹เฐชเฐ‚" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "เฐคเฑเฐตเฐฐเฐฟเฐค เฐ•เฐจเฑ†เฐ•เฑเฐŸเฑ" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "เฐ•เฑ‹เฐกเฑ เฐชเฑŠเฐ‚เฐฆเฐกเฐ‚เฐฒเฑ‹ เฐฒเฑ‹เฐชเฐ‚ เฐเฐฐเฑเฐชเฐกเฐฟเฐ‚เฐฆเฐฟ" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "เฐคเฑเฐตเฐฐเฐฟเฐค เฐ…เฐจเฑเฐธเฐ‚เฐงเฐพเฐจ เฐชเฑ‡เฐœเฑ€" #: src/widgets/pages/setup.py:63 msgid "Installing" msgstr "เฐ‡เฐจเฑโ€ŒเฐธเฑเฐŸเฐพเฐฒเฑ เฐšเฑ‡เฐธเฑเฐคเฑ‹เฐ‚เฐฆเฐฟ" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "เฐ‡เฐ‚เฐŸเฐฟเฐ—เฑเฐฐเฑ‡เฐทเฐจเฑ" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC เฐซเฑˆเฐฒเฑ" #: src/widgets/playlist/button.py:49 src/widgets/playlist/page.py:118 #: src/widgets/playlist/row.py:48 src/widgets/playlist/selector_row.py:37 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "{} เฐชเฐพเฐŸ" msgstr[1] "{} เฐชเฐพเฐŸเฐฒเฑ" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "เฐ•เฑŠเฐคเฑเฐค เฐชเฑเฐฒเฑ‡เฐฏเฐฒเฐฟเฐธเฑเฐŸเฑ" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add Songs to '{}'" msgstr "เฐชเฐพเฐŸเฐฒเฐจเฑ '{}' เฐ•เฑ เฐœเฑ‹เฐกเฐฟเฐ‚เฐšเฐ‚เฐกเฐฟ" #: src/widgets/song/row.py:147 src/widgets/song/row.py:152 msgid "Multiple Artists" msgstr "เฐฌเฐนเฑเฐณ เฐ•เฐณเฐพเฐ•เฐพเฐฐเฑเฐฒเฑ" #: src/window.py:202 msgid "Nocturne Playback" msgstr "เฐจเฑ‹เฐ•เฑเฐŸเฐฐเฑเฐจเฑ เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑ" #: src/window.py:203 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "เฐฎเฑ€ เฐชเฑเฐฒเฑ‡เฐฌเฑเฐฏเฐพเฐ•เฑ เฐ•เฑเฐฏเฑ‚ {} เฐ•เฑ‹เฐธเฐ‚ เฐ…เฐ‚เฐฆเฑเฐฌเฐพเฐŸเฑเฐฒเฑ‹ เฐ‰เฐ‚เฐฆเฐฟ!" #: src/window.py:205 msgid "Show" msgstr "เฐšเฑ‚เฐชเฐฟเฐ‚เฐšเฑ" #: src/window.py:206 msgid "Later" msgstr "เฐคเฐฐเฑเฐตเฐพเฐค" Jeffser-Nocturne-b442fee/po/tr.po000066400000000000000000001151411523240030000167610ustar00rootroot00000000000000# Turkish translations for Nocturne package. # Copyright (C) 2026 Jeffser # This file is distributed under the same license as the Nocturne package. # Muhammed Emin Akalan , 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-05 18:30-0600\n" "PO-Revision-Date: 2026-04-13 03:45-0300\n" "Last-Translator: Muhammed Emin Akalan \n" "Language-Team: Turkish\n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Mรผzik;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "Mรผzik kitaplฤฑฤŸฤฑnฤฑzฤฑ bir araya getirin" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "Modern bir Navidrome / Jellyfin istemcisi" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "ร–zellikler" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "" "ลžarkฤฑlar, sanatรงฤฑlar, albรผmler, radyolar ve รงalma listeleri arasฤฑnda keลŸif" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "ร‡alma listesi yรถnetimi" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "Jellyfin, OpenSubsonic ve yerel dosyalarla uyumluluk" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "Ses ekolayzฤฑrฤฑ ve ses gรถrselleลŸtirici" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "MPRIS entegrasyonu" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "Entegre Navidrome sunucu yรถnetimi" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "Otomatik ลŸarkฤฑ sรถzรผ getirme" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "ฤฐndirmeler ve รงevrimdฤฑลŸฤฑ mod" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "ลžฤฑk arayรผz" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "Ana sayfa" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "ลžarkฤฑ kuyruฤŸu" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:170 #: src/ui/window.blp:222 msgid "Lyrics" msgstr "ลžarkฤฑ Sรถzleri" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "ลžarkฤฑ listesi" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "Albรผm sayfasฤฑ" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "mรผzik" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:211 src/ui/song/row.blp:69 msgid "External File" msgstr "Harici Dosya" #: src/actions.py:212 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Bu parรงa harici bir dosyadan yรผklendi; kitaplฤฑk iรงindeki bir parรงaya kฤฑyasla " "daha az รถzelliฤŸe sahip olabilir." #: src/actions.py:214 src/ui/song/queue.blp:40 msgid "Close" msgstr "Kapat" #: src/actions.py:232 msgid "Navidrome server deleted successfully" msgstr "Navidrome sunucusu baลŸarฤฑyla silindi" #: src/actions.py:241 msgid "Delete Navidrome Server" msgstr "Navidrome Sunucusunu Sil" #: src/actions.py:242 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Entegre Navidrome sunucusunu silmek istediฤŸinizden emin misiniz?" #: src/actions.py:244 src/actions.py:355 src/actions.py:389 src/actions.py:674 #: src/actions.py:790 src/preferences.py:322 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "ฤฐptal" #: src/actions.py:245 msgid "Keep Data" msgstr "Verileri Sakla" #: src/actions.py:246 msgid "Delete Everything" msgstr "Her ลžeyi Sil" #: src/actions.py:301 msgid "No Name" msgstr "ฤฐsimsiz" #: src/actions.py:320 msgid "Radio updated successfully" msgstr "Radyo baลŸarฤฑyla gรผncellendi" #: src/actions.py:320 msgid "Radio added successfully" msgstr "Radyo baลŸarฤฑyla eklendi" #: src/actions.py:331 msgid "Error updating radio" msgstr "Radyo gรผncellenirken hata oluลŸtu" #: src/actions.py:331 msgid "Error adding radio" msgstr "Radyo eklenirken hata oluลŸtu" #: src/actions.py:340 src/actions.py:666 msgid "Name" msgstr "Ad" #: src/actions.py:345 msgid "Stream Url" msgstr "AkฤฑลŸ URL'si" #: src/actions.py:352 msgid "Update Radio Station" msgstr "Radyo ฤฐstasyonunu Gรผncelle" #: src/actions.py:352 msgid "Add Radio Station" msgstr "Radyo ฤฐstasyonu Ekle" #: src/actions.py:356 src/preferences.py:323 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Kaydet" #: src/actions.py:372 msgid "Radio deleted successfully" msgstr "Radyo baลŸarฤฑyla silindi" #: src/actions.py:380 msgid "Error deleting radio" msgstr "Radyo silinirken hata oluลŸtu" #: src/actions.py:386 msgid "Delete Radio Station" msgstr "Radyo ฤฐstasyonunu Sil" #: src/actions.py:387 src/actions.py:788 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "'{}' รถฤŸesini silmek istediฤŸinizden emin misiniz?" #: src/actions.py:390 src/actions.py:791 src/constants.py:343 #: src/constants.py:396 msgid "Delete" msgstr "Sil" #: src/actions.py:420 src/actions.py:447 src/actions.py:452 src/actions.py:541 #: src/actions.py:600 msgid "Playing Next" msgstr "Sฤฑradaki ร‡alฤฑnฤฑyor" #: src/actions.py:430 src/actions.py:463 src/actions.py:468 src/actions.py:556 #: src/actions.py:615 msgid "Playing Later" msgstr "Daha Sonra Oynatฤฑlacak" #: src/actions.py:447 src/actions.py:463 src/actions.py:905 src/actions.py:1040 #: src/widgets/playlist/button.py:50 src/widgets/playlist/button.py:52 #: src/widgets/playlist/dialog.py:31 src/widgets/playlist/page.py:118 #: src/widgets/playlist/page.py:120 src/widgets/playlist/row.py:49 #: src/widgets/playlist/row.py:51 src/widgets/playlist/selector_row.py:38 #: src/widgets/playlist/selector_row.py:40 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" msgstr[1] "" #: src/actions.py:497 msgid "Lyrics Saved" msgstr "ลžarkฤฑ Sรถzleri Kaydedildi" #: src/actions.py:647 msgid "Playlist updated successfully" msgstr "ร‡alma listesi baลŸarฤฑyla gรผncellendi" #: src/actions.py:647 msgid "Playlist created successfully" msgstr "ร‡alma listesi baลŸarฤฑyla oluลŸturuldu" #: src/actions.py:657 msgid "Error updating playlist" msgstr "ร‡alma listesi gรผncellenirken hata oluลŸtu" #: src/actions.py:657 msgid "Error creating playlist" msgstr "ร‡alma listesi oluลŸturulurken hata oluลŸtu" #: src/actions.py:671 msgid "Update Playlist" msgstr "ร‡alma Listesini Gรผncelle" #: src/actions.py:671 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "ร‡alma Listesi OluลŸtur" #: src/actions.py:675 msgid "Update" msgstr "Gรผncelle" #: src/actions.py:675 msgid "Create" msgstr "OluลŸtur" #: src/actions.py:695 src/actions.py:700 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:732 src/actions.py:734 src/actions.py:754 src/actions.py:756 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:761 src/actions.py:877 src/actions.py:910 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: src/actions.py:775 msgid "Playlist Deleted" msgstr "ร‡alma Listesi Silindi" #: src/actions.py:787 msgid "Delete Playlist" msgstr "ร‡alma Listesini Sil" #: src/actions.py:836 msgid "No songs found" msgstr "ลžarkฤฑ bulunamadฤฑ" #: src/actions.py:879 src/actions.py:906 src/actions.py:943 src/actions.py:979 msgid "Download Started" msgstr "ฤฐndirme BaลŸlatฤฑldฤฑ" #: src/actions.py:886 src/actions.py:917 src/actions.py:924 src/actions.py:959 #: src/actions.py:995 msgid "Already Downloaded" msgstr "Zaten ฤฐndirildi" #: src/actions.py:911 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:923 msgid "Download Skipped" msgstr "ฤฐndirme Atlandฤฑ" #: src/actions.py:947 src/actions.py:983 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: src/actions.py:1022 src/actions.py:1041 msgid "Deleted" msgstr "Silindi" #: src/constants.py:159 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "ArdฤฑลŸฤฑk" #: src/constants.py:163 msgid "Repeat All" msgstr "Tรผmรผnรผ Tekrarla" #: src/constants.py:167 msgid "Repeat One" msgstr "Tek Parรงayฤฑ Tekrarla" #: src/constants.py:172 #, python-brace-format msgid "Low ({})" msgstr "" #: src/constants.py:173 #, python-brace-format msgid "Medium ({})" msgstr "" #: src/constants.py:174 #, python-brace-format msgid "High ({})" msgstr "" #: src/constants.py:175 #, python-brace-format msgid "Ultra ({})" msgstr "" #: src/constants.py:176 msgid "Original File" msgstr "" #. Item #: src/constants.py:183 src/ui/pages/home.blp:5 msgid "Home" msgstr "Ana Sayfa" #. Item #: src/constants.py:188 src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:223 src/widgets/pages/home.py:62 msgid "Artists" msgstr "Sanatรงฤฑlar" #. Item #. Section #: src/constants.py:193 src/constants.py:255 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:231 #: src/widgets/pages/home.py:74 msgid "Playlists" msgstr "ร‡alma Listeleri" #. Section #: src/constants.py:200 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:215 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:50 msgid "Albums" msgstr "Albรผmler" #. Item #: src/constants.py:203 src/constants.py:238 src/window.py:135 msgid "All" msgstr "Tรผmรผ" #. Item #: src/constants.py:208 msgid "Random" msgstr "Rastgele" #. Item #: src/constants.py:213 src/constants.py:243 msgid "Favorites" msgstr "Favoriler" #. Item #: src/constants.py:218 msgid "Recently Added" msgstr "Son Eklenenler" #. Item #: src/constants.py:223 msgid "Recently Played" msgstr "Son ร‡alฤฑnanlar" #. Item #: src/constants.py:228 msgid "Most Played" msgstr "En ร‡ok ร‡alฤฑnanlar" #. Section #: src/constants.py:235 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:207 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "ลžarkฤฑlar" #. Item #: src/constants.py:248 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Radyolar" #: src/constants.py:262 src/constants.py:313 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "Oynat" #: src/constants.py:267 src/constants.py:300 src/constants.py:318 msgid "Shuffle" msgstr "KarฤฑลŸtฤฑr" #: src/constants.py:272 src/constants.py:323 src/constants.py:356 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "Sฤฑradakini Oynat" #: src/constants.py:277 src/constants.py:328 src/constants.py:361 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "Daha Sonra Oynat" #: src/constants.py:282 src/constants.py:376 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "ร‡alma Listesine Ekle" #: src/constants.py:287 src/constants.py:333 src/constants.py:381 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "ฤฐndir" #: src/constants.py:292 src/constants.py:391 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "Sanatรงฤฑyฤฑ Gรถster" #: src/constants.py:305 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:108 msgid "Radio" msgstr "Radyo" #: src/constants.py:338 src/constants.py:366 msgid "Edit" msgstr "Dรผzenle" #: src/constants.py:352 msgid "Select" msgstr "Seรง" #: src/constants.py:371 msgid "Edit Lyrics" msgstr "ลžarkฤฑ Sรถzlerini Dรผzenle" #: src/constants.py:386 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "Albรผmรผ Gรถster" #: src/constants.py:402 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Kaldฤฑr" #: src/constants.py:407 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "ฤฐndirmeyi Sil" #: src/constants.py:416 msgid "Visit Webpage" msgstr "Web Sayfasฤฑnฤฑ Ziyaret Et" #: src/constants.py:422 msgid "Update Server" msgstr "Sunucuyu Gรผncelle" #: src/constants.py:427 msgid "Delete Server" msgstr "Sunucuyu Sil" #: src/integrations/jellyfin.py:15 msgid "Connect to a Jellyfin server." msgstr "Bir Jellyfin sunucusuna baฤŸlanฤฑn." #: src/integrations/jellyfin.py:19 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:20 msgid "Use an existing Jellyfin instance" msgstr "Mevcut bir Jellyfin sunucusu kullan" #: src/integrations/local.py:17 src/integrations/local.py:23 #: src/integrations/local.py:509 msgid "Local Files" msgstr "Yerel Dosyalar" #: src/integrations/local.py:18 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "Nocturne'un yerel dosyalarฤฑnฤฑzฤฑ doฤŸrudan yรผklemesini saฤŸlayฤฑn; bรผyรผk " "kitaplฤฑklar iรงin รถzel bir sunucu kullanฤฑlmasฤฑ รถnerilir." #: src/integrations/local.py:20 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Devam Et" #: src/integrations/local.py:24 msgid "Limited functionality" msgstr "Sฤฑnฤฑrlฤฑ iลŸlevsellik" #: src/integrations/local.py:36 msgid "Loading Songs" msgstr "ลžarkฤฑlar Yรผkleniyor" #: src/integrations/local.py:215 msgid "No Album" msgstr "Albรผm Yok" #: src/integrations/local.py:525 src/integrations/local.py:534 msgid "Offline Mode" msgstr "ร‡evrimdฤฑลŸฤฑ Mod" #: src/integrations/local.py:526 msgid "Access your downloads" msgstr "ฤฐndirmelerinize eriลŸin" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Navidrome gibi bir OpenSubsonic sunucusuna baฤŸlanฤฑn." #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "Harici Sunucu" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "Mevcut bir OpenSubsonic / Navidrome sunucusu kullan" #: src/integrations/navidrome.py:550 src/integrations/navidrome.py:559 msgid "Managed Server" msgstr "Yรถnetilen Sunucu" #: src/integrations/navidrome.py:551 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "DoฤŸrudan Nocturne tarafฤฑndan yรถnetilen bir Navidrome sunucusuna baฤŸlanฤฑn." #: src/integrations/navidrome.py:554 msgid "Manage Server" msgstr "Sunucuyu Yรถnet" #: src/integrations/navidrome.py:560 msgid "Create and use a Navidrome instance" msgstr "Bir Navidrome sunucusu oluลŸturun ve kullanฤฑn" #: src/main.py:69 msgid "Music is Playing" msgstr "Mรผzik ร‡alฤฑyor" #: src/main.py:116 msgid "Login Failed" msgstr "GiriลŸ BaลŸarฤฑsฤฑz" #: src/preferences.py:311 msgid "Settings Page" msgstr "Ayarlar Sayfasฤฑ" #: src/preferences.py:314 msgid "User Token" msgstr "Kullanฤฑcฤฑ Belirteci" #: src/preferences.py:318 msgid "Link ListenBrainz" msgstr "ListenBrainz'i BaฤŸla" #: src/preferences.py:319 msgid "Connect your ListenBrainz account with a user token" msgstr "ListenBrainz hesabฤฑnฤฑzฤฑ bir kullanฤฑcฤฑ belirteciyle baฤŸlayฤฑn" #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "Albรผmรผ Oynat" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "Favori durumunu deฤŸiลŸtir" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "Albรผm kapaฤŸฤฑ" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "Albรผm sayfasฤฑna git" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "Sanatรงฤฑ sayfasฤฑna git" #: src/ui/album/page.blp:5 src/widgets/album/page.py:131 msgid "Album" msgstr "Albรผm" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "1 Yฤฑldฤฑz" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "2 Yฤฑldฤฑz" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "3 Yฤฑldฤฑz" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "4 Yฤฑldฤฑz" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "5 Yฤฑldฤฑz" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "Seรงenekler" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "Sanatรงฤฑ" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "Biyografiyi GeniลŸlet" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "Liste" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "ฤฐndirildi" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "Kuyruktan Kaldฤฑr" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "ฤฐndirme KuyruฤŸu" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "ฤฐndirilen ลŸarkฤฑlar รงevrimdฤฑลŸฤฑ modda dinlenebilir" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "Tamamlanan ฤฐndirmeleri Temizle" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "ลžarkฤฑ Sรถzรผ Dรผzenleyici" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "Duraklat" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "Mevcut ลŸarkฤฑ ilerleme รงubuฤŸu" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "Sonraki Satฤฑr Zamanฤฑnฤฑ ลžimdiye Ayarla" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "Zaman Damgasฤฑna Satฤฑr Ekle" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "ลžarkฤฑ Sรถzรผ Yok" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "Bu parรงa iรงin sรถz bulunamadฤฑ; baลŸlamak iรงin yeni bir satฤฑr ekleyin." #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "Zaman Damgasฤฑ" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "Zamana Git" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "Geรงerli Zamanฤฑ Ayarla" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "Satฤฑrฤฑ Kaldฤฑr" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "Izgara" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "Albรผmlerde ara" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "Listenin Sonuna Gelindi" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "Albรผm Bulunamadฤฑ" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "Sanatรงฤฑlarda ara" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "Sanatรงฤฑ Bulunamadฤฑ" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "ร–ฤŸe Bulunamadฤฑ" #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "GiriลŸ Yap" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "Sunucu Durumu" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "ร‡alฤฑลŸmฤฑyor" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "Sunucuyu Yeniden BaลŸlat" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Navidrome Sunucusunu Yรถnet" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "DiฤŸer Seรงenekler" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Sunucu Sertifikalarฤฑna Gรผven" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Kitaplฤฑk Dizini" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Kullanฤฑcฤฑ Adฤฑ" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Parola" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "ร‡alma listelerinde ara" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "ร‡alma Listesi Bulunamadฤฑ" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "Radyolarda ara" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "Radyo Ekle" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "Radyo Bulunamadฤฑ" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "Kurulum" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "Navidrome ฤฐndir" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne, Navidrome'u otomatik olarak indirecek" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "ฤฐndir (~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "ฤฐndiriliyor" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "Bir Hata OluลŸtu" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "Lรผtfen daha sonra tekrar deneyin" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "ฤฐndirme BaลŸarฤฑlฤฑ" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "Devam etmeden รถnce bir yรถnetici hesabฤฑ oluลŸturduฤŸunuzdan ve tรผm " "parรงalarฤฑnฤฑzฤฑn mรผzik dizini iรงinde olduฤŸundan emin olun." #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "Sunucu Web Sayfasฤฑ" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "ลžarkฤฑlarda ara" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "ลžarkฤฑ Bulunamadฤฑ" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Favori ลžarkฤฑlar" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "Favori ลŸarkฤฑlarda ara" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "HoลŸ Geldiniz" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "Nocturne'a HoลŸ Geldiniz" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "BaลŸlamak iรงin bir sunucu tรผrรผ seรงin" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "Oynatฤฑlฤฑyor" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "ลžarkฤฑ KapaฤŸฤฑ" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "Mevcut radyo ana sayfasฤฑna git" #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "ร–nceki" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "Sonraki" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "Yฤฑldฤฑzla" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "Kenar ร‡ubuฤŸunu Gรถster" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "ร–n Ayarlar" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "Dรผz" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "Vokal NetliฤŸi" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "YumuลŸak Caz" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "Rock" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "Klasik" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "Akustik" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "Mevcut ลŸarkฤฑ ilerleme gรถstergesi" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "Mevcut ลŸarkฤฑ kapaฤŸฤฑ" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "Geri Dรถn" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "ลžarkฤฑ Sรถzรผ Bulunamadฤฑ" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Enstrรผmantal" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "ฤฐndirilmedi" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "Bu ลŸarkฤฑnฤฑn sรถzleri henรผz indirilmedi; indirilebilir olabilirler." #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "Sรถz Dosyasฤฑ Yรผkle" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "Ayrฤฑ Pencere" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:154 msgid "Player" msgstr "Oynatฤฑcฤฑ" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:16 #: src/ui/window.blp:255 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "Tam Ekranฤฑ Aรง/Kapat" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:194 msgid "Song Details" msgstr "ลžarkฤฑ Ayrฤฑntฤฑlarฤฑ" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:160 #: src/ui/window.blp:216 msgid "Queue" msgstr "Kuyruk" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:176 #: src/ui/window.blp:228 msgid "Equalizer" msgstr "Ekolayzฤฑr" #: src/ui/playing/queue_page.blp:20 msgid "Generating next queue" msgstr "Sฤฑradaki kuyruk oluลŸturuluyor" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Otomatik Oynat" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "Mevcut kuyruk bittiฤŸinde yeni bir kuyruk oluลŸtur" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "Ses" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "Sesi Kapat" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "Tam Ses" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "ร‡alma Listesini Oynat" #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "ร‡alma Listesini Gรถster" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "ร‡alma listesi kapaฤŸฤฑ" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "ร‡alma listesi sayfasฤฑna git" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "ร‡alma Listesi" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "Ara (veya yeni liste oluลŸtur)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "Listeye Mรผzik Ekleniyor" #: src/ui/playlist/dialog.blp:34 msgid "Create new playlist and add songs" msgstr "Yeni liste oluลŸtur ve ลŸarkฤฑlarฤฑ ekle" #: src/ui/preferences.blp:7 msgid "General" msgstr "Genel" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "DavranฤฑลŸ" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "BaลŸlangฤฑรงta KuyruฤŸu Geri Yรผkle" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "KapatฤฑldฤฑฤŸฤฑnda Gizle" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "GiriลŸte Varsayฤฑlan Sayfa" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "" #: src/ui/preferences.blp:33 msgid "Session" msgstr "Oturum" #: src/ui/preferences.blp:36 msgid "ListenBrainz Link" msgstr "ListenBrainz BaฤŸlantฤฑsฤฑ" #: src/ui/preferences.blp:37 msgid "Keep track of your playback history" msgstr "Oynatma geรงmiลŸinizi takip edin" #: src/ui/preferences.blp:43 src/ui/preferences.blp:44 msgid "Link" msgstr "BaฤŸla" #: src/ui/preferences.blp:52 src/ui/preferences.blp:53 msgid "Unlink" msgstr "BaฤŸlantฤฑyฤฑ Kes" #: src/ui/preferences.blp:65 msgid "User" msgstr "Kullanฤฑcฤฑ" #: src/ui/preferences.blp:85 msgid "Logout" msgstr "ร‡ฤฑkฤฑลŸ Yap" #: src/ui/preferences.blp:95 msgid "Customization" msgstr "ร–zelleลŸtirme" #: src/ui/preferences.blp:98 msgid "Interface" msgstr "Arayรผz" #: src/ui/preferences.blp:100 msgid "Show Context Button in Rows" msgstr "Satฤฑrlarda BaฤŸlam Menรผsรผ DรผฤŸmesini Gรถster" #: src/ui/preferences.blp:101 msgid "The menu can also be shown by right clicking / long pressing" msgstr "Menรผ ayrฤฑca saฤŸ tฤฑklayarak veya uzun basarak da aรงฤฑlabilir" #: src/ui/preferences.blp:104 msgid "Show Labels in Buttons of Pages" msgstr "" #: src/ui/preferences.blp:107 msgid "Show Playlists in Sidebar" msgstr "ร‡alma Listelerini Kenar ร‡ubuฤŸunda Gรถster" #: src/ui/preferences.blp:110 msgid "Show Progressbar in Player Footer" msgstr "Alt Bilgide ฤฐlerleme ร‡ubuฤŸunu Gรถster" #: src/ui/preferences.blp:113 msgid "Make Background Translucent in Player" msgstr "Oynatฤฑcฤฑ Arka Planฤฑnฤฑ Yarฤฑ Saydam Yap" #: src/ui/preferences.blp:116 msgid "Use Sidebar Player When Possible" msgstr "" #: src/ui/preferences.blp:117 msgid "Sidebar player is only available when the window is wide enough" msgstr "" #: src/ui/preferences.blp:122 msgid "Dynamic Background" msgstr "Dinamik Arka Plan" #: src/ui/preferences.blp:123 msgid "Background generated based on the album art of the current song" msgstr "Mevcut ลŸarkฤฑnฤฑn kapak resmine gรถre oluลŸturulan arka plan" #: src/ui/preferences.blp:126 msgid "Use in Main Window" msgstr "" #: src/ui/preferences.blp:127 msgid "Experimental" msgstr "" #: src/ui/preferences.blp:133 src/ui/preferences.blp:134 #: src/ui/preferences.blp:157 src/ui/preferences.blp:158 #: src/ui/preferences.blp:181 src/ui/preferences.blp:182 msgid "Off" msgstr "Kapalฤฑ" #: src/ui/preferences.blp:138 src/ui/preferences.blp:139 #: src/ui/preferences.blp:162 src/ui/preferences.blp:163 #: src/ui/preferences.blp:186 src/ui/preferences.blp:187 msgid "Gradient" msgstr "Gradyan" #: src/ui/preferences.blp:143 src/ui/preferences.blp:144 #: src/ui/preferences.blp:167 src/ui/preferences.blp:168 #: src/ui/preferences.blp:191 src/ui/preferences.blp:192 msgid "Blur" msgstr "BulanฤฑklaลŸtฤฑrma" #: src/ui/preferences.blp:151 msgid "Use in Player" msgstr "Oynatฤฑcฤฑda Kullan" #: src/ui/preferences.blp:175 msgid "Use in Pop-out Window" msgstr "Ayrฤฑ Pencerede Kullan" #: src/ui/preferences.blp:199 msgid "Use as Accent Color" msgstr "Vurgu Rengi Olarak Kullan" #: src/ui/preferences.blp:204 msgid "Homepage" msgstr "Ana Sayfa" #: src/ui/preferences.blp:205 msgid "Reload homepage to see changes" msgstr "DeฤŸiลŸiklikleri gรถrmek iรงin ana sayfayฤฑ yenileyin" #: src/ui/preferences.blp:241 msgid "Visualizer" msgstr "GรถrselleลŸtirici" #: src/ui/preferences.blp:245 src/ui/window.blp:245 msgid "Preferences" msgstr "Tercihler" #: src/ui/preferences.blp:247 msgid "Show Visualizer in Player" msgstr "Oynatฤฑcฤฑda GรถrselleลŸtiriciyi Gรถster" #: src/ui/preferences.blp:248 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" "Oynatฤฑcฤฑda kapak resminin รผzerinde bir ses spektrumu gรถrselleลŸtiricisi gรถster" #: src/ui/preferences.blp:253 msgid "Appearance" msgstr "Gรถrรผnรผm" #: src/ui/preferences.blp:257 msgid "Number of Bars" msgstr "ร‡ubuk Sayฤฑsฤฑ" #: src/ui/preferences.blp:268 msgid "Visualizer Type" msgstr "GรถrselleลŸtirici Tรผrรผ" #: src/ui/preferences.blp:274 src/ui/preferences.blp:275 msgid "Wave" msgstr "Dalga" #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Bars" msgstr "ร‡ubuklar" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Particles" msgstr "Parรงacฤฑklar" #: src/ui/preferences.blp:292 msgid "Fill Mode" msgstr "Doldurma Modu" #: src/ui/preferences.blp:298 src/ui/preferences.blp:299 msgid "Solid" msgstr "Dรผz" #: src/ui/preferences.blp:303 src/ui/preferences.blp:304 msgid "Translucent" msgstr "Yarฤฑ Saydam" #: src/ui/preferences.blp:308 src/ui/preferences.blp:309 msgid "Border" msgstr "Kenarlฤฑk" #: src/ui/preferences.blp:317 msgid "Color" msgstr "Renk" #: src/ui/preferences.blp:321 msgid "Use Dynamic Color" msgstr "Dinamik Renk Kullan" #: src/ui/preferences.blp:324 msgid "Invert Color" msgstr "Rengi Tersine ร‡evir" #: src/ui/preferences.blp:328 msgid "Manual Color" msgstr "ร–zel Renk" #: src/ui/preferences.blp:334 msgid "Visualizer Color" msgstr "GรถrselleลŸtirici Rengi" #: src/ui/shortcuts-dialog.blp:6 msgid "Shortcuts" msgstr "Kฤฑsayollar" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "Kฤฑsayollarฤฑ Gรถster" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "Tercihleri Gรถster" #: src/ui/shortcuts-dialog.blp:20 src/ui/window.blp:250 msgid "Open Pop-Out Window" msgstr "" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "ร‡ฤฑkฤฑลŸ" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "Sรผrรผkleme simgesi" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "Seรงim kutusu" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "ลžarkฤฑ kapaฤŸฤฑ" #: src/ui/window.blp:74 msgid "Play Random Queue" msgstr "Rastgele KuyruฤŸu Oynat" #: src/ui/window.blp:87 msgid "Menu" msgstr "Menรผ" #: src/ui/window.blp:115 msgid "Random Albums" msgstr "Rastgele Albรผmler" #: src/ui/window.blp:119 msgid "Favorite Albums" msgstr "Favori Albรผmler" #: src/ui/window.blp:123 msgid "Newest Albums" msgstr "En Yeni Albรผmler" #: src/ui/window.blp:127 msgid "Recent Albums" msgstr "Son Albรผmler" #: src/ui/window.blp:131 msgid "Frequent Albums" msgstr "Sฤฑk Dinlenen Albรผmler" #: src/ui/window.blp:260 msgid "Keyboard Shortcuts" msgstr "Klavye Kฤฑsayollarฤฑ" #: src/ui/window.blp:265 msgid "About Nocturne" msgstr "Nocturne Hakkฤฑnda" #: src/widgets/album/button.py:63 src/widgets/album/page.py:147 #: src/widgets/artist/page.py:127 src/widgets/playing/control_page.py:217 #: src/widgets/song/row.py:162 msgid "Favorite" msgstr "Favorilere Ekle" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:222 #: src/widgets/song/row.py:167 msgid "Not Favorite" msgstr "Favorilerden Kaldฤฑr" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "" #: src/widgets/artist/button.py:43 src/widgets/artist/row.py:48 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "" msgstr[1] "" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "En Popรผler ลžarkฤฑlar" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "ฤฐlgili Sanatรงฤฑlar" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "Yazar" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Zaman Damgasฤฑ Yok" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "ร‡alฤฑลŸฤฑyor" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "DiฤŸer Seรงenekler" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Yerel Mรผzik KitaplฤฑฤŸฤฑ" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "Yeniden BaลŸlatฤฑldฤฑ" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "Hata" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "" #: src/widgets/pages/setup.py:62 msgid "Installing" msgstr "Kuruluyor" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Entegrasyon" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC Dosyasฤฑ" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "Yeni ร‡alma Listesi" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add songs to '{}'" msgstr "ลžarkฤฑlarฤฑ '{}' listesine ekle" #: src/widgets/song/row.py:146 src/widgets/song/row.py:151 msgid "Multiple Artists" msgstr "Birden ร‡ok Sanatรงฤฑ" #~ msgid "{} Songs" #~ msgstr "{} ลžarkฤฑ" #~ msgid "{} Songs Removed" #~ msgstr "{} ลžarkฤฑ Kaldฤฑrฤฑldฤฑ" #~ msgid "Song Removed" #~ msgstr "ลžarkฤฑ Kaldฤฑrฤฑldฤฑ" #~ msgid "{} Songs Added" #~ msgstr "{} ลžarkฤฑ Eklendi" #~ msgid "1 Song Added" #~ msgstr "1 ลžarkฤฑ Eklendi" #~ msgid "1 Song Skipped" #~ msgstr "1 ลžarkฤฑ Atlandฤฑ" #~ msgid "{} Songs Skipped" #~ msgstr "{} ลžarkฤฑ Atlandฤฑ" #~ msgid "{} Songs ({})" #~ msgstr "{} ลžarkฤฑ ({})" #~ msgid "1 Song ({})" #~ msgstr "1 ลžarkฤฑ ({})" #~ msgid "Download Started ({} Songs Skipped)" #~ msgstr "ฤฐndirme BaลŸlatฤฑldฤฑ ({} ลžarkฤฑ Atlandฤฑ)" #~ msgid "Download Started (1 Song Skipped)" #~ msgstr "ฤฐndirme BaลŸlatฤฑldฤฑ (1 ลžarkฤฑ Atlandฤฑ)" #~ msgid "Open Pop-out Window" #~ msgstr "Ayrฤฑ Pencerede Aรง" #~ msgid "Close Pop-out Window" #~ msgstr "Ayrฤฑ Pencereyi Kapat" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "ลžarkฤฑ sรถzleri LRCLIB tarafฤฑndan saฤŸlanmaktadฤฑr" #~ msgid "Player in Pop-out Window" #~ msgstr "Ayrฤฑ Pencerede Oynatฤฑcฤฑ" #~ msgid "1 Album" #~ msgstr "1 Albรผm" #~ msgid "{} Albums" #~ msgstr "{} Albรผm" #~ msgid "1 Song" #~ msgstr "1 ลžarkฤฑ" Jeffser-Nocturne-b442fee/po/zh_CN.po000066400000000000000000001170631523240030000173420ustar00rootroot00000000000000# Simplified Chinese translations for Nocturne. # Copyright (C) 2026 Ja4e # This file is distributed under the same license as the Nocturne package. # Jinyang Zhu , 2026. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-05 18:30-0600\n" "PO-Revision-Date: 2026-06-020 03:20+0100\n" "Last-Translator: Jinyang Zhu \n" "Language-Team: Simplified Chinese \n" "Language: zh-cn \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "ๅฐ†ๆ‚จ็š„้Ÿณไนๅบ“ๆ•ดๅˆๅœจไธ€่ตท" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "็Žฐๆ—ถไปฃ็š„ Navidrome / Jellyfin ๅฎขๆˆท็ซฏ" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "ๅŠŸ่ƒฝ" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "้€š่ฟ‡ๆญŒๆ›ฒใ€่‰บๆœฏๅฎถใ€ไธ“่พ‘ใ€ๆ”ถ้Ÿณๆœบๅ’Œๆ’ญๆ”พๅˆ—่กจ่ฟ›่กŒๆŽข็ดข" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "ๆ’ญๆ”พๅˆ—่กจ็ฎก็†" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "ๅ…ผๅฎน Jellyfinใ€OpenSubsonic ๅ’Œๆœฌๅœฐๆ–‡ไปถ" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "้Ÿณ้ข‘ๅ‡่กกๅ™จๅ’Œ้Ÿณ้ข‘ๅฏ่ง†ๅŒ–ๅ™จ" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Mpris ้›†ๆˆ" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "้›†ๆˆ Navidrome ๅฎžๅˆป็ฎก็†" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "่‡ชๅŠจ่Žทๅ–่ฏๆญŒ" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "ไธ‹่ฝฝๅ’Œ็ฆป็บฟๆจกๅผ" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "ๅพˆ้…ท็š„็•Œ้ข" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "ไธป้กต" #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "ๆญŒๆ›ฒ้˜Ÿๅˆ—" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:170 #: src/ui/window.blp:222 msgid "Lyrics" msgstr "ๆญŒ่ฏ" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "ๆญŒๆ›ฒ้˜Ÿๅˆ—" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "ไธ“่พ‘้กต้ข" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "้Ÿณไน" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:211 src/ui/song/row.blp:69 msgid "External File" msgstr "ๅค–้ƒจๆ–‡ไปถ" #: src/actions.py:212 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "" "Dieser Titel wurde aus einer externen Datei geladen, weshalb weniger " "Features verglichen mit einem Titel aus der Bibliothek verfรผgbar sind." #: src/actions.py:214 src/ui/song/queue.blp:40 msgid "Close" msgstr "ๅ…ณ้—ญ" #: src/actions.py:232 msgid "Navidrome server deleted successfully" msgstr "Navidrome ็š„ๆœๅŠกๅ™จๅˆ ้™คๆˆๅŠŸ" #: src/actions.py:241 msgid "Delete Navidrome Server" msgstr "ๅˆ ้™ค Navidrome ็š„ๆœๅŠกๅ™จ" #: src/actions.py:242 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "ๆ‚จ็กฎๅฎš่ฆๅˆ ้™ค้›†ๆˆ็š„ Navidrome ๆœๅŠกๅ™จๅ—๏ผŸ" #: src/actions.py:244 src/actions.py:355 src/actions.py:389 src/actions.py:674 #: src/actions.py:790 src/preferences.py:322 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "ๅ–ๆถˆ" #: src/actions.py:245 msgid "Keep Data" msgstr "ไฟ็•™ๆ•ฐๆฎ" #: src/actions.py:246 msgid "Delete Everything" msgstr "ๅˆ ๆމๅ…จ้ƒจๅ†…ๅฎน" #: src/actions.py:301 msgid "No Name" msgstr "ๆ— ๅ" #: src/actions.py:320 msgid "Radio updated successfully" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๆ›ดๆ–ฐๆˆๅŠŸ" #: src/actions.py:320 msgid "Radio added successfully" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๆทปๅŠ ๆˆๅŠŸ" #: src/actions.py:331 msgid "Error updating radio" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๆ›ดๆ–ฐๅ‡บ้”™" #: src/actions.py:331 msgid "Error adding radio" msgstr "ๆทปๅŠ ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๅ‡บ้”™" #: src/actions.py:340 src/actions.py:666 msgid "Name" msgstr "ๅๅญ—" #: src/actions.py:345 msgid "Stream Url" msgstr "ๆตๅช’ไฝ“ URL" #: src/actions.py:352 msgid "Update Radio Station" msgstr "ๆ›ดๆ–ฐๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/actions.py:352 msgid "Add Radio Station" msgstr "ๆทปๅŠ ๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/actions.py:356 src/preferences.py:323 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "ไฟๅญ˜" #: src/actions.py:372 msgid "Radio deleted successfully" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๅˆ ๆމๆˆๅŠŸ" #: src/actions.py:380 msgid "Error deleting radio" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐๅˆ ๆމๅ‡บ้”™" #: src/actions.py:386 msgid "Delete Radio Station" msgstr "ๅˆ ๆމๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/actions.py:387 src/actions.py:788 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "็กฎๅฎš่ฆๅˆ ๆމ'{}'ๅ—๏ผŸ" #: src/actions.py:390 src/actions.py:791 src/constants.py:343 #: src/constants.py:396 msgid "Delete" msgstr "ๅˆ ๆމ" #: src/actions.py:420 src/actions.py:447 src/actions.py:452 src/actions.py:541 #: src/actions.py:600 msgid "Playing Next" msgstr "ไธ‹ไธ€้ฆ–" #: src/actions.py:430 src/actions.py:463 src/actions.py:468 src/actions.py:556 #: src/actions.py:615 msgid "Playing Later" msgstr "็จๅŽๆ’ญๆ”พ" #: src/actions.py:447 src/actions.py:463 src/actions.py:905 src/actions.py:1040 #: src/widgets/playlist/button.py:50 src/widgets/playlist/button.py:52 #: src/widgets/playlist/dialog.py:31 src/widgets/playlist/page.py:118 #: src/widgets/playlist/page.py:120 src/widgets/playlist/row.py:49 #: src/widgets/playlist/row.py:51 src/widgets/playlist/selector_row.py:38 #: src/widgets/playlist/selector_row.py:40 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" msgstr[1] "" #: src/actions.py:497 msgid "Lyrics Saved" msgstr "ๆญŒ่ฏไฟๅญ˜ไบ†" #: src/actions.py:647 msgid "Playlist updated successfully" msgstr "ๅŠŸๆ’ญๆ”พๅˆ—่กจๆ›ดๆ–ฐๆˆๅŠŸ" #: src/actions.py:647 msgid "Playlist created successfully" msgstr "ๆ’ญๆ”พๅˆ—่กจๅˆ›ๅปบๆˆๅŠŸ" #: src/actions.py:657 msgid "Error updating playlist" msgstr "ๆ’ญๆ”พๅˆ—่กจๆ›ดๆ–ฐๅ‡บ้”™" #: src/actions.py:657 msgid "Error creating playlist" msgstr "ๆ’ญๆ”พๅˆ—่กจๆทปๅŠ ๅ‡บ้”™" #: src/actions.py:671 msgid "Update Playlist" msgstr "่ทŸๆ–ฐๆ’ญๆ”พๅˆ—่กจ" #: src/actions.py:671 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "ๆทปๅŠ ๆ’ญๆ”พๅˆ—่กจ" #: src/actions.py:675 msgid "Update" msgstr "ๅˆทๆ–ฐ" #: src/actions.py:675 msgid "Create" msgstr "ๆทปๅŠ " #: src/actions.py:695 src/actions.py:700 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "ๅทฒ็งป้™ค {} ้ฆ–ๆญŒๆ›ฒ" msgstr[1] "ๅทฒ็งป้™ค {} ้ฆ–ๆญŒๆ›ฒ" #: src/actions.py:732 src/actions.py:734 src/actions.py:754 src/actions.py:756 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "ๅทฒๆทปๅŠ  {} ้ฆ–ๆญŒๆ›ฒ" msgstr[1] "ๅทฒๆทปๅŠ  {} ้ฆ–ๆญŒๆ›ฒ" #: src/actions.py:761 src/actions.py:877 src/actions.py:910 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "ๅทฒ่ทณ่ฟ‡ {} ้ฆ–ๆญŒๆ›ฒ" msgstr[1] "ๅทฒ่ทณ่ฟ‡ {} ้ฆ–ๆญŒๆ›ฒ" #: src/actions.py:775 msgid "Playlist Deleted" msgstr "ๆ’ญๆ”พๅˆ—่กจๅˆ ๆމไบ†" #: src/actions.py:787 msgid "Delete Playlist" msgstr "ๅˆ ๆމๆ’ญๆ”พๅˆ—่กจ" #: src/actions.py:836 msgid "No songs found" msgstr "ๆœชๆ‰พๅˆฐๆญŒๆ›ฒๆˆ็ซ‹" #: src/actions.py:879 src/actions.py:906 src/actions.py:943 src/actions.py:979 msgid "Download Started" msgstr "ไธ‹่ฝฝๅทฒๅผ€ๅง‹" #: src/actions.py:886 src/actions.py:917 src/actions.py:924 src/actions.py:959 #: src/actions.py:995 msgid "Already Downloaded" msgstr "ๅทฒไธ‹่ฝฝ" #: src/actions.py:911 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} ้ฆ–ๆญŒๆ›ฒ ({})" msgstr[1] "{} ้ฆ–ๆญŒๆ›ฒ ({})" #: src/actions.py:923 msgid "Download Skipped" msgstr "ไธ‹่ฝฝๅทฒ่ทณ่ฟ‡" #: src/actions.py:947 src/actions.py:983 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "ไธ‹่ฝฝๅทฒๅผ€ๅง‹๏ผˆๅทฒ่ทณ่ฟ‡ {} ้ฆ–ๆญŒๆ›ฒ๏ผ‰" msgstr[1] "ไธ‹่ฝฝๅทฒๅผ€ๅง‹๏ผˆๅทฒ่ทณ่ฟ‡ {} ้ฆ–ๆญŒๆ›ฒ๏ผ‰" #: src/actions.py:1022 src/actions.py:1041 msgid "Deleted" msgstr "ๅทฒๅˆ ้™ค" #: src/constants.py:159 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "่ฟž็ปญ" #: src/constants.py:163 msgid "Repeat All" msgstr "ๅ…จ้ƒจ้‡ๅค" #: src/constants.py:167 msgid "Repeat One" msgstr "้‡ๅคไธ€ๆฌก" #: src/constants.py:172 #, python-brace-format msgid "Low ({})" msgstr "ไฝŽ ๏ผˆ{}๏ผ‰" #: src/constants.py:173 #, python-brace-format msgid "Medium ({})" msgstr "ไธญ ๏ผˆ{}๏ผ‰" #: src/constants.py:174 #, python-brace-format msgid "High ({})" msgstr "้ซ˜ ๏ผˆ{}๏ผ‰" #: src/constants.py:175 #, python-brace-format msgid "Ultra ({})" msgstr "ๆž้ซ˜ ๏ผˆ{}๏ผ‰" #: src/constants.py:176 msgid "Original File" msgstr "ๅŽŸไปถ" #. Item #: src/constants.py:183 src/ui/pages/home.blp:5 msgid "Home" msgstr "้ฆ–้กต" #. Item #: src/constants.py:188 src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:223 src/widgets/pages/home.py:62 msgid "Artists" msgstr "่‰บๆœฏๅฎถ" #. Item #. Section #: src/constants.py:193 src/constants.py:255 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:231 #: src/widgets/pages/home.py:74 msgid "Playlists" msgstr "ๆ’ญๆ”พๅˆ—่กจ" #. Section #: src/constants.py:200 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:215 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:50 msgid "Albums" msgstr "ไธ“่พ‘" #. Item #: src/constants.py:203 src/constants.py:238 src/window.py:135 msgid "All" msgstr "ๅ…จ้ƒจ" #. Item #: src/constants.py:208 msgid "Random" msgstr "้šๆœบ" #. Item #: src/constants.py:213 src/constants.py:243 msgid "Favorites" msgstr "ๆ”ถ่—" #. Item #: src/constants.py:218 msgid "Recently Added" msgstr "ๆœ€่ฟ‘ๆทปๅŠ " #. Item #: src/constants.py:223 msgid "Recently Played" msgstr "ๆœ€่ฟ‘ๆ’ญๆ”พ" #. Item #: src/constants.py:228 msgid "Most Played" msgstr "ๆ’ญๆ”พๆฌกๆ•ฐๆœ€ๅคš" #. Section #: src/constants.py:235 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:207 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "ๆญŒๆ›ฒ" #. Item #: src/constants.py:248 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "ๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/constants.py:262 src/constants.py:313 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "ๆ’ญๆ”พ" #: src/constants.py:267 src/constants.py:300 src/constants.py:318 msgid "Shuffle" msgstr "้šๆœบ" #: src/constants.py:272 src/constants.py:323 src/constants.py:356 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "ๆ’ญๆ”พไธ‹้ฆ–" #: src/constants.py:277 src/constants.py:328 src/constants.py:361 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "ไธ‹ๆฌกๆ’ญๆ”พ" #: src/constants.py:282 src/constants.py:376 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "ๆทปๅŠ ๅˆฐๆ’ญๆ”พๅˆ—่กจ" #: src/constants.py:287 src/constants.py:333 src/constants.py:381 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "ไธ‹่ฝฝ" #: src/constants.py:292 src/constants.py:391 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "ๆ˜พ็คบ่‰บๆœฏๅฎถ" #: src/constants.py:305 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:108 msgid "Radio" msgstr "ๆ”ถ้Ÿณๆœบ" #: src/constants.py:338 src/constants.py:366 msgid "Edit" msgstr "็ผ–่พ‘" #: src/constants.py:352 msgid "Select" msgstr "้€‰ๆ‹ฉ" #: src/constants.py:371 msgid "Edit Lyrics" msgstr "็ผ–ๅˆถๆญŒ่ฏ" #: src/constants.py:386 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "ๆ˜พ็คบไธ“่พ‘" #: src/constants.py:402 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "ๅˆ ๆމ" #: src/constants.py:407 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "ๅˆ ้™คไธ‹่ฝฝ" #: src/constants.py:416 msgid "Visit Webpage" msgstr "่ฎฟ้—ฎ็ฝ‘้กต" #: src/constants.py:422 msgid "Update Server" msgstr "ๆ›ดๆ–ฐๆœๅŠกๅ™จ" #: src/constants.py:427 msgid "Delete Server" msgstr "ๅˆ ๆމๆœๅŠกๅ™จ" #: src/integrations/jellyfin.py:15 msgid "Connect to a Jellyfin server." msgstr "่ฟžๆŽฅๅˆฐ Jellyfin ๆœๅŠกๅ™จใ€‚ " #: src/integrations/jellyfin.py:19 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:20 msgid "Use an existing Jellyfin instance" msgstr "ไฝฟ็”จ็Žฐๅœจๆœ‰็š„ Jellyfin ๅฎžไพ‹" #: src/integrations/local.py:17 src/integrations/local.py:23 #: src/integrations/local.py:509 msgid "Local Files" msgstr "ๆœฌๅœฐๆ–‡ไปถ" #: src/integrations/local.py:18 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "" "่ฎฉ Nocturne ็›ดๆŽฅๅŠ ่ฝฝๆ‚จ็š„ๆœฌๅœฐๆ–‡ไปถ๏ผŒๅฏนไบŽๅคงๅž‹ๅช’ไฝ“ๅบ“ใ€‚ " "ๅปบ่ฎฎไฝฟ็”จไธ“็”จๆœๅŠกๅ™จใ€‚" #: src/integrations/local.py:20 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "็ปง็ปญ" #: src/integrations/local.py:24 msgid "Limited functionality" msgstr "ๆœ‰้™็š„ๅŠŸ่ƒฝ" #: src/integrations/local.py:36 msgid "Loading Songs" msgstr "ๆญฃๅœจๅŠ ่ฝฝๆญŒๆ›ฒ" #: src/integrations/local.py:215 msgid "No Album" msgstr "ๆ— ไธ“่พ‘" #: src/integrations/local.py:525 src/integrations/local.py:534 msgid "Offline Mode" msgstr "็ฆป็บฟๆจกๅผ" #: src/integrations/local.py:526 msgid "Access your downloads" msgstr "่ฎฟ้—ฎๆ‚จ็š„ไธ‹่ฝฝ" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "่ฟžๆŽฅๅˆฐ OpenSubsonic ๆœๅŠกๅ™จ๏ผˆๅฆ‚ Navidrome๏ผ‰ใ€‚" #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "ๅค–้ƒจ็š„ๆœๅŠกๅ™จ" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "ไฝฟ็”จ็Žฐๆœ‰็š„ OpenSubsonic / Navidrome ๅฎžไพ‹" #: src/integrations/navidrome.py:550 src/integrations/navidrome.py:559 msgid "Managed Server" msgstr "็ฎก็†ไบ†ๆœๅŠกๅ™จ" #: src/integrations/navidrome.py:551 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "่ฟžๆŽฅๅˆฐ็”ฑ Nocturne ็›ดๆŽฅ็ฎก็†็š„ Navidrome ๅฎžไพ‹ใ€‚" #: src/integrations/navidrome.py:554 msgid "Manage Server" msgstr "็ฎก็†ๆœๅŠกๅ™จ" #: src/integrations/navidrome.py:560 msgid "Create and use a Navidrome instance" msgstr "ๅˆ›ๅปบๅนถไฝฟ็”จ Navidrome ๅฎžไพ‹" #: src/main.py:69 msgid "Music is Playing" msgstr "้Ÿณไนๆญฃๅœจๆ’ญๆ”พ" #: src/main.py:116 msgid "Login Failed" msgstr "็™ปๅฝ•ๅคฑ่ดฅ" #: src/preferences.py:311 msgid "Settings Page" msgstr "่ฎพ็ฝฎ้กต้ข" #: src/preferences.py:314 msgid "User Token" msgstr "็”จๆˆทไปค็‰Œ" #: src/preferences.py:318 msgid "Link ListenBrainz" msgstr "ๅ…ณ่” ListenBrainz" #: src/preferences.py:319 msgid "Connect your ListenBrainz account with a user token" msgstr "ไฝฟ็”จ็”จๆˆทไปค็‰Œๅ…ณ่”ๆ‚จ็š„ ListenBrainz ่ดฆๆˆท" #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "ๆ’ญๆ”พไธ“่พ‘" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "ๅˆ‡ๆขๆ˜Ÿๆ ‡" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "ไธ“่พ‘ๅฐ้ข" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "่พ“ๅ…ฅไธ“่พ‘้กต" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "่พ“ๅ…ฅ่‰บๆœฏๅฎถ้กต" #: src/ui/album/page.blp:5 src/widgets/album/page.py:131 msgid "Album" msgstr "ไธ“่พ‘" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "1 ๆ˜Ÿ" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "2 ๆ˜Ÿ" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "3 ๆ˜Ÿ" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "4 ๆ˜Ÿ" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "5 ๆ˜Ÿ" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "้€‰ๆ‹ฉ" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "่‰บๆœฏๅฎถ" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "ๆ‰ฉๅฑ•ไผ " #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "ๅˆ—่กจ" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "ๅทฒไธ‹่ฝฝ" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "ไปŽ้˜Ÿๅˆ—ไธญ็งป้™ค" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "ไธ‹่ฝฝ้˜Ÿๅˆ—" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "ๅทฒไธ‹่ฝฝ็š„ๆญŒๆ›ฒๅฏๅœจ็ฆป็บฟๆจกๅผไธ‹ไฝฟ็”จ" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "ๆธ…้™คๅทฒๅฎŒๆˆไธ‹่ฝฝ" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "ๆญŒ่ฏ็ผ–ๅˆถๅ™จ" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "ๆš‚ๅœ" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "็Žฐๅœจ็š„ๆญŒๆ›ฒ็š„่ฟ›ๅบฆๆก" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "ๅฐ†ไธ‹ไธ€่กŒๆ—ถ้—ดๆˆณ่ฎพ็ฝฎไธบโ€œ็Žฐๅœจโ€" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "ๆทปๅŠ ๆ—ถ้—ดๆˆณ" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "ๆฒกๆœ‰ๆญŒ่ฏ" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "ๆœชๆ‰พๅˆฐ่ฟ™้ฆ–ๆญŒๆ›ฒ็š„ๆญŒ่ฏ๏ผŒๅผ€ๅง‹ๆทปๅŠ ๆ—ถ้—ดๆˆณ" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "ๆ—ถ้—ดๆˆณ" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "ๅŽปๆ—ถ้—ดๆˆณ" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "่ฎพ็ฝฎๅฝ“ๅ‰ๆ—ถ้—ดๆˆณ" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "ๅˆ ๆމ้™ค่กŒ" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "ๆ ผๆก†" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "ๆœๅฏปไธ“่พ‘" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "ๅทฒๅˆฐ่พพๅˆ—่กจๆœซๅฐพ" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "ๆœชๆ‰พๅˆฐไธ“่พ‘" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "ๆœๅฏป่‰บๆœฏๅฎถ" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "ๆœชๆ‰พๅˆฐ่‰บๆœฏๅฎถ" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "ๆœชๆ‰พๅˆฐๅ…ƒ็ด " #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "็™ปๅฝ•" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "ๆœๅŠกๅ™จ็Šถๆ€" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "ๆœช่ฟ่กŒ" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "้‡ๅฏๅฎžไพ‹" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "็ฎก็† Navidrome ๆœๅŠกๅ™จ" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "่ทŸๅคš้€‰้กน" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "ไฟกไปปๆœๅŠกๅ™จ็š„ๆ•ฐๅญ—่ฏไนฆ" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "ๅบ“็›ฎๅฝ•" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "็”จๆˆทๅ" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "ๅฏ†็ " #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "ไฝฟ็”จๅฟซ้€Ÿ่ฟžๆŽฅ" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "ๆœๅฏปๆ’ญๆ”พๅˆ—่กจ" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "ๆœชๆ‰พๅˆฐๆ’ญๆ”พๅˆ—่กจ" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "ๆœๅฏปๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "ๆทปๅŠ ๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "ๆœชๆ‰พๅˆฐๆ”ถ้Ÿณๆœบ็ซ™ๅฐ" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "่ฎพ็ฝฎ" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "ไธ‹่ฝฝ Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne ไผš่‡ชๅŠจไธ‹่ฝฝ Navidrome" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "ไธ‹่ฝฝ (ๅคง็บฆ~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "ไธ‹่ฝฝไธญ" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "ๅ‘็”Ÿ้”™่ฏฏ" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "่ฏท็จๅŽๅ†่ฏ•" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "ไธ‹่ฝฝๆˆๅŠŸ" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "ๅœจ็ปง็ปญๆ“ไฝœไน‹ๅ‰๏ผŒ่ฏท็กฎไฟๆ‚จๅทฒๅˆ›ๅปบ็ฎก็†ๅ‘˜ๅธๆˆท" "ๅนถ็กฎไฟๆ‚จ็š„ๆ‰€ๆœ‰ๆ›ฒ็›ฎ้ƒฝไฝไบŽๆ‚จ็š„้Ÿณไน็›ฎ" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "ๅฎžไพ‹็ฝ‘้กต" #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "ๆœๅฏปๆญŒๆ›ฒ" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "ๆœชๆ‰พๅˆฐๆญŒๆ›ฒ" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "ๆ”ถ่—็š„ๆญŒๆ›ฒ" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "ๆœ็ดขๆ”ถ่—็š„ๆญŒๆ›ฒ" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "ๆฌข่ฟŽ" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "ๆฌข่ฟŽไฝฟ็”จ Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "ๆ’ญๆ”พไธญ" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "ๆญŒๆ›ฒๅฐ้ข" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "่ฎฟ้—ฎๅฝ“ๅ‰้Ÿณๆœบ็”ตๅฐไธป้กต" #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "ไธŠไธช" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "ไธ‹ไธช" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "ๆ˜Ÿ" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "ๆ˜พ็คบไพง่พนๆ " #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "้ข„่ฎพ" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "ๅนณ" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "ไบบๅฃฐ" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "ๆ—ถๅฐš็ˆตๅฃซ" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "ๆ‘‡ๆปšไน" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "็ปๅ…ธ" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "ๅŽŸๅฃฐ" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "ๅฝ“ๅ‰ๆญŒๆ›ฒ่ฟ›ๅบฆๆŒ‡็คบๅ™จ" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "ๅฝ“ๅ‰ๆญŒๆ›ฒๅฐ้ข" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "ๅ›žๅŽป" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "ๆœชๆ‰พๅˆฐๆญŒ่ฏ" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "ๆœชไธ‹่ฝฝ" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "่ฟ™ไธชๆญŒๆ›ฒ็š„ๆญŒ่ฏๆฒกๆœ‰ไธ‹่ฝฝ่ฟ‡๏ผŒไฝ†ๆœ‰ๅฏ่ƒฝ ๅฏไธ‹่ฝฝ" #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "ๅŠ ่ฝฝๆญŒ่ฏๆ–‡ไปถ" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "ๅผนๅ‡บ็ช—ๅฃ" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:154 msgid "Player" msgstr "ๆ’ญๆ”พๅ™จ" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:16 #: src/ui/window.blp:255 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "ๅˆ‡ๆขๅ…จๅฑ" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:194 msgid "Song Details" msgstr "ๆญŒๆ›ฒ่ฏฆๆƒ…" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:160 #: src/ui/window.blp:216 msgid "Queue" msgstr "้˜Ÿๅˆ—" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:176 #: src/ui/window.blp:228 msgid "Equalizer" msgstr "ๅ‡่กกๅ™จ" #: src/ui/playing/queue_page.blp:20 msgid "Generating next queue" msgstr "ๆญฃๅœจ็”Ÿๆˆไธ‹ไธ€ไธช้˜Ÿๅˆ—" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "่‡ชๅŠจๆ’ญๆ”พ" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "ๅฝ“ๅ‰้˜Ÿๅˆ—็ป“ๆŸๆ—ถ๏ผŒ็”Ÿๆˆไธ€ไธชๆ–ฐ้˜Ÿๅˆ—ใ€‚" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "้Ÿณ้‡" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "ๆ‰ฌๅฃฐๅ™จ้™้Ÿณ" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "ๆ‰ฌๅฃฐๅ™จ้Ÿณ้‡ๅ…จๅผ€" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "ๆ’ญๆ”พๅˆ—่กจ" #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "ๆ˜พ็คบๆ’ญๆ”พๅˆ—่กจ" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "ๆ’ญๆ”พๅˆ—่กจ" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "่พ“ๅ…ฅๆ’ญๆ”พๅˆ—่กจ้กต้ข" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "ๆ’ญๆ”พๅˆ—่กจ" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "ๆœๅฏป (ๆˆ–่€…ๅˆ›ๆ–ฐๆ’ญๆ”พๅˆ—่กจ)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "ๆทปๅŠ ้Ÿณไนๅˆฐๆ’ญๆ”พๅˆ—่กจ" #: src/ui/playlist/dialog.blp:34 msgid "Create new playlist and add songs" msgstr "ๅˆ›ๆ–ฐๆ’ญๆ”พๅˆ—่กจ๏ผŒๅŠ ๆญŒๆ›ฒ" #: src/ui/preferences.blp:7 msgid "General" msgstr "้€šๅธธ" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "่กŒไธบ" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "ๅฏๅŠจๆ—ถๆขๅค้˜Ÿๅˆ—" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "ๅ…ณ้—ญๆ—ถ้š่—" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "ๆจกๆ‹Ÿ้€่ฏๆญŒ่ฏ" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "ๅฝ“ๅชๆœ‰้€่กŒๆญŒ่ฏๅฏ็”จๆ—ถไฝฟ็”จ" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "็™ปๅฝ•ๅŽ้ป˜่ฎค้กต้ข" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "ๆœ€ๅคงๆฏ”็‰น็އ" #: src/ui/preferences.blp:33 msgid "Session" msgstr "ไผš่ฏ" #: src/ui/preferences.blp:36 msgid "ListenBrainz Link" msgstr "ๅ…ณ่” ListenBrainz " #: src/ui/preferences.blp:37 msgid "Keep track of your playback history" msgstr "่ฟฝ่ธชๆ‚จ็š„ๆ’ญๆ”พๅކๅฒ่ฎฐๅฝ•" #: src/ui/preferences.blp:43 src/ui/preferences.blp:44 msgid "Link" msgstr "ๅ…ณ่”" #: src/ui/preferences.blp:52 src/ui/preferences.blp:53 msgid "Unlink" msgstr "ๅ–ๆถˆๅ…ณ่”" #: src/ui/preferences.blp:65 msgid "User" msgstr "็”จๆˆท" #: src/ui/preferences.blp:85 msgid "Logout" msgstr "็™ปๅ‡บ" #: src/ui/preferences.blp:95 msgid "Customization" msgstr "่‡ชๅฎšไน‰" #: src/ui/preferences.blp:98 msgid "Interface" msgstr "็•Œ้ข" #: src/ui/preferences.blp:100 msgid "Show Context Button in Rows" msgstr "ๅœจ่กŒไธญๆ˜พ็คบไธŠไธ‹ๆ–‡ๆŒ‰้’ฎ" #: src/ui/preferences.blp:101 msgid "The menu can also be shown by right clicking / long pressing" msgstr "ไนŸๅฏไปฅ้€š่ฟ‡ๅณ้”ฎๅ•ๅ‡ป/้•ฟๆŒ‰ๆฅๆ˜พ็คบ่œๅ•ใ€‚" #: src/ui/preferences.blp:104 msgid "Show Labels in Buttons of Pages" msgstr "ๅœจ้กต้ขๆŒ‰้’ฎไธญๆ˜พ็คบๆ ‡็ญพ" #: src/ui/preferences.blp:107 msgid "Show Playlists in Sidebar" msgstr "ๅœจไพง่พนๆ ไธญๆ˜พ็คบๆ’ญๆ”พๅˆ—่กจ" #: src/ui/preferences.blp:110 msgid "Show Progressbar in Player Footer" msgstr "ๅœจๆ’ญๆ”พๅ™จๅบ•้ƒจๆ˜พ็คบ่ฟ›ๅบฆๆก" #: src/ui/preferences.blp:113 msgid "Make Background Translucent in Player" msgstr "ไฝฟๆ’ญๆ”พๅ™จ่ƒŒๆ™ฏๅŠ้€ๆ˜Ž" #: src/ui/preferences.blp:116 msgid "Use Sidebar Player When Possible" msgstr "ๅฏ็”จๆ—ถไฝฟ็”จไพง่พนๆ ๆ’ญๆ”พๅ™จ" #: src/ui/preferences.blp:117 msgid "Sidebar player is only available when the window is wide enough" msgstr "ไป…ๅฝ“็ช—ๅฃ่ถณๅคŸๅฎฝๆ—ถไพง่พนๆ ๆ’ญๆ”พๅ™จๆ‰ๅฏ็”จ" #: src/ui/preferences.blp:122 msgid "Dynamic Background" msgstr "ๅŠจๆ€่ƒŒๆ™ฏ" #: src/ui/preferences.blp:123 msgid "Background generated based on the album art of the current song" msgstr "ๆ นๆฎๅฝ“ๅ‰ๆญŒๆ›ฒ็š„ไธ“่พ‘ๅฐ้ข็”Ÿๆˆ็š„่ƒŒๆ™ฏ" #: src/ui/preferences.blp:126 msgid "Use in Main Window" msgstr "ๅœจไธป็ช—ๅฃไธญไฝฟ็”จ" #: src/ui/preferences.blp:127 msgid "Experimental" msgstr "ๅฎž้ชŒๆ€ง" #: src/ui/preferences.blp:133 src/ui/preferences.blp:134 #: src/ui/preferences.blp:157 src/ui/preferences.blp:158 #: src/ui/preferences.blp:181 src/ui/preferences.blp:182 msgid "Off" msgstr "ๅ…ณ้—ญ" #: src/ui/preferences.blp:138 src/ui/preferences.blp:139 #: src/ui/preferences.blp:162 src/ui/preferences.blp:163 #: src/ui/preferences.blp:186 src/ui/preferences.blp:187 msgid "Gradient" msgstr "ๆธๅ˜่‰ฒ" #: src/ui/preferences.blp:143 src/ui/preferences.blp:144 #: src/ui/preferences.blp:167 src/ui/preferences.blp:168 #: src/ui/preferences.blp:191 src/ui/preferences.blp:192 msgid "Blur" msgstr "ๆจก็ณŠ" #: src/ui/preferences.blp:151 msgid "Use in Player" msgstr "ๅœจๆ’ญๆ”พๅ™จไธญไฝฟ็”จ" #: src/ui/preferences.blp:175 msgid "Use in Pop-out Window" msgstr "ๅœจๅผนๅ‡บ็ช—ๅฃไธญไฝฟ็”จ" #: src/ui/preferences.blp:199 msgid "Use as Accent Color" msgstr "็”จไฝœๅผบ่ฐƒ่‰ฒ" #: src/ui/preferences.blp:204 msgid "Homepage" msgstr "้ฆ–้กต" #: src/ui/preferences.blp:205 msgid "Reload homepage to see changes" msgstr "้‡ๆ–ฐๅŠ ่ฝฝ้ฆ–้กตไปฅๆŸฅ็œ‹ๆ›ดๆ”น" #: src/ui/preferences.blp:241 msgid "Visualizer" msgstr "ๅฏ่ง†ๅŒ–ๅทฅๅ…ท" #: src/ui/preferences.blp:245 src/ui/window.blp:245 msgid "Preferences" msgstr "่ฎพ็ฝฎ" #: src/ui/preferences.blp:247 msgid "Show Visualizer in Player" msgstr "ๅœจๆ’ญๆ”พๅ™จไธญๆ˜พ็คบๅฏ่ง†ๅŒ–ๆ•ˆๆžœ" #: src/ui/preferences.blp:248 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "ๅœจๆ’ญๆ”พๅ™จ็š„ไธ“่พ‘ๅฐ้ขไธŠๆ–นๆ˜พ็คบ้Ÿณ้ข‘้ข‘่ฐฑๅฏ่ง†ๅŒ–ๆ•ˆๆžœ" #: src/ui/preferences.blp:253 msgid "Appearance" msgstr "ๅค–่ง‚" #: src/ui/preferences.blp:257 msgid "Number of Bars" msgstr "ๆกๆ•ฐ" #: src/ui/preferences.blp:268 msgid "Visualizer Type" msgstr "ๅฏ่ง†ๅŒ–็ฑปๅž‹" #: src/ui/preferences.blp:274 src/ui/preferences.blp:275 msgid "Wave" msgstr "ๆณขๆตช" #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Bars" msgstr "ๆกๅฝข" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Particles" msgstr "็ฒ’ๅญ" #: src/ui/preferences.blp:292 msgid "Fill Mode" msgstr "ๅกซๅ……ๆจกๅผ" #: src/ui/preferences.blp:298 src/ui/preferences.blp:299 msgid "Solid" msgstr "ๅฎžๅฟƒ" #: src/ui/preferences.blp:303 src/ui/preferences.blp:304 msgid "Translucent" msgstr "ๅŠ้€ๆ˜Ž" #: src/ui/preferences.blp:308 src/ui/preferences.blp:309 msgid "Border" msgstr "่พนๆก†" #: src/ui/preferences.blp:317 msgid "Color" msgstr "้ขœ่‰ฒ" #: src/ui/preferences.blp:321 msgid "Use Dynamic Color" msgstr "ไฝฟ็”จๅŠจๆ€้ขœ่‰ฒ" #: src/ui/preferences.blp:324 msgid "Invert Color" msgstr "ๅ่ฝฌ้ขœ่‰ฒ" #: src/ui/preferences.blp:328 msgid "Manual Color" msgstr "ๆ‰‹ๅŠจ้ขœ่‰ฒ" #: src/ui/preferences.blp:334 msgid "Visualizer Color" msgstr "ๅฏ่ง†ๅŒ–้ขœ่‰ฒ" #: src/ui/shortcuts-dialog.blp:6 msgid "Shortcuts" msgstr "ๅฟซๆท้”ฎ" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "ๆ˜พ็คบๅฟซๆท้”ฎ" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "ๆ˜พ็คบ่ฎพ็ฝฎ" #: src/ui/shortcuts-dialog.blp:20 src/ui/window.blp:250 msgid "Open Pop-Out Window" msgstr "ๆ‰“ๅผ€ๅผนๅ‡บ็ช—ๅฃ" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "้€€ๅ‡บ" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "ๆ‹–ๅŠจๅ›พๆ ‡" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "้€‰ๆ‹ฉๆก†" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "ๆญŒๆ›ฒๅฐ้ข" #: src/ui/window.blp:74 msgid "Play Random Queue" msgstr "้šๆœบๆ’ญๆ”พ้˜Ÿๅˆ—" #: src/ui/window.blp:87 msgid "Menu" msgstr "่œๅ•" #: src/ui/window.blp:115 msgid "Random Albums" msgstr "้šๆœบไธ“่พ‘" #: src/ui/window.blp:119 msgid "Favorite Albums" msgstr "ๆ”ถ่—็š„ไธ“่พ‘" #: src/ui/window.blp:123 msgid "Newest Albums" msgstr "ๆœ€ๆ–ฐไธ“่พ‘" #: src/ui/window.blp:127 msgid "Recent Albums" msgstr "ๆœ€่ฟ‘ไธ“่พ‘" #: src/ui/window.blp:131 msgid "Frequent Albums" msgstr "ๅธธๅฌไธ“่พ‘" #: src/ui/window.blp:260 msgid "Keyboard Shortcuts" msgstr "้”ฎ็›˜ๅฟซๆท้”ฎ" #: src/ui/window.blp:265 msgid "About Nocturne" msgstr "ๅ…ณไบŽ Nocturne" #: src/widgets/album/button.py:63 src/widgets/album/page.py:147 #: src/widgets/artist/page.py:127 src/widgets/playing/control_page.py:217 #: src/widgets/song/row.py:162 msgid "Favorite" msgstr "ๆ”ถ่—" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:222 #: src/widgets/song/row.py:167 msgid "Not Favorite" msgstr "ไธๆ”ถ่—" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "็ฌฌ {} ็ขŸ" #: src/widgets/artist/button.py:43 src/widgets/artist/row.py:48 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "{} ๅผ ไธ“่พ‘" msgstr[1] "{} ๅผ ไธ“่พ‘" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "็ƒญ้—จๆญŒๆ›ฒ" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "็›ธๅ…ณ่‰บๆœฏๅฎถ" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "ไฝœ่€…" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "ๆ— ๆ—ถ้—ดๆˆณ" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "่ฟ่กŒไธญ" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "้ขๅค–่œๅ•" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "ๆœฌๅœฐ้Ÿณไนๅบ“" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "้‡ๅฏไบ†" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "ๅ‡บ้”™" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "ๅฟซ้€Ÿ่ฟžๆŽฅ" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "่Žทๅ–ไปฃ็ ๆ—ถๅ‡บ้”™" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "ๅฟซ้€Ÿ่ฟžๆŽฅ้กต้ข" #: src/widgets/pages/setup.py:62 msgid "Installing" msgstr "ๆญฃๅœจๅฎ‰่ฃ…" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "้›†ๆˆ" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC ๆ–‡ไปถ" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "ๆ–ฐๅปบๆ’ญๆ”พๅˆ—่กจ" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add songs to '{}'" msgstr "ๅœจ '{}' ๆทปๅŠ ๆญŒๆ›ฒๆ ‡้ข˜" #: src/widgets/song/row.py:146 src/widgets/song/row.py:151 msgid "Multiple Artists" msgstr "ๅคšไฝ่‰บๆœฏๅฎถ" #, python-brace-format #~ msgid "{} Songs" #~ msgstr "{} ๆญŒๆ›ฒ" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "{} ๆญŒๆ›ฒๅˆ ๆމไบ†" #~ msgid "Song Removed" #~ msgstr "ๆญŒๆ›ฒๅˆ ๆމไบ†" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "{} ไธชๆญŒๆ›ฒๆทปๅŠ ไบ†" #~ msgid "1 Song Added" #~ msgstr "1 ไธชๆญŒๆ›ฒๆทปๅŠ ไบ†" #~ msgid "1 Song Skipped" #~ msgstr "1 ไธชๆญŒๆ›ฒ่ทณ่ฟ‡ไบ†" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "{} ไธชๆญŒๆ›ฒ่ทณ่ฟ‡ไบ†" #~ msgid "Starred" #~ msgstr "ๆ˜Ÿๆ ‡ไบ†" #~ msgid "Jellyfin (Experimental)" #~ msgstr "Jellyfin (ๅฎž้ชŒ็‰ˆ)" #~ msgid "Navidrome" #~ msgstr "Navidrome" #~ msgid "Use an existing Navidrome / Subsonic instance" #~ msgstr "ไฝฟ็”จ็Žฐๅœจๆœ‰็š„ Navidrome/Subsonic ๅฎžไพ‹" #~ msgid "Navidrome Managed" #~ msgstr "Navidrome ็ฎก็†ไบ†" #~ msgid "Instance Website" #~ msgstr "ๅฎžไพ‹็ฝ‘็ซ™" #~ msgid "Create and use Navidrome instance" #~ msgstr "ๅˆ›ๅปบๅ’Œไฝฟ็”จ Navidrome" #~ msgid "Go to Welcome Page" #~ msgstr "ๅŽปๆฌข่ฟŽ้กต้ข" #~ msgid "" #~ "Nocturne is powered by a Subsonic compatible instance, if you do not have " #~ "one Nocturne can automatically set up a Navidrome instance for you!" #~ msgstr "" #~ "Nocturne ็”ฑไธ€ไธชๅ…ผๅฎน Subsonic ็š„ๅฎžไพ‹้ฉฑๅŠจ๏ผŒๅฆ‚ๆžœๆ‚จๆฒกๆœ‰ Subsonic ๅฎžไพ‹๏ผŒ" #~ "Nocturne ๅฏไปฅ่‡ชๅŠจไธบๆ‚จ่ฎพ็ฝฎไธ€ไธช Navidrome ๅฎžไพ‹!" #~ msgid "Open Pop-out Window" #~ msgstr "ๆ‰“ๅผ€ๅผนๅ‡บ็ช—ๅฃ" #~ msgid "Close Pop-out Window" #~ msgstr "ๅ…ณ้—ญๅผนๅ‡บ็ช—ๅฃ" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "LRCLIB ๆไพ›็š„ๆญŒ่ฏ" #~ msgid "Use Dynamic Background in Player" #~ msgstr "ๅœจๆ’ญๆ”พๅ™จไธญไฝฟ็”จๅŠจๆ€่ƒŒๆ™ฏ" #~ msgid "Generate a gradient background based on the current song cover art" #~ msgstr "ๆ นๆฎๅฝ“ๅ‰ๅฐ้ข็”Ÿๆˆๆธๅ˜่ƒŒๆ™ฏ" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "ๆ’ญๆ”พๆŽงไปถ่ƒŒๆ™ฏๆจก็ณŠ๏ผˆๅฎž้ชŒๆ€งๅŠŸ่ƒฝ๏ผ‰" #~ msgid "Make background of player slightly transparent and blur the backdrop" #~ msgstr "ไฝฟๆ’ญๆ”พๅ™จ่ƒŒๆ™ฏ็•ฅๅพฎ้€ๆ˜Žๅนถๆจก็ณŠๅบ•้ข" #~ msgid "Restore Session" #~ msgstr "ๆขๅคไผš่ฏ" #~ msgid "Retrieve previous queue" #~ msgstr "ๆขๅคไธŠไธ€ไธช้˜Ÿๅˆ—" #~ msgid "Hide Nocturne when closing" #~ msgstr "ๅ…ณ้—ญๆ—ถ้š่— Nocturne" #~ msgid "Sections" #~ msgstr "ๆฟๅ—" #~ msgid "Max amount of songs to be shown" #~ msgstr "่ฆๆ˜พ็คบ็š„ๆœ€ๅคงๆ›ฒ็›ฎๆ•ฐ้‡" #~ msgid "Max amount of albums to be shown" #~ msgstr "่ฆๆ˜พ็คบ็š„ๆœ€ๅคงไธ“่พ‘ๆ•ฐ้‡" #~ msgid "Max amount of artists to be shown" #~ msgstr "่ฆๆ˜พ็คบ็š„ๆœ€ๅคง่‰บๆœฏๅฎถๆ•ฐ้‡" #~ msgid "Max amount of playlists to be shown" #~ msgstr "่ฆๆ˜พ็คบ็š„ๆœ€ๅคงๆ’ญๆ”พๅˆ—่กจๆ•ฐ้‡" #~ msgid "Player in Pop-out Window" #~ msgstr "ๅผนๅ‡บ็ช—ๅฃไธญ็š„ๆ’ญๆ”พๅ™จ" #~ msgid "1 Album" #~ msgstr "1 ไธ“่พ‘" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} ไธ“่พ‘" #~ msgid "1 Song" #~ msgstr "1 ้ฆ–ๆญŒๆ›ฒ" Jeffser-Nocturne-b442fee/po/zh_TW.po000066400000000000000000001172131523240030000173710ustar00rootroot00000000000000# Traditional Chinese translations for Nocturne. # Copyright (C) 2026 Jeffser # This file is distributed under the same license as the Nocturne package. # Yuan Chiu , 2026. # msgid "" msgstr "" "Project-Id-Version: Nocturne 1.0.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-05 18:30-0600\n" "PO-Revision-Date: 2026-05-05 20:36+0800\n" "Last-Translator: Yuan Chiu \n" "Language-Team: Traditional Chinese\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.8\n" #: data/com.jeffser.Nocturne.desktop.in:2 #: data/com.jeffser.Nocturne.metainfo.xml.in:7 msgid "Nocturne" msgstr "Nocturne" #: data/com.jeffser.Nocturne.desktop.in:10 msgid "GTK;Navidrome;Jellyfin;Music;" msgstr "GTK;Navidrome;Jellyfin;Music;" #: data/com.jeffser.Nocturne.metainfo.xml.in:8 msgid "Bring your music library together" msgstr "ๅฐ‡ๆ‚จ็š„้Ÿณๆจ‚ๅบซๆ•ดๅˆๅœจไธ€่ตท" #: data/com.jeffser.Nocturne.metainfo.xml.in:10 msgid "A modern Navidrome / Jellyfin client" msgstr "็พไปฃ็š„ Navidrome / Jellyfin ๅฎขๆˆถ็ซฏ" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "ๅŠŸ่ƒฝ" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 msgid "Exploration by songs, artists, albums, radios and playlists" msgstr "้€้ŽๆญŒๆ›ฒใ€ๆผ”ๅ‡บ่€…ใ€ๅฐˆ่ผฏใ€้›ปๅฐๅ’Œๆ’ญๆ”พๆธ…ๅ–ฎ้€ฒ่กŒๆŽข็ดข" #: data/com.jeffser.Nocturne.metainfo.xml.in:14 msgid "Playlist management" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎ็ฎก็†" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "็›ธๅฎน Jellyfinใ€OpenSubsonic ่ˆ‡ๆœฌๆฉŸๆช”ๆกˆ" #: data/com.jeffser.Nocturne.metainfo.xml.in:16 msgid "Audio equalizer and audio visualizer" msgstr "้Ÿณ่จŠ็ญ‰ๅŒ–ๅ™จ่ˆ‡้Ÿณ่จŠ่ฆ–่ฆบๅŒ–ๆ•ˆๆžœ" #: data/com.jeffser.Nocturne.metainfo.xml.in:17 msgid "Mpris integration" msgstr "Mpris ๆ•ดๅˆ" #: data/com.jeffser.Nocturne.metainfo.xml.in:18 msgid "Integrated Navidrome instance management" msgstr "ๆ•ดๅˆ Navidrome ๅฏฆไพ‹็ฎก็†" #: data/com.jeffser.Nocturne.metainfo.xml.in:19 msgid "Automatic lyrics fetching" msgstr "่‡ชๅ‹•็ฒๅ–่ฉžๆญŒ" #: data/com.jeffser.Nocturne.metainfo.xml.in:20 msgid "Downloads and offline mode" msgstr "ไธ‹่ผ‰่ˆ‡้›ข็ทšๆจกๅผ" #: data/com.jeffser.Nocturne.metainfo.xml.in:21 msgid "Cool interface" msgstr "้…ท็‚ซ็š„ไป‹้ข" #: data/com.jeffser.Nocturne.metainfo.xml.in:26 msgid "Jeffry Samuel Eduarte Rojas" msgstr "Jeffry Samuel Eduarte Rojas" #: data/com.jeffser.Nocturne.metainfo.xml.in:66 msgid "Main homepage" msgstr "ไธป้ " #: data/com.jeffser.Nocturne.metainfo.xml.in:70 msgid "Song queue" msgstr "ๅพ…ๆ’ญๆญŒๆ›ฒ" #: data/com.jeffser.Nocturne.metainfo.xml.in:74 #: src/ui/playing/popout_window.blp:319 src/ui/window.blp:170 #: src/ui/window.blp:222 msgid "Lyrics" msgstr "ๆญŒ่ฉž" #: data/com.jeffser.Nocturne.metainfo.xml.in:78 msgid "Song list" msgstr "ๆญŒๆ›ฒๆธ…ๅ–ฎ" #: data/com.jeffser.Nocturne.metainfo.xml.in:82 msgid "Album page" msgstr "ๅฐˆ่ผฏ้ ้ข" #: data/com.jeffser.Nocturne.metainfo.xml.in:87 msgid "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:88 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:89 msgid "music" msgstr "้Ÿณๆจ‚" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:211 src/ui/song/row.blp:69 msgid "External File" msgstr "ๅค–้ƒจๆช”ๆกˆ" #: src/actions.py:212 msgid "" "This track was loaded from an external file, this means it will have less " "features compared to a track inside the library" msgstr "ๆญคๆ›ฒ็›ฎๆ˜ฏๅพžๅค–้ƒจๆช”ๆกˆ่ผ‰ๅ…ฅ็š„๏ผŒ้€™ๆ„ๅ‘ณ่‘—่ˆ‡้Ÿณๆจ‚ๅบซไธญ็š„ๆ›ฒ็›ฎ็›ธๆฏ”๏ผŒๅ…ถๅŠŸ่ƒฝๆœƒ่ผƒๅฐ‘ใ€‚" #: src/actions.py:214 src/ui/song/queue.blp:40 msgid "Close" msgstr "้—œ้–‰" #: src/actions.py:232 msgid "Navidrome server deleted successfully" msgstr "Navidrome ็š„ไผบๆœๅ™จๅˆช้™คๆˆๅŠŸ" #: src/actions.py:241 msgid "Delete Navidrome Server" msgstr "ๅˆช้™ค Navidrome ็š„ไผบๆœๅ™จ" #: src/actions.py:242 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "ๆ‚จ็ขบๅฎš่ฆๅˆช้™คๆ•ดๅˆ็š„ Navidrome ไผบๆœๅ™จๅ—Ž๏ผŸ" #: src/actions.py:244 src/actions.py:355 src/actions.py:389 src/actions.py:674 #: src/actions.py:790 src/preferences.py:322 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "ๅ–ๆถˆ" #: src/actions.py:245 msgid "Keep Data" msgstr "ไฟ็•™่ณ‡ๆ–™" #: src/actions.py:246 msgid "Delete Everything" msgstr "็งป้™คๅ…จ้ƒจๅ…งๅฎน" #: src/actions.py:301 msgid "No Name" msgstr "ๆœชๅ‘ฝๅ" #: src/actions.py:320 msgid "Radio updated successfully" msgstr "้›ปๅฐๆ›ดๆ–ฐๆˆๅŠŸ" #: src/actions.py:320 msgid "Radio added successfully" msgstr "้›ปๅฐๆ–ฐๅขžๆˆๅŠŸ" #: src/actions.py:331 msgid "Error updating radio" msgstr "้›ปๅฐๆ›ดๆ–ฐๅ‡บ้Œฏ" #: src/actions.py:331 msgid "Error adding radio" msgstr "ๆ–ฐๅขž้›ปๅฐๅคฑๆ•—" #: src/actions.py:340 src/actions.py:666 msgid "Name" msgstr "ๅๅญ—" #: src/actions.py:345 msgid "Stream Url" msgstr "ๆตๅช’้ซ” URL" #: src/actions.py:352 msgid "Update Radio Station" msgstr "ๆ›ดๆ–ฐ้›ปๅฐ" #: src/actions.py:352 msgid "Add Radio Station" msgstr "ๆ–ฐๅขž้›ปๅฐ" #: src/actions.py:356 src/preferences.py:323 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "ๅ„ฒๅญ˜" #: src/actions.py:372 msgid "Radio deleted successfully" msgstr "้›ปๅฐ็งป้™คๆˆๅŠŸ" #: src/actions.py:380 msgid "Error deleting radio" msgstr "้›ปๅฐ็งป้™คๅ‡บ้Œฏ" #: src/actions.py:386 msgid "Delete Radio Station" msgstr "็งป้™ค้›ปๅฐ" #: src/actions.py:387 src/actions.py:788 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "็ขบๅฎš่ฆ็งป้™ค'{}'ๅ—Ž๏ผŸ" #: src/actions.py:390 src/actions.py:791 src/constants.py:343 #: src/constants.py:396 msgid "Delete" msgstr "็งป้™ค" #: src/actions.py:420 src/actions.py:447 src/actions.py:452 src/actions.py:541 #: src/actions.py:600 msgid "Playing Next" msgstr "ไธ‹ไธ€้ฆ–" #: src/actions.py:430 src/actions.py:463 src/actions.py:468 src/actions.py:556 #: src/actions.py:615 msgid "Playing Later" msgstr "็จๅพŒๆ’ญๆ”พ" #: src/actions.py:447 src/actions.py:463 src/actions.py:905 src/actions.py:1040 #: src/widgets/playlist/button.py:50 src/widgets/playlist/button.py:52 #: src/widgets/playlist/dialog.py:31 src/widgets/playlist/page.py:118 #: src/widgets/playlist/page.py:120 src/widgets/playlist/row.py:49 #: src/widgets/playlist/row.py:51 src/widgets/playlist/selector_row.py:38 #: src/widgets/playlist/selector_row.py:40 #, python-brace-format msgid "{} Song" msgid_plural "{} Songs" msgstr[0] "" #: src/actions.py:497 msgid "Lyrics Saved" msgstr "ๆญŒ่ฉžๅทฒๅ„ฒๅญ˜" #: src/actions.py:647 msgid "Playlist updated successfully" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎๆ›ดๆ–ฐๆˆๅŠŸ" #: src/actions.py:647 msgid "Playlist created successfully" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎๅปบ็ซ‹ๆˆๅŠŸ" #: src/actions.py:657 msgid "Error updating playlist" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎๆ›ดๆ–ฐๅ‡บ้Œฏ" #: src/actions.py:657 msgid "Error creating playlist" msgstr "ๆ–ฐๅขžๆ’ญๆ”พๆธ…ๅ–ฎๅคฑๆ•—" #: src/actions.py:671 msgid "Update Playlist" msgstr "ๆ›ดๆ–ฐๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/actions.py:671 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "ๆ–ฐๅขžๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/actions.py:675 msgid "Update" msgstr "ๆ›ดๆ–ฐ" #: src/actions.py:675 msgid "Create" msgstr "ๆ–ฐๅขž" #: src/actions.py:695 src/actions.py:700 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" #: src/actions.py:732 src/actions.py:734 src/actions.py:754 src/actions.py:756 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" #: src/actions.py:761 src/actions.py:877 src/actions.py:910 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" #: src/actions.py:775 msgid "Playlist Deleted" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎๅทฒ็งป้™ค" #: src/actions.py:787 msgid "Delete Playlist" msgstr "็งป้™คๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/actions.py:836 msgid "No songs found" msgstr "ๆ‰พไธๅˆฐไปปไฝ•ๆญŒๆ›ฒ" #: src/actions.py:879 src/actions.py:906 src/actions.py:943 src/actions.py:979 msgid "Download Started" msgstr "้–‹ๅง‹ไธ‹่ผ‰" #: src/actions.py:886 src/actions.py:917 src/actions.py:924 src/actions.py:959 #: src/actions.py:995 msgid "Already Downloaded" msgstr "ๅทฒไธ‹่ผ‰" #: src/actions.py:911 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" #: src/actions.py:923 msgid "Download Skipped" msgstr "ๅทฒ็•ฅ้Žไธ‹่ผ‰" #: src/actions.py:947 src/actions.py:983 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" #: src/actions.py:1022 src/actions.py:1041 msgid "Deleted" msgstr "ๅทฒๅˆช้™ค" #: src/constants.py:159 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "ไพๅบๆ’ญๆ”พ" #: src/constants.py:163 msgid "Repeat All" msgstr "ๅ…จ้ƒจ้‡่ค‡ๆ›ฒ็›ฎ" #: src/constants.py:167 msgid "Repeat One" msgstr "้‡่ค‡ๅ–ฎไธ€ๆ›ฒ็›ฎ" #: src/constants.py:172 #, python-brace-format msgid "Low ({})" msgstr "ไฝŽ๏ผˆ{}๏ผ‰" #: src/constants.py:173 #, python-brace-format msgid "Medium ({})" msgstr "ไธญ๏ผˆ{}๏ผ‰" #: src/constants.py:174 #, python-brace-format msgid "High ({})" msgstr "้ซ˜๏ผˆ{}๏ผ‰" #: src/constants.py:175 #, python-brace-format msgid "Ultra ({})" msgstr "ๆฅต้ซ˜๏ผˆ{}๏ผ‰" #: src/constants.py:176 msgid "Original File" msgstr "ๅŽŸๅง‹ๆช”ๆกˆ" #. Item #: src/constants.py:183 src/ui/pages/home.blp:5 msgid "Home" msgstr "้ฆ–้ " #. Item #: src/constants.py:188 src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:223 src/widgets/pages/home.py:62 msgid "Artists" msgstr "ๆผ”ๅ‡บ่€…" #. Item #. Section #: src/constants.py:193 src/constants.py:255 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:231 #: src/widgets/pages/home.py:74 msgid "Playlists" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎ" #. Section #: src/constants.py:200 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:215 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:50 msgid "Albums" msgstr "ๅฐˆ่ผฏ" #. Item #: src/constants.py:203 src/constants.py:238 src/window.py:135 msgid "All" msgstr "ๅ…จ้ƒจ" #. Item #: src/constants.py:208 msgid "Random" msgstr "้šจๆฉŸ" #. Item #: src/constants.py:213 src/constants.py:243 msgid "Favorites" msgstr "ๆˆ‘็š„ๆœ€ๆ„›" #. Item #: src/constants.py:218 msgid "Recently Added" msgstr "ๆœ€่ฟ‘ๆ–ฐๅขž" #. Item #: src/constants.py:223 msgid "Recently Played" msgstr "ๆœ€่ฟ‘ๆ’ญๆ”พ" #. Item #: src/constants.py:228 msgid "Most Played" msgstr "ๆœ€ๅธธๆ’ญๆ”พ" #. Section #: src/constants.py:235 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:207 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:32 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "ๆญŒๆ›ฒ" #. Item #: src/constants.py:248 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "้›ปๅฐ" #: src/constants.py:262 src/constants.py:313 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:244 src/ui/playing/footer.blp:103 #: src/ui/playing/popout_window.blp:127 src/ui/song/queue.blp:47 #: src/ui/song/queue.blp:49 msgid "Play" msgstr "ๆ’ญๆ”พ" #: src/constants.py:267 src/constants.py:300 src/constants.py:318 msgid "Shuffle" msgstr "้šจๆฉŸ" #: src/constants.py:272 src/constants.py:323 src/constants.py:356 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "ไธ‹ไธ€้ฆ–ๆ’ญๆ”พ" #: src/constants.py:277 src/constants.py:328 src/constants.py:361 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "ๅŠ ๅ…ฅๅพ…ๆ’ญๆธ…ๅ–ฎ" #: src/constants.py:282 src/constants.py:376 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "ๅŠ ๅ…ฅ่‡ณๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/constants.py:287 src/constants.py:333 src/constants.py:381 #: src/ui/pages/setup.blp:23 src/ui/playing/lyrics_page.blp:147 #: src/ui/playing/lyrics_page.blp:150 src/ui/song/queue.blp:79 #: src/ui/song/queue.blp:81 msgid "Download" msgstr "ไธ‹่ผ‰" #: src/constants.py:292 src/constants.py:391 #: src/ui/playing/control_page.blp:102 msgid "Show Artist" msgstr "้กฏ็คบๆผ”ๅ‡บ่€…" #: src/constants.py:305 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:108 msgid "Radio" msgstr "้›ปๅฐ" #: src/constants.py:338 src/constants.py:366 msgid "Edit" msgstr "็ทจ่ผฏ" #: src/constants.py:352 msgid "Select" msgstr "้ธๅ–" #: src/constants.py:371 msgid "Edit Lyrics" msgstr "็ทจ่ผฏๆญŒ่ฉž" #: src/constants.py:386 src/ui/album/button.blp:51 #: src/ui/playing/control_page.blp:117 msgid "Show Album" msgstr "้กฏ็คบๅฐˆ่ผฏ" #: src/constants.py:402 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "็งป้™ค" #: src/constants.py:407 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "ๅˆช้™คไธ‹่ผ‰้ …็›ฎ" #: src/constants.py:416 msgid "Visit Webpage" msgstr "ๅ‰ๅพ€็ถฒ้ " #: src/constants.py:422 msgid "Update Server" msgstr "ๆ›ดๆ–ฐไผบๆœๅ™จ" #: src/constants.py:427 msgid "Delete Server" msgstr "็งป้™คไผบๆœๅ™จ" #: src/integrations/jellyfin.py:15 msgid "Connect to a Jellyfin server." msgstr "้€ฃ็ทšๅˆฐ Jellyfin ไผบๆœๅ™จใ€‚" #: src/integrations/jellyfin.py:19 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:20 msgid "Use an existing Jellyfin instance" msgstr "ไฝฟ็”จ็พๅœจๆœ‰็š„ Jellyfin ๅฏฆไพ‹" #: src/integrations/local.py:17 src/integrations/local.py:23 #: src/integrations/local.py:509 msgid "Local Files" msgstr "ๆœฌๅœฐๆช”ๆกˆ" #: src/integrations/local.py:18 msgid "" "Let Nocturne load your local files directly, for big libraries it is " "recommended to use a dedicated server." msgstr "่ฎ“ Nocturne ็›ดๆŽฅ่ผ‰ๅ…ฅไฝ ็š„ๆœฌๆฉŸๆช”ๆกˆ๏ผ›่‹ฅ้Ÿณๆจ‚ๅบซ่ผƒๅคง๏ผŒๅปบ่ญฐๆ”น็”จๅฐˆ็”จไผบๆœๅ™จใ€‚" #: src/integrations/local.py:20 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "็นผ็บŒ" #: src/integrations/local.py:24 msgid "Limited functionality" msgstr "ๆœ‰้™็š„ๅŠŸ่ƒฝ" #: src/integrations/local.py:36 msgid "Loading Songs" msgstr "่ผ‰ๅ…ฅๆญŒๆ›ฒไธญ" #: src/integrations/local.py:215 msgid "No Album" msgstr "็„กๅฐˆ่ผฏ" #: src/integrations/local.py:525 src/integrations/local.py:534 msgid "Offline Mode" msgstr "้›ข็ทšๆจกๅผ" #: src/integrations/local.py:526 msgid "Access your downloads" msgstr "ๅญ˜ๅ–ไฝ ็š„ไธ‹่ผ‰ๅ…งๅฎน" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "้€ฃ็ทšๅˆฐ Navidrome ้€™้กž OpenSubsonic ไผบๆœๅ™จใ€‚" #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "็พๆœ‰็š„ๅค–้ƒจไผบๆœๅ™จ" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "ไฝฟ็”จๆ—ขๆœ‰็š„ OpenSubsonic / Navidrome ๅฏฆไพ‹" #: src/integrations/navidrome.py:550 src/integrations/navidrome.py:559 msgid "Managed Server" msgstr "ๆœฌๆฉŸ็š„Navidromeไผบๆœๅ™จ" #: src/integrations/navidrome.py:551 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "้€ฃ็ทšๅˆฐ็”ฑ Nocturne ็›ดๆŽฅ็ฎก็†็š„ Navidrome ๅฏฆไพ‹ใ€‚" #: src/integrations/navidrome.py:554 msgid "Manage Server" msgstr "็ฎก็†ไผบๆœๅ™จ" #: src/integrations/navidrome.py:560 msgid "Create and use a Navidrome instance" msgstr "ๅปบ็ซ‹ไธฆไฝฟ็”จ Navidrome ๅฏฆไพ‹" #: src/main.py:69 msgid "Music is Playing" msgstr "ๆญฃๅœจๆ’ญๆ”พ้Ÿณๆจ‚" #: src/main.py:116 msgid "Login Failed" msgstr "็™ปๅ…ฅๅคฑๆ•—" #: src/preferences.py:311 msgid "Settings Page" msgstr "่จญๅฎš้ ้ข" #: src/preferences.py:314 msgid "User Token" msgstr "ไฝฟ็”จ่€…ๆฌŠๆ–" #: src/preferences.py:318 msgid "Link ListenBrainz" msgstr "้€ฃ็ต ListenBrainz" #: src/preferences.py:319 msgid "Connect your ListenBrainz account with a user token" msgstr "ไฝฟ็”จไฝฟ็”จ่€…ๆฌŠๆ–้€ฃ็ตไฝ ็š„ ListenBrainz ๅธณ่™Ÿ" #: src/ui/album/button.blp:22 msgid "Play Album" msgstr "ๆ’ญๆ”พๅฐˆ่ผฏ" #: src/ui/album/button.blp:37 src/ui/album/page.blp:152 #: src/ui/artist/page.blp:150 src/ui/song/row.blp:112 msgid "Toggle star" msgstr "ๅˆ‡ๆ›ๆ˜Ÿๆจ™" #: src/ui/album/button.blp:65 src/ui/album/page.blp:50 src/ui/album/row.blp:32 msgid "Album cover art" msgstr "ๅฐˆ่ผฏๅฐ้ข" #: src/ui/album/button.blp:92 msgid "Enter album page" msgstr "้€ฒๅ…ฅๅฐˆ่ผฏ้ ้ข" #: src/ui/album/button.blp:117 src/ui/album/page.blp:85 msgid "Enter artist page" msgstr "้€ฒๅ…ฅๆผ”ๅ‡บ่€…้ ้ข" #: src/ui/album/page.blp:5 src/widgets/album/page.py:131 msgid "Album" msgstr "ๅฐˆ่ผฏ" #: src/ui/album/page.blp:95 src/ui/artist/page.blp:93 #: src/ui/playing/control_page.blp:136 msgid "1 Star" msgstr "1 ้ก†ๆ˜Ÿ" #: src/ui/album/page.blp:104 src/ui/artist/page.blp:102 #: src/ui/playing/control_page.blp:145 msgid "2 Stars" msgstr "2 ้ก†ๆ˜Ÿ" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:154 msgid "3 Stars" msgstr "3 ้ก†ๆ˜Ÿ" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:163 msgid "4 Stars" msgstr "4 ้ก†ๆ˜Ÿ" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:172 msgid "5 Stars" msgstr "5 ้ก†ๆ˜Ÿ" #: src/ui/album/row.blp:48 src/ui/artist/row.blp:40 src/ui/playlist/row.blp:48 #: src/ui/song/row.blp:127 msgid "Options" msgstr "ๆ›ดๅคš้ธ้ …" #: src/ui/artist/page.blp:5 msgid "Artist" msgstr "ๆผ”ๅ‡บ่€…" #: src/ui/artist/page.blp:83 msgid "Expand Biography" msgstr "ๅฑ•้–‹็ฐกไป‹" #: src/ui/containers/carousel.blp:19 src/ui/containers/carousel.blp:21 #: src/ui/containers/wrapbox.blp:19 src/ui/containers/wrapbox.blp:21 #: src/ui/pages/albums_all.blp:15 src/ui/pages/artists.blp:15 #: src/ui/pages/playlists.blp:15 src/ui/pages/songs_all.blp:15 #: src/ui/pages/songs_starred.blp:15 src/ui/song/queue.blp:16 #: src/ui/song/queue.blp:18 msgid "List" msgstr "ๆธ…ๅ–ฎ" #: src/ui/containers/download_row.blp:31 msgid "Downloaded" msgstr "ๅทฒไธ‹่ผ‰" #: src/ui/containers/download_row.blp:52 msgid "Remove From Queue" msgstr "ๅพžไฝ‡ๅˆ—็งป้™ค" #: src/ui/containers/downloads_queue_button.blp:10 msgid "Downloads Queue" msgstr "ไธ‹่ผ‰ไฝ‡ๅˆ—" #: src/ui/containers/downloads_queue_button.blp:11 msgid "Downloaded songs are available in offline mode" msgstr "ๅทฒไธ‹่ผ‰็š„ๆญŒๆ›ฒๅฏๅœจ้›ข็ทšๆจกๅผไฝฟ็”จ" #: src/ui/containers/downloads_queue_button.blp:19 msgid "Clear Done Downloads" msgstr "ๆธ…้™คๅทฒๅฎŒๆˆไธ‹่ผ‰" #: src/ui/lyrics/dialog.blp:5 msgid "Lyrics Editor" msgstr "ๆญŒ่ฉž็ทจ่ผฏๅ™จ" #: src/ui/lyrics/dialog.blp:61 src/ui/playing/control_page.blp:259 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "ๆšซๅœ" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:194 #: src/ui/playing/popout_window.blp:213 msgid "Current song progress bar" msgstr "็›ฎๅ‰ๆญŒๆ›ฒ้€ฒๅบฆๅˆ—" #: src/ui/lyrics/dialog.blp:103 msgid "Set Next Line Timestamp to Now" msgstr "ๅฐ‡ไธ‹ไธ€่กŒๆ™‚้–“ๆˆณ่จญ็‚บ็›ฎๅ‰ๆ™‚้–“" #: src/ui/lyrics/dialog.blp:112 msgid "Add Line at Timestamp" msgstr "ๆ–ฐๅขžๆ™‚้–“ๆˆณ" #: src/ui/lyrics/dialog.blp:133 msgid "No Lyrics" msgstr "ๆฒ’ๆœ‰ๆญŒ่ฉž" #: src/ui/lyrics/dialog.blp:134 msgid "No lyrics found for this track, to get started add a new line" msgstr "ๆ‰พไธๅˆฐ้€™้ฆ–ๆญŒ็š„ๆญŒ่ฉž๏ผŒ่ซ‹ๅ…ˆๆ–ฐๅขžไธ€่กŒๅ…งๅฎน" #: src/ui/lyrics/edit_row.blp:9 msgid "Timestamp" msgstr "ๆ™‚้–“ๆˆณ" #: src/ui/lyrics/edit_row.blp:15 src/ui/lyrics/edit_row.blp:17 msgid "Go to Timestamp" msgstr "ๅ‰ๅพ€ๆ™‚้–“ๆˆณ" #: src/ui/lyrics/edit_row.blp:27 src/ui/lyrics/edit_row.blp:29 msgid "Set Current Timestamp" msgstr "่จญๅฎš็›ฎๅ‰ๆ™‚้–“ๆˆณ" #: src/ui/lyrics/edit_row.blp:39 src/ui/lyrics/edit_row.blp:41 msgid "Remove Line" msgstr "ๅˆช้™คๆญค่กŒ" #: src/ui/pages/albums_all.blp:20 src/ui/pages/artists.blp:20 #: src/ui/pages/playlists.blp:20 src/ui/pages/songs_all.blp:20 #: src/ui/pages/songs_starred.blp:20 msgid "Grid" msgstr "ๆ ผๆก†" #: src/ui/pages/albums_all.blp:31 msgid "Search albums" msgstr "ๆœๅฐ‹ๅฐˆ่ผฏ" #: src/ui/pages/albums_all.blp:99 src/ui/pages/albums.blp:53 #: src/ui/pages/artists.blp:98 src/ui/pages/songs_all.blp:100 msgid "Reached End of List" msgstr "ๅทฒๅˆฐ้”ๅˆ—่กจๆœซๅฐพ" #: src/ui/pages/albums_all.blp:115 src/ui/pages/albums.blp:69 msgid "No Albums Found" msgstr "ๆœชๆ‰พๅˆฐๅฐˆ่ผฏ" #: src/ui/pages/artists.blp:31 msgid "Search artists" msgstr "ๆœๅฐ‹ๆผ”ๅ‡บ่€…" #: src/ui/pages/artists.blp:114 msgid "No Artists Found" msgstr "ๆœชๆ‰พๅˆฐๆผ”ๅ‡บ่€…" #: src/ui/pages/home.blp:47 msgid "No Elements Found" msgstr "ๆœชๆ‰พๅˆฐๅ…ƒ็ด " #. Login Button #: src/ui/pages/login.blp:5 src/ui/pages/login.blp:16 #: src/ui/pages/login.blp:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "็™ปๅ…ฅ" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "ไผบๆœๅ™จ็‹€ๆ…‹" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "ๆœชๅŸท่กŒ" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "้‡ๆ–ฐๅ•Ÿๅ‹•ๅฏฆไพ‹" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "็ฎก็† Navidrome ไผบๆœๅ™จ" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "ๆ›ดๅคš้ธ้ …" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "ไฟกไปปไผบๆœๅ™จ็š„ๆ†‘่ญ‰" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "ๅช’้ซ”ๅบซ่ณ‡ๆ–™ๅคพ" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "ๅธณ่™Ÿๅ็จฑ" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "ๅฏ†็ขผ" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "ไฝฟ็”จๅฟซ้€Ÿ้€ฃ็ทš" #: src/ui/pages/playlists.blp:31 msgid "Search playlists" msgstr "ๆœๅฐ‹ๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/pages/playlists.blp:106 msgid "No Playlists Found" msgstr "ๆœชๆ‰พๅˆฐๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/pages/radios.blp:16 msgid "Search radios" msgstr "ๆœๅฐ‹้›ปๅฐ" #: src/ui/pages/radios.blp:26 msgid "Add Radio" msgstr "ๆ–ฐๅขž้›ปๅฐ" #: src/ui/pages/radios.blp:61 msgid "No Radios Found" msgstr "ๆœชๆ‰พๅˆฐ้›ปๅฐ" #: src/ui/pages/setup.blp:5 msgid "Setup" msgstr "่จญๅฎš" #: src/ui/pages/setup.blp:19 msgid "Navidrome Download" msgstr "ไธ‹่ผ‰ Navidrome" #: src/ui/pages/setup.blp:20 msgid "Nocturne will download Navidrome automatically" msgstr "Nocturne ๆœƒ่‡ชๅ‹•ไธ‹่ผ‰ Navidrome" #: src/ui/pages/setup.blp:22 msgid "Download (~20โ€ฏmb)" msgstr "ไธ‹่ผ‰ (ๅคง็ด„~20 MB)" #: src/ui/pages/setup.blp:34 msgid "Downloading" msgstr "ไธ‹่ผ‰ไธญ" #: src/ui/pages/setup.blp:46 msgid "An Error Occurred" msgstr "็™ผ็”Ÿ้Œฏ่ชค" #: src/ui/pages/setup.blp:47 msgid "Please try again later" msgstr "่ซ‹็จๅพŒๅ†่ฉฆ" #: src/ui/pages/setup.blp:55 msgid "Download Successful" msgstr "ไธ‹่ผ‰ๆˆๅŠŸ" #: src/ui/pages/setup.blp:56 msgid "" "Before continuing make sure you create an admin account and also make sure " "all your tracks are inside your music directory" msgstr "" "ๅœจ็นผ็บŒๆ“ไฝœไน‹ๅ‰๏ผŒ่ซ‹็ขบไฟๆ‚จๅทฒๅปบ็ซ‹็ฎก็†ๅ“กๅธณๆˆถ๏ผŒไธฆ็ขบไฟๆ‚จ็š„ๆ‰€ๆœ‰ๆ›ฒ็›ฎ้ƒฝไฝๆ–ผๆ‚จ็š„้Ÿณๆจ‚็›ฎ" "้Œ„ไธญ" #: src/ui/pages/setup.blp:63 msgid "Instance Webpage" msgstr "ๅฏฆไพ‹็ถฒ้ " #: src/ui/pages/songs_all.blp:31 msgid "Search songs" msgstr "ๆœๅฐ‹ๆญŒๆ›ฒ" #: src/ui/pages/songs_all.blp:116 src/ui/pages/songs_starred.blp:95 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "ๆœชๆ‰พๅˆฐๆญŒๆ›ฒ" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "ๆœ€ๆ„›ๆญŒๆ›ฒ" #: src/ui/pages/songs_starred.blp:31 msgid "Search favorite songs" msgstr "ๆœๅฐ‹ๆœ€ๆ„›ๆญŒๆ›ฒ" #: src/ui/pages/welcome.blp:5 msgid "Welcome" msgstr "ๆญก่ฟŽ" #: src/ui/pages/welcome.blp:16 msgid "Welcome to Nocturne" msgstr "ๆญก่ฟŽไฝฟ็”จ Nocturne" #: src/ui/pages/welcome.blp:17 msgid "Select an instance type to get started" msgstr "้ธๆ“‡ไธ€็จฎๅฏฆไพ‹้กžๅž‹ๅณๅฏ้–‹ๅง‹" #: src/ui/playing/control_page.blp:5 msgid "Playing" msgstr "ๆ’ญๆ”พไธญ" #: src/ui/playing/control_page.blp:55 src/ui/playing/popout_window.blp:282 msgid "Song Cover" msgstr "ๆญŒๆ›ฒๅฐ้ข" #: src/ui/playing/control_page.blp:94 msgid "Visit current radio homepage" msgstr "่จชๅ•็•ถๅ‰้ŸณๆฉŸ้›ป่‡บไธป้ " #: src/ui/playing/control_page.blp:229 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "ไธŠไธ€้ฆ–" #: src/ui/playing/control_page.blp:273 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "ไธ‹ไธ€้ฆ–" #: src/ui/playing/control_page.blp:293 msgid "Star" msgstr "ๆ˜Ÿ" #: src/ui/playing/control_page.blp:300 msgid "Show Sidebar" msgstr "้กฏ็คบๅด้‚Šๆฌ„" #: src/ui/playing/equalizer_page.blp:21 msgid "Presets" msgstr "้ ่จญ" #: src/ui/playing/equalizer_page.blp:26 msgid "Flat" msgstr "ๅนณ" #: src/ui/playing/equalizer_page.blp:33 msgid "Vocal Clarity" msgstr "ไบบ่ฒๆธ…ๆ™ฐ" #: src/ui/playing/equalizer_page.blp:38 msgid "Smooth Jazz" msgstr "ๆŸ”ๅ’Œ็ˆตๅฃซ" #: src/ui/playing/equalizer_page.blp:43 msgid "Rock" msgstr "ๆ–ๆปพๆจ‚" #: src/ui/playing/equalizer_page.blp:48 msgid "Classic" msgstr "็ถ“ๅ…ธ" #: src/ui/playing/equalizer_page.blp:53 msgid "Acoustic" msgstr "ๅŽŸ่ฒ" #: src/ui/playing/footer.blp:12 msgid "Current song progress indicator" msgstr "็›ฎๅ‰ๆญŒๆ›ฒ้€ฒๅบฆๆŒ‡็คบๅ™จ" #: src/ui/playing/footer.blp:34 msgid "Current song cover art" msgstr "็›ฎๅ‰ๆญŒๆ›ฒๅฐ้ข" #: src/ui/playing/lyrics_page.blp:24 src/ui/playing/lyrics_page.blp:65 #: src/ui/playing/lyrics_page.blp:95 src/ui/playing/lyrics_page.blp:99 #: src/ui/playing/lyrics_page.blp:112 src/ui/playing/lyrics_page.blp:116 msgid "Go Back" msgstr "่ฟ”ๅ›ž" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "ๆœชๆ‰พๅˆฐๆญŒ่ฉž" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Instrumental" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "ๆœชไธ‹่ผ‰" #: src/ui/playing/lyrics_page.blp:136 msgid "" "The lyrics for this song have not been downloaded yet, they might be " "available for download" msgstr "้€™ๅ€‹ๆญŒๆ›ฒ็š„ๆญŒ่ฉžๆฒ’ๆœ‰ไธ‹่ผ‰้Ž๏ผŒไฝ†ๆœ‰ๅฏ่ƒฝ ๅฏไธ‹่ผ‰" #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 msgid "Load Lyrics File" msgstr "่ผ‰ๅ…ฅๆญŒ่ฉžๆช”ๆกˆ" #: src/ui/playing/popout_window.blp:5 msgid "Pop-out Window" msgstr "ๅฝˆๅ‡บ่ฆ–็ช—" #: src/ui/playing/popout_window.blp:223 src/ui/window.blp:154 msgid "Player" msgstr "ๆ’ญๆ”พๅ™จ" #: src/ui/playing/popout_window.blp:240 src/ui/shortcuts-dialog.blp:16 #: src/ui/window.blp:255 src/widgets/playing/popout_window.py:63 msgid "Toggle Fullscreen" msgstr "ๅˆ‡ๆ›ๅ…จ่žขๅน•" #: src/ui/playing/popout_window.blp:293 src/ui/window.blp:194 msgid "Song Details" msgstr "ๆญŒๆ›ฒ่ฉณ็ดฐ่ณ‡่จŠ" #: src/ui/playing/popout_window.blp:313 src/ui/window.blp:160 #: src/ui/window.blp:216 msgid "Queue" msgstr "ๅพ…ๆ’ญๆญŒๆ›ฒ" #: src/ui/playing/popout_window.blp:325 src/ui/window.blp:176 #: src/ui/window.blp:228 msgid "Equalizer" msgstr "็ญ‰ๅŒ–ๅ™จ" #: src/ui/playing/queue_page.blp:20 msgid "Generating next queue" msgstr "ๆญฃๅœจ็”ข็”Ÿไธ‹ไธ€ๅ€‹ๅพ…ๆ’ญๆญŒๆ›ฒๆธ…ๅ–ฎ" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "่‡ชๅ‹•ๆ’ญๆ”พ" #: src/ui/playing/queue_page.blp:25 msgid "Generate a new queue when the current one ends" msgstr "็›ฎๅ‰ๆญŒๆ›ฒๆธ…ๅ–ฎ็ตๆŸๆ™‚๏ผŒๅฐ‡ๆœƒ็”ข็”Ÿๆ–ฐ็š„ๅพ…ๆ’ญๆธ…ๅ–ฎใ€‚" #: src/ui/playing/volume_button.blp:5 msgid "Volume" msgstr "้Ÿณ้‡" #: src/ui/playing/volume_button.blp:12 msgid "Speaker Mute" msgstr "ๆš่ฒๅ™จ้œ้Ÿณ" #: src/ui/playing/volume_button.blp:30 msgid "Speaker Full Volume" msgstr "ๆš่ฒๅ™จ้Ÿณ้‡ๅ…จ้–‹" #: src/ui/playlist/button.blp:29 msgid "Play Playlist" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/playlist/button.blp:43 msgid "Show Playlist" msgstr "้กฏ็คบๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/playlist/button.blp:56 src/ui/playlist/page.blp:50 #: src/ui/playlist/row.blp:32 src/ui/playlist/selector_row.blp:24 msgid "Playlist cover art" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎๅฐ้ข" #: src/ui/playlist/button.blp:68 msgid "Enter playlist page" msgstr "้€ฒๅ…ฅๆ’ญๆ”พๆธ…ๅ–ฎ้ ้ข" #: src/ui/playlist/dialog.blp:6 src/ui/playlist/page.blp:5 #: src/widgets/playlist/page.py:86 msgid "Playlist" msgstr "ๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/playlist/dialog.blp:11 msgid "Search (or create new playlist)" msgstr "ๆœๅฐ‹ (ๆˆ–่€…ๅปบ็ซ‹ๆ–ฐ็š„ๆ’ญๆ”พๆธ…ๅ–ฎ)" #: src/ui/playlist/dialog.blp:20 msgid "Adding Music to Playlist" msgstr "ๅฐ‡้Ÿณๆจ‚ๅŠ ๅ…ฅ่‡ณๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/playlist/dialog.blp:34 msgid "Create new playlist and add songs" msgstr "ๅปบ็ซ‹ๆ–ฐ็š„ๆ’ญๆ”พๆธ…ๅ–ฎ่ˆ‡ๅŠ ๅ…ฅๆญŒๆ›ฒ" #: src/ui/preferences.blp:7 msgid "General" msgstr "ไธ€่ˆฌ" #: src/ui/preferences.blp:10 msgid "Behavior" msgstr "่กŒ็‚บ" #: src/ui/preferences.blp:12 msgid "Restore Queue on Launch" msgstr "ๅ•Ÿๅ‹•ๆ™‚๏ผŒๆขๅพฉๅ…ˆๅ‰็š„ๅพ…ๆ’ญๆญŒๆ›ฒๆธ…ๅ–ฎ" #: src/ui/preferences.blp:15 msgid "Hide on Close" msgstr "้—œ้–‰่ฆ–็ช—ๆ™‚๏ผŒๅƒ…้šฑ่—่ฆ–็ช—ไธ็ตๆŸ็จ‹ๅผ" #: src/ui/preferences.blp:18 msgid "Simulate Word by Word Lyrics" msgstr "" #: src/ui/preferences.blp:19 msgid "Used when only line by line lyrics are available" msgstr "" #: src/ui/preferences.blp:22 msgid "Default Page on Login" msgstr "็™ปๅ…ฅๅพŒ้ ่จญ้ ้ข" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "ๆœ€้ซ˜ไฝๅ…ƒ็އ" #: src/ui/preferences.blp:33 msgid "Session" msgstr "ๅทฅไฝœ้šŽๆฎต" #: src/ui/preferences.blp:36 msgid "ListenBrainz Link" msgstr "ListenBrainz ้€ฃ็ต" #: src/ui/preferences.blp:37 msgid "Keep track of your playback history" msgstr "่ฟฝ่นคไฝ ็š„ๆ’ญๆ”พๆญทๅฒ" #: src/ui/preferences.blp:43 src/ui/preferences.blp:44 msgid "Link" msgstr "้€ฃ็ต" #: src/ui/preferences.blp:52 src/ui/preferences.blp:53 msgid "Unlink" msgstr "่งฃ้™ค้€ฃ็ต" #: src/ui/preferences.blp:65 msgid "User" msgstr "ไฝฟ็”จ่€…" #: src/ui/preferences.blp:85 msgid "Logout" msgstr "็™ปๅ‡บ" #: src/ui/preferences.blp:95 msgid "Customization" msgstr "่‡ช่จ‚" #: src/ui/preferences.blp:98 msgid "Interface" msgstr "ไป‹้ข" #: src/ui/preferences.blp:100 msgid "Show Context Button in Rows" msgstr "ๅœจ่กŒไธญ้กฏ็คบไธŠไธ‹ๆ–‡ๆŒ‰้ˆ•" #: src/ui/preferences.blp:101 msgid "The menu can also be shown by right clicking / long pressing" msgstr "ไนŸๅฏไปฅ้€้Žๅณ้ต้ปžๆ“Š/้•ทๆŒ‰ไพ†้กฏ็คบ้ธๅ–ฎใ€‚" #: src/ui/preferences.blp:104 msgid "Show Labels in Buttons of Pages" msgstr "ๅœจ้ ้ขๆŒ‰้ˆ•้กฏ็คบๆจ™็ฑค" #: src/ui/preferences.blp:107 msgid "Show Playlists in Sidebar" msgstr "ๅœจๅด้‚Šๆฌ„้กฏ็คบๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/ui/preferences.blp:110 msgid "Show Progressbar in Player Footer" msgstr "ๅœจๆ’ญๆ”พๅ™จๅบ•้ƒจ้กฏ็คบ้€ฒๅบฆๅˆ—" #: src/ui/preferences.blp:113 msgid "Make Background Translucent in Player" msgstr "่ฎ“ๆ’ญๆ”พๅ™จ่ƒŒๆ™ฏๅŠ้€ๆ˜Ž" #: src/ui/preferences.blp:116 msgid "Use Sidebar Player When Possible" msgstr "ไปฅๅด้‚Šๆฌ„ๆ’ญๆ”พๅ™จๅฝขๅผ้กฏ็คบ" #: src/ui/preferences.blp:117 msgid "Sidebar player is only available when the window is wide enough" msgstr "ๅด้‚Šๆฌ„ๆ’ญๆ”พๅ™จๅƒ…ๅœจ่ฆ–็ช—ๅฏฌๅบฆ่ถณๅค ๆ™‚ๅฏ็”จ" #: src/ui/preferences.blp:122 msgid "Dynamic Background" msgstr "ๅ‹•ๆ…‹่ƒŒๆ™ฏ" #: src/ui/preferences.blp:123 msgid "Background generated based on the album art of the current song" msgstr "ๆ นๆ“š็›ฎๅ‰ๆญŒๆ›ฒๅฐ้ข็”ข็”Ÿ่ƒŒๆ™ฏ" #: src/ui/preferences.blp:126 msgid "Use in Main Window" msgstr "ๅฅ—็”จๆ–ผไธป่ฆ–็ช—" #: src/ui/preferences.blp:127 msgid "Experimental" msgstr "ๅฏฆ้ฉ—ๆ€งๅŠŸ่ƒฝ" #: src/ui/preferences.blp:133 src/ui/preferences.blp:134 #: src/ui/preferences.blp:157 src/ui/preferences.blp:158 #: src/ui/preferences.blp:181 src/ui/preferences.blp:182 msgid "Off" msgstr "้—œ้–‰" #: src/ui/preferences.blp:138 src/ui/preferences.blp:139 #: src/ui/preferences.blp:162 src/ui/preferences.blp:163 #: src/ui/preferences.blp:186 src/ui/preferences.blp:187 msgid "Gradient" msgstr "ๆผธๅฑค" #: src/ui/preferences.blp:143 src/ui/preferences.blp:144 #: src/ui/preferences.blp:167 src/ui/preferences.blp:168 #: src/ui/preferences.blp:191 src/ui/preferences.blp:192 msgid "Blur" msgstr "ๆจก็ณŠ" #: src/ui/preferences.blp:151 msgid "Use in Player" msgstr "ๅฅ—็”จๆ–ผๆ’ญๆ”พๅ™จ" #: src/ui/preferences.blp:175 msgid "Use in Pop-out Window" msgstr "ๅฅ—็”จๆ–ผๅฝˆๅ‡บ่ฆ–็ช—" #: src/ui/preferences.blp:199 msgid "Use as Accent Color" msgstr "ไฝœ็‚บๅผท่ชฟ่‰ฒ" #: src/ui/preferences.blp:204 msgid "Homepage" msgstr "้ฆ–้ " #: src/ui/preferences.blp:205 msgid "Reload homepage to see changes" msgstr "้‡ๆ–ฐ่ผ‰ๅ…ฅ้ฆ–้ ไปฅๆŸฅ็œ‹ๆ›ดๆ”น" #: src/ui/preferences.blp:241 msgid "Visualizer" msgstr "่ฆ–่ฆบๅŒ–ๆ•ˆๆžœ" #: src/ui/preferences.blp:245 src/ui/window.blp:245 msgid "Preferences" msgstr "่จญๅฎš" #: src/ui/preferences.blp:247 msgid "Show Visualizer in Player" msgstr "ๅœจๆ’ญๆ”พๅ™จไธญ้กฏ็คบ่ฆ–่ฆบๅŒ–ๆ•ˆๆžœ" #: src/ui/preferences.blp:248 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "ๅœจๆ’ญๆ”พๅ™จไธญๆ–ผๅฐˆ่ผฏๅฐ้ขไธŠๆ–น้กฏ็คบ้Ÿณ่จŠ้ ป่ญœ่ฆ–่ฆบๅŒ–ๆ•ˆๆžœ" #: src/ui/preferences.blp:253 msgid "Appearance" msgstr "ๅค–่ง€" #: src/ui/preferences.blp:257 msgid "Number of Bars" msgstr "้•ทๆขๆ•ธ้‡" #: src/ui/preferences.blp:268 msgid "Visualizer Type" msgstr "่ฆ–่ฆบๅŒ–้กžๅž‹" #: src/ui/preferences.blp:274 src/ui/preferences.blp:275 msgid "Wave" msgstr "ๆณขๅฝข" #: src/ui/preferences.blp:279 src/ui/preferences.blp:280 msgid "Bars" msgstr "้•ทๆข" #: src/ui/preferences.blp:284 src/ui/preferences.blp:285 msgid "Particles" msgstr "็ฒ’ๅญ" #: src/ui/preferences.blp:292 msgid "Fill Mode" msgstr "ๅกซๆปฟๆจกๅผ" #: src/ui/preferences.blp:298 src/ui/preferences.blp:299 msgid "Solid" msgstr "ๅฏฆๅฟƒ" #: src/ui/preferences.blp:303 src/ui/preferences.blp:304 msgid "Translucent" msgstr "ๅŠ้€ๆ˜Ž" #: src/ui/preferences.blp:308 src/ui/preferences.blp:309 msgid "Border" msgstr "้‚Šๆก†" #: src/ui/preferences.blp:317 msgid "Color" msgstr "้ก่‰ฒ" #: src/ui/preferences.blp:321 msgid "Use Dynamic Color" msgstr "ไฝฟ็”จๅ‹•ๆ…‹้ก่‰ฒ" #: src/ui/preferences.blp:324 msgid "Invert Color" msgstr "ๅ่ฝ‰้ก่‰ฒ" #: src/ui/preferences.blp:328 msgid "Manual Color" msgstr "ๆ‰‹ๅ‹•้ก่‰ฒ" #: src/ui/preferences.blp:334 msgid "Visualizer Color" msgstr "่ฆ–่ฆบๅŒ–้ก่‰ฒ" #: src/ui/shortcuts-dialog.blp:6 msgid "Shortcuts" msgstr "ๅฟซๆท้ต" #: src/ui/shortcuts-dialog.blp:8 msgid "Show Shortcuts" msgstr "้กฏ็คบๅฟซๆท้ต" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Preferences" msgstr "้กฏ็คบๅๅฅฝ่จญๅฎš" #: src/ui/shortcuts-dialog.blp:20 src/ui/window.blp:250 msgid "Open Pop-Out Window" msgstr "ๅฝˆๅ‡บๆ’ญๆ”พ่ฆ–็ช—" #: src/ui/shortcuts-dialog.blp:24 msgid "Quit" msgstr "้›ข้–‹" #: src/ui/song/row.blp:41 msgid "Drag icon" msgstr "ๆ‹–ๆ›ณๅœ–็คบ" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "้ธๅ–ๆ–นๅกŠ" #: src/ui/song/small_row.blp:37 msgid "Song cover art" msgstr "ๆญŒๆ›ฒๅฐ้ข" #: src/ui/window.blp:74 msgid "Play Random Queue" msgstr "้šจๆฉŸๆ’ญๆ”พไฝ‡ๅˆ—" #: src/ui/window.blp:87 msgid "Menu" msgstr "้ธๅ–ฎ" #: src/ui/window.blp:115 msgid "Random Albums" msgstr "้šจๆฉŸๅฐˆ่ผฏ" #: src/ui/window.blp:119 msgid "Favorite Albums" msgstr "ๆœ€ๆ„›ๅฐˆ่ผฏ" #: src/ui/window.blp:123 msgid "Newest Albums" msgstr "ๆœ€ๆ–ฐๅฐˆ่ผฏ" #: src/ui/window.blp:127 msgid "Recent Albums" msgstr "ๆœ€่ฟ‘ๅฐˆ่ผฏ" #: src/ui/window.blp:131 msgid "Frequent Albums" msgstr "ๅธธ่ฝๅฐˆ่ผฏ" #: src/ui/window.blp:260 msgid "Keyboard Shortcuts" msgstr "้ต็›คๅฟซๆท้ต" #: src/ui/window.blp:265 msgid "About Nocturne" msgstr "้—œๆ–ผ Nocturne" #: src/widgets/album/button.py:63 src/widgets/album/page.py:147 #: src/widgets/artist/page.py:127 src/widgets/playing/control_page.py:217 #: src/widgets/song/row.py:162 msgid "Favorite" msgstr "ๆœ€ๆ„›" #: src/widgets/album/button.py:68 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:222 #: src/widgets/song/row.py:167 msgid "Not Favorite" msgstr "ๆŒ‰ไธ€ไธ‹ๅŠ ๅ…ฅๅˆฐๆœ€ๆ„›" #: src/widgets/album/page.py:18 #, python-brace-format msgid "Disc {}" msgstr "็ฌฌ {} ็‰‡" #: src/widgets/artist/button.py:43 src/widgets/artist/row.py:48 #, python-brace-format msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "็†ฑ้–€ๆญŒๆ›ฒ" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "็›ธ้—œๆผ”ๅ‡บ่€…" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "ไฝœ่€…" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "็„กๆ™‚้–“ๆˆณ" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "ๅŸท่กŒไธญ" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "้กๅค–้ธๅ–ฎ" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "ๆœฌๅœฐ้Ÿณๆจ‚ๅบซ" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "ๅทฒ้‡ๆ–ฐๅ•Ÿๅ‹•" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "้Œฏ่ชค" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "ๅฟซ้€Ÿ้€ฃ็ทš" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "ๅ–ๅพ—ไปฃ็ขผๅคฑๆ•—" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "ๅฟซ้€Ÿ้€ฃ็ทš้ ้ข" #: src/widgets/pages/setup.py:62 msgid "Installing" msgstr "ๆญฃๅœจๅฎ‰่ฃ" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "ๆ•ดๅˆ" #: src/widgets/playing/lyrics_page.py:215 msgid "LRC File" msgstr "LRC ๆช”ๆกˆ" #: src/widgets/playlist/dialog.py:55 msgid "New Playlist" msgstr "ๆ–ฐๅขžๆ’ญๆ”พๆธ…ๅ–ฎ" #: src/widgets/playlist/selector_row.py:34 #, python-brace-format msgid "Add songs to '{}'" msgstr "ๅฐ‡ๆญŒๆ›ฒๅŠ ๅ…ฅ '{}'" #: src/widgets/song/row.py:146 src/widgets/song/row.py:151 msgid "Multiple Artists" msgstr "ๅคšไฝๆผ”ๅ‡บ่€…" #, python-brace-format #~ msgid "{} Songs" #~ msgstr "{} ๆญŒๆ›ฒ" #, python-brace-format #~ msgid "{} Songs Removed" #~ msgstr "{} ๆญŒๆ›ฒๅทฒ็งป้™ค" #~ msgid "Song Removed" #~ msgstr "ๆญŒๆ›ฒๅทฒ็งป้™ค" #, python-brace-format #~ msgid "{} Songs Added" #~ msgstr "ๅทฒๆ–ฐๅขž {} ้ฆ–ๆญŒๆ›ฒ" #~ msgid "1 Song Added" #~ msgstr "ๅทฒๆ–ฐๅขž 1 ้ฆ–ๆญŒๆ›ฒ" #~ msgid "1 Song Skipped" #~ msgstr "ๅทฒ็•ฅ้Ž 1 ้ฆ–ๆญŒๆ›ฒ" #, python-brace-format #~ msgid "{} Songs Skipped" #~ msgstr "ๅทฒ็•ฅ้Ž {} ้ฆ–ๆญŒๆ›ฒ" #, python-brace-format #~ msgid "{} Songs ({})" #~ msgstr "{} ้ฆ–ๆญŒๆ›ฒ๏ผˆ{}๏ผ‰" #, python-brace-format #~ msgid "1 Song ({})" #~ msgstr "1 ้ฆ–ๆญŒๆ›ฒ๏ผˆ{}๏ผ‰" #, python-brace-format #~ msgid "Download Started ({} Songs Skipped)" #~ msgstr "้–‹ๅง‹ไธ‹่ผ‰๏ผˆๅทฒ็•ฅ้Ž {} ้ฆ–ๆญŒๆ›ฒ๏ผ‰" #~ msgid "Download Started (1 Song Skipped)" #~ msgstr "้–‹ๅง‹ไธ‹่ผ‰๏ผˆๅทฒ็•ฅ้Ž 1 ้ฆ–ๆญŒๆ›ฒ๏ผ‰" #~ msgid "1 Album" #~ msgstr "1 ๅผตๅฐˆ่ผฏ" #, python-brace-format #~ msgid "{} Albums" #~ msgstr "{} ๅผตๅฐˆ่ผฏ" #~ msgid "1 Song" #~ msgstr "1 ้ฆ–ๆญŒๆ›ฒ" #~ msgid "Starred" #~ msgstr "ๆ˜Ÿๆจ™ไบ†" #~ msgid "Jellyfin (Experimental)" #~ msgstr "Jellyfin (ๅฏฆ้ฉ—็‰ˆ)" #~ msgid "Navidrome" #~ msgstr "Navidrome" #~ msgid "Use an existing Navidrome / Subsonic instance" #~ msgstr "ไฝฟ็”จ็พๅœจๆœ‰็š„ Navidrome/Subsonic ๅฏฆไพ‹" #~ msgid "Navidrome Managed" #~ msgstr "ๆœฌๆฉŸ็ซฏ็š„ Navidrome ไผบๆœๅ™จ" #~ msgid "Instance Website" #~ msgstr "ๅฏฆไพ‹็ถฒ็ซ™" #~ msgid "Create and use Navidrome instance" #~ msgstr "ๅปบ็ซ‹ๅ’Œไฝฟ็”จ Navidrome" #~ msgid "Go to Welcome Page" #~ msgstr "ๅŽปๆญก่ฟŽ้ ้ข" #~ msgid "" #~ "Nocturne is powered by a Subsonic compatible instance, if you do not have " #~ "one Nocturne can automatically set up a Navidrome instance for you!" #~ msgstr "" #~ "Nocturne ็”ฑไธ€ๅ€‹ๅ…ผๅฎน Subsonic ็š„ๅฏฆไพ‹้ฉ…ๅ‹•๏ผŒๅฆ‚ๆžœๆ‚จๆฒ’ๆœ‰ Subsonic ๅฏฆไพ‹๏ผŒ" #~ "Nocturne ๅฏไปฅ่‡ชๅ‹•็‚บๆ‚จ่จญ็ฝฎไธ€ๅ€‹ Navidrome ๅฏฆไพ‹!" #~ msgid "Open Pop-out Window" #~ msgstr "ๆ‰“้–‹ๅฝˆๅ‡บ็ช—ๅฃ" #~ msgid "Close Pop-out Window" #~ msgstr "้—œ้–‰ๅฝˆๅ‡บ็ช—ๅฃ" #~ msgid "Lyrics are Provided by LRCLIB" #~ msgstr "LRCLIB ๆไพ›็š„ๆญŒ่ฉž" #~ msgid "Use Dynamic Background in Player" #~ msgstr "ๅœจๆ’ญๆ”พๅ™จไธญไฝฟ็”จๅ‹•ๆ…‹่ƒŒๆ™ฏ" #~ msgid "Generate a gradient background based on the current song cover art" #~ msgstr "ๆ นๆ“š็•ถๅ‰ๅฐ้ข็”Ÿๆˆๆผธ่ฎŠ่ƒŒๆ™ฏ" #~ msgid "Blur Background in Player (Experimental)" #~ msgstr "ๆ’ญๆ”พๆŽงไปถ่ƒŒๆ™ฏๆจก็ณŠ๏ผˆๅฏฆ้ฉ—ๆ€งๅŠŸ่ƒฝ๏ผ‰" #~ msgid "Make background of player slightly transparent and blur the backdrop" #~ msgstr "ไฝฟๆ’ญๆ”พๅ™จ่ƒŒๆ™ฏ็•ฅๅพฎ้€ๆ˜Žไธฆๆจก็ณŠๅบ•้ข" #~ msgid "Restore Session" #~ msgstr "ๆขๅพฉๆœƒ่ฉฑ" #~ msgid "Retrieve previous queue" #~ msgstr "ๆขๅพฉไธŠไธ€ๅ€‹ๅพ…ๆ’ญๆญŒๆ›ฒๆธ…ๅ–ฎ" #~ msgid "Hide Nocturne when closing" #~ msgstr "้—œ้–‰ๆ™‚้šฑ่— Nocturne" #~ msgid "Sections" #~ msgstr "ๆฟๅกŠ" #~ msgid "Max amount of songs to be shown" #~ msgstr "่ฆ้กฏ็คบ็š„ๆœ€ๅคง้บด็›ฎๆ•ธ้‡" #~ msgid "Max amount of albums to be shown" #~ msgstr "่ฆ้กฏ็คบ็š„ๆœ€ๅคงๅฐˆ่ผฏๆ•ธ้‡" #~ msgid "Max amount of artists to be shown" #~ msgstr "่ฆ้กฏ็คบ็š„ๆœ€ๅคงๆผ”ๅ‡บ่€…ๆ•ธ้‡" #~ msgid "Max amount of playlists to be shown" #~ msgstr "่ฆ้กฏ็คบ็š„ๆœ€ๅคงๆ’ญๆ”พๆธ…ๅ–ฎๆ•ธ้‡" #~ msgid "Player in Pop-out Window" #~ msgstr "ๅฝˆๅ‡บ็ช—ๅฃไธญ็š„ๆ’ญๆ”พๅ™จ" Jeffser-Nocturne-b442fee/src/000077500000000000000000000000001523240030000161425ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/__init__.py000066400000000000000000000000001523240030000202410ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/actions.py000066400000000000000000001222701523240030000201600ustar00rootroot00000000000000# actions.py from .integrations import get_current_integration, models import random, threading, os, shutil, pathlib from datetime import datetime, UTC, timedelta from . import widgets as Widgets from gi.repository import Gio, Adw, Gtk, GLib, Gst from .constants import DATA_DIR, BASE_NAVIDROME_DIR, DOWNLOADS_DIR # -- HELPER -- def __show_page(window, page:Adw.NavigationPage): application = window.get_application() active_window = application.props.active_window if active_window.__gtype_name__ == 'NocturnePopoutWindow': page_dialog = None for dialog in active_window.get_dialogs(): if dialog.__gtype_name__ == 'NocturnePageDialog': GLib.idle_add(dialog.navigation_view.push, page) return GLib.idle_add(Widgets.PageDialog(page).present, active_window) else: GLib.idle_add(active_window.main_bottom_sheet.set_open, False) GLib.idle_add(active_window.main_split_view.set_show_content, True) GLib.idle_add(active_window.main_navigationview.push, page) for dialog in active_window.get_dialogs(): GLib.idle_add(dialog.close) def __show_custom_toast(window, model_id:str, title_property:str, subtitle:str, icon_name:str=None): # Thread safe integration = get_current_integration() model = integration.loaded_models.get(model_id) custom_widget = Adw.ActionRow( title=model.get_property(title_property) if model else title_property, subtitle=subtitle ) if icon_name: GLib.idle_add(custom_widget.set_icon_name, icon_name) else: album_art = Gtk.Image( css_classes=['card'], height_request=48, width_request=48, overflow=Gtk.Overflow.HIDDEN, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, icon_name="music-note-symbolic", pixel_size=32 ) if model: if paintable := model.get_property('gdkPaintable'): GLib.idle_add(album_art.set_from_paintable, paintable) GLib.idle_add(album_art.set_pixel_size, 48) GLib.idle_add(custom_widget.add_prefix, album_art) toast = Adw.Toast( custom_title=custom_widget, timeout=2 ) GLib.idle_add(window.get_application().props.active_window.toast_overlay.add_toast, toast) def __save_playlist_resume(window): # Check if current queue origin is a playlist if so, save the resume state # Call in different thread integration = get_current_integration() queue_origin = integration.loaded_models.get('currentSong').get_property('queueOrigin') or "" if model := integration.loaded_models.get(queue_origin): if isinstance(model, models.Playlist): timestamp = integration.loaded_models.get('currentSong').get_property('positionSeconds') current_song = integration.loaded_models.get('currentSong').get_property('songId') integration.savePlaylistResume( queue_origin_id=queue_origin, song_id=current_song, current_timestamp=timestamp ) def __replace_queue(window, songs:list, current_id:str=None, origin_id:str=""): integration = get_current_integration() __save_playlist_resume(window) integration.loaded_models.get('currentSong').set_property('queueOrigin', origin_id) queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') GLib.idle_add(queue_model.remove_all) if len(songs) > 0: if current_id is None: current_id = songs[0] GLib.idle_add(queue_model.splice, 0, 0, [Gtk.StringObject.new(SongId) for SongId in songs] ) GLib.idle_add(integration.loaded_models.get('currentSong').set_property, 'songId', current_id) if Gio.Settings(schema_id="com.jeffser.Nocturne").get_value('auto-play').unpack(): threading.Thread(target=generate_auto_play_queue, args=(window, False), daemon=True).start() def __play_next(window, songs:list): integration = get_current_integration() __save_playlist_resume(window) integration.loaded_models.get('currentSong').set_property('queueOrigin', "") current_song_id = integration.loaded_models.get('currentSong').get_property('songId') queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') if queue_model.get_property('n-items') == 0 or not current_song_id: __replace_queue(window, songs) else: current_song_index = 0 for i, so in enumerate(list(queue_model)): # so=string object song_id = so.get_string() if song_id in songs and song_id != current_song_id: GLib.idle_add(queue_model.remove, i) elif song_id == current_song_id: current_song_index = i + 1 GLib.idle_add(queue_model.splice, current_song_index, 0, [Gtk.StringObject.new(SongId) for SongId in songs if SongId != current_song_id] ) def __play_later(window, songs:list): integration = get_current_integration() __save_playlist_resume(window) integration.loaded_models.get('currentSong').set_property('queueOrigin', "") current_song_id = integration.loaded_models.get('currentSong').get_property('songId') queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') if queue_model.get_property('n-items') == 0 or not current_song_id: __replace_queue(window, songs) else: for i, so in enumerate(list(queue_model)): # so=string object song_id = so.get_string() if song_id in songs and song_id != current_song_id: GLib.idle_add(queue_model.remove, i) GLib.idle_add(queue_model.splice, queue_model.get_property('n-items'), 0, [Gtk.StringObject.new(SongId) for SongId in songs if SongId != current_song_id] ) # -- MISC -- def reload(window): application = window.get_application() active_window = application.props.active_window if active_window.__gtype_name__ == 'NocturnePopoutWindow': for dialog in active_window.get_dialogs(): if dialog.__gtype_name__ == 'NocturnePageDialog': dialog.navigation_view.get_visible_page().reload() else: active_window.main_navigationview.get_visible_page().reload() def show_error(window, message:str): if app := window.get_application(): dialog = Adw.AlertDialog( heading=_("Error"), body=message or _("No details provided") ) dialog.add_response('close', _('Close')) GLib.idle_add(dialog.choose, app.props.active_window, None, None) def search(window): homepage = None if app := window.get_application(): active_window = app.props.active_window if active_window == app.main_window: active_window.replace_root_page('home') homepage = active_window.main_navigationview.find_page('home') else: for dialog in active_window.get_dialogs(): if dialog.__gtype_name__ == 'NocturnePageDialog': homepage = dialog.navigation_view.find_page('home') if not homepage: __show_page(active_window, Widgets.HomePage()) for dialog in active_window.get_dialogs(): if dialog.__gtype_name__ == 'NocturnePageDialog': homepage = dialog.navigation_view.find_page('home') if homepage: GLib.idle_add(homepage.search_bar.set_search_mode, not homepage.search_bar.get_search_mode()) def launch_playback(window): GLib.idle_add(window.main_stack.set_visible_child_name, 'loading') integration = get_current_integration() prev_month = datetime.now().replace(day=1) - timedelta(days=1) top_songs = [] for songId, plays in integration.getPlaybackScrobble(prev_month.strftime("%m-%Y")): integration.verifySong(songId, use_threading=False) if songId in integration.loaded_models: top_songs.append((songId, plays)) if len(top_songs) > 5: GLib.idle_add(window.main_stack.set_visible_child_name, 'playback') GLib.idle_add(window.main_stack.get_child_by_name('playback').setup, top_songs, prev_month) __replace_queue(window, [s[0] for s in top_songs]) else: toast = Adw.Toast( title=_("Not enough data found for Nocturne Playback ({})").format(prev_month.strftime("%B %Y")), timeout=2 ) GLib.idle_add(window.toast_overlay.add_toast, toast) GLib.idle_add(window.main_stack.set_visible_child_name, 'content') def generate_auto_play_queue(window, replace_on_finish:bool): integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('generatingQueue', True) queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') generated_queue_model = integration.loaded_models.get('currentSong').get_property('generatedQueue') generated_queue_model.remove_all() song_list = [] if queue_model.get_property('n-items') > 0: artists = [] for so in list(queue_model): if so: if model := integration.loaded_models.get(so.get_string()): artists.append(model.artistId) if len(artists) > 0: main_artist = max(set(artists), key=artists.count) song_list = integration.getSimilarSongs(main_artist) # Remove repeated songs, if it ends up being less than 5 then just generate a random queue song_list = [s for s in song_list if s not in [so.get_string() for so in list(queue_model)]] if len(song_list) < 5: song_list = integration.getRandomSongs() generated_queue_model.splice( 0, 0, [Gtk.StringObject.new(s) for s in song_list] ) integration.loaded_models.get('currentSong').set_property('generatingQueue', False) if replace_on_finish: __replace_queue(window, [so.get_string() for so in list(generated_queue_model)]) def set_equalizer_preset(window, preset_name:str): preset = { "flat": [0.0] * 6, "clarity": [-2.0, -4.0, 2.0, 5.0, 3.0, 1.0], "jazz": [4.0, 2.0, 1.0, -2.0, -5.0, -8.0], "rock": [5.0, 2.0, -2.0, 3.0, 2.0, 0.0], "classic": [2.0, -2.0, 0.0, 1.0, 2.0, 4.0], "acoustic": [-2.0, 3.0, 1.0, -2.0, 4.0, 2.0] }.get(preset_name, [0.0] * 6) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") for i, value in enumerate(preset): settings.set_double("eq-band-{}".format(i), value) def replace_root_page(window, page_tag:str): try: index = [i for i, item in enumerate(list(window.main_sidebar.get_items())) if item.get_visible() and item.page_tag == page_tag][0] GLib.idle_add(window.main_sidebar.set_selected, index) except Exception as e: pass GLib.idle_add(window.replace_root_page, page_tag) def visit_url(window, url:str): if url.startswith('file://'): url = Gio.File.new_for_path(url.removeprefix('file://')).get_uri() os.system('xdg-open {}'.format(url)) return Gio.AppInfo.launch_default_for_uri(url, None) def toggle_star(window, model_id:str): integration = get_current_integration() if model_id in integration.loaded_models: model = integration.loaded_models.get(model_id) if model.get_property('starred'): if integration.unstar(model.get_property('id')): model.set_property('starred', False) else: if integration.star(model.get_property('id')): model.set_property('starred', True) def logout(window): integration = get_current_integration() threading.Thread(target=integration.terminate_instance, daemon=True).start() settings = Gio.Settings(schema_id="com.jeffser.Nocturne") settings.set_string('integration-user', '') settings.set_string('selected-instance-type', '') threading.Thread(target=__replace_queue, args=(window,[]), daemon=True).start() GLib.idle_add(window.main_stack.set_visible_child_name, 'welcome') GLib.idle_add(replace_root_page, window, 'home') if settings.get_value('welcome-mode-home').unpack() == 'instance': pfp_destination_path = os.path.join(DATA_DIR, 'pfp') if os.path.isfile(pfp_destination_path): os.remove(pfp_destination_path) settings.set_string("welcome-user-home", "") settings.set_string("welcome-mode-home", "") if window.get_application().player.mpris_published: window.get_application().player.mpris.unpublish() for dialog in window.get_dialogs(): GLib.idle_add(dialog.close) for page in list(window.main_navigationview): if isinstance(page, Adw.NavigationPage): GLib.idle_add(page.reset) def show_external_file_warning(window): dialog = Adw.AlertDialog( heading=_("External File"), body=_("This track was loaded from an external file, this means it will have less features compared to a track inside the library") ) dialog.add_response('close', _('Close')) GLib.idle_add(dialog.choose, window, None, lambda *_: None, None) def update_navidrome_server(window): GLib.idle_add(window.main_stack.set_visible_child_name, 'setup') if dialog := window.get_visible_dialog(): GLib.idle_add(dialog.close) def delete_navidrome_server(window): def response(dialog, task): selected_option = dialog.choose_finish(task) if selected_option == "delete": shutil.rmtree(BASE_NAVIDROME_DIR) elif selected_option == "keep_data": os.remove(os.path.join(BASE_NAVIDROME_DIR, 'navidrome')) elif selected_option == "cancel": return toast = Adw.Toast( title=_("Navidrome server deleted successfully"), timeout=2 ) window.toast_overlay.add_toast(toast) window.main_stack.set_visible_child_name('welcome') if dialog := window.get_visible_dialog(): dialog.close() dialog = Adw.AlertDialog( heading=_("Delete Navidrome Server"), body=_("Are you sure you want to delete the integrated Navidrome server?") ) dialog.add_response('cancel', _('Cancel')) dialog.add_response("keep_data", _("Keep Data")) dialog.add_response("delete", _("Delete Everything")) dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE) GLib.idle_add(dialog.choose, window, None, response) def open_popout_window(window, fullscreened:bool=False): if not window.get_application().popout_window: window.get_application().popout_window = Widgets.PopoutWindow( application=window.get_application(), fullscreened=fullscreened ) GLib.idle_add(window.get_application().popout_window.present) def toggle_fullscreen(window): integration = get_current_integration() if integration.loaded_models.get('currentSong').get_property('queueModel').get_property('n-items') > 0: if popout_window := window.get_application().popout_window: GLib.idle_add(popout_window.present) if popout_window.is_fullscreen(): GLib.idle_add(popout_window.unfullscreen) else: GLib.idle_add(popout_window.fullscreen) else: open_popout_window(window, True) def set_rating(window, data): integration = get_current_integration() if model := integration.loaded_models.get(data.get('model_id')): if new_rating := data.get('rating'): if new_rating == model.get_property('userRating'): new_rating = 0 if integration.setRating(data.get('model_id'), new_rating): model.set_property('userRating', new_rating) # -- PLAYER -- def player_toggle(window): player = window.get_application().player success, state, pending = player.gst.get_state(Gst.CLOCK_TIME_NONE) if success: if state == Gst.State.PLAYING: # Pause the song player.gst.set_state(Gst.State.PAUSED) else: # Play the song player.gst.set_state(Gst.State.PLAYING) def player_play(window): window.get_application().player.gst.set_state(Gst.State.PLAYING) def player_pause(window): window.get_application().player.gst.set_state(Gst.State.PAUSED) def player_next(window): window.get_application().player.handle_song_change_request("next") def player_previous(window): window.get_application().player.handle_song_change_request("previous") def player_raise_volume(window): settings = Gio.Settings(schema_id="com.jeffser.Nocturne") volume = settings.get_value('volume').unpack() volume = max(0, min(1, volume + 0.1)) settings.set_double('volume', volume) def player_lower_volume(window): settings = Gio.Settings(schema_id="com.jeffser.Nocturne") volume = settings.get_value('volume').unpack() volume = max(0, min(1, volume - 0.1)) settings.set_double('volume', volume) # -- RADIO -- def play_radio(window, model_id:str): integration = get_current_integration() if model_id in [so.get_string() for so in integration.loaded_models.get('currentSong').get_property('queueModel')]: integration.loaded_models.get('currentSong').set_property('songId', model_id) else: __replace_queue(window, [model_id]) def update_radio(window, model_id:str=""): integration = get_current_integration() model = integration.loaded_models.get(model_id) if model_id else None def response(dialog, task, name_el, stream_el): if dialog.choose_finish(task) == 'save': name = name_el.get_text().strip() or _("No Name") stream = stream_el.get_text().strip() message = "" if not stream.startswith('http'): stream = 'http://' + stream if name and (stream or not stream_el.get_visible()): integration = get_current_integration() if model_id: result = integration.updateInternetRadioStation( model_id, name, stream ) else: result = integration.createInternetRadioStation( name, stream ) if result: message=_("Radio updated successfully") if model_id else _("Radio added successfully") if model_id: model.set_property('title', name) model.set_property('radioStreamUrl', stream) else: threading.Thread(target=window.main_navigationview.get_visible_page().reload, daemon=True).start() if not message: message = _("Error updating radio") if model_id else _("Error adding radio") toast = Adw.Toast( title=message, timeout=2 ) GLib.idle_add(window.toast_overlay.add_toast, toast) list_box = Gtk.ListBox( selection_mode=Gtk.SelectionMode.NONE, css_classes=['boxed-list'] ) name_el = Adw.EntryRow(title=_("Name")) if model and model.get_property('radioStreamUrl'): name_el.set_text(model.get_property('title')) list_box.append(name_el) stream_el = Adw.EntryRow( title=_("Stream Url") ) if model and model.get_property('radioStreamUrl'): stream_el.set_text(model.get_property('radioStreamUrl')) list_box.append(stream_el) dialog = Adw.AlertDialog( heading=_("Update Radio Station") if model_id else _("Add Radio Station"), extra_child=list_box ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("save", _("Save")) dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED) dialog.choose(window, None, lambda *prms: threading.Thread(target=response, args=prms, daemon=True).start(), name_el, stream_el) def add_radio(window): update_radio(window) def delete_radio(window, model_id:str): integration = get_current_integration() model = integration.loaded_models.get(model_id) def do_delete(): if integration.deleteInternetRadioStation(model_id): message = _("Radio deleted successfully") del integration.loaded_models[model_id] threading.Thread(target=window.main_navigationview.get_visible_page().reload, daemon=True).start() else: message = _("Error deleting radio") toast = Adw.Toast( title=message, timeout=2 ) GLib.idle_add(window.toast_overlay.add_toast, toast) def response(dialog, task): if dialog.choose_finish(task) == 'delete': threading.Thread(target=do_delete, daemon=True).start() dialog = Adw.AlertDialog( heading=_("Delete Radio Station"), body=_("Are you sure you want to delete '{}'?").format(model.get_property('title')) ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("delete", _("Delete")) dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE) GLib.idle_add(dialog.choose, window, None, response) # -- SONG -- def play_song(window, model_id:str): integration = get_current_integration() if model_id in [so.get_string() for so in integration.loaded_models.get('currentSong').get_property('queueModel')]: integration.loaded_models.get('currentSong').set_property('songId', model_id) else: __replace_queue(window, [model_id]) def play_song_from_list(window, data:dict): if song_id := data.get('songId'): __replace_queue(window, data.get('songs', [song_id]), song_id, origin_id=data.get('originId', "")) def play_song_next(window, model_id:str): __play_next(window, [model_id]) __show_custom_toast(window, model_id, 'title', _("Playing Next")) def play_song_later(window, model_id:str): __play_later(window, [model_id]) __show_custom_toast(window, model_id, 'title', _("Playing Later")) def play_songs(window, song_list:list): __replace_queue(window, song_list) def play_songs_next(window, song_list:list): __play_next(window, song_list) if len(song_list)> 1: __show_custom_toast(window, None, _("{} Songs").format(len(song_list)), _("Playing Next"), "list-high-priority-symbolic") else: __show_custom_toast(window, song_list[0], "title", _("Playing Next")) def play_songs_later(window, song_list:list): __play_later(window, song_list) if len(song_list) > 1: __show_custom_toast(window, None, _("{} Songs").format(len(song_list)), _("Playing Later"), "list-low-priority-symbolic") else: __show_custom_toast(window, song_list[0], "title", _("Playing Later")) def edit_lyrics(window): GLib.idle_add(Widgets.LyricsDialog().present, window.get_application().props.active_window) def play_random_queue(window): integration = get_current_integration() __replace_queue(window, integration.getRandomSongs()) def show_song_details(window, model_id): dialog = Widgets.SongDetailsDialog(model_id) GLib.idle_add(dialog.present, window.get_application().props.active_window) # -- ALBUM -- def show_album(window, model_id:str): __show_page(window, Widgets.AlbumPage(model_id)) def show_album_from_song(window, model_id:str): integration = get_current_integration() if model := integration.loaded_models.get(model_id): if album_id := model.get_property('albumId'): __show_page(window, Widgets.AlbumPage(album_id)) def play_album(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if album := integration.loaded_models.get(model_id): __replace_queue(window, [s.get('id') for s in album.get_property('song')], origin_id=model_id) def play_album_next(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if album := integration.loaded_models.get(model_id): __play_next(window, [s.get('id') for s in album.get_property('song')]) __show_custom_toast(window, model_id, 'name', _('Playing Next')) def play_album_later(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if album := integration.loaded_models.get(model_id): __play_later(window, [s.get('id') for s in album.get_property('song')]) __show_custom_toast(window, model_id, 'name', _('Playing Later')) def play_album_shuffle(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if album := integration.loaded_models.get(model_id): song_list = [s.get('id') for s in album.get_property('song')] random.shuffle(song_list) __replace_queue(window, song_list, origin_id=model_id) # -- PLAYLIST -- def show_playlist(window, model_id:str): __show_page(window, Widgets.PlaylistPage(model_id)) def play_playlist(window, model_id:str): integration = get_current_integration() if playlist := integration.loaded_models.get(model_id): integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) __replace_queue(window, [s.get('id') for s in playlist.get_property('entry')], origin_id=model_id) def resume_playlist(window, model_id:str): integration = get_current_integration() integration.verifyPlaylist(model_id, force_update=True, use_threading=False) current_id, timestamp = integration.getPlaylistResume(model_id) integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) __replace_queue( window, [s.get('id') for s in playlist.get_property('entry')], origin_id=model_id, current_id=current_id ) nanoseconds = int(timestamp * Gst.SECOND) GLib.timeout_add(100, lambda: window.get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) and False) def play_playlist_next(window, model_id:str): integration = get_current_integration() if playlist := integration.loaded_models.get(model_id): integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) __play_next(window, [s.get('id') for s in playlist.get_property('entry')]) __show_custom_toast(window, model_id, 'name', _("Playing Next")) def play_playlist_later(window, model_id:str): integration = get_current_integration() if playlist := integration.loaded_models.get(model_id): integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) __play_later(window, [s.get('id') for s in playlist.get_property('entry')]) __show_custom_toast(window, model_id, 'name', _("Playing Later")) def play_playlist_shuffle(window, model_id:str): integration = get_current_integration() if playlist := integration.loaded_models.get(model_id): integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) song_list = [s.get('id') for s in playlist.get_property('entry')] random.shuffle(song_list) __replace_queue(window, song_list, origin_id=model_id) def update_playlist(window, model_id:str=None): integration = get_current_integration() def do_update(name:str): if result := integration.createPlaylist(name, playlistId=model_id): integration.verifyPlaylist(result, force_update=True, use_threading=False) window.setup_sidebar() window.main_navigationview.get_visible_page().reload() message = _("Playlist updated successfully") if model_id else _("Playlist created successfully") else: message = _("Error updating playlist") if model_id else _("Error creating playlist"), toast = Adw.Toast( title=message, timeout=2 ) GLib.idle_add(window.toast_overlay.add_toast, toast) def response(dialog, task, name_el): if dialog.choose_finish(task) == 'create': if name := name_el.get_text(): threading.Thread(target=do_update, args=(name,), daemon=True).start() list_box = Gtk.ListBox( selection_mode=Gtk.SelectionMode.NONE, css_classes=['boxed-list'] ) name_el = Adw.EntryRow(title=_("Name")) if model := integration.loaded_models.get(model_id): name_el.set_text(model.get_property('name')) list_box.append(name_el) dialog = Adw.AlertDialog( heading=_("Update Playlist") if model_id else _("Create Playlist"), extra_child=list_box ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("create", _("Update") if model_id else _("Create")) dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED) GLib.idle_add(dialog.choose, window, None, response, name_el) def create_playlist(window): update_playlist(window) def remove_songs_from_playlist(window, data:dict): playlist_id = data.get('playlist', "") song_list = data.get('indexes', []) if playlist_id and song_list: integration = get_current_integration() if result := integration.updatePlaylist(playlist_id, songIndexToRemove=song_list): __show_custom_toast(window, playlist_id, "name", ngettext("{} Song Removed", "{} Songs Removed", len(song_list)).format(len(song_list))) def prompt_add_songs_to_playlist(window, song_list:list): dialog = Widgets.playlist.PlaylistDialog(song_list) GLib.idle_add(dialog.present, window.get_application().props.active_window) def prompt_add_song_to_playlist(window, model_id:str): dialog = Widgets.playlist.PlaylistDialog([model_id]) GLib.idle_add(dialog.present, window.get_application().props.active_window) def prompt_add_album_to_playlist(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if model := integration.loaded_models.get(model_id): dialog = Widgets.playlist.PlaylistDialog([s.get('id') for s in model.get_property('song')]) GLib.idle_add(dialog.present, window.get_application().props.active_window) def add_songs_to_playlist(window, data): integration = get_current_integration() for dialog in window.get_dialogs(): GLib.idle_add(dialog.close) if data.get('new_playlist'): if response := integration.createPlaylist(name=data.get('new_playlist'), songId=data.get('songs')): integration.verifyPlaylist(response, force_update=True, use_threading=False) message = ngettext("{} Song Added", "{} Songs Added", len(data.get("songs"))).format(len(data.get("songs"))) __show_custom_toast(window, response, "name", message) window.setup_sidebar() elif data.get('playlist'): integration.verifyPlaylist(data.get('playlist'), force_update=True, use_threading=False) if model := integration.loaded_models.get(data.get('playlist')): existing_songs = [e.get('id') for e in model.get_property('entry')] songs = [s for s in data.get('songs') if s not in existing_songs] response = integration.updatePlaylist( playlistId=data.get('playlist'), songIdToAdd=songs ) message = [] if len(songs) > 0: message.append(ngettext("{} Song Added", "{} Songs Added", len(songs)).format(len(songs))) skipped_songs = len(data.get('songs')) - len(songs) if skipped_songs > 0: message.append(ngettext("{} Song Skipped", "{} Songs Skipped", skipped_songs).format(skipped_songs)) __show_custom_toast(window, data.get('playlist'), 'name', ' | '.join(message)) def delete_playlist(window, model_id:str): integration = get_current_integration() model = integration.loaded_models.get(model_id) def response(dialog, task, model): if dialog.choose_finish(task) == "delete": if result := integration.deletePlaylist(model.get_property('id')): __show_custom_toast(window, model.get_property('id'), "name", _("Playlist Deleted")) show_toast(model) del integration.loaded_models[model.id] window.main_navigationview.get_visible_page().reload() window.setup_sidebar() dialog = Adw.AlertDialog( heading=_("Delete Playlist"), body=_("Are you sure you want to delete '{}'?").format(model.get_property('name')) ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("delete", _("Delete")) dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE) GLib.idle_add(dialog.choose, window, None, response, model) # -- ARTIST -- def show_artist(window, model_id:str): __show_page(window, Widgets.ArtistPage(model_id)) def show_artist_from_song(window, model_id:str): integration = get_current_integration() if model := integration.loaded_models.get(model_id): if artist_id := model.get_property('artistId'): __show_page(window, Widgets.ArtistPage(artist_id)) def show_artist_from_album(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if model := integration.loaded_models.get(model_id): if artist_id := model.get_property('artistId'): GLib.idle_add(__show_page, window, Widgets.ArtistPage(artist_id)) elif artists := model.get_property('artists'): if artist_id := artists[0].get('id'): GLib.idle_add(__show_page, window, Widgets.ArtistPage(artist_id)) def play_shuffle_artist(window, model_id:str): integration = get_current_integration() integration.verifyArtist(model_id, force_update=True, use_threading=False) model = integration.loaded_models.get(model_id) if model: songs = [] for album in model.get_property('album'): integration.verifyAlbum(album.get('id'), force_update=True, use_threading=False) album_model = integration.loaded_models.get(album.get('id')) if album_model: songs.extend([s.get('id') for s in album_model.get_property('song')]) if len(songs) > 0: play_songs(window, random.sample(songs, min(20, len(songs)))) def play_radio_artist(window, model_id:str): integration = get_current_integration() songs = integration.getSimilarSongs(model_id) if len(songs) > 0: play_songs(window, songs) else: toast = Adw.Toast( title=_("No songs found") ) GLib.idle_add(window.toast_overlay.add_toast, toast) # -- DOWNLOADS -- def __request_song_download(model_id:str) -> bool: # returns true if download is started (not finished) # should be called in different thread (cause of verifySong) integration = get_current_integration() integration.verifySong(model_id, use_threading=False) if song_model := integration.loaded_models.get(model_id): download_queue = integration.loaded_models.get('currentSong').get_property('downloadQueueModel') download_model = models.SongDownload(songId=model_id) artist_name = song_model.get_property('artist').split(';')[0] if artists := song_model.get_property('artists'): artist_name = artists[0].get('name') file_title = '{} - {} - {}'.format(song_model.get_property('title'), song_model.get_property('album'), artist_name).replace('/', ' ') if any(pathlib.Path(DOWNLOADS_DIR).glob('{}.*'.format(file_title))): return False found, position = download_queue.find_with_equal_func( download_model, lambda item_a, item_b, ud: item_a.get_property('songId') == item_b.get_property('songId'), 0 ) if found: return False GLib.idle_add(download_queue.insert, 0, download_model) callback = lambda frac, md=download_model: md.set_property('progress', frac) threading.Thread(target=integration.downloadSong, args=(model_id, file_title, callback), daemon=True).start() return True return False def download_song(window, model_id:str): is_downloading = __request_song_download(model_id) if is_downloading: __show_custom_toast( window, model_id, 'title', _("Download Started") ) else: __show_custom_toast( window, model_id, 'title', _("Already Downloaded") ) def download_songs(window, model_list:str): if len(model_list) == 1: download_song(window, model_list[0]) return integration = get_current_integration() successful_starts = 0 for songId in model_list: successful_starts += 1 if __request_song_download(songId) else 0 if len(model_list) == successful_starts: __show_custom_toast( window, None, _("{} Songs").format(len(model_list)), _("Download Started") ) elif successful_starts > 0: skipped_songs = len(song_ids) - successful_starts skip_message = ngettext("{} Song Skipped", "{} Songs Skipped", skipped_songs).format(skipped_songs) message = ngettext("{} Song ({})", "{} Songs ({})", successful_starts).format(successful_starts, skip_message) __show_custom_toast( window, None, message, _("Already Downloaded") ) else: __show_custom_toast( window, None, _("Download Skipped"), _("Already Downloaded") ) def download_album(window, model_id:str): integration = get_current_integration() integration.verifyAlbum(model_id, force_update=True, use_threading=False) if model := integration.loaded_models.get(model_id): song_ids = [song.get('id') for song in model.get_property('song')] successful_starts = 0 for songId in song_ids: successful_starts += 1 if __request_song_download(songId) else 0 if len(song_ids) == successful_starts: __show_custom_toast( window, model_id, 'name', _("Download Started") ) elif successful_starts > 0: skipped_songs = len(song_ids) - successful_starts message = ngettext("Download Started ({} Song Skipped)", "Download Started ({} Songs Skipped)", skipped_songs).format(skipped_songs) __show_custom_toast( window, model_id, 'name', message ) else: __show_custom_toast( window, model_id, 'name', _("Already Downloaded") ) def download_playlist(window, model_id:str): integration = get_current_integration() integration.verifyPlaylist(model_id, force_update=True, use_threading=False) if model := integration.loaded_models.get(model_id): song_ids = [song.get('id') for song in model.get_property('entry')] successful_starts = 0 for songId in song_ids: successful_starts += 1 if __request_song_download(songId) else 0 if len(song_ids) == successful_starts: __show_custom_toast( window, model_id, 'name', _("Download Started") ) elif successful_starts > 0: skipped_songs = len(song_ids) - successful_starts message = ngettext("Download Started ({} Song Skipped)", "Download Started ({} Songs Skipped)", skipped_songs).format(skipped_songs) __show_custom_toast( window, model_id, 'name', message ) else: __show_custom_toast( window, model_id, 'name', _("Already Downloaded") ) def __request_download_delete(model_id:str) -> bool: # returns true if download is deleted # should call del loaded_models[id] after toast is shown integration = get_current_integration() if model := integration.loaded_models.get(model_id): try: model.set_property('deleted', True) os.remove(model.get_property('path')) return True except: return False return False def delete_download(window, model_id:str): integration = get_current_integration() deleted = __request_download_delete(model_id) if deleted: __show_custom_toast( window, model_id, 'title', _("Deleted") ) del integration.loaded_models[model_id] def delete_downloads(window, model_list:list): integration = get_current_integration() successful_deletes = 0 for songId in model_list: deleted = __request_download_delete(songId) successful_deletes += 1 if deleted else 0 if deleted: del integration.loaded_models[songId] __show_custom_toast( window, None, _("{} Songs").format(successful_deletes), _("Deleted") ) Jeffser-Nocturne-b442fee/src/constants.py000066400000000000000000000550161523240030000205370ustar00rootroot00000000000000# constants.py import os, subprocess, json, shutil, uuid from tinytag import TinyTag CLI_ARGUMENTS = { "Play Control": { '--player-next': { 'message': 'skip to the next song', 'action-name': 'player_next' }, '--player-previous': { 'message': 'skip to the previous song', 'action-name': 'player_previous' }, '--player-pause': { 'message': 'pauses the current song', 'action-name': 'player_pause' }, '--player-play': { 'message': 'plays the current song', 'action-name': 'player_play' }, '--player-toggle': { 'message': 'toggles between playing and pausing the current song', 'action-name': 'player_toggle' }, '--player-raise-volume': { 'message': 'raises volume', 'action-name': 'player_raise_volume' }, '--player-lower-volume': { 'message': 'lowers volume', 'action-name': 'player_lower_volume' } }, "Songs": { '--song-show': { 'message': 'show details of the specified song', 'action-name': 'show_song_details', 'metavar': 'SONG_ID' }, '--song-play': { 'message': 'play the specified song', 'action-name': 'play_song', 'metavar': 'SONG_ID' }, '--play-song-later': { 'message': 'play the specified song later', 'action-name': 'play_song_later', 'metavar': 'SONG_ID' }, '--play-song-next': { 'message': 'play the specified song next', 'action-name': 'play_song_next', 'metavar': 'SONG_ID' } }, "Albums": { '--album-show': { 'message': 'show the specified album', 'action-name': 'show_album', 'metavar': 'ALBUM_ID' }, '--album-show-from-song': { 'message': 'show the album of the specified song', 'action-name': 'show_album_from_song', 'metavar': 'SONG_ID' }, '--album-play': { 'message': 'play the specified album', 'action-name': 'play_album', 'metavar': 'ALBUM_ID' }, '--album-play-later': { 'message': 'play the specified album later', 'action-name': 'play_album_later', 'metavar': 'ALBUM_ID' }, '--album-play-next': { 'message': 'play the specified album next', 'action-name': 'play_album_next', 'metavar': 'ALBUM_ID' }, '--album-play-shuffle': { 'message': 'shuffle and play the specified album', 'action-name': 'play_album_shuffle', 'metavar': 'ALBUM_ID' }, }, "Artists": { '--artist-show': { 'message': 'show the specified artist', 'action-name': 'show_artist', 'metavar': 'ARTIST_ID' }, '--artist-show-from-album': { 'message': 'show the artist of the specified album', 'action-name': 'show_artist_from_album', 'metavar': 'ALBUM_ID' }, '--artist-show-from-song': { 'message': 'show the artist of the specified song', 'action-name': 'show_artist_from_song', 'metavar': 'SONG_ID' }, '--artist-play-shuffle': { 'message': 'shuffle and play music from the specified artist', 'action-name': 'play_shuffle_artist', 'metavar': 'ARTIST_ID' }, '--artist-play-radio': { 'message': 'start a radio based on music from the specified artist', 'action-name': 'play_radio_artist', 'metavar': 'ARTIST_ID' }, }, "Playlists": { '--playlist-show': { 'message': 'show the specified playlist', 'action-name': 'show_playlist', 'metavar': 'PLAYLIST_ID' }, '--playlist-play': { 'message': 'play the specified playlist', 'action-name': 'play_playlist', 'metavar': 'PLAYLIST_ID' }, '--playlist-play-later': { 'message': 'play the specified playlist later', 'action-name': 'play_playlist_later', 'metavar': 'PLAYLIST_ID' }, '--playlist-play-next': { 'message': 'play the specified playlist next', 'action-name': 'play_playlist_next', 'metavar': 'PLAYLIST_ID' }, '--playlist-play-shuffle': { 'message': 'shuffle and play the specified playlist', 'action-name': 'play_playlist_shuffle', 'metavar': 'PLAYLIST_ID' } }, "Miscellaneous": { '--toggle-fullscreen': { 'message': 'toggle the fullscreen mode', 'action-name': 'toggle_fullscreen' }, '--logout': { 'message': 'logout from the current instance', 'action-name': 'logout' }, '--search': { 'message': 'get top results based on query (JSON)', 'action-name': 'search', 'metavar': 'QUERY' } } } DISCORD_APP_ID = "1504946005764739092" IN_FLATPAK = bool(os.getenv("FLATPAK_ID")) COPYRIGHT_NOTICE = """Nocturne ยฉ 2026 Jeffry Samuel, Jeffser Nocturne is an independent application and is not affiliated with, endorsed, or sponsored by OpenSubsonic, Bandcamp, or Jellyfin. All logos and illustrative assets used within the app are the property of their respective owners. All rights are reserved by their respective holders, and will be taken down if requested. Nocturne functions strictly as a client application. All network connections and data transfers are performed exclusively at the request and authorization of the server owner. Nocturne does not independently access or host any content. Nocturne does not facilitate, encourage, or provide mechanisms for music piracy. Users are responsible for ensuring they have the legal right to access and stream the content hosted on the servers they connect to.""" def get_xdg_home(env: str, default: str) -> str: base = os.getenv(env) or os.path.expanduser(default) if IN_FLATPAK: return base path = os.path.join(base, "com.jeffser.Nocturne") if not os.path.exists(path): os.makedirs(path) return path COMPATIBLE_EXTENSIONS = { 'audio': ['.mp3', '.flac', '.m4a', '.oga', '.ogg', '.opus', '.wav'], 'video': ['.mp4', '.mov', '.m4v', '.avi', '.webm', '.mkv'] } DATA_DIR = get_xdg_home("XDG_DATA_HOME", "~/.local/share") CONFIG_DIR = get_xdg_home("XDG_CONFIG_HOME", "~/.config") CACHE_DIR = get_xdg_home("XDG_CACHE_HOME", "~/.cache") INTEGRATIONS_DIR = os.path.join(DATA_DIR, "integrations") os.makedirs(INTEGRATIONS_DIR, exist_ok=True) # DEPRECATED DONT USE THESE VARIABLES OLD_JELLYFIN_DATA_DIR = os.path.join(DATA_DIR, "jellyfin") if os.path.isdir(OLD_JELLYFIN_DATA_DIR) and not os.path.isdir(os.path.join(INTEGRATIONS_DIR, 'NocturneIntegrationJellyfin')): os.rename(OLD_JELLYFIN_DATA_DIR, os.path.join(INTEGRATIONS_DIR, 'NocturneIntegrationJellyfin')) OLD_LOCAL_DATA_DIR = os.path.join(DATA_DIR, "local") if os.path.isdir(OLD_LOCAL_DATA_DIR) and not os.path.isdir(os.path.join(INTEGRATIONS_DIR, 'NocturneIntegrationLocal')): os.rename(OLD_LOCAL_DATA_DIR, os.path.join(INTEGRATIONS_DIR, 'NocturneIntegrationLocal')) # ---------- DOWNLOAD_QUEUE_DIR = os.path.join(DATA_DIR, 'downloading') if os.path.isdir(DOWNLOAD_QUEUE_DIR): shutil.rmtree(DOWNLOAD_QUEUE_DIR) os.makedirs(DOWNLOAD_QUEUE_DIR, exist_ok=True) DOWNLOADS_DIR = os.path.join(DATA_DIR, 'downloads') os.makedirs(DOWNLOADS_DIR, exist_ok=True) DOWNLOAD_MIME_MAP = { "audio/mpeg": ".mp3", "audio/flac": ".flac", "audio/x-flac": ".flac", "audio/ogg": ".ogg", "audio/opus": ".opus", "audio/wav": ".wav", "audio/mp4": ".m4a", "audio/x-m4a": ".m4a" } # Fallback only used if the system does not have a keyring FALLBACK_PASSWORD_PATH = os.path.join(CONFIG_DIR, 'pass.txt') # Retrieves device ID or generates a UUID if device ID not found # Used for Jellyfin session tracking def get_device_id(): device_id = None if IN_FLATPAK: try: from pydbus import SessionBus bus = SessionBus() flatpak_portal = bus.get("org.freedesktop.portal.Flatpak", "/org/freedesktop/portal/Flatpak") device_id = flatpak_portal.GetMachineId().strip() except Exception as e: pass else: for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]: if os.path.exists(path): with open(path, "r") as f: device_id = f.read().strip() if not device_id: #generate UUID as fallback fallback_path = os.path.join(CONFIG_DIR, "device_id") if os.path.exists(fallback_path): with open(fallback_path, "r") as f: device_id = f.read().strip() else: device_id = str(uuid.uuid4()) with open(fallback_path, "w") as f: f.write(device_id) return device_id BASE_NAVIDROME_DIR = os.path.join(DATA_DIR, "navidrome") os.makedirs(BASE_NAVIDROME_DIR, exist_ok=True) NAVIDROME_ENV = { "ND_DATAFOLDER": BASE_NAVIDROME_DIR, "ND_PORT": "4534", "ND_LOGLEVEL": "ERROR", "ND_ENABLEINSIGHTSCOLLECTOR": "false" } def get_navidrome_path() -> str | None: NAVIDROME_PATH = os.path.join(BASE_NAVIDROME_DIR, 'navidrome') if os.path.isfile(NAVIDROME_PATH): return NAVIDROME_PATH def get_navidrome_env() -> dict: return { **os.environ.copy(), **NAVIDROME_ENV } def check_if_navidrome_ready() -> bool: # checks if admin has already been created navidrome_path = get_navidrome_path() if navidrome_path: try: output = subprocess.check_output([navidrome_path, "user", "list", "-f", "json", "-n", "--loglevel", "error"], stderr=subprocess.STDOUT, env=get_navidrome_env()).strip() output_json = json.loads(output) return len(output_json) > 0 except Exception as e: pass return False NOCTURNE_VERSION = "" def get_nocturne_version() -> str: global NOCTURNE_VERSION return NOCTURNE_VERSION def set_version(version_str:str): global NOCTURNE_VERSION NOCTURNE_VERSION = version_str def get_display_time(seconds:float, show_ms:bool=False) -> str: if not show_ms: total_seconds = round(max(0, seconds)) else: total_seconds = max(0, seconds) hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) if show_ms: seconds_str = f"{seconds:05.2f}" else: seconds_str = f"{int(seconds):02d}" if hours > 0: # Format H:MM:SS.ms return f"{int(hours)}:{int(minutes):02d}:{seconds_str}" else: # Format MM:SS.ms return f"{int(minutes):02d}:{seconds_str}" def _normalize_artists(values:list[str]) -> list[str]: artists = [] for value in values: for artist_name in value.split(';'): artist_name = artist_name.strip() if artist_name and artist_name not in artists: artists.append(artist_name) return artists def get_song_info_from_file(file_path:str, star_list:list=[], is_external_file:bool=False) -> dict | None: tag = TinyTag.get(file_path) if not tag: return None artists = _normalize_artists(tag.as_dict().get('artist', [])) album_artists = _normalize_artists([tag.albumartist or ""]) album_artist = album_artists[0] if album_artists else (artists[0] if artists else "") song = { 'path': file_path, 'coverArt': file_path, 'duration': tag.duration or 0, 'title': tag.title or os.path.basename(file_path), 'album': tag.album or "", 'artist': album_artist, 'artists': [{ 'id': "ARTIST:{}".format(artist_name), 'name': artist_name, 'starred': "ARTIST:{}".format(artist_name) in star_list } for artist_name in artists], 'track': tag.track or 0, 'isExternalFile': is_external_file, 'discNumber': tag.disc or 0, 'year':tag.year or 0, 'albumGain': tag.extra.get('replaygain_album_gain') or tag.extra.get('REPLAYGAIN_ALBUM_GAIN') or 1, 'trackGain': tag.extra.get('replaygain_track_gain') or tag.extra.get('REPLAYGAIN_TRACK_GAIN') or 1 } if not is_external_file: song["artistId"] = "ARTIST:{}".format(album_artist) if album_artist else "" song["albumId"] = "ALBUM:{}".format(song.get("album")) if song.get('album') else "" return song TRANSLATORS = [ "Jeffry Samuel (Spanish) https://jeffser.com", "Jordi Bultรณ (Catalan) https://github.com/formajestically", "Ibai Oihanguren Sala (Basque) https://ibaios.eus", "Martin Prokoph (German) https://github.com/Motschen", "Aleksandr Shamaraev (Russian) https://github.com/AlexanderShad", "gttn-84 (Russian) https://github.com/gttn-84", "Saul Gman (Simplified Chinese) https://github.com/Ja4e", "Muhammed Emin Akalan (Turkish) https://github.com/muhammedeminakalan", "Yuan Chiu (Traditional Chinese) https://yuaner.tw", "Milo Ivir (Croatian) https://github.com/milotype", "Aryan Karamtoth (Telugu) https://github.com/spaciouskarter78", "Rikelry Souza (Brazilian Portuguese) https://github.com/Rikelry" ] PLAYBACK_MODES = { 'consecutive': { 'icon-name': 'media-playlist-consecutive-symbolic', 'display-name': _("Consecutive") }, 'repeat-all': { 'icon-name': 'media-playlist-repeat-symbolic', 'display-name': _("Repeat All") }, 'repeat-one': { 'icon-name': 'media-playlist-repeat-song-symbolic', 'display-name': _("Repeat One") } } BITRATE_OPTIONS = { _("Low ({})"): 64, _("Medium ({})"): 128, _("High ({})"): 192, _("Ultra ({})"): 320, _("Original File"): 0 } # All items and sections must have unique names (even if items are on different sections) SIDEBAR_MENU = { 'root': { # Section (These are not grouped and can't be disabled) 'items': { 'home': { # Item 'title': _("Home"), 'icon-name': "user-home-symbolic", 'page-tag': 'home' } } }, 'albums': { # Section 'title': _("Albums"), 'page-tag': 'albums-all', 'icon-name': "music-queue-symbolic", 'items': { 'albums-all': { # Item 'title': _("All"), 'icon-name': "music-queue-symbolic", 'page-tag': 'albums-all' }, 'albums-random': { # Item 'title': _("Random"), 'icon-name': "playlist-shuffle-symbolic", 'page-tag': 'albums-random' }, 'albums-starred': { # Item 'title': _("Favorites"), 'icon-name': "heart-filled-symbolic", 'page-tag': 'albums-starred' }, 'albums-newest': { # Item 'title': _("Recently Added"), 'icon-name': "list-add-symbolic", 'page-tag': 'albums-newest' }, 'albums-recent': { # Item 'title': _("Recently Played"), 'icon-name': "media-playback-start-symbolic", 'page-tag': 'albums-recent' }, 'albums-frequent': { # Item 'title': _("Most Played"), 'icon-name': "media-playlist-repeat-symbolic", 'page-tag': 'albums-frequent' } } }, 'artists': { # Section 'title': _("Artists"), 'page-tag': 'artists', 'icon-name': "music-artist-symbolic", 'items': { 'artists-all': { # Item 'title': _("All"), 'icon-name': "music-artist-symbolic", 'page-tag': 'artists' }, 'artists-starred': { # Item 'title': _("Favorites"), 'icon-name': "heart-filled-symbolic", 'page-tag': 'artists-starred' } } }, 'songs': { # Section 'title': _("Songs"), 'page-tag': 'songs-all', 'icon-name': "music-note-symbolic", 'items': { 'songs-all': { # Item 'title': _("All"), 'icon-name': "music-note-symbolic", 'page-tag': 'songs-all' }, 'songs-starred': { # Item 'title': _("Favorites"), 'icon-name': "heart-filled-symbolic", 'page-tag': 'songs-starred' }, 'radios': { # Item 'title': _("Radios"), 'icon-name': "sound-wave-symbolic", 'page-tag': 'radios' } } }, 'playlists': { # Section 'title': _("Playlists"), 'page-tag': 'playlists', 'icon-name': "playlist-symbolic", 'items': { 'playlists-all': { # Item 'title': _("All"), 'icon-name': "playlist-symbolic", 'page-tag': "playlists" }, 'playlists-group': { # Item (special) 'title': _("Playlists"), 'icon-name': "playlist-symbolic" } } } } CONTEXT_ALBUM = { "play": { "name": _("Play"), "icon-name": "media-playback-start-symbolic", "action-name": "app.play_album" }, "shuffle": { "name": _("Shuffle"), "icon-name": "playlist-shuffle-symbolic", "action-name": "app.play_album_shuffle" }, "play-next": { "name": _("Play Next"), "icon-name": "list-high-priority-symbolic", "action-name": "app.play_album_next" }, "play-later": { "name": _("Play Later"), "icon-name": "list-low-priority-symbolic", "action-name": "app.play_album_later" }, "add-to-playlist": { "name": _("Add To Playlist"), "icon-name": "playlist-symbolic", "action-name": "app.prompt_add_album_to_playlist" }, "download": { "name": _("Download"), "icon-name": "folder-download-symbolic", "action-name": "app.download_album" }, "show-artist": { "name": _("Show Artist"), "icon-name": "music-artist-symbolic", "action-name": "app.show_artist_from_album" } } CONTEXT_ARTIST = { "shuffle": { "name": _("Shuffle"), "icon-name": "playlist-shuffle-symbolic", "action-name": "app.play_shuffle_artist" }, "radio": { "name": _("Radio"), "icon-name": "sound-symbolic", "action-name": "app.play_radio_artist" } } CONTEXT_PLAYLIST = { "play": { "name": _("Play"), "icon-name": "media-playback-start-symbolic", "action-name": "app.play_playlist" }, "resume": { "name": _("Resume"), "icon-name": "playback-options-symbolic", "action-name": "app.resume_playlist" }, "shuffle": { "name": _("Shuffle"), "icon-name": "media-playlist-shuffle-symbolic", "action-name": "app.play_playlist_shuffle" }, "play-next": { "name": _("Play Next"), "icon-name": "list-high-priority-symbolic", "action-name": "app.play_playlist_next" }, "play-later": { "name": _("Play Later"), "icon-name": "list-low-priority-symbolic", "action-name": "app.play_playlist_later" }, "download": { "name": _("Download"), "icon-name": "folder-download-symbolic", "action-name": "app.download_playlist" }, "edit": { "name": _("Edit"), "icon-name": "document-edit-symbolic", "action-name": "app.update_playlist" }, "delete": { "name": _("Delete"), "css": ["error"], "icon-name": "user-trash-symbolic", "action-name": "app.delete_playlist" } } CONTEXT_SONG = { "select": { "name": _("Select"), "icon-name": "object-select-symbolic" }, "play-next": { "name": _("Play Next"), "icon-name": "list-high-priority-symbolic", "action-name": "app.play_song_next" }, "play-later": { "name": _("Play Later"), "icon-name": "list-low-priority-symbolic", "action-name": "app.play_song_later" }, "edit-radio": { "name": _("Edit"), "icon-name": "document-edit-symbolic", "action-name": "app.update_radio" }, "add-to-playlist": { "name": _("Add To Playlist"), "icon-name": "playlist-symbolic", "action-name": "app.prompt_add_song_to_playlist" }, "download": { "name": _("Download"), "icon-name": "folder-download-symbolic", "action-name": "app.download_song" }, "show-album": { "name": _("Show Album"), "icon-name": "music-queue-symbolic", "action-name": "app.show_album_from_song" }, "show-artist": { "name": _("Show Artist"), "icon-name": "music-artist-symbolic", "action-name": "app.show_artist_from_song" }, "delete-radio": { "name": _("Delete"), "css": ["error"], "icon-name": "user-trash-symbolic", "action-name": "app.delete_radio" }, "remove": { "name": _("Remove"), "css": ["error"], "icon-name": "user-trash-symbolic" }, "delete-download": { "name": _("Delete Download"), "css": ["error"], "icon-name": "user-trash-symbolic", "action-name": "app.delete_download" }, "details": { "name": _("Show Details"), "icon-name": "info-outline-symbolic", "action-name": "app.show_song_details" }, "rating": { "special": "song-rating" } } CONTEXT_MANAGED_NAVIDROME_SERVER = { "visit": { "name": _("Visit Webpage"), "icon-name": "globe-symbolic", "action-name": "app.visit_url", "action-target": "http://127.0.0.1:4534" }, "update": { "name": _("Update Server"), "icon-name": "update-symbolic", "action-name": "app.update_navidrome_server" }, "delete": { "name": _("Delete Server"), "css": ["error"], "icon-name": "user-trash-symbolic", "action-name": "app.delete_navidrome_server" } } CONTEXT_LYRICS = { "edit": { "name": _("Edit Lyrics"), "icon-name": "document-edit-symbolic", "action-name": "app.edit_lyrics" }, "remove": { "name": _("Remove Lyrics"), "css": ["error"], "icon-name": "user-trash-symbolic", } } Jeffser-Nocturne-b442fee/src/icons/000077500000000000000000000000001523240030000172555ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/icons/bandcamp-symbolic.svg000066400000000000000000000026511523240030000233660ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/check-plain-symbolic.svg000066400000000000000000000013361523240030000237760ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/cross-large-circle-filled-symbolic.svg000066400000000000000000000016201523240030000265310ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/cross-large-symbolic.svg000066400000000000000000000015121523240030000240350ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/dialog-warning-symbolic.svg000066400000000000000000000012111523240030000245120ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/document-edit-symbolic.svg000066400000000000000000000010761523240030000243620ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/external-link-symbolic.svg000066400000000000000000000024241523240030000243740ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/folder-download-symbolic.svg000066400000000000000000000011751523240030000247010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/folder-open-symbolic.svg000066400000000000000000000031221523240030000240250ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/globe-symbolic.svg000066400000000000000000000052701523240030000227110ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/go-next-symbolic.svg000066400000000000000000000011761523240030000232030ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/go-previous-symbolic.svg000066400000000000000000000011761523240030000241010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/grid-filled-symbolic.svg000066400000000000000000000013421523240030000237770ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/heart-filled-symbolic.svg000066400000000000000000000011251523240030000241540ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/heart-outline-thick-symbolic.svg000066400000000000000000000023271523240030000255010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/info-outline-symbolic.svg000066400000000000000000000014551523240030000242320ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/jellyfin-symbolic.svg000066400000000000000000000014351523240030000234340ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/list-add-symbolic.svg000066400000000000000000000003441523240030000233170ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/list-drag-handle-symbolic.svg000066400000000000000000000017061523240030000247400ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/list-high-priority-symbolic.svg000066400000000000000000000013561523240030000253710ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/list-low-priority-symbolic.svg000066400000000000000000000013671523240030000252550ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playback-pause-symbolic.svg000066400000000000000000000010071523240030000255710ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playback-start-symbolic.svg000066400000000000000000000010521523240030000256110ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playlist-consecutive-symbolic.svg000066400000000000000000000012241523240030000270570ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playlist-repeat-song-symbolic.svg000066400000000000000000000032621523240030000267600ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playlist-repeat-symbolic.svg000066400000000000000000000024061523240030000260130ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-playlist-shuffle-symbolic.svg000066400000000000000000000032671523240030000261750ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-skip-backward-symbolic.svg000066400000000000000000000013331523240030000254140ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/media-skip-forward-symbolic.svg000066400000000000000000000013321523240030000253010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/music-artist-symbolic.svg000066400000000000000000000012571523240030000242460ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/music-note-symbolic.svg000066400000000000000000000014061523240030000237010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/music-queue-symbolic.svg000066400000000000000000000015061523240030000240610ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/network-offline-symbolic.svg000066400000000000000000000033561523240030000247350ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/network-server-symbolic.svg000066400000000000000000000030701523240030000246120ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/non-starred-symbolic.svg000066400000000000000000000026661523240030000240630ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/object-select-symbolic.svg000066400000000000000000000013471523240030000243450ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/open-menu-symbolic.svg000066400000000000000000000004721523240030000235230ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/opensubsonic-symbolic.svg000066400000000000000000000033541523240030000243310ustar00rootroot00000000000000Created by potrace 1.16, written by Peter Selinger 2001-2019Jeffser-Nocturne-b442fee/src/icons/paintbrush-symbolic.svg000066400000000000000000000010671523240030000240000ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/pan-end-symbolic.svg000066400000000000000000000011071523240030000231360ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/pan-start-symbolic.svg000066400000000000000000000010611523240030000235240ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/paper-symbolic.svg000066400000000000000000000007651523240030000227340ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/person-symbolic.svg000066400000000000000000000006021523240030000231210ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/pip-in-symbolic.svg000066400000000000000000000020001523240030000230010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/pip-out-symbolic.svg000066400000000000000000000016421523240030000232150ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/playback-options-symbolic.svg000066400000000000000000000042721523240030000251010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/playlist-infinite-symbolic.svg000066400000000000000000000031151523240030000252610ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/playlist-shuffle-symbolic.svg000066400000000000000000000032621523240030000251130ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/playlist-symbolic.svg000066400000000000000000000007211523240030000234560ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/playlist2-symbolic.svg000066400000000000000000000006741523240030000235470ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/preferences-system-symbolic.svg000066400000000000000000000037451523240030000254510ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/sad-computer-symbolic.svg000066400000000000000000000043301523240030000242200ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/sidebar-show-right-symbolic.svg000066400000000000000000000012001523240030000253100ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/snapshots-alt-symbolic.svg000066400000000000000000000017551523240030000244250ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/sound-symbolic.svg000066400000000000000000000021071523240030000227450ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/sound-wave-symbolic.svg000066400000000000000000000014471523240030000237130ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/speaker-0-symbolic.svg000066400000000000000000000020701523240030000234030ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/speaker-1-symbolic.svg000066400000000000000000000016221523240030000234060ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/speaker-2-symbolic.svg000066400000000000000000000027121523240030000234100ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/speaker-3-symbolic.svg000066400000000000000000000026311523240030000234110ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/starred-symbolic.svg000066400000000000000000000013171523240030000232630ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/step-back-symbolic.svg000066400000000000000000000015611523240030000234710ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/stopwatch-symbolic.svg000066400000000000000000000020131523240030000236250ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/system-search-symbolic.svg000066400000000000000000000011411523240030000244010ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/text-justify-center-symbolic.svg000066400000000000000000000010551523240030000255530ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/update-symbolic.svg000066400000000000000000000025011523240030000230750ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/user-home-symbolic.svg000066400000000000000000000016421523240030000235240ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/user-trash-symbolic.svg000066400000000000000000000017611523240030000237170ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/video-clip-symbolic.svg000066400000000000000000000016451523240030000236560ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/view-fullscreen-symbolic.svg000066400000000000000000000016011523240030000247250ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/view-list-bullet-symbolic.svg000066400000000000000000000022211523240030000250220ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/view-more-symbolic.svg000066400000000000000000000007631523240030000235350ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/view-unfullscreen-symbolic.svg000066400000000000000000000013071523240030000252730ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/icons/window-close-symbolic.svg000066400000000000000000000013771523240030000242370ustar00rootroot00000000000000 Jeffser-Nocturne-b442fee/src/integrations/000077500000000000000000000000001523240030000206505ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/integrations/__init__.py000066400000000000000000000025611523240030000227650ustar00rootroot00000000000000# __init__.py from .jellyfin import Jellyfin from .navidrome import Navidrome, NavidromeIntegrated, Bandcamp from .local import Local, Offline from .base import Base from . import models, secret from ..constants import DATA_DIR import os, requests from gi.repository import Gio integration = None def get_all_subclasses(cls): subclasses = set() for s in cls.__subclasses__(): subclasses.add(s) subclasses.update(get_all_subclasses(s)) return subclasses def get_available_integrations() -> dict: integrations = {} for cls in get_all_subclasses(Base): integrations[cls.__gtype_name__] = cls return integrations def get_current_integration(): global integration return integration def set_current_integration(new_integration): global integration integration = new_integration settings = Gio.Settings(schema_id="com.jeffser.Nocturne") settings.set_string('selected-instance-type', integration.__gtype_name__) settings.set_string('integration-ip', integration.get_property('url')) settings.set_string('integration-user', integration.get_property('user')) if integration.__gtype_name__ != 'NocturneIntegrationOffline': settings.set_string("integration-library-dir", integration.get_property('libraryDir')) settings.set_boolean("integration-trust-server", integration.get_property('trustServer')) Jeffser-Nocturne-b442fee/src/integrations/base.py000066400000000000000000000641221523240030000221410ustar00rootroot00000000000000# base.py from gi.repository import GLib, GObject, Gdk from . import models, secret, sql_instance from ..constants import get_nocturne_version, INTEGRATIONS_DIR, DATA_DIR import requests, urllib3, time, os, json, threading, logging, syncedlyrics from datetime import datetime from requests.adapters import HTTPAdapter, Retry from concurrent.futures import ThreadPoolExecutor logger = logging.getLogger(__name__) # Just so that the logs don't get cluttered with warnings if trust-server = True urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class CacheManager(GObject.Object): __gtype_name__ = 'NocturneCacheManager' # Completely thread safe timeout = GObject.Property(type=int, default=20) # Max time waiting for origin thread to finish (seconds) permanence = GObject.Property(type=int, default=5) # How long will the cache object last (seconds) results = {} events = {} lock = threading.Lock() def delete_result(self, cache_id:str): with self.lock: if cache_id in self.results: del self.results[cache_id] def insert_result(self, cache_id:str, result:object): with self.lock: self.results[cache_id] = result GLib.timeout_add(self.get_property('permanence') * 1000, self.delete_result, cache_id) def get_result(self, cache_id:str, job:callable, *job_args) -> object: # Will either pull result from cache or do the job (callable) # Call this in a different thread, job will be done in that thread # Job should return a tuple: state, object. # Where state dictates if it should be saved in cache and object is what is returned result = None is_origin = False with self.lock: if result := self.results.get(cache_id): # Case 1: Result is in cache return result if self.events.get(cache_id) is not None: # Case 2: Pending event = self.events.get(cache_id) is_origin = False else: # Case 3: First request, create event if job is None: return None event = threading.Event() self.events[cache_id] = event is_origin = True if is_origin: try: state, result = job(*job_args) if state: self.insert_result(cache_id, result) finally: event.set() with self.lock: del self.events[cache_id] else: event.wait(timeout=self.get_property('timeout')) result = self.results.get(cache_id) if result: logger.info(f'(Cache) Job Skipped : {cache_id}') else: # No result, just do the job at this point state, result = job(*job_args) return result # DO NOT USE DIRECTLY class Base(GObject.Object): __gtype_name__ = 'NocturneIntegrationBase' # For how to fill these checkout navidrome.py and local.py login_page_metadata = {} button_metadata = {} limitations = () # Always have a currentSong inside loaded_models loaded_models = {'currentSong': models.CurrentSong()} url = GObject.Property(type=str) trustServer = GObject.Property(type=bool, default=False) user = GObject.Property(type=str) libraryDir = GObject.Property(type=str) # Show spinner in sidebar with message as tooltip text if set loadingMessage = GObject.Property(type=str) # See example in get_sql_schema sqlSchema = {} # Set up thread executor threads = ThreadPoolExecutor(max_workers=30) # Re-usable session_adapter for shared connection pool _session_adapter = HTTPAdapter( pool_connections=5, pool_maxsize=30, max_retries=Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504] ) ) @property def session(self): session = requests.Session() session.mount("http://", self._session_adapter) session.mount("https://", self._session_adapter) return session # Epic custom lightweight cache manager cache_manager = CacheManager() # For usage in connect_to_current_song song_connections = { 'songId': '', 'connectionId': '', 'callbacks': {} # param : [list of callbacks] } def __init__(self, *args, **kwargs): # do not change super().__init__(*args, **kwargs) self.loaded_models.get('currentSong').connect('notify::songId', lambda *_: self.song_changed()) def current_song_property_changed(self, param:str, value:object): # do not change for callback in self.song_connections.get('callbacks', {}).get(param, []): callback(value) def song_changed(self): # do not change previousSongId = self.song_connections.get('songId', '') currentSongId = self.loaded_models.get('currentSong').get_property('songId') if previousSongId != currentSongId: if previousSong := self.loaded_models.get(previousSongId): try: previousSong.disconnect(self.song_connections.get('connectionId', '')) except: pass if currentSongId: if currentSongId not in self.loaded_models: self.verifySong(currentSongId) if currentSongModel := self.loaded_models.get(currentSongId): self.song_connections['songId'] = currentSongId self.song_connections['connectionId'] = currentSongModel.connect('notify', lambda item, gparam: GLib.idle_add(self.current_song_property_changed, gparam.get_name(), item.get_property(gparam.get_name()))) for param in list(self.song_connections.get('callbacks', {})): self.current_song_property_changed(param, currentSongModel.get_property(param)) def open_json(self, filename:str, fallback={}) -> dict: # please use sql when possible try: with open(os.path.join(self.getIntegrationDir(), filename), 'r') as f: return json.load(f) except: pass return fallback def save_json(self, filename:str, data:dict): # save JSON to instance specific file try: with open(os.path.join(self.getIntegrationDir(), filename), 'w') as f: json.dump(data, f) except Exception: pass def get_sql_schema(self) -> dict: return { 'playlist_resume': { 'id': 'TEXT PRIMARY KEY', 'song_id': 'TEXT NOT NULL', 'timestamp': 'FLOAT DEFAULT 0' }, 'playback_scrobble': { 'month': 'TEXT NOT NULL', 'song_id': 'TEXT NOT NULL', 'amount': 'INTEGER DEFAULT 1', 'UNIQUE': '(month, song_id)' }, 'lyrics': { 'id': 'TEXT PRIMARY KEY', 'type': 'TEXT NOT NULL', 'content': 'TEXT NOT NULL' }, **self.sqlSchema } def check_if_ready(self, row) -> bool: # gets called to see if it is ready to show login page return True def connect_to_current_song(self, parameter:str, callback:callable): # do not modify this function, it works as is in any instance if parameter in list(self.song_connections.get('callbacks')): self.song_connections['callbacks'][parameter].append(callback) else: self.song_connections['callbacks'][parameter] = [callback] if current_song_id := self.loaded_models.get('currentSong').get_property('songId'): if current_song_model := self.loaded_models.get(current_song_id): callback(current_song_model.get_property(parameter)) def connect_to_model(self, model_id:str, parameter:str, callback:callable) -> str: # do not modify this function, it works as is in any instance connection_id = "" if model_id in self.loaded_models: connection_id = self.loaded_models.get(model_id).connect( 'notify::{}'.format(parameter), lambda *_, p=parameter, mid=model_id, cb=callback: cb(self.loaded_models.get(mid).get_property(p)) ) callback(self.loaded_models.get(model_id).get_property(parameter)) return connection_id def save_cache_image(self, model_id:str, size:int, image_data:bytes): # do not modify this function, it works as is in any instance # should be called in updateCoverArt conn, cursor = sql_instance.get_cache_connection() cursor.execute( "INSERT OR IGNORE INTO images (integration, model, size, image) values (?, ?, ?, ?)", (self.__gtype_name__, model_id, size, image_data) ) conn.commit() conn.close() def get_cache_image(self, model_id:str, size:int) -> bytes: # do not modify this function, it works as is in any instance # should be called in updateCoverArt conn, cursor = sql_instance.get_cache_connection() cursor.execute( "SELECT image from images WHERE integration=? AND model=? AND size=?", (self.__gtype_name__, model_id, size) ) result = b'' if row := cursor.fetchone(): result = row[0] or b'' conn.commit() conn.close() return result def start_instance(self) -> bool: # always called in different thread, because it might take a couple of seconds to get started print('WARNING', 'start_instance', 'not implemented') return False def terminate_instance(self): # called when the instance is no longer used print('WARNING', 'terminate_instance', 'not implemented') def on_login(self): # gets called in different thread when the login is successful # optional pass def get_stream_url(self, song_id:str) -> str: # should return a valid url for a gst stream print('WARNING', 'get_stream_url', 'not implemented') return "" def getIntegrationDir(self) -> str: # do not modify this function directory = os.path.join(INTEGRATIONS_DIR, self.__gtype_name__) os.makedirs(directory, exist_ok=True) return directory def getCoverArtBytes(self, model_id:str, size:int) -> bytes: # Used to send bytes to different parts of the codebase instead of full paintables, also called by getCoverArt print('WARNING', 'getCoverArtBytes', 'not implemented') return b'' def updateCoverArt(self, model_id:str): # update both gdkPaintable and gdkPaintableBig print('WARNING', 'updateCoverArt', 'not implemented') def getCoverArtUrl(self, model_id:str) -> str: # Returns URL that can be used to get coverArt directly by external services # Returns empty string when a url is not available print('WARNING', 'getCoverArtUrl', 'not implemented') return "" def ping(self) -> dict: # return True if logged in and connection is successful # when implementing also do super().ping() to prepare SQL try: sql_instance.ensure_schema(self) sql_instance.ensure_cache_schema() return {'status': 'ok'} except: return { 'status': 'error', 'message': _('Could not generate SQL database') } def getAlbumList(self, list_type:str="recent", size:int=10, offset:int=0) -> list: # add non existing elements to self.loaded_models, returns lists of IDs, nothing more # list_type = random, newest, frequent, recent, starred print('WARNING', 'getAlbumList', 'not implemented') return [] def getArtists(self, size:int=10) -> list: # add non existing elements to self.loaded_models, returns lists of IDs, nothing more print('WARNING', 'getArtists', 'not implemented') return [] def getPlaylists(self) -> list: # add non existing elements to self.loaded_models, returns lists of IDs, nothing more print('WARNING', 'getPlaylists', 'not implemented') return [] def getStarred(self, item_type:str) -> list: # returns a list of IDs of items of a given type print('WARNING', 'getStarred', 'not implemented') return [] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): # verifies that element is fully loaded with all it's metadata, should also call for updateCoverArt print('WARNING', 'verifyArtist', 'not implemented') def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): # verifies that element is fully loaded with all it's metadata, should also call for updateCoverArt print('WARNING', 'verifyAlbum', 'not implemented') def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True): # verifies that element is fully loaded with all it's metadata, should also call for updateCoverArt print('WARNING', 'verifyPlaylist', 'not implemented') def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True): # verifies that element is fully loaded with all it's metadata, should also call for updateCoverArt print('WARNING', 'verifySong', 'not implemented') def star(self, model_id:str) -> bool: # stars an element, should return True if change is done print('WARNING', 'star', 'not implemented') return False def unstar(self, model_id:str) -> bool: # unstars an element, should return True if change is done print('WARNING', 'unstar', 'not implemented') return False def getPlayQueue(self) -> tuple: # returns the song ID to be played and a list of IDs print('WARNING', 'getPlayQueue', 'not implemented') return "", [] def savePlayQueue(self, id_list:list, current:str, position:int) -> bool: # save the play queue for retrieving later, called on close, return True if ok print('WARNING', 'savePlayQueue', 'not implemented') return False def getSimilarSongs(self, model_id:str, count:int=20) -> list: # returns list of IDs of similar songs to id, if it can not be implemented just return the result of getRandomSongs print('WARNING', 'getSimilarSongs', 'not implemented') return [] def getRandomSongs(self, size:int=20) -> list: # returns a list of song IDs print('WARNING', 'getRandomSongs', 'not implemented') return [] def saveLyrics(self, songId:str, content:str, lyrics_type:str): # Do not modify, works as is conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO lyrics (id, type, content) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET type = excluded.type, content = excluded.content """ cursor.execute(query, (songId, lyrics_type, content)) conn.commit() conn.close() def deleteLyrics(self, songId:str): conn, cursor = sql_instance.get_connection(self) cursor.execute("DELETE FROM lyrics WHERE id = ?", (songId,)) conn.commit() conn.close() # Do not modify, works as is def getLyrics(self, songId:str, requestOnline:bool=False) -> tuple: # call this function first super().getLyrics(args) # it loads lyrics from db, as a last resource if model := self.loaded_models.get(songId): # Radio if model.get_property('radioStreamUrl'): return 'radio', '' # Online if requestOnline: def job(track_name, artist_name): return True, syncedlyrics.search( "[{}] [{}]".format(track_name, artist_name), enhanced=True, synced_only=True ) if content := self.cache_manager.get_result(f'OnlineLyrics:{songId}', job, model.get_property('title'), model.get_property('artist')): self.saveLyrics(songId, content, 'lrc') return 'lrc', content else: return 'not-found', '' # Legacy: Move existing lrc files to DB file_name_without_ext = '{}|{}|{}|{}'.format( model.get_property('title'), model.get_property('artist'), model.get_property('album') or model.get_property('title'), model.get_property('duration') ) lrc_path = os.path.join(DATA_DIR, 'lyrics', file_name_without_ext+'.lrc') plain_path = os.path.join(DATA_DIR, 'lyrics', file_name_without_ext+'.txt') if os.path.isfile(lrc_path): with open(lrc_path, 'r') as f: if content := f.read(): self.saveLyrics(songId, content, 'lrc') try: os.remove(lrc_path) except: pass if os.path.isfile(plain_path): try: os.remove(plain_path) except: pass else: return 'not-found', '' conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT type, content FROM lyrics WHERE id = ?", (songId,)) row = cursor.fetchone() conn.close() if row and row[0] in ('plain', 'lrc') and row[1]: return row[0], row[1] return 'not-found', '' def search(self, query:str, artistCount:int=0, artistOffset:int=0, albumCount:int=0, albumOffset:int=0, songCount:int=0, songOffset:int=0, playlistCount:int=0, playlistOffset:int=0) -> dict: # returns a dict with results trucated with the count and offset, the dict has keys for album, artist and song, the values are lists of IDs # for an example view local.py print('WARNING', 'search', 'not implemented') return {'artist': [], 'album': [], 'song': [], 'playlist': []} def systemSearch(self, query:str): # similar to 'search' but it will always just return the top 5 results for each category # and instead of separating categories it is a dict of other dicts like this # {'ID': {'display': VARIANT, 'type': VARIANT, 'icon': VARIANT}} # The values for display, type and icon should be GVariants, see Jellyfin for example print('WARNING', 'systemSearch', 'not implemented') return {} def getInternetRadioStations(self) -> list: # returns a list of Song IDs with the property radioStreamUrl set # make sure the id also exists in self.loaded_models, no need to be verified print('WARNING', 'getInternetRadioStations', 'not implemented') return [] def createInternetRadioStation(self, name:str, radioStreamUrl:str) -> bool: # returns True if created successfully print('WARNING', 'createInternetRadioStation', 'not implemented') return False def updateInternetRadioStation(self, model_id:str, name:str, radioStreamUrl:str) -> bool: # returns True if updated successfully print('WARNING', 'updateInternetRadioStation', 'not implemented') return False def deleteInternetRadioStation(self, model_id:str) -> bool: # returns True if deleted successfully print('WARNING', 'deleteInternetRadioStation', 'not implemented') return False def createPlaylist(self, name:str=None, playlistId:str=None, songId:list=[]) -> str: # returns id if created successfully print('WARNING', 'createPlaylist', 'not implemented') return "" def updatePlaylist(self, playlistId:str, songIdToAdd:list=[], songIndexToRemove:list=[]) -> bool: # returns True if updated successfully print('WARNING', 'updatePlaylist', 'not implemented') return False def deletePlaylist(self, model_id:str) -> bool: # returns True if deleted successfully print('WARNING', 'deletePlaylist', 'not implemented') return False def setRating(self, model_id:str, rating:int=0) -> bool: # returns True if rated successfully print('WARNING', 'setRating', 'not implemented') return False def getTopSongs(self, artist_id:str, count:int=10) -> list: # returns list of ids print('WARNING', 'getTopSongs', 'not implemented') return [] def downloadSong(self, model_id:str, file_title:str, progress_callback:callable): # from constants.py # file_title does NOT include extension (.mp3, .flac, etc) # download into DOWNLOAD_QUEUE_DIR # on finish move file to DOWNLOADS_DIR # see navidrome.py for example print('WARNING', 'downloadSong', 'not implemented') def scrobble(self, model_id:str, submission:bool=True): # the id is for a Song, this is how views are stored # called when a song is played # if you need to inherit this, also call super().scrobble(id) so that listenbrainz can also get the scrobble # Playback (monthly scrobble) date_formated = datetime.now().strftime("%m-%Y") conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO playback_scrobble (month, song_id) VALUES (?, ?) ON CONFLICT(month, song_id) DO UPDATE SET amount = amount + 1; """ cursor.execute(query, (date_formated, model_id)) conn.commit() conn.close() # ListenBrainz if model := self.loaded_models.get(model_id): if token := secret.get_plain_password("listenbrainz"): listen_payload = { "track_metadata": { "artist_name": model.get_property("artist"), "track_name": model.get_property("title"), "release_name": model.get_property("album"), "additional_info": { "submission_client": "com.jeffser.Nocturne", "submission_client_version": get_nocturne_version(), "media_player": "Nocturne" } } } if submission: listen_payload["listened_at"] = int(time.time() - (self.loaded_models.get('currentSong').get_property('positionSeconds') or 0)) payload = { "listen_type": "single" if submission else "playing_now", "payload": [listen_payload] } headers = { "Authorization": f"Token {token}", "Content-Type": "application/json" } try: response = requests.post("https://api.listenbrainz.org/1/submit-listens", json=payload, headers=headers) except: pass # Playlist Resume queue_origin_id = self.loaded_models.get('currentSong').get_property('queueOrigin') current_timestamp = self.loaded_models.get('currentSong').get_property('positionSeconds') if model := self.loaded_models.get(queue_origin_id): if isinstance(model, models.Playlist): conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO playlist_resume (id, song_id, timestamp) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET song_id = excluded.song_id, timestamp = excluded.timestamp; """ cursor.execute(query, (queue_origin_id, model_id, current_timestamp)) conn.commit() conn.close() def getPlaylistResume(self, model_id:str) -> tuple: # Works as is, no need to modify # Returns song_id, timestamp (seconds float) if playlist := self.loaded_models.get(model_id): conn, cursor = sql_instance.get_connection(self) cursor.execute( "SELECT song_id, timestamp FROM playlist_resume WHERE id=?", (playlist.get_property('id'),) ) result = cursor.fetchone() conn.close() if result: return result[0], result[1] return "", 0 def savePlaylistResume(self, queue_origin_id:str, song_id:str, current_timestamp:float): # Works as is, no need to modify if model := self.loaded_models.get(queue_origin_id): if isinstance(model, models.Playlist): conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO playlist_resume (id, song_id, timestamp) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET song_id = excluded.song_id, timestamp = excluded.timestamp; """ cursor.execute(query, (queue_origin_id, song_id, current_timestamp)) conn.commit() conn.close() def getSongDetails(self, model_id:str) -> models.SongDetails: # Fill and return songDetails # Do NOT add it to loaded_models return models.SongDetails() def getPlaybackScrobble(self, month:str, top:int=50) -> list: # Works as is, no need to modify # Month in format %m-%Y # Returns list of tuples (song_id, amount) conn, cursor = sql_instance.get_connection(self) query = """ SELECT song_id, amount FROM playback_scrobble WHERE month = ? ORDER BY amount DESC LIMIT ?; """ cursor.execute(query, (month, top)) results = cursor.fetchall() conn.close() return results def getServerInformation(self) -> dict: # should return these keys: # picture : gdk.Paintable # username : str # title : str # link : str print('WARNING', 'getServerInformation', 'not implemented') return {} Jeffser-Nocturne-b442fee/src/integrations/discord_rpc.py000066400000000000000000000232721523240030000235230ustar00rootroot00000000000000# discord_rpc.py import json, os, socket, struct, threading, time, uuid, requests, ipaddress from urllib.parse import urlparse from gi.repository import GLib, Gst, Gio from . import get_current_integration from ..constants import DISCORD_APP_ID class DiscordRPC: MAX_FIELD_LENGTH = 127 def __init__(self, player): self.player = player self.socket = None self.state_lock = threading.Lock() self.socket_lock = threading.Lock() self.pending = None self.worker_running = False self.generation = 0 self.update_source_id = None def close(self): with self.state_lock: self.generation += 1 self.pending = None if self.update_source_id: GLib.source_remove(self.update_source_id) self.update_source_id = None with self.socket_lock: self._clear() if self.socket: try: self.socket.close() except OSError: pass self.socket = None def update(self): if not self.player.settings.get_value("discord-rpc-enabled").unpack(): self.close() return with self.state_lock: if self.update_source_id: GLib.source_remove(self.update_source_id) self.update_source_id = GLib.timeout_add(500, self._queue_update) def _queue_update(self): activity = self._get_activity() with self.state_lock: self.update_source_id = None self.generation += 1 self.pending = (self.generation, activity) if self.worker_running: return GLib.SOURCE_REMOVE self.worker_running = True threading.Thread(target=self._run, daemon=True).start() return GLib.SOURCE_REMOVE def _run(self): while True: with self.state_lock: if not self.pending: self.worker_running = False return generation, activity = self.pending if integration := get_current_integration(): success, state, pending = self.player.gst.get_state(0) if state == Gst.State.PLAYING: if song_id := integration.loaded_models.get("currentSong").get_property("songId"): activity["assets"]["large_image"] = self._get_cover_art(song_id) or "logo" self.pending = None with self.socket_lock: with self.state_lock: if generation != self.generation: continue if not self.socket and not self._connect(): continue with self.state_lock: if self.pending or generation != self.generation: continue if not self._set_activity(activity): self._disconnect() def _set_activity(self, activity): return self._send(1, { "cmd": "SET_ACTIVITY", "args": { "pid": os.getpid(), "activity": activity }, "nonce": uuid.uuid4().hex }) def _get_cover_art(self, song_id) -> str: # Returns public URL for cover art or empty string integration = get_current_integration() if not song_id or not integration: return "" if Gio.Settings(schema_id="com.jeffser.Nocturne").get_value('discord-instance-art-share').unpack(): # Check if getCoverArtUrl provides a public endpoint if url_str := integration.getCoverArtUrl(song_id): parsed_url = urlparse(url_str) scheme = parsed_url.scheme.lower() if scheme in ('http', 'https'): hostname = parsed_url.hostname is_domain = False try: ipaddress.ip_address(hostname) except ValueError: is_domain = True if is_domain: return url_str mbid = "" if song_details := integration.getSongDetails(song_id): mbid = song_details.get_property('musicBrainzId') if not mbid: if song_model := integration.loaded_models.get(song_id): # Get MusicBrainz ID headers = { "User-Agent": "Nocturne/1.0 ( https://jeffser.com/nocturne )" } search_url = "https://musicbrainz.org/ws/2/release" artist_name = song_model.get_property("artist") if artists := song_model.get_property("artists"): if len(artists) > 0: artist_name = artists[0].get('name') or artist_name params = { "query": f'release:"{song_model.get_property("album")}" AND artist:"{artist_name}"', "fmt": "json" } try: response = requests.get(search_url, headers=headers, params=params) response.raise_for_status() data = response.json() if not data.get("releases"): return "" mbid = data.get("releases")[0].get("id") except requests.exceptions.RequestException as e: return "" if mbid: caa_url = f"https://coverartarchive.org/release/{mbid}/front-500" image_response = requests.get(caa_url, headers=headers, allow_redirects=True) if image_response.status_code == 200: return image_response.url or "" return "" def _get_activity(self): integration = get_current_integration() if not integration: return None current_song = integration.loaded_models.get("currentSong") song_id = current_song.get_property("songId") if not song_id: return None song = integration.loaded_models.get(song_id) success, state, pending = self.player.gst.get_state(0) if current_song.get_property("buttonState") == "play" and pending != Gst.State.PLAYING: return { "details": _("Browsing"), "type": 0, "assets": { "large_image": "logo", "large_text": "Nocturne" } } if song and song.get_property("radioStreamUrl"): title = current_song.get_property("displaySongTitle") or song.get_property("title") artist = current_song.get_property("displaySongArtist") or song.get_property("artist") else: title = song.get_property("title") if song else current_song.get_property("displaySongTitle") if song and song.get_property("artists"): artist = ", ".join([a.get("name") for a in song.get_property("artists")]) else: artist = song.get_property("artist") if song else current_song.get_property("displaySongArtist") activity = { "details": self._truncate(title or _("Listening to music")), "state": self._truncate(artist) if artist else None, "type": 2, "assets": { "large_text": self._truncate(song.get_property("album") if song else "Nocturne") } } activity["assets"]["large_image"] = "logo" if song and song.get_property("duration") > 0: position = max(current_song.get_property("positionSeconds"), 0) started_at = int(time.time() - position) activity["timestamps"] = { "start": started_at, "end": started_at + song.get_property("duration") } return activity def _connect(self): for path in self._get_socket_paths(): try: rpc_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) rpc_socket.settimeout(2) rpc_socket.connect(path) self.socket = rpc_socket if self._send(0, {"v": 1, "client_id": DISCORD_APP_ID}): self._receive() return True except OSError: self._disconnect() return False def _get_socket_paths(self): dirs = [ os.environ.get("XDG_RUNTIME_DIR"), os.environ.get("TMPDIR"), "/tmp" ] return [ os.path.join(directory, "discord-ipc-{}".format(index)) for directory in dirs if directory and os.path.isdir(directory) for index in range(10) ] def _truncate(self, field): return field if len(field) <= self.MAX_FIELD_LENGTH else field[:self.MAX_FIELD_LENGTH - 1] + "..." def _send(self, op, payload): try: data = json.dumps(payload).encode("utf-8") self.socket.sendall(struct.pack(" str: return 'MediaBrowser Client="Nocturne", Device="{}", DeviceId="{}", Version="{}"'.format(platform.node(), get_device_id(), get_nocturne_version()) def get_base_header(self) -> dict: headers = { "Authorization": self.AUTH_HEADER, "Accept": "application/json" } if token := self.get_property('accessToken'): headers["Authorization"] += ', Token="{}"'.format(token) return headers def get_url(self, action:str, **keys) -> str: action = action.format(userId=self.get_property('userId'), **keys) return '{}/{}'.format(self.get_property('url').strip('/'), action) def make_request(self, action:str, json:dict={}, params:dict={}, mode:str="GET", action_keys:dict={}) -> dict: def request_job(url): try: with self.session as current_session: if mode == 'GET': response = current_session.get( url, params=params, json=json, headers=self.get_base_header(), verify=not self.get_property('trustServer'), timeout=(3.05, 10) ) elif mode == 'POST': response = current_session.post( url, params=params, json=json, headers=self.get_base_header(), verify=not self.get_property('trustServer'), timeout=(3.05, 10) ) elif mode == 'DELETE': response = current_session.delete( url, params=params, json=json, headers=self.get_base_header(), verify=not self.get_property('trustServer'), timeout=(3.05, 10) ) elif mode == 'RAWGET': # Get without calling json() response = current_session.get( self.get_url(action, **action_keys), params=params, json=json, headers=self.get_base_header(), verify=not self.get_property('trustServer'), timeout=(3.05, 10) ) return response.status_code in (200, 201), response if response.status_code in (200, 201): return True, response.json() elif response.status_code == 204: return True, {'state': 'ok'} except Exception as e: logger.error(f"action error {action}: {e}") return False, {} action_url = self.get_url(action, **action_keys) request_id = '({}) {}?{}'.format(mode, action_url, urlencode(params)) return self.cache_manager.get_result(request_id, request_job, action_url) def get_rating(self, model_id) -> int: conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT rating FROM ratings WHERE id = ?", (model_id,)) result = cursor.fetchone() conn.close() return result[0] if result else 0 def start_instance(self) -> bool: return True def terminate_instance(self): pass def get_stream_url(self, song_id:str) -> str: if model := self.loaded_models.get(song_id): if radioStreamUrl := model.get_property('radioStreamUrl'): try: with self.session.get(radioStreamUrl, stream=True, timeout=10) as r: r.raise_for_status() content_type = r.headers.get('Content-Type', '').lower() if 'mpegurl' in content_type or 'text/plain' in content_type or 'octet-stream' in content_type: # It is a playlist text file, extract url for line in r.iter_lines(decode_unicode=True): line = line.decode('utf-8') if line and not line.startswith('#'): return line.strip() except: pass return radioStreamUrl elif model.get_property('isExternalFile'): return 'file://{}'.format(model.get_property('path')) base_url = self.get_url('Audio/{}/stream'.format(song_id)) max_bitrate = Gio.Settings(schema_id="com.jeffser.Nocturne").get_value('max-bitrate').unpack() if max_bitrate == 0: return '{}?static=true&api_key={}'.format( base_url, self.get_property('accessToken') ) else: return '{}?static=true&audioBitrate={}&api_key={}'.format( base_url, max_bitrate*1000, self.get_property('accessToken') ) def initiateQuickConnect(self) -> dict: return self.make_request( action='QuickConnect/Initiate', mode='POST', ) def checkQuickConnect(self, secret_str:str) -> bool: response = self.make_request( action='QuickConnect/Connect', params={'secret': secret_str} ) if response.get('Authenticated'): secret.store_password(response.get("Secret")) return True return False def getCoverArtBytes(self, model_id:str, size:int) -> bytes: try: if not model_id: return b'' url = 'Items/{id}/Images/Primary' if model := self.loaded_models.get(model_id): if image_url := self.loaded_models.get(model_id).get_property('coverArt'): if image_url != "None": url = image_url else: return b'' #will otherwise return a 404 error response = self.make_request( action=url, action_keys={'id': model_id}, params={ 'maxWidth': size, 'quality': 90 }, mode="RAWGET" ) response.raise_for_status() return response.content except Exception as e: logger.error(f"can't get image from {model_id}: {e}") return b'' def updateCoverArt(self, model_id:str=''): if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): local.Local.updateCoverArt(self, model_id) return if model.get_property('coverArt') == "None": return None #will otherwise return a 404 error sizes = { 'gdkPaintableBig': 720, 'gdkPaintable': 240 } for property_name, size in sizes.items(): if not model.get_property(property_name): raw_bytes = self.get_cache_image(model_id, size) save_cache = not raw_bytes if not raw_bytes: raw_bytes = self.getCoverArtBytes(model_id, size) if not raw_bytes and isinstance(model, models.Song): raw_bytes = self.getCoverArtBytes(model.get_property('albumId'), size) if raw_bytes: model.set_property('coverArt', model.get_property('albumId')) # For getCoverArtUrl if raw_bytes: try: gbytes = GLib.Bytes.new(raw_bytes) texture = Gdk.Texture.new_from_bytes(gbytes) model.set_property(property_name, texture) if save_cache: self.save_cache_image(model_id, size, raw_bytes) except Exception as e: logger.error(f"can't convert image from {model_id} (size {size}): {e}") def getCoverArtUrl(self, model_id) -> str: if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): return "" params = { 'maxWidth': 240, 'quality': 90 } if token := self.get_property('accessToken'): params['api_key'] = token if model.get_property('coverArt') and model.get_property('coverArt') != "None": url = self.get_url(model.get_property('coverArt')) else: url = self.get_url('Items/{id}/Images/Primary', id=model_id) return '{}?{}'.format(url, urlencode(params)) return "" def ping(self) -> dict: self.set_property('accessToken', "") self.set_property('userId', "") response = self.make_request( action='Users/AuthenticateWithQuickConnect', json={ "Secret": secret.get_plain_password() }, mode='POST' ) self.set_property('accessToken', response.get('AccessToken')) self.set_property('userId', response.get('User', {}).get('Id')) if self.get_property("accessToken") and self.get_property("userId"): self.set_property("user", response.get('User', {}).get('Name')) else: response = self.make_request( action='Users/AuthenticateByName', json={ 'Username': self.get_property('user'), 'Pw': secret.get_plain_password() }, mode='POST' ) self.set_property('accessToken', response.get('AccessToken')) self.set_property('userId', response.get('User', {}).get('Id')) if self.get_property('accessToken') and self.get_property('userId'): libraries = self.make_request( action='Users/{userId}/Views', mode='GET' ).get("Items", []) for library in libraries: library_found = False if library.get("CollectionType") == "music": if not library_found: library_found = True self.set_property('libraryId', library.get("Id")) else: #TODO implement method for selecting a library self.set_property('libraryId', '') logger.warning("Multiple music libraries found, reverting to include all Jellyfin libraries.") break return super().ping() return { 'status': 'error', 'message': _('Could not log in') } def getAlbumList(self, list_type:str="recent", size:int=10, offset:int=0) -> list: params = { "IncludeItemTypes": "MusicAlbum", "Recursive": "true", "Limit": size, "StartIndex": offset, "Fields": "ArtistItems,IsFavorite", "ParentId": self.get_property("libraryId") } if list_type == "random": params["SortBy"] = "Random" elif list_type == "newest": params["SortBy"] = "DateCreated" params["SortOrder"] = "Descending" elif list_type == "frequent": params["SortBy"] = "PlayCount" params["SortOrder"] = "Descending" elif list_type == "recent": params["SortBy"] = "DatePlayed" params["SortOrder"] = "Descending" elif list_type == "starred": params["Filters"] = "IsFavorite" albums = self.make_request( action='Users/{userId}/Items', mode='GET', params=params ).get('Items', []) self.__bulk_verify("MusicAlbum", albums) return [album.get("Id") for album in albums] def getArtists(self, size:int=10) -> list: artists = self.make_request( action='Artists/AlbumArtists', mode='GET', params={ "Limit": size, "Recursive": "true", "Fields": "Overview,SimilarItems,UserData", "SortBy": "Random", "SortOrder": "Ascending", "ParentId": self.get_property("libraryId") } ).get('Items', []) self.__bulk_verify("MusicArtist", artists) return [artist.get("Id") for artist in artists] def getPlaylists(self) -> list: playlists = self.make_request( action='Users/{userId}/Items', mode='GET', params={ "IncludeItemTypes": "Playlist", "Recursive": "true", "Fields": "None" } ).get('Items', []) id_list = [] self.__bulk_verify("Playlist", playlists) return [playlist.get("Id") for playlist in playlists] def getStarred(self, item_type:str) -> list: check = { "artist": "MusicArtist", "album": "MusicAlbum", "song": "Audio", "playlist": "Playlist" } if item_type == 'artist': items = self.make_request( action='Artists/AlbumArtists', mode="GET", params={ "userId": self.get_property("userId"), "parentId": self.get_property("libraryId"), "Recursive": "true", "Filters": "isFavorite" } ).get('Items', []) else: params = { "IncludeItemTypes": check[item_type], "Recursive": "true", "Fields": "Id", "Filters": "IsFavorite" } if item_type != 'playlist': params["ParentId"] = self.get_property("libraryId") items = self.make_request( action="Users/{userId}/Items", mode="GET", params=params ).get("Items", []) self.__bulk_verify(check[item_type], items) return [item.get("Id") for item in items] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True, artist_object:models.Artist=None, lite:bool=False): def run(): artist = artist_object if artist is None: artist = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if artist.get("Id"): primary_tag = artist.get('ImageTags', {}).get('Primary', '') cover_art = f"Items/{model_id}/Images/Primary?={primary_tag}" if primary_tag else "None" self.loaded_models.get(model_id).update_data( id=artist.get("Id"), name=artist.get("Name"), coverArt=cover_art, starred=artist.get("UserData", {}).get("IsFavorite", False), biography=artist.get("Overview", ""), userRating=self.get_rating(artist.get("Id")) ) #Queue background session requests in order of importance: albums -> coverArt -> similar artists self.threads.submit(get_albums) self.threads.submit(self.updateCoverArt, artist.get("Id")) if not lite: self.threads.submit(get_similar) elif model_id in self.loaded_models: del self.loaded_models[model_id] def get_albums(): params={ "AlbumArtistIds": [model_id], "IncludeItemTypes": "MusicAlbum", "Recursive": "true", "SortBy": "PremiereDate" } if lite: params["Limit"]=0 #Prevents complex db query params["Fields"]=None params["EnableImages"]="false" params["EnableUserData"]="false" del params["SortBy"] albums_request = self.make_request( action='Users/{userId}/Items', mode="GET", params=params ) albums = albums_request.get("Items", []) if not lite: self.__bulk_verify("MusicAlbum", albums) self.loaded_models.get(model_id).update_data( albumCount=albums_request.get("TotalRecordCount"), album=[{"id": alb.get("Id"), "name": alb.get("Name")} for alb in albums], ) def get_similar(): similar = self.make_request( action='/Items/{id}/Similar?userId={userId}', action_keys={"id": model_id}, params={"limit": 12}, mode="GET" ).get("Items", []) self.__bulk_verify("MusicArtist", similar) self.loaded_models.get(model_id).update_data( similarArtist=[{"id": sim.get("Id"), "name": sim.get("Name")} for sim in similar] ) if not model_id or not model_id.strip(): logger.debug("Empty Artist model_id, aborting.") return if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Artist(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(run) else: run() def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True, album_object:models.Album=None, lite:bool=False): def run(): album = album_object if album is None: album = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if album.get("Id"): songs=[] if not lite: songs = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "ParentId": model_id, "IncludeItemTypes": "Audio", "Recursive": "true", "Fields": "RunTimeTicks,IndexNumber,ParentIndexNumber,ProductionYear", "SortBy": "ParentIndexNumber,IndexNumber", "SortOrder": "Ascending" } ).get("Items", []) primary_tag = album.get('ImageTags', {}).get('Primary', '') cover_art = f"Items/{model_id}/Images/Primary?={primary_tag}" if primary_tag else "None" duration = int(sum(song.get("RunTimeTicks", 0) for song in songs) / 10000000) for i, song in enumerate(songs): if model := self.loaded_models.get(song.get("Id")): model.update_data(track=song.get("IndexNumber") or i) self.loaded_models.get(model_id).update_data( id=album.get("Id"), name=album.get("Name"), artist=album.get("AlbumArtist"), artistId=album.get("ArtistItems", [{}])[0].get("Id") if album.get("ArtistItems") else None, coverArt=cover_art, songCount=len(songs), duration=duration, artists=[{"id": art.get("Id"), "name": art.get("Name")} for art in album.get("ArtistItems", [])], song=[{"id": song.get("Id"), "name": song.get("Name")} for song in songs], starred=album.get("UserData", {}).get("IsFavorite", False), userRating=self.get_rating(album.get("Id")), year=album.get("ProductionYear", 0) ) self.threads.submit(self.updateCoverArt, album.get("Id")) elif model_id in self.loaded_models: del self.loaded_models[model_id] if not model_id or not model_id.strip(): logger.debug("Empty Album model_id, aborting.") return if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Album(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(run) else: run() def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True, playlist_object:models.Playlist=None, lite:bool=False): def run(): playlist = playlist_object if playlist is None: playlist = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if playlist.get("Id"): primary_tag = playlist.get('ImageTags', {}).get('Primary', '') cover_art = f"Items/{model_id}/Images/Primary?={primary_tag}" if primary_tag else "None" self.loaded_models.get(model_id).update_data( id=playlist.get("Id"), name=playlist.get("Name"), coverArt=cover_art ) if use_threading: self.threads.submit(get_songs) else: get_songs() self.threads.submit(self.updateCoverArt, playlist.get("Id")) elif model_id in self.loaded_models: del self.loaded_models[model_id] def get_songs(): params = { "UserId": self.get_property("userId"), "Fields": "RunTimeTicks" } if(lite): params["Limit"]=0 params["Fields"]=None params["EnableImages"]="false" params["EnableUserData"]="false" songs_response = self.make_request( action='Playlists/{id}/Items', action_keys={"id": model_id}, mode="GET", params=params ) songs = songs_response.get("Items", []) duration = int(sum(song.get("RunTimeTicks", 0) for song in songs) / 10000000) self.loaded_models.get(model_id).update_data( songCount=songs_response.get("TotalRecordCount"), duration=duration, entry=[{"id": song.get("Id"), "name": song.get("Name")} for song in songs] ) if not model_id or not model_id.strip(): logger.debug("Empty Playlist model_id, aborting.") return if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Playlist(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(run) else: run() def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True, song_dict:dict={}): def run(): song = song_dict if not song: params = { "Fields": "ArtistItems,AlbumId,RunTimeTicks,UserData,IndexNumber,ParentIndexNumber" } song = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode='GET', params=params ) if song.get("Id"): cover_art = "None" #Check for cover art on Song object and query Album object if it's missing if primary_tag := song.get('ImageTags', {}).get('Primary', ''): cover_art = f"Items/{model_id}/Images/Primary?={primary_tag}" else: album = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": song.get("AlbumId")}, mode="GET" ) if album_id := album.get("Id"): if primary_tag := album.get('ImageTags', {}).get('Primary', ''): cover_art = f"Items/{album_id}/Images/Primary?={primary_tag}" duration = int(song.get("RunTimeTicks", 0) / 10000000) self.loaded_models.get(model_id).update_data( id=song.get("Id"), title=song.get("Name"), album=song.get("Album"), albumId=song.get("AlbumId"), artist=song.get("AlbumArtist"), artistId=(song.get("ArtistItems") or [{}])[0].get("Id"), coverArt=cover_art, duration=duration, artists=[{"id": art.get("Id"), "name": art.get("Name")} for art in song.get("ArtistItems", [])], starred=song.get("UserData", {}).get("IsFavorite", False), track=song.get("IndexNumber") or 0, discNumber=song.get("ParentIndexNumber") or 0, albumGain=song.get("AlbumNormalizationGain", song.get("NormalizationGain")) or 0.0, trackGain=song.get("NormalizationGain") or 0.0, userRating=self.get_rating(model_id) ) self.threads.submit(self.updateCoverArt, song.get("Id")) elif model_id in self.loaded_models: self.loaded_models.get(model_id).set_property('deleted', True) del self.loaded_models[model_id] if not model_id or not model_id.strip(): logger.debug("Empty Song model_id, aborting.") return if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Song(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(run) else: run() def star(self, model_id:str) -> bool: response = self.make_request( action='Users/{userId}/FavoriteItems/{id}', action_keys={"id": model_id}, mode='POST' ) return response.get('IsFavorite', False) def unstar(self, model_id:str) -> bool: response = self.make_request( action='Users/{userId}/FavoriteItems/{id}', action_keys={"id": model_id}, mode='DELETE' ) return not response.get('IsFavorite', False) def getPlayQueue(self) -> tuple: queue_dict = self.open_json('queue.json') song_list = [model_id for model_id in queue_dict.get('id', [])] current = queue_dict.get('current', "") if current not in song_list: if len(song_list) > 0: current = song_list[0] else: current = "" return current, song_list def savePlayQueue(self, id_list:list, current:str, position:int) -> bool: final_id_list = [] for model_id in id_list: if model := self.loaded_models.get(model_id): if not model.isExternalFile: final_id_list.append(model_id) if current not in final_id_list: if len(final_id_list) > 0: current = final_id_list[0] else: current = "" queue_dict = { 'id': final_id_list, 'current': current, 'position': position } self.save_json('queue.json', queue_dict) return True def getSimilarSongs(self, model_id:str, count:int=20) -> list: artist_songs = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "ArtistIds": model_id, "IncludeItemTypes": "Audio", "Recursive": "true", "Limit": 1, } ).get('Items', []) if len(artist_songs) == 0: return [] songs = self.make_request( action='Items/{id}/Similar', action_keys={"id": artist_songs[0].get("Id")}, mode='GET', params={ "UserId": self.get_property("userId"), "Limit": count, "IncludeItemTypes": "Audio", "Fields": "ArtistItems,RunTimeTicks,UserData" } ).get("Items", []) self.__bulk_verify("Audio", songs) return [song.get("Id") for song in songs] def getRandomSongs(self, size:int=20) -> list: songs = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "IncludeItemTypes": "Audio", "Recursive": "true", "Fields": "RunTimeTicks,UserData,ArtistItems", "Limit": size, "SortBy": "Random", "MediaTypes": "Audio", "ParentId":self.get_property("libraryId") } ).get('Items', []) self.__bulk_verify("Audio", songs) return [song.get("Id") for song in songs] def getLyrics(self, songId:str, requestOnline:bool=False) -> tuple: # Initial Checks if songId not in self.loaded_models: return 'not-found', '' # 1. Database lyrics_type, content = super().getLyrics(songId) if lyrics_type != 'not-found': return lyrics_type, content # 2. Integration def job(): result = self.make_request( action='Audio/{id}/Lyrics', action_keys={'id': songId}, mode='GET' ) if result.get('Lyrics', [{}])[0].get('Start'): # is lrc lines = [] for line in result.get('Lyrics', []): ms = line.get('Start') / 10000 minutes = int(ms // 60000) seconds = int((ms % 60000) // 1000) centiseconds = int((ms % 1000) // 10) timestamp = f"[{minutes:02d}:{seconds:02d}.{centiseconds:02d}]" lines.append(f"{timestamp} {line.get('Text').strip()}") if content := '\n'.join(lines): return True, { 'type': 'lrc', 'content': content } else: text = '\n'.join([line.get('Text') for line in result.get('Lyrics', [])]) if text: return True, { 'type': 'plain', 'content': text } return True, {} if content_dict := self.cache_manager.get_result(f'IntegrationLyrics:{songId}', job): if content := content_dict.get('content'): if lyrics_type := content_dict.get('type'): if lyrics_type in ('lrc', 'plain'): self.saveLyrics(songId, content, lyrics_type) return lyrics_type, content # 3. Online if requestOnline: return super().getLyrics(songId, requestOnline) return 'not-found-locally', '' def __fetch_type(self, item_type:str, query:str, limit:int=5, offset:int=0, fields:str="", verify:bool=False): if limit == 0: return [] # Method exclusive to Jellyfin, helper for searches items = [] if item_type == "MusicArtist": items = self.make_request( action='Artists/AlbumArtists', mode="GET", params={ "userId": self.get_property("userId"), "parentId": self.get_property("libraryId"), "SearchTerm": query, "Recursive": "true", "Limit": limit, "StartIndex": offset, "Fields": fields } ).get('Items', []) else: params = { "SearchTerm": query, "IncludeItemTypes": item_type, "Recursive": "true", "Limit": limit, "StartIndex": offset, "Fields": fields } if item_type != "Playlist": params["ParentId"] = self.get_property("libraryId") items = self.make_request( action='Users/{userId}/Items', mode="GET", params=params ).get('Items', []) if verify: self.__bulk_verify(item_type, items) return items def __bulk_verify(self, item_type:str, items:list): #Method exclusive to Jellyfin, pre-verifies response objects so the UI loads faster for item in items: if item_type == "MusicArtist": self.verifyArtist(item.get("Id"), artist_object=item, use_threading=False, lite=True) elif item_type == "MusicAlbum": self.verifyAlbum(item.get("Id"), album_object=item, use_threading=False, lite=True) elif item_type == "Audio": self.verifySong(item.get("Id"), use_threading=False, song_dict=item) elif item_type == "Playlist": self.verifyPlaylist(item.get("Id"), use_threading=False, playlist_object=item, lite=True) def search(self, query:str, artistCount:int=0, artistOffset:int=0, albumCount:int=0, albumOffset:int=0, songCount:int=0, songOffset:int=0, playlistCount:int=0, playlistOffset:int=0) -> dict: return { 'artist': [item.get("Id") for item in self.__fetch_type("MusicArtist", query, artistCount, artistOffset, verify=True)], 'album': [item.get("Id") for item in self.__fetch_type("MusicAlbum", query, albumCount, albumOffset, verify=True)], 'song': [item.get("Id") for item in self.__fetch_type("Audio", query, songCount, songOffset, verify=True)], 'playlist': [item.get("Id") for item in self.__fetch_type("Playlist", query, playlistCount, playlistOffset, verify=True)] } def systemSearch(self, query:str) -> dict: results = {} # Artists for artist in self.__fetch_type('MusicArtist', query): icon_bytes = self.getCoverArtBytes(artist.get('Id'), 128) results[artist.get('Id')] = { 'display': GLib.Variant('s', artist.get('Name')), 'type': GLib.Variant('s', 'artist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Albums for album in self.__fetch_type('MusicAlbum', query): if artist := album.get('AlbumArtist'): display_name = '{} โ€ข {}'.format(album.get('Name'), artist) else: display_name = album.get('Name') icon_bytes = self.getCoverArtBytes(album.get('Id'), 128) results[album.get('Id')] = { 'display': GLib.Variant('s', display_name), 'type': GLib.Variant('s', 'album'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Songs for song in self.__fetch_type('Audio', query): if artist := song.get('AlbumArtist'): display_name = '{} โ€ข {}'.format(song.get('Name'), artist) else: display_name = song.get('Name') icon_bytes = self.getCoverArtBytes(song.get('Id'), 128) results[song.get('Id')] = { 'display': GLib.Variant('s', display_name), 'type': GLib.Variant('s', 'song'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Playlist for playlist in self.__fetch_type('Playlist', query): icon_bytes = self.getCoverArtBytes(playlist.get('Id'), 128) results[playlist.get('Id')] = { 'display': GLib.Variant('s', playlist.get('Name')), 'type': GLib.Variant('s', 'playlist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } return results def getInternetRadioStations(self) -> list: radios = self.make_request( action='LiveTv/Channels', mode='GET', params={ "userId": self.get_property("userId"), "type": "Radio" } ).get('Items', []) id_list = [] for radio in radios: if radio.get("Id") not in self.cache_actions.get('deleted-radios'): primary_tag = radio.get('ImageTags', {}).get('Primary', '') cover_art = f"Items/{radio.get('Id')}/Images/Primary?={primary_tag}" if primary_tag else "None" radio_model = models.Song( id=radio.get("Id"), title=radio.get("Name"), duration=-1, coverArt=cover_art ) self.loaded_models[radio.get("Id")] = radio_model raw_url = None radio_metadata = test_radio = self.make_request( action='Items/{id}/PlaybackInfo', action_keys={'id': radio.get('Id')}, params={ "fields": "Path", "userId": self.get_property("userId") } ).get('MediaSources', []) if len(radio_metadata) > 0: raw_url = radio_metadata[0].get('Path') if not raw_url: raw_url = self.get_stream_url(radio.get("Id")) self.loaded_models.get(radio.get("Id")).set_property("radioStreamUrl", raw_url) id_list.append(radio.get("Id")) self.threads.submit(self.updateCoverArt, radio.get("Id")) return id_list def createInternetRadioStation(self, name:str, radioStreamUrl:str) -> bool: radio = self.make_request( action='LiveTv/TunerHosts', mode='POST', json={ "Url": radioStreamUrl, "Type": "M3U", "FriendlyName": name } ) if radio.get('Id'): self.loaded_models[radio.get("Id")] = models.Song( id=radio.get("Id"), title=radio.get("FriendlyName"), duration=-1, radioStreamUrl=radioStreamUrl ) return True return False def deleteInternetRadioStation(self, model_id:str) -> bool: response = self.make_request( action='LiveTv/TunerHosts', mode='DELETE', params={ "id": model_id } ) if response.get('state') == 'ok': self.cache_actions['deleted-radios'].append(model_id) return True return False def createPlaylist(self, name:str=None, playlistId:str=None, songId:list=[]) -> str: if playlistId: #TODO update name if self.updatePlaylist(playlistId=playlistId, songIdToAdd=songId): return playlistId else: return '' response = self.make_request( action='Playlists', mode="POST", params={ "UserId": self.get_property("userId"), "MediaType": "Audio" }, json={ "Name": name, "Ids": ",".join(songId) } ) return response.get("Id", "") def updatePlaylist(self, playlistId:str, songIdToAdd:list=[], songIndexToRemove:list=[]) -> bool: if songIndexToRemove: current_items = self.make_request( action='Playlists/{id}/Items', action_keys={"id": playlistId}, mode="GET", params={ "UserId": self.get_property("userId") } ).get("Items", []) entry_ids_to_remove = [] for index in songIndexToRemove: index = int(index) if 0 <= index < len(current_items): entry_ids_to_remove.append(current_items[index].get("PlaylistItemId")) if entry_ids_to_remove: self.make_request( action='Playlists/{id}/Items', action_keys={"id": playlistId}, mode="DELETE", params={ "EntryIds": ",".join(entry_ids_to_remove) } ) if songIdToAdd: self.make_request( action="Playlists/{id}/Items", action_keys={"id": playlistId}, mode="POST", params={ "Ids": ",".join(songIdToAdd), "UserId": self.get_property("userId") } ) return True def deletePlaylist(self, model_id:str) -> bool: response = self.make_request( action='Items/{id}', action_keys={'id': model_id}, mode="DELETE" ) return response.get("state") == "ok" def setRating(self, model_id:str, rating:int=0) -> bool: conn, cursor = sql_instance.get_connection(self) if rating == 0: cursor.execute("DELETE FROM ratings WHERE id = ?", (model_id,)) else: query = """ INSERT INTO ratings (id, rating) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET rating = excluded.rating """ cursor.execute(query, (model_id, rating)) conn.commit() conn.close() return True def getTopSongs(self, artist_id:str, count:int=10) -> list: songs = self.make_request( action='Users/{userId}/Items', mode='GET', params={ 'ArtistIds': artist_id, 'IncludeItemTypes': 'Audio', 'SortBy': 'PlayCount', 'SortOrder': 'Descending', 'Limit': count, 'Recursive': 'true', 'ParentId': self.get_property("libraryId") } ).get('Items', []) return [song.get('Id') for song in songs if song.get('Id')] def downloadSong(self, model_id:str, file_title:str, progress_callback:callable): try: with self.session.get(self.get_url('Items/{id}/Download', id=model_id), headers=self.get_base_header(), stream=True) as r: r.raise_for_status() total_size = int(r.headers.get('content-length', 0)) downloaded_size = 0 extension = DOWNLOAD_MIME_MAP.get(r.headers.get('Content-Type'), '.mp3') file_name = '{}{}'.format(file_title, extension) file_path = os.path.join(DOWNLOAD_QUEUE_DIR, file_name) with open(file_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded_size += len(chunk) if total_size > 0: progress_callback(downloaded_size / total_size) os.replace(file_path, os.path.join(DOWNLOADS_DIR, file_name)) except Exception as e: logger.error(f"can't download song {model_id}: {e}") def getSongDetails(self, model_id:str) -> models.SongDetails: song = self.make_request( action='Users/{userId}/Items/{id}', action_keys={'id': model_id}, mode='GET', params={ 'fields': 'MediaSources,Genres,ArtistItems,Path,ProductionYear,Taglines' } ) # Limitations: # - no bpm return models.SongDetails( id=model_id, title=song.get('Name'), album=song.get('Album'), albumId=song.get('AlbumId'), artist=song.get('Artists')[0] if song.get('Artists') else "", artistId=song.get('ArtistItems')[0].get('Id', '') if song.get('ArtistItems') else "", musicBrainzId=song.get("ProviderIds", {}).get("MusicBrainzTrack") or "", track=song.get('IndexNumber', 0), year=song.get('ProductionYear', 0), size=song.get('MediaSources', [{}])[0].get('Size', 0), suffix=song.get('MediaSources', [{}])[0].get('Container', _("Unknown")), starred=song.get('UserData', {}).get('IsFavorite', False), duration=song.get('RunTimeTicks', 1) / 10_000_000, bitRate=song.get('MediaSources', [{}])[0].get('Bitrate', 1) / 1000, bitDepth=song.get('MediaSources', [{}])[0].get('MediaStreams', [{}])[0].get('BitDepth', 0), samplingRate=song.get('MediaSources', [{}])[0].get('MediaStreams', [{}])[0].get('SampleRate', 1), path=song.get('Path'), discNumber=song.get('ParentIndexNumber', 0), genres=[{'name': genre} for genre in song.get('Genres', [])], artists=[{'name': art.get('Name'), 'id': art.get('Id')} for art in song.get('ArtistItems', [])], trackGain=song.get('NormalizationGain', 0.0), albumGain=song.get('NormalizationGain', 0.0) ) def getServerInformation(self) -> dict: server_information = { 'link': self.get_property('url').strip('/'), 'username': self.get_property('user').title() } try: response = self.make_request( action='Users/{userId}/Images/Primary', params={ "maxWidth": 240, "quality": 90 }, mode='RAWGET' ) response_bytes = response.content if response.status_code in (200, 201) else b'' if response_bytes and len(response_bytes) > 0: gbytes = GLib.Bytes.new(response_bytes) server_information['picture'] = Gdk.Texture.new_from_bytes(gbytes) except Exception as e: logger.error(f"can't get server information: {e}") try: info = self.make_request( action="System/Info", mode="GET" ) server_information["title"] = "{} {}".format(info.get("ServerName"), info.get("Version")) except Exception as e: logger.error(f"can't get server information: {e}") return server_information Jeffser-Nocturne-b442fee/src/integrations/local.py000066400000000000000000000743651523240030000223330ustar00rootroot00000000000000# local.py from gi.repository import GLib, GObject, Gdk, Gio from . import models, sql_instance from .base import Base import random, threading, io, pathlib, re, os, time, uuid, pwd, getpass, shutil, logging from PIL import Image from tinytag import TinyTag from ..constants import DOWNLOADS_DIR, COMPATIBLE_EXTENSIONS, get_song_info_from_file from concurrent.futures import ThreadPoolExecutor, as_completed logger = logging.getLogger(__name__) class Local(Base): __gtype_name__ = 'NocturneIntegrationLocal' album_artist_ids = set() login_page_metadata = { 'icon-name': "folder-open-symbolic", 'title': _("Local Files"), 'description': _("Let Nocturne load your local files directly, for big libraries it is recommended to use a dedicated server."), 'entries': ['library-dir'], 'login-label': _("Continue") } button_metadata = { 'title': _("Local Files"), 'subtitle': _("Limited functionality") } limitations = ('no-max-bitrate',) sqlSchema = { 'stars': { 'id': 'TEXT PRIMARY KEY' }, 'radios': { 'id': 'TEXT PRIMARY KEY', 'name': 'TEXT NOT NULL', 'stream_url': 'TEXT NOT NULL' }, 'ratings': { 'id': 'TEXT PRIMARY KEY', 'rating': 'INTEGER DEFAULT 1' }, 'scrobble': { 'id': 'TEXT PRIMARY KEY', 'plays': 'INTEGER DEFAULT 1', 'last_play': 'INTEGER DEFAULT 0', 'album_id': 'TEXT NOT NULL', 'artist_id': 'TEXT NOT NULL' } } def get_rating(self, model_id) -> int: conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT rating FROM ratings WHERE id = ?", (model_id,)) result = cursor.fetchone() conn.close() return result[0] if result else 0 def on_login(self): # Goes through the whole directory retrieving all the metadata audio_data_list = [] path_obj = pathlib.Path(self.get_property('libraryDir')) self.album_artist_ids = set() def load_songs(): # load songs, albums, artists self.set_property('loadingMessage', _("Loading Songs")) futures = {} with ThreadPoolExecutor() as executor: for file_path in path_obj.rglob("*"): # Exclude any hidden files/folders within the library path if any(part.startswith(".") for part in file_path.relative_to(path_obj).parts): continue if file_path.suffix.lower() in COMPATIBLE_EXTENSIONS.get('audio') + COMPATIBLE_EXTENSIONS.get('video'): song_id = 'SONG:{}'.format(file_path) self.loaded_models[song_id] = models.Song( id=song_id, path=file_path, coverArt=file_path, userRating=self.get_rating(song_id) ) futures[executor.submit(self.verifySong, model_id=song_id, use_threading=False)] = song_id for future in as_completed(futures): song_id = futures[future] try: future.result() except Exception as e: logger.error(f"can't load song {song_id}: {e}") self.set_property('loadingMessage', "") threading.Thread(target=load_songs, daemon=True).start() # Load radios conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT id, name, stream_url FROM radios") for radio in cursor.fetchall(): self.loaded_models[radio[0]] = models.Song( id=radio[0], title=radio[1], radioStreamUrl=radio[2], duration=-1 ) conn.close() self.load_playlists() def load_playlists(self): # Load playlists playlist_dict = self.open_json('playlists.json') for playlist_id, playlist in playlist_dict.items(): if playlist_id not in self.loaded_models: path_str = "" if len(playlist.get('songId', [])) > 0: if model := self.loaded_models.get(playlist.get('songId')[0]): path_str = model.get_property('path') self.loaded_models[playlist_id] = models.Playlist( id=playlist_id, name=playlist.get('name'), songCount=len(playlist.get('songId', [])), entry=[{'id': model_id} for model_id in playlist.get('songId', [])], coverArt = path_str ) # ----------- # def ping(self) -> dict: gio_file = Gio.File.new_for_path(self.get_property('libraryDir')) if new_path := gio_file.get_path(): self.set_property('libraryDir', new_path) return super().ping() return { 'status': 'error', 'message': _('Could not locate path, try again') } def get_stream_url(self, song_id:str) -> str: if model := self.loaded_models.get(song_id): if radioStreamUrl := model.get_property('radioStreamUrl'): try: with self.session.get(radioStreamUrl, stream=True, timeout=10) as r: r.raise_for_status() content_type = r.headers.get('Content-Type', '').lower() if 'mpegurl' in content_type or 'text/plain' in content_type or 'octet-stream' in content_type: # It is a playlist text file, extract url for line in r.iter_lines(decode_unicode=True): line = line.decode('utf-8') if line and not line.startswith('#'): return line.strip() except: pass return radioStreamUrl return 'file://{}'.format(model.get_property('path')) return "" def getCoverArtBytes(self, model_id:str, size:int) -> bytes: try: coverArtPath = None if model := self.loaded_models.get(model_id): coverArtPath = model.get_property('coverArt') if not coverArtPath: return b'' tag = TinyTag.get(coverArtPath, image=True) if tag is None: return b'' image_data = tag.get_image() if not image_data: return b'' img = Image.open(io.BytesIO(image_data)) w_percent = (size / float(img.size[0])) height = int((float(img.size[1]) * float(w_percent))) resized_img = img.resize((size, height), Image.LANCZOS) buffer = io.BytesIO() resized_img.save(buffer, format="JPEG", quality=85) raw_data = buffer.getvalue() return raw_data except Exception as e: logger.error(f"can't get image from {model_id}: {e}") return b'' def updateCoverArt(self, model_id:str): if model := self.loaded_models.get(model_id): sizes = { 'gdkPaintableBig': 720, 'gdkPaintable': 240 } for property_name, size in sizes.items(): if not model.get_property(property_name): try: if response_bytes := self.getCoverArtBytes(model_id, size): gbytes = GLib.Bytes.new(response_bytes) texture = Gdk.Texture.new_from_bytes(gbytes) model.set_property(property_name, texture) except Exception as e: logger.error(f"can't get image from {model_id} (size {720 if big else 240}): {e}") def getCoverArtUrl(self, model_id:str) -> str: if model := self.loaded_models.get(model_id): directory = os.path.join(self.getIntegrationDir(), 'covers') file_name = '{} - {}.png'.format(model.get_property('title'), model.get_property('artist')).replace('/', '-') path = os.path.join(directory, file_name) if os.path.isfile(path): return "file://{}".format(path) try: shutil.rmtree(directory, ignore_errors=True) os.makedirs(directory, exist_ok=True) paintable = self.model.get_property('gdkPaintable') if not paintable: self.updateCoverArt(model_id) paintable = self.model.get_property('gdkPaintable') if paintable: paintable.save_to_png(path) return "file://{}".format(path) except Exception as e: logger.error(f"can't get image url from {model_id}: {e}") pass return "" def getAlbumList(self, list_type:str="recent", size:int=10, offset:int=0) -> list: album_list = [] if list_type == "random": album_list = [model_id for model_id in list(self.loaded_models) if model_id.startswith('ALBUM:')] random.shuffle(album_list) elif list_type == "newest": albums = {} # id : creation_time for model in [self.loaded_models.get(model_id) for model_id in list(self.loaded_models) if model_id.startswith('ALBUM:')]: albums[model.get_property('id')] = pathlib.Path(model.get_property('coverArt')).stat().st_ctime album_list = sorted(albums, key=lambda x: albums.get(x), reverse=True) elif list_type in ("frequent", "recent"): album_views = {} conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT album_id, plays, last_play FROM scrobble") for row in cursor.fetchall(): album_id = row[0] plays = row[1] last_play = row[2] if album_id in album_views: album_views[album_id]['plays'] += plays album_views[album_id]['last_play'] = max(album_views.get(album_id).get('last_play'), last_play) else: album_views[album_id] = { 'plays': plays, 'last_play': last_play } conn.close() if list_type == "frequent": album_list = sorted(album_views, key=lambda x: album_views.get(x).get('plays'), reverse=True) elif list_type == "recent": album_list = sorted(album_views, key=lambda x: album_views.get(x).get('last_play'), reverse=True) elif list_type == "starred": album_list = [model_id for model_id, model in self.loaded_models.items() if model_id.startswith('ALBUM:') and model.starred] else: album_list = [model_id for model_id in list(self.loaded_models) if model_id.startswith('ALBUM:')] return [model_id for model_id in album_list if model_id in self.loaded_models][offset:size+offset] def getArtists(self, size:int=10) -> list: return [model_id for model_id in list(self.loaded_models) if model_id in self.album_artist_ids][:size] def getPlaylists(self) -> list: self.load_playlists() return [model_id for model_id in list(self.loaded_models) if model_id.startswith('PLAYLIST:')] def getStarred(self, item_type:str) -> list: conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT id FROM stars") star_list = [str(r[0]) for r in cursor.fetchall()] conn.close() query = f"{item_type.upper()}:" return [item_id for item_id in star_list if item_id.startswith(query) and item_id in self.loaded_models] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.updateCoverArt, args=(model_id,), daemon=True).start() def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.updateCoverArt, args=(model_id,), daemon=True).start() def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.updateCoverArt, args=(model_id,), daemon=True).start() def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True): def run(): conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT id FROM stars") star_list = [str(r[0]) for r in cursor.fetchall()] conn.close() # Updating Song Model song = get_song_info_from_file(self.loaded_models.get(model_id).get_property("path"), star_list=star_list) if not song: return song["id"] = model_id song["starred"] = song.get("id") in star_list self.loaded_models.get(model_id).update_data(**song) # Making Album Model album_id = song.get('albumId') if not album_id: album_id = 'ALBUM:NO_ALBUM:{}'.format(song.get('artists')[0].get('id')) if album_id: if album_id in self.loaded_models: if {'id': model_id} not in self.loaded_models.get(album_id).get_property('song'): self.loaded_models.get(album_id).song.append({'id': model_id}) else: album = { 'id': album_id, 'coverArt': song.get('path'), 'name': song.get('album') or _("No Album"), 'artist': song.get('artist'), 'artistId': song.get('artistId'), 'song': [{'id': model_id}], 'year':int(song.get('year') or "0"), 'starred': album_id in star_list, 'userRating': self.get_rating(album_id) } self.loaded_models[album.get('id')] = models.Album(**album) # Making Artist Model def update_artist(artist_id:str, artist_name:str): if artist_id not in self.loaded_models: self.loaded_models[artist_id] = models.Artist( id=artist_id, coverArt=song.get('path'), name=artist_name, album=[], albumCount=0, starred=artist_id in star_list, userRating=self.get_rating(artist_id) ) album_list = self.loaded_models.get(artist_id).album if album_id and not any(album.get('id') == album_id for album in album_list): self.loaded_models.get(artist_id).album.append({'id': album_id}) self.loaded_models.get(artist_id).albumCount += 1 artist_id = song.get('artistId') if artist_id: self.album_artist_ids.add(artist_id) update_artist(artist_id, song.get('artist')) for artist in song.get('artists', []): if artist.get('id'): update_artist(artist.get('id'), artist.get('name')) self.updateCoverArt(model_id) # safe check before loading song = self.loaded_models.get(model_id) if force_update or (song and not song.get_property('title')): if use_threading: threading.Thread(target=run, daemon=True).start() else: run() def star(self, model_id:str) -> bool: conn, cursor = sql_instance.get_connection(self) cursor.execute("INSERT OR IGNORE INTO stars (id) VALUES (?)", (model_id,)) conn.commit() conn.close() return True def unstar(self, model_id:str) -> bool: conn, cursor = sql_instance.get_connection(self) cursor.execute("DELETE FROM stars WHERE id=?", (model_id,)) conn.commit() conn.close() return True def getPlayQueue(self) -> tuple: queue_dict = self.open_json('queue.json') song_list = [model_id for model_id in queue_dict.get('id', []) if model_id in self.loaded_models] current = queue_dict.get('current', "") if current not in song_list: if len(song_list) > 0: current = song_list[0] else: current = "" return current, song_list def savePlayQueue(self, id_list:list, current:str, position:int) -> bool: final_id_list = [] for model_id in id_list: if model := self.loaded_models.get(model_id): if not model.get_property('isExternalFile'): final_id_list.append(model_id) if current not in final_id_list: if len(final_id_list) > 0: current = final_id_list[0] else: current = "" queue_dict = { 'id': final_id_list, 'current': current, 'position': position } self.save_json('queue.json', queue_dict) return True def getSimilarSongs(self, model_id:str, count:int=20) -> list: # out of the scope of Local return self.getRandomSongs(count) def getRandomSongs(self, size:int=20) -> list: songs = [song_id for song_id in list(self.loaded_models) if song_id.startswith('SONG:')] return random.sample(songs, k=min(size, len(songs))) def getLyrics(self, songId:str, requestOnline:bool=False) -> dict: # Initial Checks if songId not in self.loaded_models: return 'not-found', '' # 1. Database lyrics_type, content = super().getLyrics(songId) if lyrics_type != 'not-found': return lyrics_type, content # 2. Integration def job(): if model := self.loaded_models.get(songId): tag = TinyTag.get(model.get_property('path')) if lyrics_str := tag.extra.get('lyrics'): if lyrics_str.startswith('['): return True, {'type': 'lrc', 'content': lyrics_str} else: return True, {'type': 'plain', 'content': lyrics_str} return True, {} if content_dict := self.cache_manager.get_result(f'IntegrationLyrics:{songId}', job): if content := content_dict.get('content'): if lyrics_type := content_dict.get('type'): if lyrics_type in ('lrc', 'plain'): self.saveLyrics(songId, content, lyrics_type) return lyrics_type, content # 3. Online if requestOnline: return super().getLyrics(songId, requestOnline) return 'not-found-locally', '' def search(self, query:str, artistCount:int=0, artistOffset:int=0, albumCount:int=0, albumOffset:int=0, songCount:int=0, songOffset:int=0, playlistCount:int=0, playlistOffset:int=0) -> dict: all_artists = [model for model_id, model in self.loaded_models.items() if model_id in self.album_artist_ids] all_albums = [model for model_id, model in self.loaded_models.items() if model_id.startswith('ALBUM:')] all_songs = [model for model_id, model in self.loaded_models.items() if model_id.startswith('SONG:')] all_playlists = [self.loaded_models.get(playlistId) for playlistId in self.getPlaylists() if playlistId in self.loaded_models] return { 'artist': [model.id for model in all_artists if re.search(query, model.name, re.IGNORECASE)][artistOffset:artistCount+artistOffset], 'album': [model.id for model in all_albums if re.search(query, ' '.join([model.name, model.artist]), re.IGNORECASE)][albumOffset:albumCount+albumOffset], 'song': [model.id for model in all_songs if re.search(query, ' '.join([model.title, model.artist, model.album]), re.IGNORECASE)][songOffset:songCount+songOffset], 'playlist': [model.id for model in all_playlists if re.search(query, model.name, re.IGNORECASE)][playlistOffset:playlistCount+playlistOffset] } def systemSearch(self, query:str) -> dict: results = {} all_artists = [model for model_id, model in self.loaded_models.items() if model_id in self.album_artist_ids] all_albums = [model for model_id, model in self.loaded_models.items() if model_id.startswith('ALBUM:')] all_songs = [model for model_id, model in self.loaded_models.items() if model_id.startswith('SONG:')] all_playlists = [self.loaded_models.get(playlistId) for playlistId in self.getPlaylists() if playlistId in self.loaded_models] # Artists for artist in [model for model in all_artists if re.search(query, model.name, re.IGNORECASE)][:5]: icon_bytes = self.getCoverArtBytes(artist.id, 128) results[artist.id] = { 'display': GLib.Variant('s', artist.name), 'type': GLib.Variant('s', 'artist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Albums for album in [model for model in all_albums if re.search(query, ' '.join([model.name, model.artist]), re.IGNORECASE)][:5]: icon_bytes = self.getCoverArtBytes(album.id, 128) results[album.id] = { 'display': GLib.Variant('s', album.name), 'type': GLib.Variant('s', 'album'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Songs for song in [model for model in all_songs if re.search(query, ' '.join([model.title, model.artist, model.album]), re.IGNORECASE)][:5]: icon_bytes = self.getCoverArtBytes(song.id, 128) results[song.id] = { 'display': GLib.Variant('s', song.title), 'type': GLib.Variant('s', 'song'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Playlists for playlist in [model for model in all_playlists if re.search(query, model.name, re.IGNORECASE)][:5]: icon_bytes = self.getCoverArtBytes(playlist.id, 128) results[playlist.id] = { 'display': GLib.Variant('s', playlist.name), 'type': GLib.Variant('s', 'playlist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } return results def getInternetRadioStations(self) -> list: return [model_id for model_id in list(self.loaded_models) if model_id.startswith('RADIO:')] def createInternetRadioStation(self, name:str, radioStreamUrl:str) -> bool: radio_id = 'RADIO:{}'.format(str(uuid.uuid4())) return self.updateInternetRadioStation(radio_id, name, radioStreamUrl) def updateInternetRadioStation(self, model_id:str, name:str, radioStreamUrl:str) -> bool: conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO radios (id, name, stream_url) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = excluded.name, stream_url = excluded.stream_url """ cursor.execute(query, (model_id, name, radioStreamUrl)) conn.commit() conn.close() return True def deleteInternetRadioStation(self, model_id:str) -> bool: conn, cursor = sql_instance.get_connection(self) cursor.execute("DELETE FROM radios WHERE id = ?", (model_id,)) conn.commit() conn.close() return True def createPlaylist(self, name:str=None, playlistId:str=None, songId:list=[]) -> str: playlist_dict = self.open_json('playlists.json') playlistId = playlistId or 'PLAYLIST:{}'.format(str(uuid.uuid4())) playlist_dict[playlistId] = { 'name': name, 'songId': songId } path_str = "" if len(songId) > 0: if model := self.loaded_models.get(songId[0]): path_str = model.get_property('path') self.loaded_models[playlistId] = models.Playlist( id=playlistId, name=name, songCount=len(songId), entry=[{'id': model_id} for model_id in songId], coverArt = path_str ) self.save_json('playlists.json', playlist_dict) return playlistId def updatePlaylist(self, playlistId:str, songIdToAdd:list=[], songIndexToRemove:list=[]) -> bool: playlist_dict = self.open_json('playlists.json') if playlistId in playlist_dict: songs = playlist_dict.get(playlistId).get('songId') for index in songIndexToRemove: songs.pop(int(index)) songs.extend(songIdToAdd) playlist_dict[playlistId]['songId'] = songs if model := self.loaded_models.get(playlistId): songId = playlist_dict.get(playlistId).get('songId') model.set_property('songCount', len(songId)) model.set_property('entry', [{'id': model_id} for model_id in songId]) path_str = "" if len(songId) > 0: if model := self.loaded_models.get(songId[0]): path_str = model.get_property('path') model.set_property('coverArt', path_str) self.save_json('playlists.json', playlist_dict) return True def deletePlaylist(self, model_id:str) -> bool: playlist_dict = self.open_json('playlists.json') if model_id in playlist_dict: del playlist_dict[model_id] self.save_json('playlists.json', playlist_dict) return True def getTopSongs(self, artist_id:str, count:int=10) -> list: artist_scrobbles = {} conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT id, plays, artist_id FROM scrobble") for song in cursor.fetchall(): song_id = song[0] plays = song[1] artist = song[2] if not artist: if model := self.loaded_models.get(song_id): artist = model.get_property('artistId') if artist == artist_id: artist_scrobbles[song_id] = plays conn.close() return sorted(artist_scrobbles, key=artist_scrobbles.get, reverse=True)[:count] def downloadSong(self, model_id:str, file_title:str, progress_callback:callable): if model := self.loaded_models.get(model_id): source_path = model.get_property('path') extension = pathlib.Path(source_path).suffix shutil.copy2(source_path, os.path.join(DOWNLOADS_DIR, '{}{}'.format(file_title, extension))) progress_callback(1) def scrobble(self, model_id:str, submission:bool=True): if not model_id: return if model := self.loaded_models.get(model_id): if model.get_property('isExternalFile') or model.get_property('radioStreamUrl'): return if submission: conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO scrobble (id, plays, last_play, album_id, artist_id) VALUES (?, 1, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET plays = plays + 1, last_play = excluded.last_play, album_id = excluded.album_id, artist_id = excluded.artist_id """ cursor.execute(query, (model_id, int(time.time()), model.get_property('albumId'), model.get_property('artistId'))) conn.commit() conn.close() super().scrobble(model_id, submission=submission) def setRating(self, model_id:str, rating:int=0) -> bool: conn, cursor = sql_instance.get_connection(self) if rating == 0: cursor.execute("DELETE FROM ratings WHERE id = ?", (model_id,)) else: query = """ INSERT INTO ratings (id, rating) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET rating = excluded.rating """ cursor.execute(query, (model_id, rating)) conn.commit() conn.close() return True def getSongDetails(self, model_id:str) -> models.SongDetails: if model := self.loaded_models.get(model_id): tag = TinyTag.get(model.get_property('path')) # Limitations: # - no bitDepth # - no bpm # - no trackGain # - no albumGain return models.SongDetails( id=model_id, title=tag.title, album=tag.album, albumId=model.get_property('albumId'), artist=tag.artist, artistId=model.get_property('artistId'), track=tag.track or 0, year=int(tag.year or "0"), size=tag.filesize, suffix=os.path.splitext(model.get_property('path'))[1].replace('.', ''), starred=model.get_property('starred'), duration=tag.duration, bitRate=int(tag.bitrate or "0"), samplingRate=int(tag.samplerate or "0"), path=model.get_property('path'), discNumber=tag.disc or 0, genres=[{'name': tag.genre}] if tag.genre else [], artists=model.get_property('artists') ) return models.SongDetails() def getServerInformation(self) -> dict: server_information = { 'link': 'file://{}'.format(self.get_property('libraryDir')), 'title': _("Local Files"), 'username': os.getlogin() } return server_information class Offline(Local): __gtype_name__ = 'NocturneIntegrationOffline' login_page_metadata = { 'icon-name': 'network-offline-symbolic' } button_metadata = { 'title': _("Offline Mode"), 'subtitle': _("Access your downloads") } limitations = ('no-downloads', 'no-max-bitrate') libraryDir = GObject.Property(type=str, default=DOWNLOADS_DIR) def getServerInformation(self) -> dict: server_information = { 'title': _("Offline Mode"), 'username': os.getlogin() } return server_information Jeffser-Nocturne-b442fee/src/integrations/meson.build000066400000000000000000000005021523240030000230070ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'integrations' widgets = [ '__init__.py', 'secret.py', 'models.py', 'local.py', 'navidrome.py', 'base.py', 'jellyfin.py', 'discord_rpc.py', 'sql_instance.py', ] install_data(widgets, install_dir: moduledir) Jeffser-Nocturne-b442fee/src/integrations/models.py000066400000000000000000000224031523240030000225060ustar00rootroot00000000000000# models.py from gi.repository import GObject, Gtk, Gdk, Gio class Album(GObject.Object): __gtype_name__ = 'NocturneModelAlbum' id = GObject.Property(type=str) coverArt = GObject.Property(type=str) gdkPaintable = GObject.Property(type=Gdk.Paintable) gdkPaintableBig = GObject.Property(type=Gdk.Paintable) name = GObject.Property(type=str) artist = GObject.Property(type=str) artistId = GObject.Property(type=str) songCount = GObject.Property(type=int) duration = GObject.Property(type=int) artists = GObject.Property(type=GObject.TYPE_PYOBJECT) #list song = GObject.Property(type=GObject.TYPE_PYOBJECT) #list starred = GObject.Property(type=bool, default=False) userRating = GObject.Property(type=int) year = GObject.Property(type=int) def __init__(self, **kwargs): super().__init__() self.update_data(**kwargs) def update_data(self, **kwargs): for prop in self.list_properties(): if prop.get_name() in kwargs: if self.get_property(prop.get_name()) != kwargs.get(prop.get_name()): self.set_property(prop.get_name(), kwargs.get(prop.get_name())) elif self.get_property(prop.get_name()) is None: if prop.value_type.name == 'PyObject': #LIST self.set_property(prop.get_name(), []) else: self.set_property(prop.get_name(), prop.get_default_value()) class Artist(GObject.Object): __gtype_name__ = 'NocturneModelArtist' id = GObject.Property(type=str) coverArt = GObject.Property(type=str) gdkPaintable = GObject.Property(type=Gdk.Paintable) gdkPaintableBig = GObject.Property(type=Gdk.Paintable) name = GObject.Property(type=str) albumCount = GObject.Property(type=int) album = GObject.Property(type=GObject.TYPE_PYOBJECT) #list starred = GObject.Property(type=bool, default=False) biography = GObject.Property(type=str) similarArtist = GObject.Property(type=GObject.TYPE_PYOBJECT) #list userRating = GObject.Property(type=int) def __init__(self, **kwargs): super().__init__() self.update_data(**kwargs) def update_data(self, **kwargs): for prop in self.list_properties(): if prop.get_name() in kwargs: if self.get_property(prop.get_name()) != kwargs.get(prop.get_name()): self.set_property(prop.get_name(), kwargs.get(prop.get_name())) elif self.get_property(prop.get_name()) is None: if prop.value_type.name == 'PyObject': #LIST self.set_property(prop.get_name(), []) else: self.set_property(prop.get_name(), prop.get_default_value()) class Playlist(GObject.Object): __gtype_name__ = 'NocturneModelPlaylist' id = GObject.Property(type=str) coverArt = GObject.Property(type=str) gdkPaintable = GObject.Property(type=Gdk.Paintable) gdkPaintableBig = GObject.Property(type=Gdk.Paintable) name = GObject.Property(type=str) songCount = GObject.Property(type=int) duration = GObject.Property(type=int) readonly = GObject.Property(type=bool, default=False) entry = GObject.Property(type=GObject.TYPE_PYOBJECT) #list def __init__(self, **kwargs): super().__init__() self.update_data(**kwargs) def update_data(self, **kwargs): for prop in self.list_properties(): if prop.get_name() in kwargs: if self.get_property(prop.get_name()) != kwargs.get(prop.get_name()): self.set_property(prop.get_name(), kwargs.get(prop.get_name())) elif self.get_property(prop.get_name()) is None: if prop.value_type.name == 'PyObject': #LIST self.set_property(prop.get_name(), []) else: self.set_property(prop.get_name(), prop.get_default_value()) class Song(GObject.Object): __gtype_name__ = 'NocturneModelSong' id = GObject.Property(type=str) coverArt = GObject.Property(type=str) gdkPaintable = GObject.Property(type=Gdk.Paintable) gdkPaintableBig = GObject.Property(type=Gdk.Paintable) title = GObject.Property(type=str) album = GObject.Property(type=str) artist = GObject.Property(type=str) duration = GObject.Property(type=int) albumId = GObject.Property(type=str) discNumber = GObject.Property(type=int) albumGain = GObject.Property(type=float) trackGain = GObject.Property(type=float) artistId = GObject.Property(type=str) artists = GObject.Property(type=GObject.TYPE_PYOBJECT) # list starred = GObject.Property(type=bool, default=False) track = GObject.Property(type=int) #Track N in album isExternalFile = GObject.Property(type=bool, default=False) userRating = GObject.Property(type=int) deleted = GObject.Property(type=bool, default=False) radioStreamUrl = GObject.Property(type=str) path = GObject.Property(type=str) # For use in Local def __init__(self, **kwargs): super().__init__() self.update_data(**kwargs) def update_data(self, **kwargs): for prop in self.list_properties(): if prop.get_name() in kwargs: if self.get_property(prop.get_name()) != kwargs.get(prop.get_name()): self.set_property(prop.get_name(), kwargs.get(prop.get_name())) elif self.get_property(prop.get_name()) is None: if prop.value_type.name == 'PyObject': #LIST self.set_property(prop.get_name(), []) else: self.set_property(prop.get_name(), prop.get_default_value()) class SongDetails(GObject.Object): __gtype_name__ = 'NocturneSongDetails' id = GObject.Property(type=str) title = GObject.Property(type=str) album = GObject.Property(type=str, nick=_("Album")) albumId = GObject.Property(type=str) artist = GObject.Property(type=str, nick=_("Display Artist")) artistId = GObject.Property(type=str) musicBrainzId = GObject.Property(type=str, nick=_("MusicBrainz ID")) track = GObject.Property(type=int, nick=_("Track Number")) year = GObject.Property(type=int, nick=_("Year")) size = GObject.Property(type=int, nick=_("Size")) suffix = GObject.Property(type=str, nick=_("Suffix")) starred = GObject.Property(type=bool, default=False, nick=_("Favorite")) duration = GObject.Property(type=int, nick=_("Duration")) bitRate = GObject.Property(type=int, nick=_("Bit Rate")) bitDepth = GObject.Property(type=int, nick=_("Bit Depth")) samplingRate = GObject.Property(type=int, nick=_("Sampling Rate")) channelCount = GObject.Property(type=int, nick=_("Channel Count")) path = GObject.Property(type=str, nick=_("Path")) discNumber = GObject.Property(type=int, nick=_("Disc Number")) bpm = GObject.Property(type=int, nick=_("BPM")) genres = GObject.Property(type=GObject.TYPE_PYOBJECT, nick=_("Genres")) # list artists = GObject.Property(type=GObject.TYPE_PYOBJECT, nick=_("Artists")) # list trackGain = GObject.Property(type=float, nick=_("Track Gain")) albumGain = GObject.Property(type=float, nick=_("Album Gain")) def __init__(self, **kwargs): super().__init__() self.update_data(**kwargs) def update_data(self, **kwargs): for prop in self.list_properties(): if prop.get_name() in kwargs: if self.get_property(prop.get_name()) != kwargs.get(prop.get_name()): self.set_property(prop.get_name(), kwargs.get(prop.get_name())) elif self.get_property(prop.get_name()) is None: if prop.value_type.name == 'PyObject': #LIST self.set_property(prop.get_name(), []) else: self.set_property(prop.get_name(), prop.get_default_value()) class SongDownload(GObject.Object): __gtype_name__ = 'NocturneSongDownload' songId = GObject.Property(type=str) progress = GObject.Property(type=float, default=0.0) # 0-1 class CurrentSong(GObject.Object): __gtype_name__ = 'NocturneModelCurrentSong' # Not really currentSong, more like currentState at this point songId = GObject.Property(type=str) videoId = GObject.Property(type=str) # Should check if videoId == songId to see if the current song has video positionSeconds = GObject.Property(type=float, default=0.0) buttonState = GObject.Property(type=str, default="play") # play, pause (for use in state stacks) magnitudes = GObject.Property(type=GObject.TYPE_PYOBJECT) # dict seeking = GObject.Property(type=bool, default=False) queueOrigin = GObject.Property(type=str, default="") # set to the id of playlist / album where the queue originated from queueModel = GObject.Property(type=Gio.ListStore, default=Gio.ListStore.new(item_type=Gtk.StringObject)) generatedQueue = GObject.Property(type=Gio.ListStore, default=Gio.ListStore.new(item_type=Gtk.StringObject)) generatingQueue = GObject.Property(type=bool, default=False) downloadQueueModel = GObject.Property(type=Gio.ListStore, default=Gio.ListStore.new(item_type=SongDownload)) displaySongTitle = GObject.Property(type=str) displaySongArtist = GObject.Property(type=str) accentColor = GObject.Property(type=GObject.TYPE_PYOBJECT) # list [r,g,b] for use in spectrum, set in player Jeffser-Nocturne-b442fee/src/integrations/navidrome.py000066400000000000000000001241451523240030000232150ustar00rootroot00000000000000# navidrome.py from gi.repository import GLib, GObject, Gdk, Gio from . import secret, models, local, sql_instance from ..constants import get_navidrome_path, get_navidrome_env, CONTEXT_MANAGED_NAVIDROME_SERVER, DOWNLOAD_QUEUE_DIR, DOWNLOADS_DIR, DOWNLOAD_MIME_MAP from .base import Base import random, threading, subprocess, os, re, logging, time, uuid from urllib.parse import urlencode, urlparse logger = logging.getLogger(__name__) class Navidrome(Base): __gtype_name__ = 'NocturneIntegrationNavidrome' _use_apikey_auth = False login_page_metadata = { 'icon-name': "opensubsonic-symbolic", 'title': "External Server", 'description': _("Connect to an OpenSubsonic server like Navidrome."), 'entries': ['url', 'user', 'password', 'trust-server'] } button_metadata = { 'title': _("External Server"), 'subtitle': _("Use an existing OpenSubsonic instance") } url = GObject.Property(type=str, default="http://127.0.0.1:4533") def get_base_params(self) -> dict: params = { 'v': '1.16.1', 'c': 'Nocturne', 'f': 'json' } if self._use_apikey_auth: params['apiKey'] = secret.get_plain_password() else: salt, token = secret.get_hashed_password() params['u'] = self.get_property('user') params['t'] = token params['s'] = salt return params def get_url(self, action:str) -> str: return '{}/rest/{}'.format(self.get_property('url').strip('/'), action) + ('.view' if self.__gtype_name__ == 'NocturneIntegrationBandcamp' else '') def send_request(self, action:str, params:dict={}): def request_job(url, parameters) -> tuple: with self.session as current_session: result = current_session.get( url, params={**self.get_base_params(), **parameters}, verify=not self.get_property('trustServer') ) return result.status_code in (200, 201), result action_url = self.get_url(action) request_id = '{}?{}&apikey={}'.format(action_url, urlencode(params), self._use_apikey_auth) return self.cache_manager.get_result(request_id, request_job, action_url, params) def make_request(self, action:str, params:dict={}) -> dict: try: response = self.send_request(action, params) if response.status_code in (200, 201): data = response.json().get('subsonic-response', {}) if data.get('status') == 'failed' and data.get('error', {}).get('code') in (41, 42): self._use_apikey_auth = True response = self.send_request(action, params) if response.status_code in (200, 201): return response.json().get('subsonic-response', {}) return data except Exception as e: logger.error(f"action error {action}: {e}") return {} # ----------- # def get_stream_url(self, song_id:str) -> str: # streams are handled by gst not requests if model := self.loaded_models.get(song_id): if radioStreamUrl := model.get_property('radioStreamUrl'): try: with self.session.get(radioStreamUrl, stream=True, timeout=10) as r: r.raise_for_status() content_type = r.headers.get('Content-Type', '').lower() if 'mpegurl' in content_type or 'text/plain' in content_type or 'octet-stream' in content_type: # It is a playlist text file, extract url for line in r.iter_lines(decode_unicode=True): line = line.decode('utf-8') if line and not line.startswith('#'): return line.strip() except: pass return radioStreamUrl elif model.get_property('isExternalFile'): return 'file://{}'.format(model.get_property('path')) max_bitrate = Gio.Settings(schema_id="com.jeffser.Nocturne").get_value('max-bitrate').unpack() params = self.get_base_params() params['id'] = song_id if max_bitrate != 0: params['maxBitRate'] = max_bitrate query_string = urlencode(params) return '{}/rest/stream?{}'.format(self.get_property('url').strip('/'), query_string) def getCoverArtBytes(self, model_id:str, size:int) -> bytes: try: cover_id = model_id if model := self.loaded_models.get(model_id): cover_id = model.get_property('coverArt') or model_id params={ 'id': cover_id } if size != -1: params['size'] = size response = self.send_request( action='getCoverArt', params=params ) response.raise_for_status() return response.content except Exception as e: logger.error(f"can't get image from {model_id}: {e}") return b'' def updateCoverArt(self, model_id:str): if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): local.Local.updateCoverArt(self, model_id) return sizes = { 'gdkPaintableBig': 720, 'gdkPaintable': 240 } for property_name, size in sizes.items(): if not model.get_property(property_name): raw_bytes = self.get_cache_image(model_id, size) save_cache = not raw_bytes if not raw_bytes: raw_bytes = self.getCoverArtBytes(model_id, size) if not raw_bytes and isinstance(model, models.Song): raw_bytes = self.getCoverArtBytes(model.get_property('albumId'), size) if raw_bytes: model.set_property('coverArt', model.get_property('albumId')) # For getCoverArtUrl if raw_bytes: try: gbytes = GLib.Bytes.new(raw_bytes) texture = Gdk.Texture.new_from_bytes(gbytes) model.set_property(property_name, texture) if save_cache: self.save_cache_image(model_id, size, raw_bytes) except Exception as e: logger.error(f"can't convert image from {model_id} (size {size}): {e}") def getCoverArtUrl(self, model_id:str) -> str: if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): return "" params = { **self.get_base_params(), 'id': model.get_property('coverArt') or model.get_property('id'), 'size': 240 } return '{}?{}'.format(self.get_url('getCoverArt'), urlencode(params)) return "" def ping(self) -> dict: response = self.make_request('ping') if response.get('status') == 'ok': return super().ping() else: return { 'status': 'error', 'message': '{}\n"{}"'.format(_('Server returned an error'), response.get('error', {}).get('message', _('Unknown Error'))) } def getAlbumList(self, list_type:str="recent", size:int=10, offset:int=0) -> list: # returns a list of IDs params = { 'type': list_type, 'size': size, 'offset': offset } response = self.make_request('getAlbumList2', params) album_ids = [] for album_dict in response.get('albumList2', {}).get('album', []): if list_type != 'starred' or album_dict.get('starred'): if new_id := str(album_dict.get('id', '')): album_dict['id'] = new_id album_ids.append(new_id) if new_id in self.loaded_models: self.loaded_models.get(new_id).update_data(**album_dict) else: self.loaded_models[new_id] = models.Album(**album_dict) return album_ids def getArtists(self, size:int=10) -> list: # if size == -1 then it will return every artist id in their names alphabetical order response = self.make_request('getArtists') artist_dicts = [] for index in response.get('artists', {}).get('index', []): artist_dicts.extend(index.get('artist', [])) if len(artist_dicts) == 0: return [] if size != -1: # randomize the dicts artist_dicts = random.sample(artist_dicts, min(size, len(artist_dicts))) artist_ids = [] for artist_dict in artist_dicts: if new_id := str(artist_dict.get('id', '')): artist_dict['id'] = new_id artist_ids.append(new_id) if new_id in self.loaded_models: self.loaded_models.get(new_id).update_data(**artist_dict) else: self.loaded_models[new_id] = models.Artist(**artist_dict) return artist_ids def getPlaylists(self) -> list: # returns list of playlist ids response = self.make_request('getPlaylists') playlist_ids = [] for playlist_dict in response.get('playlists', {}).get('playlist', []): if new_id := str(playlist_dict.get('id', '')): playlist_dict['id'] = new_id playlist_ids.append(new_id) if new_id in self.loaded_models: self.loaded_models.get(new_id).update_data(**playlist_dict) else: self.loaded_models[new_id] = models.Playlist(**playlist_dict) return playlist_ids def getStarred(self, item_type:str) -> list: items = self.make_request('getStarred').get('starred', {}).get(item_type, []) return [item.get('id') for item in items] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): def update(): base_response = self.make_request('getArtist', {'id': model_id}) base_artist = base_response.get('artist', {}) detail_response = self.make_request('getArtistInfo2', {'id': model_id}) detail_artist = detail_response.get('artistInfo2', {}) if not isinstance(detail_artist, dict): detail_artist = {} artist_dict = {**base_artist, **detail_artist} if artist_dict.get('id'): self.loaded_models.get(model_id).update_data(**artist_dict) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Artist(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(update) else: update() self.threads.submit(self.updateCoverArt, model_id) def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): def update(): response = self.make_request('getAlbum', {'id': model_id}) album_dict = response.get('album', {}) if album_dict.get('id'): self.loaded_models.get(model_id).update_data(**album_dict) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Album(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(update) else: update() self.threads.submit(self.updateCoverArt, model_id) def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True): def update(): response = self.make_request('getPlaylist', {'id': model_id}) playlist_dict = response.get('playlist', {}) if playlist_dict.get('id'): self.loaded_models.get(model_id).update_data(**playlist_dict) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Playlist(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(update) else: update() self.threads.submit(self.updateCoverArt, model_id) def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True): def update(): response = self.make_request('getSong', {'id': model_id}) song_dict = response.get('song', {}) if song_dict.get('id'): if 'artists' not in song_dict and song_dict.get('artistId'): song_dict['artists'] = [{ 'id': song_dict.get('artistId'), 'name': song_dict.get('artist') }] gains = song_dict.get('replayGain') or {} self.loaded_models.get(model_id).update_data(**song_dict, albumGain=gains.get('albumGain', 0.0), trackGain=gains.get('trackGain', 0.0)) elif model_id in self.loaded_models: self.loaded_models.get(model_id).set_property('deleted', True) del self.loaded_models[model_id] if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Song(id=model_id) force_update = True if force_update: if use_threading: self.threads.submit(update) else: update() self.threads.submit(self.updateCoverArt, model_id) def star(self, model_id:str) -> bool: response = None if model := self.loaded_models.get(model_id): if isinstance(model, models.Album): response = self.make_request('star', {'albumId': model_id}) elif isinstance(model, models.Artist): response = self.make_request('star', {'artistId': model_id}) if not response: response = self.make_request('star', {'id': model_id}) return response.get('status') == 'ok' def unstar(self, model_id:str) -> bool: response = None if model := self.loaded_models.get(model_id): if isinstance(model, models.Album): response = self.make_request('unstar', {'albumId': model_id}) elif isinstance(model, models.Artist): response = self.make_request('unstar', {'artistId': model_id}) if not response: response = self.make_request('unstar', {'id': model_id}) return response.get('status') == 'ok' def getPlayQueue(self) -> tuple: # used to retrieve sessions from other clients *at launch* # returns currentId and list for queue response = self.make_request('getPlayQueue') play_queue = response.get('playQueue', {}) song_list = play_queue.get('entry', []) for song_dict in song_list: new_id = str(song_dict.get('id', '')) if new_id not in self.loaded_models: song_dict['id'] = new_id self.loaded_models[new_id] = models.Song(**song_dict) else: self.verifySong(song_dict.get('id'), force_update=True) return play_queue.get('current'), [s.get('id') for s in song_list] def savePlayQueue(self, id_list:list, current:str, position:int) -> bool: # used to save session *on close* so that other clients can retrieve it # position is in ms # return true if ok response = self.make_request('savePlayQueue', { 'id': id_list, 'current': current, 'position': position }) return response.get('status') == 'ok' def getSimilarSongs(self, model_id:str, count:int=20) -> list: # Receives an artist id response = self.make_request('getSimilarSongs', { 'id': model_id, 'count': count }) songs = response.get('similarSongs', {}).get('song', []) for song in songs: self.verifySong(song.get('id')) return [s.get('id') for s in songs if s.get('id')] def getRandomSongs(self, size:int=20) -> list: response = self.make_request('getRandomSongs', { 'size': size }) songs = response.get('randomSongs', {}).get('song', []) for song in songs: self.verifySong(song.get('id')) return [s.get('id') for s in songs if s.get('id')] def getLyrics(self, songId:str, requestOnline:bool=False) -> tuple: # Initial Checks if songId not in self.loaded_models: return 'not-found', '' # 1. Database lyrics_type, content = super().getLyrics(songId) if lyrics_type != 'not-found': return lyrics_type, content # 2. Integration def job(): lyrics_data = self.make_request('getLyricsBySongId', {'id': songId}).get('lyricsList') or {} lyrics = (lyrics_data.get('structuredLyrics') or [{}])[0] if lyrics.get('synced'): lrc_lines = [] for line in lyrics.get('line', []): ms = line.get('start') minutes = ms // 60000 seconds = (ms % 60000) // 1000 centiseconds = (ms % 1000) // 10 timestamp = f"[{minutes:02d}:{seconds:02d}.{centiseconds:02d}]" lrc_lines.append(f"{timestamp} {line.get('value').strip()}") if content := '\n'.join(lrc_lines): return True, content return True, '' if content := self.cache_manager.get_result(f'IntegrationLyrics:{songId}', job): self.saveLyrics(songId, content, 'lrc') return 'lrc', content # 3. Online if requestOnline: return super().getLyrics(songId, requestOnline) return 'not-found-locally', '' def search(self, query:str, artistCount:int=0, artistOffset:int=0, albumCount:int=0, albumOffset:int=0, songCount:int=0, songOffset:int=0, playlistCount:int=0, playlistOffset:int=0) -> dict: response = self.make_request('search3', { 'query': query, 'artistCount': artistCount, 'artistOffset': artistOffset, 'albumCount': albumCount, 'albumOffset': albumOffset, 'songCount': songCount, 'songOffset': songOffset }) search_results = response.get('searchResult3') for model in search_results.get('artist') or []: model['id'] = str(model.get('id', '')) if model.get('id') not in self.loaded_models: self.loaded_models[model.get('id')] = models.Artist(**model) for model in search_results.get('album') or []: model['id'] = str(model.get('id', '')) if model.get('id') not in self.loaded_models: self.loaded_models[model.get('id')] = models.Album(**model) for model in search_results.get('song') or []: model['id'] = str(model.get('id', '')) if model.get('id') not in self.loaded_models: self.loaded_models[model.get('id')] = models.Song(**model) # Playlists playlist_ids = [] for playlistId in self.getPlaylists(): if playlist := self.loaded_models.get(playlistId): if re.search(query, playlist.get_property('name'), re.IGNORECASE): playlist_ids.append(playlistId) return { 'artist': [m.get('id') for m in search_results.get('artist') or []], 'album': [m.get('id') for m in search_results.get('album') or []], 'song': [m.get('id') for m in search_results.get('song') or []], 'playlist': playlist_ids[playlistOffset:playlistCount+playlistOffset] } def systemSearch(self, query:str) -> dict: results = {} response = self.make_request('search3', { 'query': query, 'artistCount': 5, 'albumCount': 5, 'songCount': 5, }).get('searchResult3', {}) # Artists for artist in response.get('artist', []): icon_bytes = self.getCoverArtBytes(artist.get('id'), 128) results[artist.get('id')] = { 'display': GLib.Variant('s', artist.get('name')), 'type': GLib.Variant('s', 'artist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Albums for album in response.get('album', []): display_name = album.get('name') try: if artists := album.get('artists'): if len(artists) > 0: if artist := artists[0].get('name'): display_name = '{} โ€ข {}'.format(album.get('name'), artist) except: display_name = album.get('name') icon_bytes = self.getCoverArtBytes(album.get('id'), 128) results[album.get('id')] = { 'display': GLib.Variant('s', display_name), 'type': GLib.Variant('s', 'album'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Songs for song in response.get('song', []): display_name = song.get('title') try: if artists := song.get('artists'): if len(artists) > 0: if artist := artists[0].get('name'): display_name = '{} โ€ข {}'.format(song.get('title'), artist) except: display_name = song.get('title') icon_bytes = self.getCoverArtBytes(song.get('id'), 128) results[song.get('id')] = { 'display': GLib.Variant('s', display_name), 'type': GLib.Variant('s', 'song'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } # Playlists for playlistId in self.getPlaylists(): if playlist := self.loaded_models.get(playlistId): if re.search(query, playlist.get_property('name'), re.IGNORECASE): icon_bytes = self.getCoverArtBytes(playlistId, 128) results[playlistId] = { 'display': GLib.Variant('s', playlist.get_property('name')), 'type': GLib.Variant('s', 'playlist'), 'icon': GLib.Variant('ay', bytearray(icon_bytes)) } return results def getInternetRadioStations(self) -> list: response = self.make_request('getInternetRadioStations') radios = response.get('internetRadioStations', {}).get('internetRadioStation', []) for radio in radios: radio['id'] = str(radio.get('id', '')) if radio.get('id') not in self.loaded_models: self.loaded_models[radio.get('id')] = models.Song( id=radio.get('id'), title=radio.get('name'), radioStreamUrl=radio.get('streamUrl'), duration=-1 ) return [radio.get('id') for radio in radios] def createInternetRadioStation(self, name:str, radioStreamUrl:str) -> bool: # returns true if ok parsedStreamUrl = urlparse(radioStreamUrl) response = self.make_request('createInternetRadioStation', { 'name': name, 'streamUrl': radioStreamUrl, 'homepageUrl': '{}://{}'.format(parsedStreamUrl.scheme, parsedStreamUrl.netloc) }) return response.get('status') == 'ok' def updateInternetRadioStation(self, model_id:str, name:str, radioStreamUrl:str) -> bool: # returns true if ok parsedStreamUrl = urlparse(radioStreamUrl) response = self.make_request('updateInternetRadioStation', { 'id': model_id, 'name': name, 'streamUrl': radioStreamUrl, 'homepageUrl': '{}://{}'.format(parsedStreamUrl.scheme, parsedStreamUrl.netloc) }) return response.get('status') == 'ok' def deleteInternetRadioStation(self, model_id:str) -> bool: # returns true if ok response = self.make_request('deleteInternetRadioStation', { 'id': model_id }) return response.get('status') == 'ok' def createPlaylist(self, name:str=None, playlistId:str=None, songId:list=[]) -> str: # returns id # if playlistId is added then the name is updated if playlistId: response = self.make_request('updatePlaylist', { 'playlistId': playlistId, 'name': name }) if response.get('status') == 'ok': return playlistId else: response = self.make_request('createPlaylist', { 'name': name, 'songId': songId }) return response.get('playlist', {}).get('id') return '' def updatePlaylist(self, playlistId:str, songIdToAdd:list=[], songIndexToRemove:list=[]) -> bool: # returns true if ok response = self.make_request('updatePlaylist', { 'playlistId': playlistId, 'songIdToAdd': songIdToAdd, 'songIndexToRemove': songIndexToRemove }) return response.get('status') == 'ok' def deletePlaylist(self, model_id:str) -> bool: # returns true if ok response = self.make_request('deletePlaylist', { 'id': model_id }) return response.get('status') == 'ok' def setRating(self, model_id:str, rating:int=0) -> bool: response = self.make_request('setRating', { 'id': model_id, 'rating': rating }) if response.get('status') == 'ok': self.loaded_models.get(model_id).set_property('userRating', rating) return True return False def getTopSongs(self, artist_id:str, count:int=10) -> list: model = self.loaded_models.get(artist_id) if not model or not model.get_property('name'): self.verifyArtist(artist_id, force_update=True, use_threading=False) top_songs = self.make_request('getTopSongs', { 'artist': model.get_property('name'), 'count': count }).get('topSongs', {}).get('song', []) return [song.get('id') for song in top_songs if song.get('id')] def downloadSong(self, model_id:str, file_title:str, progress_callback:callable): params = { **self.get_base_params(), 'id': model_id } try: with self.session.get(self.get_url('download'), params=params, stream=True) as r: r.raise_for_status() total_size = int(r.headers.get('content-length', 0)) downloaded_size = 0 extension = DOWNLOAD_MIME_MAP.get(r.headers.get('Content-Type'), '.mp3') file_name = '{}{}'.format(file_title, extension) file_path = os.path.join(DOWNLOAD_QUEUE_DIR, file_name) with open(file_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded_size += len(chunk) if total_size > 0: progress_callback(downloaded_size / total_size) os.replace(file_path, os.path.join(DOWNLOADS_DIR, file_name)) except: pass def scrobble(self, model_id:str, submission:bool = True): # Registers the song as played, useful for keeping track of "most played" albums and the sorts if model := self.loaded_models.get(model_id) : if not model.isExternalFile: self.make_request('scrobble', { 'id': model_id, 'submission': submission }) super().scrobble(model_id, submission) def getSongDetails(self, model_id:str) -> models.SongDetails: song_dict = self.make_request('getSong', { 'id': model_id, }).get('song', {}) song_dict['trackGain'] = song_dict.get('replayGain', {}).get('trackGain') or 0.0 song_dict['albumGain'] = song_dict.get('replayGain', {}).get('albumGain') or 0.0 return models.SongDetails(**song_dict) def getServerInformation(self) -> dict: server_information = { 'link': self.get_property('url').strip('/'), 'username': self.get_property('user').title() } try: response = self.send_request('ping') if response.status_code in (200, 201): data = response.json().get('subsonic-response', {}) server_information['title'] = "{} {}".format(data.get('type'), data.get('serverVersion')).title() except Exception as e: logger.error(f"can't get server information: {e}") try: response = self.send_request( action='getAvatar', params={ 'username': self.get_property('user') } ) response_bytes = response.content if response.status_code in (200, 201) else b'' if response_bytes and len(response_bytes) > 0: gbytes = GLib.Bytes.new(response_bytes) server_information['picture'] = Gdk.Texture.new_from_bytes(gbytes) except Exception as e: logger.error(f"can't get profile picture: {e}") return server_information class NavidromeIntegrated(Navidrome): __gtype_name__ = 'NocturneIntegrationNavidromeIntegrated' login_page_metadata = { 'icon-name': "music-note-symbolic", 'title': _("Managed Server"), 'description': _("Connect to a Navidrome instance directly managed by Nocturne."), 'entries': ['status', 'library-dir', 'user', 'password'], 'extra-menu': { 'title': _("Manage Server"), 'context': CONTEXT_MANAGED_NAVIDROME_SERVER } } button_metadata = { 'title': _("Managed Server"), 'subtitle': _("Create and use a Navidrome instance") } url = GObject.Property(type=str, default="http://127.0.0.1:4534") serverRunning = GObject.Property(type=bool, default=False) process = None def ping(self) -> dict: gio_file = Gio.File.new_for_path(self.get_property('libraryDir')) if new_path := gio_file.get_path(): self.set_property('libraryDir', new_path) if self.start_instance(): return super().ping() return { 'status': 'error', 'message': _('Could not locate path, try again') } def check_if_ready(self, row) -> bool: if get_navidrome_path(): return True else: row.get_root().main_stack.set_visible_child_name('setup') row.get_root().main_stack.get_child_by_name('setup').set_integration(self) return False def start_instance(self) -> bool: path = get_navidrome_path() env = get_navidrome_env() library_directory = self.get_property('libraryDir') if self.process: return True try: if path and env and library_directory: env["ND_MUSICFOLDER"] = library_directory self.process = subprocess.Popen([path], env=env) self.set_property('serverRunning', True) return True else: self.set_property('serverRunning', False) return False except Exception as e: logger.error(f"can't start server: {e}") self.set_property('serverRunning', False) return False def terminate_instance(self): if self.process: self.process.terminate() self.process = None self.set_property('serverRunning', False) class Bandcamp(Navidrome): __gtype_name__ = 'NocturneIntegrationBandcamp' login_page_metadata = { 'icon-name': "bandcamp-symbolic", 'title': "Bandcamp", 'description': _("Important: Use your Subsonic login information."), 'entries': ['user', 'password'], 'additional-link': { 'label': _("Bandcamp User Settings"), 'url': 'https://bandcamp.com/settings?pane=fan' } } button_metadata = { 'title': "Bandcamp (Beta)", 'subtitle': _("Explore your online library") } url = GObject.Property(type=str, default="https://bandcamp.com/api/subsonic") sqlSchema = { 'radios': { 'id': 'TEXT PRIMARY KEY', 'name': 'TEXT NOT NULL', 'stream_url': 'TEXT NOT NULL' }, 'ratings': { 'id': 'TEXT PRIMARY KEY', 'rating': 'INTEGER DEFAULT 1' }, 'scrobble': { 'id': 'TEXT PRIMARY KEY', 'plays': 'INTEGER DEFAULT 1', 'last_play': 'INTEGER DEFAULT 0', 'album_id': 'TEXT NOT NULL', 'artist_id': 'TEXT NOT NULL' } } def getServerInformation(self) -> dict: server_info = super().getServerInformation() server_info['link'] = 'https://bandcamp.com' return server_info # Implementation of Radios --------- def getInternetRadioStations(self) -> list: conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT id, name, stream_url FROM radios") radio_ids = [] for radio in cursor.fetchall(): radio_ids.append(radio[0]) self.loaded_models[radio[0]] = models.Song( id=radio[0], title=radio[1], radioStreamUrl=radio[2], duration=-1 ) conn.close() return radio_ids def createInternetRadioStation(self, name:str, radioStreamUrl:str) -> bool: radio_id = 'RADIO:{}'.format(str(uuid.uuid4())) return self.updateInternetRadioStation(radio_id, name, radioStreamUrl) def updateInternetRadioStation(self, model_id:str, name:str, radioStreamUrl:str) -> bool: conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO radios (id, name, stream_url) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = excluded.name, stream_url = excluded.stream_url """ cursor.execute(query, (model_id, name, radioStreamUrl)) conn.commit() conn.close() return True def deleteInternetRadioStation(self, model_id:str) -> bool: conn, cursor = sql_instance.get_connection(self) cursor.execute("DELETE FROM radios WHERE id = ?", (model_id,)) conn.commit() conn.close() return True def getRandomSongs(self, size:int=20) -> list: song_ids = [] albums = self.make_request('getAlbumList2', { 'type': 'random', 'size': 20 }).get('albumList2', {}).get('album', []) for album in albums: album_detail = self.make_request('getAlbum', { 'id': album.get('id') }).get('album', {}) song_ids.extend([s.get('id') for s in album_detail.get('song')]) song_ids = random.sample(song_ids, size) for song_id in song_ids: self.verifySong(song_id) return song_ids # Implementation of Ratings --------- def get_rating(self, model_id) -> int: # For internal use conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT rating FROM ratings WHERE id = ?", (model_id,)) result = cursor.fetchone() conn.close() return result[0] if result else 0 def setRating(self, model_id:str, rating:int=0) -> bool: conn, cursor = sql_instance.get_connection(self) if rating == 0: cursor.execute("DELETE FROM ratings WHERE id = ?", (model_id,)) else: query = """ INSERT INTO ratings (id, rating) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET rating = excluded.rating """ cursor.execute(query, (model_id, rating)) conn.commit() conn.close() return True def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): super().verifyArtist(model_id, force_update, use_threading) if model := self.loaded_models.get(model_id): model.set_property('userRating', self.get_rating(model_id)) def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): super().verifyAlbum(model_id, force_update, use_threading) if model := self.loaded_models.get(model_id): model.set_property('userRating', self.get_rating(model_id)) def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True): super().verifySong(model_id, force_update, use_threading) if model := self.loaded_models.get(model_id): model.set_property('userRating', self.get_rating(model_id)) # Implementation of Play Queue --------- def getPlayQueue(self) -> tuple: queue_dict = self.open_json('queue.json') song_list = [model_id for model_id in queue_dict.get('id', []) if model_id in self.loaded_models] current = queue_dict.get('current', "") if current not in song_list: if len(song_list) > 0: current = song_list[0] else: current = "" return current, song_list def savePlayQueue(self, id_list:list, current:str, position:int) -> bool: final_id_list = [] for model_id in id_list: if model := self.loaded_models.get(model_id): if not model.get_property('isExternalFile'): final_id_list.append(model_id) if current not in final_id_list: if len(final_id_list) > 0: current = final_id_list[0] else: current = "" queue_dict = { 'id': final_id_list, 'current': current, 'position': position } self.save_json('queue.json', queue_dict) return True # Implementation of Scrobble / album pages def scrobble(self, model_id:str, submission:bool = True): if not model_id: return if model := self.loaded_models.get(model_id): if model.get_property('isExternalFile') or model.get_property('radioStreamUrl'): return if submission: conn, cursor = sql_instance.get_connection(self) query = """ INSERT INTO scrobble (id, plays, last_play, album_id, artist_id) VALUES (?, 1, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET plays = plays + 1, last_play = excluded.last_play, album_id = excluded.album_id, artist_id = excluded.artist_id """ cursor.execute(query, (model_id, int(time.time()), model.get_property('albumId'), model.get_property('artistId'))) conn.commit() conn.close() super().scrobble(model_id, submission) def getAlbumList(self, list_type:str="recent", size:int=10, offset:int=0) -> list: if list_type in ("frequent", "recent"): album_views = {} conn, cursor = sql_instance.get_connection(self) cursor.execute("SELECT album_id, plays, last_play FROM scrobble") for row in cursor.fetchall(): album_id = row[0] plays = row[1] last_play = row[2] if album_id in album_views: album_views[album_id]['plays'] += plays album_views[album_id]['last_play'] = max(album_views.get(album_id).get('last_play'), last_play) else: album_views[album_id] = { 'plays': plays, 'last_play': last_play } conn.close() if list_type == "frequent": album_list = sorted(album_views, key=lambda x: album_views.get(x).get('plays'), reverse=True) else: album_list = sorted(album_views, key=lambda x: album_views.get(x).get('last_play'), reverse=True) return [model_id for model_id in album_list if model_id in self.loaded_models][offset:size+offset] else: return super().getAlbumList(list_type, size, offset) def downloadSong(self, model_id:str, file_title:str, progress_callback:callable): params = { **self.get_base_params(), 'id': model_id, 'maxBitRate': 0, 'format': 'raw' } try: with self.session.get(self.get_url('stream'), params=params, stream=True) as r: r.raise_for_status() total_size = int(r.headers.get('content-length', 0)) downloaded_size = 0 extension = DOWNLOAD_MIME_MAP.get(r.headers.get('Content-Type'), '.mp3') file_name = '{}{}'.format(file_title, extension) file_path = os.path.join(DOWNLOAD_QUEUE_DIR, file_name) with open(file_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded_size += len(chunk) if total_size > 0: progress_callback(downloaded_size / total_size) os.replace(file_path, os.path.join(DOWNLOADS_DIR, file_name)) except: pass Jeffser-Nocturne-b442fee/src/integrations/secret.py000066400000000000000000000032161523240030000225110ustar00rootroot00000000000000# secret.py from gi.repository import Secret import hashlib, secrets, string from ..constants import FALLBACK_PASSWORD_PATH BASE_SCHEMA = Secret.Schema.new( "com.jeffser.Nocturne.Password", Secret.SchemaFlags.NONE, { "type": Secret.SchemaAttributeType.STRING } ) def store_password(password:str, schema_type:str="password"): try: attributes = {"type": schema_type} Secret.password_store_sync( BASE_SCHEMA, attributes, Secret.COLLECTION_DEFAULT, "Nocturne Login", password, None ) except Exception as e: with open(FALLBACK_PASSWORD_PATH, 'w') as f: f.write(password) def get_plain_password(schema_type:str="password") -> str: # returns plain password try: attributes = {"type": schema_type} return Secret.password_lookup_sync( BASE_SCHEMA, attributes, None ) except Exception as e: with open(FALLBACK_PASSWORD_PATH, 'r') as f: return f.read() return "" def get_hashed_password(schema_type:str="password") -> tuple: # returns salt, hashed password salt = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8)) salted_password = get_plain_password(schema_type) + salt hashed_password = hashlib.md5(salted_password.encode('utf-8')).hexdigest() return salt, hashed_password def remove_password(schema_type:str="password", callback:callable=lambda:None): Secret.password_clear( BASE_SCHEMA, {"type": schema_type}, None, callback, None ) Jeffser-Nocturne-b442fee/src/integrations/sql_instance.py000066400000000000000000000136751523240030000237210ustar00rootroot00000000000000# sql_instance.py import sqlite3, os from ..constants import CACHE_DIR def get_cache_connection() -> tuple: conn = sqlite3.connect(os.path.join(CACHE_DIR, 'cache_database.db')) cursor = conn.cursor() return conn, cursor def ensure_cache_schema(): conn, cursor = get_cache_connection() schema_dict = { 'images': { 'integration': 'TEXT NOT NULL', 'model': 'TEXT NOT NULL', 'size': 'INTEGER DEFAULT 1', 'image': 'BLOB', 'UNIQUE': '(integration, model, size)' } } try: for table_name, columns in schema_dict.items(): cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,) ) if not cursor.fetchone(): column_defs = ", ".join([f"{name} {props}" for name, props in columns.items()]) query = f"CREATE TABLE {table_name} ({column_defs});" cursor.execute(query) conn.commit() except sqlite3.Error as e: print("Error", e) return def get_connection(integration) -> tuple: conn = sqlite3.connect(os.path.join(integration.getIntegrationDir(), 'database.db')) cursor = conn.cursor() return conn, cursor def ensure_schema(integration): conn, cursor = get_connection(integration) schema_dict = integration.get_sql_schema() try: for table_name, columns in schema_dict.items(): cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,) ) if not cursor.fetchone(): column_defs = ", ".join([f"{name} {props}" for name, props in columns.items()]) query = f"CREATE TABLE {table_name} ({column_defs});" cursor.execute(query) conn.commit() except sqlite3.Error as e: print("Error", e) return # JSON Migration cursor = conn.cursor() ## Playlist Resume (Base) if 'playlist_resume' in schema_dict: if playlist_resume_dict := integration.open_json('playlist_resume.json'): to_insert = [] for playlistId, songId in playlist_resume_dict.items(): to_insert.append((playlistId, songId)) query = """ INSERT INTO playlist_resume (id, song_id) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET song_id = excluded.song_id; """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'playlist_resume.json')) ## Playback (Base) if 'playback_scrobble' in schema_dict: if playback_dict := integration.open_json('playback.json'): to_insert = [] for month, data in playback_dict.items(): for songId, amount in data.items(): to_insert.append((month, songId, amount)) query = """ INSERT INTO playback_scrobble (month, song_id, amount) VALUES (?, ?, ?) ON CONFLICT (month, song_id) DO UPDATE SET amount = excluded.amount; """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'playback.json')) ## Stars (Local) if 'stars' in schema_dict: if stars_dict := integration.open_json('stars.json'): to_insert = [] for songId, value in stars_dict.items(): if value: to_insert.append((songId,)) query = """ INSERT OR IGNORE INTO stars (id) VALUES (?) """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'stars.json')) ## Radios (Local) if 'radios' in schema_dict: if radios_dict := integration.open_json('radios.json'): to_insert = [] for radioId, data in radios_dict.items(): row = (radioId, data.get('name'), data.get('streamUrl')) if all(row): to_insert.append(row) query = """ INSERT INTO radios (id, name, stream_url)` VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET name = excluded.name, stream_url = excluded.stream_url """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'radios.json')) ## Ratings (Local and Jellyfin) if 'ratings' in schema_dict: if ratings_dict := integration.open_json('ratings.json'): to_insert = [(songId, rating) for songId, rating in ratings_dict.items() if rating > 0] query = """ INSERT INTO ratings (id, rating) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET rating = excluded.rating """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'ratings.json')) ## Scrobble (Local) if 'scrobble' in schema_dict: if scrobble_dict := integration.open_json('scrobble.json'): to_insert = [] for songId, data in scrobble_dict.items(): to_insert.append((songId, data.get("plays"), data.get("last_play"), data.get("album", ""), data.get("artist", ""))) query = """ INSERT INTO scrobble (id, plays, last_play, album_id, artist_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET plays = excluded.plays, last_play = excluded.last_play, album_id = excluded.album_id, artist_id = excluded.artist_id """ cursor.executemany(query, to_insert) os.remove(os.path.join(integration.getIntegrationDir(), 'scrobble.json')) conn.commit() conn.close() Jeffser-Nocturne-b442fee/src/main.py000066400000000000000000000273251523240030000174510ustar00rootroot00000000000000# main.py # # Copyright 2026 Jeffry Samuel # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later import sys, threading, logging, argparse, sys from pydbus import SessionBus import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') gi.require_version('Secret', '1') gi.require_version('Gst', '1.0') gi.require_version('Xdp', '1.0') gi.require_version('XdpGtk4', '1.0') from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Xdp, XdpGtk4 from .window import NocturneWindow from .preferences import NocturnePreferences from .constants import get_song_info_from_file, CLI_ARGUMENTS, TRANSLATORS, COPYRIGHT_NOTICE, set_version from .integrations import get_current_integration, set_current_integration, get_available_integrations, models from .widgets.playing import Player from .widgets.pages import LoginDialog GLib.set_prgname('com.jeffser.Nocturne') GLib.set_application_name("Nocturne") logger = logging.getLogger(__name__) parser = argparse.ArgumentParser(description="Nocturne has to already be running for the options to work") class NocturneService: """ """ def __init__(self, app): self.app = app def Search(self, query:str) -> dict: if integration := get_current_integration(): results = integration.systemSearch(query) return results return {} class NocturneApplication(Adw.Application): __gtype_name__ = 'NocturneApplication' """The main application singleton class.""" def __init__(self, version): self.version = version self.external_songs = [] self.main_window = None self.popout_window = None self.player = None self.inhibit_cookie = None self.idle_inhibit_cookie = None self.css_provider = Gtk.CssProvider() Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) super().__init__(application_id='com.jeffser.Nocturne', flags=Gio.ApplicationFlags.DEFAULT_FLAGS | Gio.ApplicationFlags.HANDLES_OPEN, resource_base_path='/com/jeffser/Nocturne') self.create_action('quit', lambda *_: self.quit(), ['q']) self.create_action('about', self.on_about_action) self.create_action('preferences', self.on_preferences_action, ['comma']) def inhibit_suspend(self): if self.inhibit_cookie is None: self.inhibit_cookie = self.inhibit( self.get_active_window(), Gtk.ApplicationInhibitFlags.SUSPEND, _("Music is Playing") ) def uninhibit_suspend(self): if self.inhibit_cookie is not None: self.uninhibit(self.inhibit_cookie) self.inhibit_cookie = None def inhibit_idle(self, window=None): if self.idle_inhibit_cookie is None: self.idle_inhibit_cookie = self.inhibit( window or self.get_active_window(), Gtk.ApplicationInhibitFlags.IDLE, _("Fullscreen Player Active") ) def uninhibit_idle(self): if self.idle_inhibit_cookie is not None: self.uninhibit(self.idle_inhibit_cookie) self.idle_inhibit_cookie = None def load_default_integration(self): settings = Gio.Settings(schema_id="com.jeffser.Nocturne") selected_local_folder = settings.get_value("integration-library-dir").unpack() if selected_instance := settings.get_value("selected-instance-type").unpack(): if integration_type := get_available_integrations().get(selected_instance): integration = integration_type( url=settings.get_value('integration-ip').unpack(), user=settings.get_value('integration-user').unpack(), trustServer=settings.get_value('integration-trust-server').unpack() ) directory = settings.get_value('integration-library-dir').unpack() if Gio.File.new_for_path(directory).query_exists(): integration.set_property('libraryDir', directory) threading.Thread(target=self.try_login, args=(integration,), daemon=True).start() return self.main_window.main_stack.set_visible_child_name('welcome') def try_login(self, integration): # call on different thread ping_result = integration.ping() if ping_result.get('status') == 'ok': set_current_integration(integration) integration.on_login() GLib.idle_add(self.main_window.main_stack.set_visible_child_name, "content") GLib.idle_add(self.main_window.setup) if not self.player: self.player = Player(self) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") default_page = settings.get_value('default-page-tag').unpack() or 'home' self.main_window.activate_action("app.replace_root_page", GLib.Variant('s', default_page)) if settings.get_value("restore-session").unpack(): threading.Thread(target=self.player.restore_play_queue, daemon=True).start() if dialog := self.main_window.get_visible_dialog(): dialog.close() else: GLib.idle_add(self.main_window.main_stack.set_visible_child_name, 'welcome') toast = Adw.Toast( title=_("Login Failed"), action_name='app.show_error', action_target=GLib.Variant('s', ping_result.get('message')), button_label=_("Show Details") ) dialog = self.main_window.get_visible_dialog() if not isinstance(dialog, LoginDialog): dialog = LoginDialog(integration) GLib.idle_add(dialog.present, self.main_window) GLib.idle_add(dialog.toast_overlay.add_toast, toast) GLib.idle_add(dialog.login_button_el.set_sensitive, True) def do_activate(self): if not self.main_window: self.main_window = NocturneWindow(application=self) self.load_default_integration() self.main_window.present() app_service = NocturneService(self) if 'linux' in sys.platform: bus = SessionBus() dbus_proxy = bus.get('org.freedesktop.DBus', '/org/freedesktop/DBus') if not dbus_proxy.NameHasOwner('com.jeffser.Nocturne.Service'): bus.publish('com.jeffser.Nocturne.Service', ('/com/jeffser/Nocturne/Service', app_service)) def do_open(self, files, n_files=None, hint=None): self.external_songs = [] integration = get_current_integration() for file in files: result_path = file.get_path() audio_info = get_song_info_from_file(result_path, is_external_file=True) audio_info['id'] = 'EXTERNAL_SONG:{}'.format(result_path) if audio_info: self.external_songs.append(models.Song(**audio_info)) if integration: integration.loaded_models[audio_info.get('id')] = self.external_songs[-1] if self.main_window and integration: target_value = GLib.Variant('as', [a.id for a in self.external_songs]) self.main_window.activate_action('app.play_songs', target_value) self.external_songs = [] else: self.do_activate() def on_about_action(self, *args): about = Adw.AboutDialog( application_icon="com.jeffser.Nocturne", application_name="Nocturne", copyright=COPYRIGHT_NOTICE, developer_name="Jeffry Samuel", issue_url="https://github.com/Jeffser/Nocturne/issues", license="GPL-3.0-or-later", support_url="https://github.com/Jeffser/Nocturne/discussions", version=self.version, website="https://jeffser.com/nocturne", developers=['Jeffser https://jeffser.com'], designers=['Jeffser https://jeffser.com'], translator_credits='\n'.join(TRANSLATORS) ) about.present(self.props.active_window) def on_preferences_action(self, widget, _): NocturnePreferences().present(self.props.active_window) def create_action(self, name, callback, shortcuts=None, parameter_type=None): action = Gio.SimpleAction.new(name, parameter_type) action.connect("activate", callback) self.add_action(action) if shortcuts: self.set_accels_for_action(f"app.{name}", shortcuts) def main(version): logging.basicConfig( format="%(levelname)s\t[%(filename)s | %(funcName)s] %(message)s", level=logging.INFO, handlers=[logging.StreamHandler(stream=sys.stdout)] ) for group_name, group_data in CLI_ARGUMENTS.items(): group = parser.add_argument_group(group_name) for argument_name, argument_data in group_data.items(): if action_name := argument_data.get('action-name'): if metavar := argument_data.get('metavar'): group.add_argument(argument_name, type=str, metavar=metavar, help=argument_data.get('message'), dest=action_name) else: group.add_argument(argument_name, action='store_true', help=argument_data.get('message'), dest=action_name) exit = False try: args, unknown = parser.parse_known_args() actions_to_call = {} # action-name : value for action, value in vars(args).items(): if action == 'search' and value and isinstance(value, str): if app_service := SessionBus().get("com.jeffser.Nocturne.Service"): # .Service cause it returns results results = {} for item_id, item in app_service.Search(value).items(): if item.get('type') not in results: results[item.get('type')] = [] results[item.get('type')].append({ 'display': item.get('display'), 'id': item_id }) print(results) exit = True elif value: actions_to_call[action] = [GLib.Variant('s', value)] if isinstance(value, str) else [] if len(actions_to_call) > 0: if app_service := SessionBus().get("com.jeffser.Nocturne"): for action, value in actions_to_call.items(): print(action, value) app_service["org.gtk.Actions"].Activate(action, value, {}) exit = True except Exception as e: logger.error(e) sys.exit(1) finally: if exit: sys.exit(0) logger.info(f"Nocturne version: {version}") set_version(version) return NocturneApplication(version).run(sys.argv) Jeffser-Nocturne-b442fee/src/meson.build000066400000000000000000000053771523240030000203200ustar00rootroot00000000000000pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name() moduledir = pkgdatadir / 'nocturne' gnome = import('gnome') blueprints = files( 'ui/window.blp', 'ui/shortcuts-dialog.blp', 'ui/preferences.blp', 'ui/playing/footer.blp', 'ui/playing/control_page.blp', 'ui/playing/queue_page.blp', 'ui/playing/lyrics_page.blp', 'ui/playing/popout_window.blp', 'ui/playing/volume_button.blp', 'ui/playing/playback_mode_button.blp', 'ui/playing/equalizer_page.blp', 'ui/playing/cover_art.blp', 'ui/pages/home.blp', 'ui/pages/login.blp', 'ui/pages/artists.blp', 'ui/pages/playlists.blp', 'ui/pages/starred.blp', 'ui/pages/songs_all.blp', 'ui/pages/albums.blp', 'ui/pages/albums_all.blp', 'ui/pages/radios.blp', 'ui/pages/welcome.blp', 'ui/pages/setup.blp', 'ui/pages/playback.blp', 'ui/album/button.blp', 'ui/album/page.blp', 'ui/album/row.blp', 'ui/artist/button.blp', 'ui/artist/page.blp', 'ui/artist/row.blp', 'ui/playlist/button.blp', 'ui/playlist/page.blp', 'ui/playlist/row.blp', 'ui/playlist/dialog.blp', 'ui/playlist/selector_row.blp', 'ui/song/row.blp', 'ui/song/queue.blp', 'ui/song/small_row.blp', 'ui/song/details_dialog.blp', 'ui/song/button.blp', 'ui/containers/carousel.blp', 'ui/containers/wrapbox.blp', 'ui/containers/page_dialog.blp', 'ui/containers/downloads_queue_button.blp', 'ui/containers/download_row.blp', 'ui/lyrics/dialog.blp', 'ui/lyrics/edit_row.blp', ) blueprints = custom_target('blueprints', input: blueprints, output: '.', command: [ find_program('blueprint-compiler', version: '>= 0.18.0'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@' ], ) gnome.compile_resources( 'nocturne', 'nocturne.gresource.xml', gresource_bundle: true, install: true, install_dir: pkgdatadir, dependencies: blueprints, source_dir: meson.current_source_dir(), ) python = import('python') conf = configuration_data() conf.set('PYTHON', python.find_installation('python3').full_path()) conf.set('VERSION', meson.project_version()) conf.set('localedir', get_option('prefix') / get_option('localedir')) conf.set('pkgdatadir', pkgdatadir) configure_file( input: 'nocturne.in', output: 'nocturne', configuration: conf, install: true, install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x' ) configure_file( input: 'nocturne_search_provider.py.in', output: 'nocturne_search_provider', configuration: conf, install: true, install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x' ) nocturne_sources = [ '__init__.py', 'main.py', 'window.py', 'actions.py', 'constants.py', 'preferences.py', ] install_data(nocturne_sources, install_dir: moduledir) subdir('widgets') subdir('integrations')Jeffser-Nocturne-b442fee/src/nocturne.gresource.xml000066400000000000000000000252421523240030000225230ustar00rootroot00000000000000 style.css icons/text-justify-center-symbolic.svg icons/media-playback-pause-symbolic.svg icons/media-playback-start-symbolic.svg icons/media-skip-backward-symbolic.svg icons/media-skip-forward-symbolic.svg icons/non-starred-symbolic.svg icons/playlist-shuffle-symbolic.svg icons/starred-symbolic.svg icons/sound-symbolic.svg icons/view-more-symbolic.svg icons/playlist-symbolic.svg icons/playlist2-symbolic.svg icons/list-high-priority-symbolic.svg icons/list-low-priority-symbolic.svg icons/external-link-symbolic.svg icons/object-select-symbolic.svg icons/user-trash-symbolic.svg icons/window-close-symbolic.svg icons/list-drag-handle-symbolic.svg icons/speaker-0-symbolic.svg icons/speaker-1-symbolic.svg icons/speaker-2-symbolic.svg icons/speaker-3-symbolic.svg icons/music-queue-symbolic.svg icons/media-playlist-consecutive-symbolic.svg icons/media-playlist-repeat-song-symbolic.svg icons/media-playlist-repeat-symbolic.svg icons/list-add-symbolic.svg icons/music-artist-symbolic.svg icons/music-note-symbolic.svg icons/sound-wave-symbolic.svg icons/user-home-symbolic.svg icons/sad-computer-symbolic.svg icons/open-menu-symbolic.svg icons/person-symbolic.svg icons/document-edit-symbolic.svg icons/preferences-system-symbolic.svg icons/go-previous-symbolic.svg icons/go-next-symbolic.svg icons/check-plain-symbolic.svg icons/stopwatch-symbolic.svg icons/snapshots-alt-symbolic.svg icons/paper-symbolic.svg icons/globe-symbolic.svg icons/grid-filled-symbolic.svg icons/view-list-bullet-symbolic.svg icons/folder-open-symbolic.svg icons/update-symbolic.svg icons/pip-in-symbolic.svg icons/pip-out-symbolic.svg icons/view-fullscreen-symbolic.svg icons/view-unfullscreen-symbolic.svg icons/step-back-symbolic.svg icons/heart-filled-symbolic.svg icons/heart-outline-thick-symbolic.svg icons/network-server-symbolic.svg icons/paintbrush-symbolic.svg icons/cross-large-circle-filled-symbolic.svg icons/cross-large-symbolic.svg icons/playback-options-symbolic.svg icons/info-outline-symbolic.svg icons/dialog-warning-symbolic.svg icons/pan-start-symbolic.svg icons/pan-end-symbolic.svg icons/video-clip-symbolic.svg icons/sidebar-show-right-symbolic.svg icons/system-search-symbolic.svg icons/network-offline-symbolic.svg icons/bandcamp-symbolic.svg icons/jellyfin-symbolic.svg icons/opensubsonic-symbolic.svg icons/playlist-infinite-symbolic.svg ui/window.ui ui/shortcuts-dialog.ui ui/preferences.ui ui/playing/footer.ui ui/playing/control_page.ui ui/playing/queue_page.ui ui/playing/lyrics_page.ui ui/playing/popout_window.ui ui/playing/volume_button.ui ui/playing/playback_mode_button.ui ui/playing/equalizer_page.ui ui/playing/cover_art.ui ui/pages/home.ui ui/pages/login.ui ui/pages/artists.ui ui/pages/playlists.ui ui/pages/starred.ui ui/pages/songs_all.ui ui/pages/albums.ui ui/pages/albums_all.ui ui/pages/radios.ui ui/pages/welcome.ui ui/pages/setup.ui ui/pages/playback.ui ui/album/button.ui ui/album/page.ui ui/album/row.ui ui/artist/button.ui ui/artist/page.ui ui/artist/row.ui ui/playlist/button.ui ui/playlist/page.ui ui/playlist/row.ui ui/playlist/dialog.ui ui/playlist/selector_row.ui ui/song/row.ui ui/song/queue.ui ui/song/small_row.ui ui/song/details_dialog.ui ui/song/button.ui ui/containers/carousel.ui ui/containers/wrapbox.ui ui/containers/page_dialog.ui ui/containers/downloads_queue_button.ui ui/containers/download_row.ui ui/lyrics/dialog.ui ui/lyrics/edit_row.ui Jeffser-Nocturne-b442fee/src/nocturne.in000077500000000000000000000025541523240030000203400ustar00rootroot00000000000000#!@PYTHON@ # nocturne.in # # Copyright 2026 Jeffry Samuel # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later import os import sys import signal import locale import gettext VERSION = '@VERSION@' pkgdatadir = '@pkgdatadir@' localedir = '@localedir@' sys.path.insert(1, pkgdatadir) signal.signal(signal.SIGINT, signal.SIG_DFL) if hasattr(locale, 'bindtextdomain'): locale.bindtextdomain('nocturne', localedir) locale.textdomain('nocturne') gettext.install('nocturne', localedir, names=['ngettext']) if __name__ == '__main__': import gi from gi.repository import Gio resource = Gio.Resource.load(os.path.join(pkgdatadir, 'nocturne.gresource')) resource._register() from nocturne import main sys.exit(main.main(VERSION)) Jeffser-Nocturne-b442fee/src/nocturne_search_provider.py.in000066400000000000000000000105171523240030000242210ustar00rootroot00000000000000#!/usr/bin/python3 import gettext import uuid from pydbus import SessionBus from gi.repository import GLib, Gio _ = gettext.gettext ACTION_STRINGS = { 'artist': _("Show Artist"), 'album': _("Play Album"), 'song': _("Play Song"), 'playlist': _("Play Playlist") } class NocturneSearchProvider: """ """ last_results = {} allowed_types = [] def __init__(self): self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") def GetInitialResultSet(self, terms): possible_types = [ 'artist', 'album', 'song', 'playlist' ] self.allowed_types = [] for type_name in possible_types: if self.settings.get_value('gnome-search-include-{}s'.format(type_name)).unpack(): self.allowed_types.append(type_name) return [] def ActivateResult(self, identifier, terms, uid=None): if item := self.last_results.get(identifier): try: bus = SessionBus() if app_service := bus.get("com.jeffser.Nocturne"): actions = { 'artist': 'show_artist', 'album': 'play_album', 'song': 'play_song', 'playlist': 'play_playlist' } if action := actions.get(item.get('type')): app_service["org.gtk.Actions"].Activate(action, [GLib.Variant('s', identifier)], {}) except Exception as e: print(e) def GetSubsearchResultSet(self, previousResults, terms): query = ' '.join(terms) self.last_results = {} try: bus = SessionBus() if app_service := bus.get("com.jeffser.Nocturne.Service"): for item_id, item in app_service.Search(query).items(): if item.get('type') in self.allowed_types: self.last_results[item_id] = item except Exception as e: print('s',e) return list(self.last_results) def GetResultMetas(self, identifiers): results = [] for identifier in identifiers: if item := self.last_results.get(identifier): if display := item.get('display'): icon = None try: if icon_bytes := item.get('icon'): if len(icon_bytes) > 0: gbytes = GLib.Bytes.new(icon_bytes) icon = Gio.BytesIcon.new(gbytes).serialize() except: pass if not icon: icon_name = 'avatar-default-symbolic' if item.get('type') == 'artist' else 'media-playback-start-symbolic' icon = Gio.ThemedIcon.new(icon_name).serialize() results.append({ 'id': GLib.Variant('s', identifier), 'name': GLib.Variant('s', display), 'description': GLib.Variant('s', ACTION_STRINGS.get(item.get('type'), _("Open"))), 'icon': icon }) return results bus = SessionBus() bus.publish("com.jeffser.Nocturne.SearchProvider", NocturneSearchProvider()) loop = GLib.MainLoop() loop.run() Jeffser-Nocturne-b442fee/src/preferences.py000066400000000000000000000521721523240030000210240ustar00rootroot00000000000000# preferences.py from gi.repository import GObject, Gtk, Adw, GLib, Gio, Gdk, Pango, Xdp, XdpGtk4 from .integrations import get_current_integration, secret from .constants import SIDEBAR_MENU, BITRATE_OPTIONS, IN_FLATPAK, CACHE_DIR, DATA_DIR import os, threading, shutil # Handles sections and items of sidebar class NocturneSidebarPreferencesExpanderRow(Adw.ExpanderRow): __gtype_name__ = 'NocturneSidebarPreferencesExpanderRow' def __init__(self, **kwargs): super().__init__(**kwargs) self.toggle_group = Adw.ToggleGroup( valign=Gtk.Align.CENTER ) self.toggle_group.add( Adw.Toggle( label=_("Item"), name="item", tooltip=_("Item") ) ) self.toggle_group.add( Adw.Toggle( label=_("Section"), name="section", tooltip=_("Section") ) ) self.add_suffix(self.toggle_group) self.bind_property( 'enable-expansion', self.toggle_group, "active-name", GObject.BindingFlags.SYNC_CREATE, lambda bind, value: "section" if value else "item" ) self.toggle_group.bind_property( 'active-name', self, "enable-expansion", GObject.BindingFlags.SYNC_CREATE, lambda bind, value: value == "section" ) self.connect('notify::enable-expansion', self.section_toggled) def add_row(self, row): row.connect("notify::active", self.item_toggled) super().add_row(row) def item_toggled(self, row, gparam): active = row.get_active() name = row.get_name() settings = Gio.Settings(schema_id="com.jeffser.Nocturne") disabled_items = settings.get_value('sidebar-disabled-items').unpack() if active and name in disabled_items: disabled_items.remove(name) elif not active and name not in disabled_items: disabled_items.append(name) settings.set_value('sidebar-disabled-items', GLib.Variant('as', disabled_items)) if main_window := self.get_root().get_application().main_window: GLib.idle_add(main_window.setup_sidebar) def section_toggled(self, row, gparam): active = row.get_enable_expansion() name = row.get_name() settings = Gio.Settings(schema_id="com.jeffser.Nocturne") disabled_sections = settings.get_value('sidebar-disabled-sections').unpack() if active and name in disabled_sections: disabled_sections.remove(name) elif not active and name not in disabled_sections: disabled_sections.append(name) settings.set_value('sidebar-disabled-sections', GLib.Variant('as', disabled_sections)) if main_window := self.get_root().get_application().main_window: GLib.idle_add(main_window.setup_sidebar) @Gtk.Template(resource_path='/com/jeffser/Nocturne/preferences.ui') class NocturnePreferences(Adw.PreferencesDialog): __gtype_name__ = 'NocturnePreferencesDialog' # General ## Behavior restore_el = Gtk.Template.Child() hide_on_close_el = Gtk.Template.Child() simulate_wbwl_el = Gtk.Template.Child() auto_download_lyrics_el = Gtk.Template.Child() use_gain_el = Gtk.Template.Child() default_page_el = Gtk.Template.Child() bitrate_el = Gtk.Template.Child() ## Gnome Search gnome_search_group_el = Gtk.Template.Child() gnome_search_artists_el = Gtk.Template.Child() gnome_search_albums_el = Gtk.Template.Child() gnome_search_songs_el = Gtk.Template.Child() gnome_search_playlists_el = Gtk.Template.Child() ## Session session_group_el = Gtk.Template.Child() listenbrainz_stack_el = Gtk.Template.Child() instance_avatar_el = Gtk.Template.Child() instance_icon_el = Gtk.Template.Child() instance_el = Gtk.Template.Child() discord_rpc_el = Gtk.Template.Child() discord_coverart_share_el = Gtk.Template.Child() # Customization ## Interface context_button_el = Gtk.Template.Child() context_label_el = Gtk.Template.Child() footer_big_mode_el = Gtk.Template.Child() translucent_player_el = Gtk.Template.Child() use_sidebar_player_el = Gtk.Template.Child() hide_singles_el = Gtk.Template.Child() button_size_el = Gtk.Template.Child() ## Dynamic Background global_dynamic_bg_el = Gtk.Template.Child() player_dynamic_bg_el = Gtk.Template.Child() popout_dynamic_bg_el = Gtk.Template.Child() dynamic_accent_el = Gtk.Template.Child() ## Homepage home_mode_el = Gtk.Template.Child() hp_frequent_albums_el = Gtk.Template.Child() hp_songs_el = Gtk.Template.Child() hp_albums_el = Gtk.Template.Child() hp_artists_el = Gtk.Template.Child() hp_playlists_el = Gtk.Template.Child() ## Sidebar sidebar_group = Gtk.Template.Child() # Visualizer ## Preferences visualizer_el = Gtk.Template.Child() ## Appearance visualizer_bar_n_el = Gtk.Template.Child() visualizer_type_el = Gtk.Template.Child() visualizer_fill_el = Gtk.Template.Child() ## Color visualizer_auto_color_el = Gtk.Template.Child() visualizer_invert_auto_color_el = Gtk.Template.Child() visualizer_manual_color_el = Gtk.Template.Child() def __init__(self): super().__init__() settings = Gio.Settings(schema_id="com.jeffser.Nocturne") integration = get_current_integration() # General ## Behavior settings.bind( "restore-session", self.restore_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "hide-on-close", self.hide_on_close_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "simulate-word-by-word-lyrics", self.simulate_wbwl_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "auto-download-lyrics", self.auto_download_lyrics_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "use-gain", self.use_gain_el, "active", Gio.SettingsBindFlags.DEFAULT ) self.default_page_dict = {} selected_page = settings.get_value('default-page-tag').unpack() for section in SIDEBAR_MENU.values(): for item in section.get('items', []).values(): if section.get('title') and item.get('page-tag') != "radios": title = '{} ({})'.format(section.get('title'), item.get('title')) else: title = item.get('title') self.default_page_dict[title] = item.get('page-tag') self.default_page_el.get_model().append(title) if item.get('page-tag') == selected_page: self.default_page_el.set_selected(len(self.default_page_dict) - 1) self.max_bitrate_dict = {} selected_bitrate = settings.get_value('max-bitrate').unpack() for title, kbps in BITRATE_OPTIONS.items(): if kbps != 0: title = title.format('{} kbps'.format(kbps)) self.max_bitrate_dict[title] = kbps self.bitrate_el.get_model().append(title) if kbps == selected_bitrate: self.bitrate_el.set_selected(len(self.max_bitrate_dict) - 1) if integration: self.bitrate_el.set_visible('no-max-bitrate' not in integration.limitations) else: self.bitrate_el.set_visible(False) ## Gnome Search self.gnome_search_group_el.set_visible("GNOME" in os.environ.get("XDG_CURRENT_DESKTOP", "").upper()) settings.bind( "gnome-search-include-artists", self.gnome_search_artists_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "gnome-search-include-albums", self.gnome_search_albums_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "gnome-search-include-songs", self.gnome_search_songs_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "gnome-search-include-playlists", self.gnome_search_playlists_el, "active", Gio.SettingsBindFlags.DEFAULT ) ## Session settings.bind( "discord-rpc-enabled", self.discord_rpc_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "discord-instance-art-share", self.discord_coverart_share_el, "active", Gio.SettingsBindFlags.DEFAULT ) ### Check Flatpak permissions (Discord) if IN_FLATPAK: settings.connect("changed::discord-rpc-enabled", self.show_discord_flatpak_warning) GLib.idle_add(self.show_discord_flatpak_warning, settings, "discord-rpc-enabled") self.listenbrainz_stack_el.set_visible_child_name("unlink" if secret.get_plain_password(schema_type="listenbrainz") else "link") ### Instance Row self.session_group_el.set_visible(integration) self.instance_el.set_visible(False) threading.Thread(target=self.update_instance_row).start() # Customization ## Interface settings.bind( "show-context-button", self.context_button_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "show-context-button-label", self.context_label_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "use-big-footer", self.footer_big_mode_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "player-blur-bg", self.translucent_player_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "use-sidebar-player", self.use_sidebar_player_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "hide-singles", self.hide_singles_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "button-size", self.button_size_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) ## Dynamic Background settings.bind( "global-dynamic-bg-mode", self.global_dynamic_bg_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "player-dynamic-bg-mode", self.player_dynamic_bg_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "popout-dynamic-bg-mode", self.popout_dynamic_bg_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "use-dynamic-accent", self.dynamic_accent_el, "active", Gio.SettingsBindFlags.DEFAULT ) ## Homepage settings.bind( "welcome-mode-home", self.home_mode_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "n-frequent-albums-home", self.hp_frequent_albums_el, "value", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "n-songs-home", self.hp_songs_el, "value", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "n-albums-home", self.hp_albums_el, "value", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "n-artists-home", self.hp_artists_el, "value", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "n-playlists-home", self.hp_playlists_el, "value", Gio.SettingsBindFlags.DEFAULT ) ## Sidebar disabled_sections = settings.get_value('sidebar-disabled-sections').unpack() disabled_items = settings.get_value('sidebar-disabled-items').unpack() for section_id, section_data in SIDEBAR_MENU.items(): if section_id != 'root': section_expander = NocturneSidebarPreferencesExpanderRow( title=section_data.get("title"), enable_expansion=section_id not in disabled_sections, name=section_id ) self.sidebar_group.add(section_expander) for item_id, item_data in section_data.get('items', {}).items(): row = Adw.SwitchRow( title=item_data.get("title"), active=item_id not in disabled_items, name=item_id ) if icon_name := item_data.get("icon-name"): row.add_prefix( Gtk.Image(icon_name=icon_name) ) section_expander.add_row(row) # Visualizer ## Preferences settings.bind( "show-visualizer", self.visualizer_el, "active", Gio.SettingsBindFlags.DEFAULT ) ## Appearance settings.bind( "visualizer-bar-n", self.visualizer_bar_n_el, "value", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "visualizer-type", self.visualizer_type_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "visualizer-fill-mode", self.visualizer_fill_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) ## Color settings.bind( "visualizer-auto-color", self.visualizer_auto_color_el, "active", Gio.SettingsBindFlags.DEFAULT ) settings.bind( "visualizer-auto-color-invert", self.visualizer_invert_auto_color_el, "active", Gio.SettingsBindFlags.DEFAULT ) try: rgb_str = settings.get_value('visualizer-manual-color').unpack() rgb_list = [float(c) for c in rgb_str.split(',')] except: rgb_list = [0.11, 0.44, 0.85] self.visualizer_manual_color_el.set_rgba(Gdk.RGBA( red=rgb_list[0], green=rgb_list[1], blue=rgb_list[2], alpha=1 )) def update_instance_row(self): if integration := get_current_integration(): data = integration.getServerInformation() GLib.idle_add(self.instance_el.set_title, data.get('username', "")) GLib.idle_add(self.instance_el.set_subtitle, data.get('title', "")) GLib.idle_add(self.instance_el.set_tooltip_text, data.get('link')) GLib.idle_add(self.instance_el.set_action_target_value, GLib.Variant('s', data.get('link', ''))) GLib.idle_add(self.instance_icon_el.set_visible, data.get('link')) GLib.idle_add(self.instance_el.set_activatable, data.get('link')) GLib.idle_add(self.instance_avatar_el.set_custom_image, data.get('picture')) GLib.idle_add(self.instance_avatar_el.set_text, data.get('username', '')) GLib.idle_add(self.instance_el.set_visible, data) @Gtk.Template.Callback() def default_page_changed(self, combo_row, ud): page_tag = self.default_page_dict.get(combo_row.get_selected_item().get_string(), 'home') Gio.Settings(schema_id="com.jeffser.Nocturne").set_string('default-page-tag', page_tag) @Gtk.Template.Callback() def max_bitrate_changed(self, combo_row, ud): bitrate = self.max_bitrate_dict.get(combo_row.get_selected_item().get_string(), 0) Gio.Settings(schema_id="com.jeffser.Nocturne").set_int('max-bitrate', bitrate) @Gtk.Template.Callback() def visualizer_manual_color_changed(self, btn, ud): rgb = ','.join([str(round(c, 3)) for c in list(btn.get_rgba())[:-1]]) Gio.Settings(schema_id="com.jeffser.Nocturne").set_string('visualizer-manual-color', rgb) @Gtk.Template.Callback() def listenbrainz_link_requested(self, button): def on_response(dialog, result, token_entry_el): response = dialog.choose_finish(result) if response == "save": if token := token_entry_el.get_text(): secret.store_password( token, schema_type="listenbrainz" ) self.listenbrainz_stack_el.set_visible_child_name("unlink") container = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=10 ) container.append(Gtk.LinkButton( label=_("Settings Page"), uri="https://listenbrainz.org/settings/" )) token_el = Gtk.Entry(placeholder_text=_("User Token")) container.append(token_el) dialog = Adw.AlertDialog( heading=_("Link ListenBrainz"), body=_("Connect your ListenBrainz account with a user token"), extra_child=container ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("save", _("Save")) dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED) dialog.choose( self.get_root(), None, on_response, token_el ) @Gtk.Template.Callback() def listenbrainz_unlink_requested(self, button): secret.remove_password( schema_type="listenbrainz", callback=lambda: self.listenbrainz_stack_el.set_visible_child_name("link") ) @Gtk.Template.Callback() def delete_image_cache_requested(self, button): db_path = os.path.join(CACHE_DIR, 'cache_database.db') if os.path.isfile(db_path): os.remove(db_path) button.set_end_icon_name('check-plain-symbolic') button.set_sensitive(False) @Gtk.Template.Callback() def home_mode_changed(self, toggle_group, gparam): root = self.get_root() if not root: return settings = Gio.Settings(schema_id="com.jeffser.Nocturne") home_mode = self.home_mode_el.get_active_name() pfp_destination_path = os.path.join(DATA_DIR, 'pfp') if os.path.isfile(pfp_destination_path): os.remove(pfp_destination_path) settings.set_string("welcome-user-home", "") def on_portal_response(portal, response): try: if result := portal.get_user_information_finish(response): settings.set_string("welcome-user-home", result["name"]) shutil.copy2(result["image"].removeprefix('file://'), pfp_destination_path) except: self.home_mode_el.set_active_name('') def instance_information_run(): try: integration = get_current_integration() data = integration.getServerInformation() if username := data.get('username'): settings.set_string("welcome-user-home", username) if profile_picture := data.get('picture'): profile_picture.save_to_png(pfp_destination_path) else: self.home_mode_el.set_active_name('') except: self.home_mode_el.set_active_name('') if home_mode == 'system': portal = Xdp.Portal() portal.get_user_information( XdpGtk4.parent_new_gtk(root), "", Xdp.UserInformationFlags.NONE, None, on_portal_response ) elif home_mode == 'instance': threading.Thread(target=instance_information_run).run() def show_discord_flatpak_warning(self, settings, key): if settings.get_value(key).unpack(): directory = os.environ.get("XDG_RUNTIME_DIR") if 'discord-ipc-0' not in os.listdir(directory): dialog = Adw.AlertDialog( heading=_("Flatpak Sandbox Warning"), body=_("To connect to Discord, an additional permission is required, once you run the following command, please restart Nocturne"), extra_child=Gtk.Label( label='sudo flatpak override com.jeffser.Nocturne --filesystem=xdg-run/discord-ipc-0', css_classes=['rounded-corner', 'osd', 'p10'], selectable=True, wrap=True, wrap_mode=Pango.WrapMode.WORD ) ) dialog.add_response('c', _("Close")) dialog.choose(self.get_root(), None, lambda *_, st=settings, ky=key: st.set_boolean(ky, False)) Jeffser-Nocturne-b442fee/src/style.css000066400000000000000000000120571523240030000200210ustar00rootroot00000000000000.p0, button.force_p0, menubutton.force_p0 > button { padding: 0; } .p10 { padding: 10px; } .m10 { margin: 10px; } .lrc_list_p0 > row { padding:0; } .lrc_list_p0 > row > row { margin: 0; } .lrc_list, .lrc_list row:selected { background: transparent; } .lrc_list row > label { transition: opacity .2s, color .2s; opacity: 50%; font-size: 20px; margin: 5px; font-weight: bold; } .lrc_list row:selected > label { opacity: 100%; font-size: 30px; color: var(--accent-color); } .lrc_list row { border-radius: 12px; margin-left: 5px; margin-right: 5px; } button.active_appearance_button:disabled { filter: none; } searchbar > revealer > box { padding: 0; } window:not(.popout-window).showing-footer sidebar scrolledwindow > viewport > *, window:not(.popout-window).showing-footer navigation-view-page > toolbarview > scrolledwindow > viewport > *, window:not(.popout-window).showing-footer navigation-view-page > toolbarview > stack > scrolledwindow > viewport > * { padding-bottom: 100px; } window:not(.popout-window):not(.showing-footer) navigation-view-page > toolbarview > scrolledwindow > viewport > *, window:not(.popout-window):not(.showing-footer) navigation-view-page > toolbarview > stack > scrolledwindow > viewport > * { padding-bottom: 10px; } popover button label, button.no_bold_button, .no-bold { font-weight: normal; } button.p0, menubutton.p0 > button, label.ph5 { padding: 0 5px 0 5px; } avatar { box-shadow: 0 0 0 1px rgba(0,0,6,0.03), 0 1px 3px 1px rgba(0,0,6,0.07), 0 2px 6px 2px rgba(0,0,6,0.03); } scale trough highlight { min-height: 12px; min-width: 12px; background: var(--accent-color); } scale trough slider { min-height: 0; min-width: 0; margin: 0; opacity: 0; } overlay.album-button button.osd { opacity: 0; transition: opacity .2s; margin: 10px; } overlay.album-button image.card { transition: filter .2s; } overlay.album-button:hover button.osd { opacity: 1; } overlay.album-button:hover image.card { filter: brightness(75%); } .rounded-corner { border-top-left-radius: 9px; border-top-right-radius: 9px; border-bottom-left-radius: 9px; border-bottom-right-radius: 9px; } window:not(.global-dynamic-bg-gradient):not(.global-dynamic-bg-blur) .carousel-overlay-right { background: linear-gradient(to left, @window_bg_color 0%, transparent 100% ); } window:not(.global-dynamic-bg-gradient):not(.global-dynamic-bg-blur) .carousel-overlay-left { background: linear-gradient(to right, @window_bg_color 0%, transparent 100% ); } window overlay-split-view > widget:nth-child(3) { background: transparent; } window.global-dynamic-bg-gradient overlay-split-view .main_sidebar, window.global-dynamic-bg-blur overlay-split-view .main_sidebar, window navigation-split-view > *:nth-child(1) { background: color-mix(in srgb, var(--window-bg-color) 10%, transparent); } window, window:not(.popout-window) bottom-sheet#main-bottom-sheet sheet > stack, window:not(.popout-window) overlay-split-view .main_sidebar { transition: background-image 0.5s ease-in-out, background 0.5s ease-in-out, backdrop-filter 0.5s ease-in-out; background-repeat: no-repeat; background-size: cover; background-position: center center; } window.popout-window .sidebar-pane { background: transparent; } bottom-sheet#main-bottom-sheet sheet .sidebar-pane { background: color-mix(in srgb, var(--sidebar-bg-color) 10%, transparent); } window.popout-window .fullscreen-bottom-bar { backdrop-filter: blur(10px); padding: 20px; } window.player-translucent:not(.popout-window) bottom-sheet#main-bottom-sheet sheet.background { backdrop-filter: blur(10px); background: color-mix(in srgb, var(--window-bg-color) 80%, transparent); } window.popout-window.big-mode navigation-split-view > .sidebar-pane { box-shadow: none; } window.popout-window.big-mode .view_box, window.popout-window.big-mode navigation-view-page > toolbarview stack > scrolledwindow > * > *, window.popout-window.big-mode navigation-view-page > toolbarview stack > * > scrolledwindow > * > * { padding-top: 45px; } window .main_sidebar .queue_page_padding_top > * > * { padding-top: 45px; } switch:checked { background: var(--accent-color); } window:not(.popout-window).showing-footer toast { margin-bottom: 100px; } window:not(.popout-window) dialog toast { margin-bottom: 10px; } @media (prefers-color-scheme: light) { list.boxed-list, .card { background-color: rgba(255,255,255,0.25); } } window.fullscreen headerbar { opacity: 0; transition: opacity .2s; } window.fullscreen headerbar:hover { opacity: 1; } window.fullscreen dialog headerbar { opacity: 1; } .small_song_button > box { margin: 5px; margin-top: 0; margin-bottom: 0; } .p50 { padding:50px; } .big-album-title { font-size: 3rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; } @media (prefers-color-scheme: light) { toggle-group > toggle.image-button:checked { background: color-mix(in srgb, var(--card-bg-color), transparent); } } toggle-group > toggle.image-button:checked { color: var(--accent-color); } Jeffser-Nocturne-b442fee/src/ui/000077500000000000000000000000001523240030000165575ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/album/000077500000000000000000000000001523240030000176575ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/album/button.blp000066400000000000000000000047211523240030000216750ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneAlbumButton: Gtk.Box { orientation: vertical; spacing: 5; halign: center; Gtk.Overlay { styles [ "album-button" ] [overlay] Gtk.Button play_el { icon-name: "media-playback-start-symbolic"; styles [ "circular", "osd" ] tooltip-text: _("Play Album"); action-name: "app.play_album"; valign: end; halign: start; } [overlay] Gtk.Button star_el { styles [ "circular", "osd" ] child: Adw.Spinner {}; action-name: "app.toggle_star"; accessibility { label: _("Toggle star"); } valign: end; halign: end; } child: Gtk.Button cover_button_el { styles [ "force_p0", "flat" ] action-name: "app.show_album"; halign: center; tooltip-text: _("Show Album"); child: Gtk.Image cover_el { styles [ "card" ] icon-size: large; overflow: hidden; hexpand: true; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Album cover art"); } }; Gtk.GestureClick { button: 3; released => $show_popover_image(); } Gtk.GestureLongPress { pressed => $show_popover_image(); } }; } Gtk.Button name_button_el { Gtk.GestureClick { button: 3; released => $show_popover_name(); } Gtk.GestureLongPress { pressed => $show_popover_name(); } styles [ "flat", "p0" ] accessibility { label: _("Enter album page"); } child: Gtk.Box{ orientation: vertical; spacing: 6; Gtk.Label name_el { styles [ "title-3" ] ellipsize: end; max-width-chars: 1; } Gtk.Label year_el { styles [ "dimmed" ] visible: false; ellipsize: end; max-width-chars: 1; } }; action-name: "app.show_album"; } Gtk.Button artist_el { Gtk.GestureClick { button: 3; released => $show_popover_artist(); } Gtk.GestureLongPress { pressed => $show_popover_artist(); } styles [ "dimmed", "flat", "p0" ] accessibility { label: _("Enter artist page"); } child: Gtk.Label { ellipsize: end; max-width-chars: 1; }; } }Jeffser-Nocturne-b442fee/src/ui/album/page.blp000066400000000000000000000106531523240030000212770ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneAlbumPage: Adw.NavigationPage { title: _("Album"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Adw.Clamp clamp_el { tightening-threshold: 800; maximum-size: 2000; child: Gtk.Box { styles [ "p10" ] orientation: vertical; spacing: 10; Adw.WrapBox { // Information Container orientation: horizontal; line-spacing: 10; child-spacing: 10; justify: fill; wrap-policy: minimum; Gtk.Image cover_el { styles [ "card" ] icon-size: large; height-request: 240; width-request: 240; overflow: hidden; halign: center; valign: start; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Album cover art"); } } Gtk.Box { orientation: vertical; spacing: 10; valign: center; Gtk.Label name_el { styles [ "big-album-title" ] xalign: 0; wrap: true; wrap-mode: word_char; lines: 2; halign: start; ellipsize: end; valign: center; visible: false; } Gtk.Button artist_el { styles [ "flat", "dimmed", "p0" ] halign: start; valign: center; visible: false; action-name: "app.show_artist"; action-target: "''"; accessibility { label: _("Enter artist page"); } } Gtk.Box rating_container { styles [ "linked" ] Gtk.Button { name: "1"; tooltip-text: _("1 Star"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "2"; tooltip-text: _("2 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "3"; tooltip-text: _("3 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "4"; tooltip-text: _("4 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "5"; tooltip-text: _("5 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } } Adw.WrapBox context_wrap_el { orientation: horizontal; line-spacing: 10; child-spacing: 10; Gtk.Button star_el { styles [ "circular" ] child: Adw.Spinner {}; action-name: "app.toggle_star"; accessibility { label: _("Toggle star"); } } } } } $NocturneSongQueue song_list_el { header-label: _("Songs"); header-icon-name: "music-note-symbolic"; } }; }; }; }; } Jeffser-Nocturne-b442fee/src/ui/album/row.blp000066400000000000000000000016541523240030000211730ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneAlbumRow: Adw.ActionRow { action-name: "app.show_album"; activatable: true; use-markup: false; Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } [prefix] Gtk.Image cover_el { styles [ "card" ] height-request: 48; width-request: 48; margin-top: 10; margin-bottom: 10; overflow: hidden; valign: center; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Album cover art"); } } [suffix] Gtk.MenuButton menu_button_el { styles [ "circular", "flat", ] icon-name: "view-more-symbolic"; valign: center; notify::active => $on_context_button_active(); popover: Gtk.Popover { child: Adw.Spinner {}; }; tooltip-text: _("Options"); } }Jeffser-Nocturne-b442fee/src/ui/artist/000077500000000000000000000000001523240030000200655ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/artist/button.blp000066400000000000000000000013421523240030000220770ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneArtistButton: Gtk.Button { Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } styles [ "flat" ] action-name: "app.show_artist"; child: Gtk.Box { orientation: vertical; spacing: 5; Adw.Avatar avatar_el { custom-image: Adw.SpinnerPaintable { widget: avatar_el; }; icon-name: "music-artist-symbolic"; } Gtk.Label name_el { styles [ "title-3" ] ellipsize: end; max-width-chars: 1; } Gtk.Label album_count_el { styles [ "dimmed" ] ellipsize: end; max-width-chars: 1; } }; }Jeffser-Nocturne-b442fee/src/ui/artist/page.blp000066400000000000000000000113401523240030000214770ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneArtistPage: Adw.NavigationPage { title: _("Artist"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Adw.Clamp clamp_el { tightening-threshold: 800; maximum-size: 2000; child: Gtk.Box { styles [ "p10" ] orientation: vertical; spacing: 10; Adw.WrapBox { // Information Container orientation: horizontal; line-spacing: 10; child-spacing: 10; justify: fill; wrap-policy: minimum; Adw.Avatar avatar_el { size: 240; valign: start; custom-image: Adw.SpinnerPaintable { widget: avatar_el; }; icon-name: "music-artist-symbolic"; } Gtk.Box { orientation: vertical; spacing: 10; valign: center; Gtk.Label name_el { styles [ "title-1" ] xalign: 0; wrap: true; wrap-mode: word_char; lines: 2; halign: start; ellipsize: end; valign: center; visible: false; } Gtk.Button { styles [ "flat", "force_p0", "no_bold_button" ] Gtk.Label biography_el { styles [ "document" ] wrap: true; wrap-mode: word_char; ellipsize: end; lines: 4; valign: center; hexpand: true; max-width-chars: 1; } tooltip-text: _("Expand Biography"); clicked => $on_biography_clicked(); } Gtk.Box rating_container { styles [ "linked" ] Gtk.Button { name: "1"; tooltip-text: _("1 Star"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "2"; tooltip-text: _("2 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "3"; tooltip-text: _("3 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "4"; tooltip-text: _("4 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "5"; tooltip-text: _("5 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } } Adw.WrapBox context_wrap_el { orientation: horizontal; line-spacing: 10; child-spacing: 10; Gtk.Button star_el { styles [ "circular" ] child: Adw.Spinner {}; action-name: "app.toggle_star"; accessibility { label: _("Toggle star"); } } } } } $NocturneWrapbox top_songs_wrapbox { visible: false; header-label: _("Top Songs"); header-icon-name: "music-note-symbolic"; } $NocturneWrapbox album_wrapbox { header-label: _("Albums"); header-icon-name: "music-queue-symbolic"; } $NocturneCarousel artist_carousel { header-label: _("Related Artists"); header-icon-name: "music-artist-symbolic"; } }; }; }; }; }Jeffser-Nocturne-b442fee/src/ui/artist/row.blp000066400000000000000000000014241523240030000213740ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneArtistRow: Adw.ActionRow { action-name: "app.show_artist"; activatable: true; use-markup: false; Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } [prefix] Adw.Avatar avatar_el { styles [ "p10" ] size: 48; custom-image: Adw.SpinnerPaintable { widget: avatar_el; }; icon-name: "music-artist-symbolic"; } [suffix] Gtk.MenuButton menu_button_el { styles [ "circular", "flat", ] icon-name: "view-more-symbolic"; valign: center; notify::active => $on_context_button_active(); popover: Gtk.Popover { child: Adw.Spinner {}; }; tooltip-text: _("Options"); } }Jeffser-Nocturne-b442fee/src/ui/containers/000077500000000000000000000000001523240030000207245ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/containers/carousel.blp000066400000000000000000000040471523240030000232450ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneCarousel: Gtk.Box { orientation: vertical; spacing: 10; Gtk.Box { orientation: horizontal; spacing: 10; hexpand: true; margin-start: 10; margin-end: 10; Gtk.Button header_button { styles [ "flat", "title-2", "active_appearance_button" ] halign: start; sensitive: false; child: Adw.ButtonContent { label: bind template.header-label; icon-name: bind template.header-icon-name; }; tooltip-text: bind template.header-label; visible: bind $handle_visible_bind(template.header-label); action-name: bind $handle_action_name_bind(template.header-page-tag); action-target: bind $handle_action_target_bind(template.header-page-tag); } Gtk.Box pan_container_el { styles [ "linked", "accent" ] halign: end; valign: center; hexpand: true; visible: false; Gtk.Button pan_start_el { tooltip-text: _("Pan to the Left"); icon-name: "pan-start-symbolic"; clicked => $pan_start(); } Gtk.Button pan_end_el { tooltip-text: _("Pan to the Right"); icon-name: "pan-end-symbolic"; clicked => $pan_end(); } } } Gtk.Overlay { [overlay] Adw.Bin { halign: start; vexpand: true; width-request: 10; styles [ "carousel-overlay-left" ] } [overlay] Adw.Bin { halign: end; vexpand: true; width-request: 10; styles [ "carousel-overlay-right" ] } child: Adw.Carousel list_el { hexpand: true; halign: fill; spacing: 10; allow-long-swipes: true; allow-mouse-drag: true; allow-scroll-wheel: false; page-changed => $page_changed(); overflow: visible; Gtk.EventControllerScroll { scroll => $on_scroll(); flags: both_axes; } Adw.Spinner { height-request: 240; width-request: 240; } }; } }Jeffser-Nocturne-b442fee/src/ui/containers/download_row.blp000066400000000000000000000021461523240030000241240ustar00rootroot00000000000000using Gtk 4.0; template $NocturneDownloadRow: Gtk.ListBoxRow { styles [ "p10" ] activatable: false; child: Gtk.Box { orientation: horizontal; spacing: 5; Gtk.Box { orientation: vertical; spacing: 5; Gtk.Label title_label { styles [ "heading" ] label: ""; hexpand: true; halign: start; xalign: 0; vexpand: true; valign: center; } Gtk.Label done_label { styles [ "dim-label", "caption" ] label: _("Downloaded"); hexpand: true; halign: start; xalign: 0; vexpand: true; valign: center; visible: false; } Gtk.ProgressBar progressbar { hexpand: true; notify::fraction => $progressbar_frac_changed(); } } Gtk.Button done_button { styles [ "flat", "circular" ] valign: center; visible: false; tooltip-text: _("Remove From Queue"); icon-name: "check-plain-symbolic"; clicked => $remove_from_queue(); } }; }Jeffser-Nocturne-b442fee/src/ui/containers/downloads_queue_button.blp000066400000000000000000000015671523240030000262250ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneDownloadsQueueButton: Gtk.MenuButton { visible: false; notify::active => $button_activated(); icon-name: "folder-download-symbolic"; popover: Gtk.Popover { child: Adw.PreferencesGroup { title: _("Downloads Queue"); description: _("Downloaded songs are available in offline mode"); header-suffix: Gtk.Button { styles [ "flat", "circular" ] valign: center; icon-name: "cross-large-symbolic"; tooltip-text: _("Clear Done Downloads"); clicked => $clear_done_downloads(); }; Gtk.ScrolledWindow { propagate-natural-height: true; max-content-height: 480; child: Gtk.ListBox download_list_el { styles [ "boxed-list" ] selection-mode: none; }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/containers/page_dialog.blp000066400000000000000000000002471523240030000236610ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePageDialog: Adw.Dialog { content-width: 1000; content-height: 700; child: Adw.NavigationView navigation_view {}; }Jeffser-Nocturne-b442fee/src/ui/containers/wrapbox.blp000066400000000000000000000015671523240030000231160ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneWrapbox: Gtk.Box { orientation: vertical; spacing: 10; Gtk.Button header_button { styles [ "flat", "title-2", "active_appearance_button" ] margin-start: 10; margin-end: 10; halign: start; sensitive: false; child: Adw.ButtonContent { label: bind template.header-label; icon-name: bind template.header-icon-name; }; tooltip-text: bind template.header-label; visible: bind $handle_visible_bind(template.header-label); action-name: bind $handle_action_name_bind(template.header-page-tag); action-target: bind $handle_action_target_bind(template.header-page-tag); } Adw.WrapBox list_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; Adw.Spinner { height-request: 240; width-request: 240; } } }Jeffser-Nocturne-b442fee/src/ui/lyrics/000077500000000000000000000000001523240030000200645ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/lyrics/dialog.blp000066400000000000000000000075101523240030000220250ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneLyricsDialog: Adw.Dialog { title: _("Lyrics Editor"); content-width: 620; content-height: 720; child: Adw.ToolbarView { [top] Adw.HeaderBar { show-end-title-buttons: false; show-start-title-buttons: false; [start] Gtk.Button { label: _("Cancel"); tooltip-text: _("Cancel"); clicked => $cancel_clicked(); } [end] Gtk.Button { label: _("Save"); tooltip-text: _("Save"); styles [ "suggested-action" ] clicked => $save_clicked(); } } [bottom] Gtk.ActionBar { [center] Gtk.Box { spacing: 5; Gtk.Stack state_stack_el { transition-type: crossfade; notify::visible-child-name => $state_stack_changed(); Gtk.StackPage { name: "play"; child: Gtk.Button { icon-name: "media-playback-start-symbolic"; valign: center; action-name: "app.player_play"; tooltip-text: _("Play"); styles [ "circular", "accent" ] }; } Gtk.StackPage { name: "pause"; child: Gtk.Button { icon-name: "media-playback-pause-symbolic"; valign: center; action-name: "app.player_pause"; tooltip-text: _("Pause"); styles [ "circular", "accent" ] }; } } Gtk.SpinButton position_spin { adjustment: Gtk.Adjustment { lower: 0; upper: 1; value: 0; step-increment: 0.10; }; digits: 2; output => $position_spin_changed(); } Gtk.Scale progress_el { orientation: horizontal; hexpand: true; GestureClick { pressed => $seek_start(); stopped => $seek_end(); } adjustment: Gtk.Adjustment { lower: 0; upper: 1; value: 0; value-changed => $progress_bar_changed(); }; styles [ "p0" ] accessibility { label: _("Current song progress bar"); } } Gtk.Button { tooltip-text: _("Set Next Line Timestamp to Now"); icon-name: "stopwatch-symbolic"; clicked => $set_next_timestamp(); styles [ "circular" ] } Gtk.Button { tooltip-text: _("Add Line at Timestamp"); icon-name: "list-add-symbolic"; clicked => $add_line(); styles [ "circular" ] } } } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "empty"; child: Adw.StatusPage { title: _("No Lyrics"); description: _("No lyrics found for this track, to get started add a new line"); }; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; max-content-width: 500; max-content-height: 400; child: Adw.Clamp { maximum-size: 800; tightening-threshold: 600; child: Gtk.ListBox lrc_list_el { margin-start: 10; margin-end: 10; margin-top: 10; margin-bottom: 10; selection-mode: none; styles [ "boxed-list" ] }; }; }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/lyrics/edit_row.blp000066400000000000000000000025471523240030000224070ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneLyricEditRow: Adw.EntryRow { activatable: false; [prefix] Gtk.MenuButton ts_button { valign: center; tooltip-text: _("Timestamp"); icon-name: "view-more-symbolic"; popover: Gtk.Popover { child: Gtk.Box { orientation: vertical; Gtk.Button { tooltip-text: _("Go to Timestamp"); child: Adw.ButtonContent { label: _("Go to Timestamp"); icon-name: "snapshots-alt-symbolic"; halign: start; }; clicked => $go_to_timestamp(); styles [ "flat" ] } Gtk.Button { tooltip-text: _("Set Current Timestamp"); child: Adw.ButtonContent { label: _("Set Current Timestamp"); icon-name: "stopwatch-symbolic"; halign: start; }; clicked => $set_current_timestamp(); styles [ "flat" ] } Gtk.Button { tooltip-text: _("Remove Line"); child: Adw.ButtonContent { label: _("Remove Line"); icon-name: "user-trash-symbolic"; halign: start; }; clicked => $remove(); styles [ "flat", "error" ] } }; }; styles [ "flat" ] } }Jeffser-Nocturne-b442fee/src/ui/pages/000077500000000000000000000000001523240030000176565ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/pages/albums.blp000066400000000000000000000035341523240030000216450ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneAlbumsPage: Adw.NavigationPage { child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; margin-top: 50; child: Gtk.Box { orientation: vertical; spacing: 5; $NocturneWrapbox list_el { margin-top: 10; margin-bottom: 10; margin-start: 10; margin-end: 10; valign: start; } Gtk.Stack end_stack { Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "end"; child: Gtk.Label { styles [ "dimmed" ] label: _("Reached End of List"); halign: center; wrap: true; wrap-mode: word_char; justify: center; }; } } }; }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Albums Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/albums_all.blp000066400000000000000000000063241523240030000224750ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneAlbumsAllPage: Adw.NavigationPage { title: _("Albums"); tag: "albums-all"; child: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Adw.ToggleGroup toggle_group_el { active-name: "list"; Adw.Toggle { icon-name: "view-list-bullet-symbolic"; tooltip: _("List"); name: "list"; } Adw.Toggle { icon-name: "grid-filled-symbolic"; tooltip: _("Grid"); name: "grid"; } } title-widget: Gtk.SearchEntry search_entry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Albums"); search-changed => $on_search(); accessibility { label: _("Search albums"); } }; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; child: Gtk.Box { orientation: vertical; spacing: 5; Gtk.Stack { transition-type: crossfade; visible-child-name: bind toggle_group_el.active-name; interpolate-size: true; vhomogeneous: false; Gtk.StackPage { name: "list"; child: Gtk.ListBox list_el { styles [ "boxed-list", "m10" ] selection-mode: none; valign: start; }; } Gtk.StackPage { name: "grid"; child: Adw.WrapBox wrapbox_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; justify: spread; justify-last-line: true; styles [ "m10" ] }; } } Gtk.Stack end_stack { Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "end"; child: Gtk.Label { styles [ "dimmed" ] label: _("Reached End of List"); halign: center; wrap: true; wrap-mode: word_char; justify: center; }; } } }; }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Albums Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/artists.blp000066400000000000000000000061761523240030000220600ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneArtistsPage: Adw.NavigationPage { title: _("Artists"); tag: "artists"; child: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Adw.ToggleGroup toggle_group_el { active-name: "list"; Adw.Toggle { icon-name: "view-list-bullet-symbolic"; tooltip: _("List"); name: "list"; } Adw.Toggle { icon-name: "grid-filled-symbolic"; tooltip: _("Grid"); name: "grid"; } } title-widget: Gtk.SearchEntry search_entry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Artists"); search-changed => $on_search(); accessibility { label: _("Search artists"); } }; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; Gtk.Box { orientation: vertical; spacing: 5; Gtk.Stack { transition-type: crossfade; visible-child-name: bind toggle_group_el.active-name; interpolate-size: true; vhomogeneous: false; Gtk.StackPage { name: "list"; child: Gtk.ListBox list_el { styles [ "boxed-list", "m10" ] selection-mode: none; valign: start; }; } Gtk.StackPage { name: "grid"; child: Adw.WrapBox wrapbox_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; justify: spread; justify-last-line: true; }; } } Gtk.Stack end_stack { Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "end"; child: Gtk.Label { styles [ "dimmed" ] label: _("Reached End of List"); halign: center; wrap: true; wrap-mode: word_char; justify: center; }; } } } }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Artists Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/home.blp000066400000000000000000000073431523240030000213140ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneHomePage: Adw.NavigationPage { title: _("Home"); tag: "home"; child: Adw.ToolbarView { extend-content-to-top-edge: bind search_toggle.active inverted; [top] Adw.HeaderBar header_bar { [start] Gtk.ToggleButton search_toggle { icon-name: "system-search-symbolic"; tooltip-text: _("Toggle Search"); active: bind search_bar.search-mode-enabled; toggled => $search_mode_toggled(); } title-widget: Gtk.SearchBar search_bar { search-mode-enabled: bind search_toggle.active; child: Gtk.SearchEntry search_entry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Search"); search-changed => $on_search(); }; }; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Adw.Clamp main_clamp { maximum-size: 2000; tightening-threshold: 1700; child: Gtk.Box main_container { orientation: vertical; spacing: 10; Gtk.Box welcome_container { visible: false; styles [ "p10" ] orientation: horizontal; spacing: 10; Adw.Avatar welcome_avatar { size: 100; } Gtk.Box { valign: center; orientation: vertical; spacing: 5; Gtk.Label welcome_username_label { styles [ "title-4", "dim-label" ] xalign: 0; lines: 2; wrap-mode: word_char; ellipsize: end; } Gtk.Label { styles [ "title-1" ] label: _("Welcome back"); xalign: 0; lines: 2; wrap-mode: word; ellipsize: end; } } } $NocturneCarousel frequent_album_carousel { header-label: _("On Replay"); header-icon-name: "playlist-infinite-symbolic"; header-page-tag: "albums-all"; } $NocturneWrapbox song_wrapbox { header-label: _("Songs"); header-icon-name: "music-note-symbolic"; header-page-tag: "songs-all"; } $NocturneCarousel album_carousel { header-label: _("Albums"); header-icon-name: "music-queue-symbolic"; header-page-tag: "albums-all"; } $NocturneCarousel artist_carousel { header-label: _("Artists"); header-icon-name: "music-artist-symbolic"; header-page-tag: "artists"; } $NocturneCarousel playlist_carousel { header-label: _("Playlists"); header-icon-name: "playlist-symbolic"; header-page-tag: "playlists"; } }; }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Elements Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/login.blp000066400000000000000000000071461523240030000214750ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneLoginDialog: Adw.Dialog { title: _("Login"); child: Adw.ToastOverlay toast_overlay { child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Adw.StatusPage status_page { title: _("Login"); child: Gtk.Box { spacing: 20; orientation: vertical; halign: center; width-request: 320; Gtk.ListBox { styles [ "boxed-list" ] hexpand: true; selection-mode: none; Adw.ActionRow { child: Gtk.LinkButton link_button_el { }; } Adw.ActionRow server_status_el { title: _("Server Status"); subtitle: _("Not Running"); tooltip-text: _("Restart Instance"); activatable: true; activated => $server_restart_requested(); notify::subtitle => $update_login_sensitivity(); [prefix] Gtk.Image { icon-name: "update-symbolic"; } [suffix] Gtk.MenuButton extra_menu_el { icon-name: "preferences-system-symbolic"; tooltip-text: _("Manage Navidrome Server"); popover: Gtk.Popover {}; valign: center; styles [ "flat" ] } } Adw.EntryRow url_el { title: _("Url"); changed => $update_login_sensitivity(); [suffix] Gtk.MenuButton url_options_el { styles [ "flat" ] icon-name: "view-more-symbolic"; tooltip-text: _("More Options"); valign: center; popover: Gtk.Popover { child: Gtk.ListBox { selection-mode: none; Adw.SwitchRow trust_server_el { title: _("Trust Server Certificates"); } }; }; } } Adw.ActionRow directory_el { title: _("Library Directory"); notify::subtitle => $library_changed(); notify::subtitle => $update_login_sensitivity(); activated => $open_local_directory(); activatable: true; [prefix] Gtk.Image { icon-name: "folder-open-symbolic"; valign: center; } } Adw.EntryRow user_el { title: _("Username"); changed => $update_login_sensitivity(); } Adw.PasswordEntryRow password_el { title: _("Password"); entry-activated => $login_button_clicked(); changed => $update_login_sensitivity(); } } Gtk.Button login_button_el { styles [ "suggested-action", "pill" ] label: _("Login"); tooltip-text: _("Login"); clicked => $login_button_clicked(); halign: center; } Gtk.Button quick_connect_button_el { styles [ "pill" ] label: _("Use Quick Connect"); tooltip-text: _("Use Quick Connect"); clicked => $quick_connect_button_clicked(); halign: center; } }; }; }; }; } Jeffser-Nocturne-b442fee/src/ui/pages/playback.blp000066400000000000000000000065641523240030000221560ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaybackPage: Adw.NavigationPage { title: _("Playback"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; [start] Gtk.Button { icon-name: "user-home-symbolic"; clicked => $go_home(); tooltip-text: _("Go Home"); } [start] Gtk.ToggleButton spectrum_toggle { icon-name: "sound-symbolic"; active: bind spectrum.visible; tooltip-text: _("Toggle Visualizer"); } } content: Gtk.Overlay { child: $NocturneSpectrum spectrum { visible: bind spectrum_toggle.active; opacity: 0.25; }; [overlay] Gtk.ScrolledWindow status_page { propagate-natural-width: true; propagate-natural-height: true; child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; margin-top: 45; child: Gtk.Box { styles [ "p10" ] orientation: vertical; spacing: 25; Gtk.Label { styles [ "title-1" ] label: _("Nocturne Playback"); justify: center; halign: center; wrap: true; wrap-mode: word_char; lines: 2; ellipsize: end; } Gtk.Label { label: _("These are your most played songs from last month"); justify: center; halign: center; wrap: true; wrap-mode: word_char; lines: 2; ellipsize: end; } Gtk.Stack playlist_stack { hhomogeneous: false; Gtk.StackPage { name: "active"; child: Gtk.Button { styles [ "suggested-action", "pill" ] clicked => $save_playlist(); tooltip-text: _("Save Playlist"); child: Adw.ButtonContent { label: _("Save Playlist"); icon-name: "playlist-symbolic"; }; halign: center; }; } Gtk.StackPage { name: "inactive"; child: Gtk.Button { styles [ "pill" ] sensitive: false; tooltip-text: _("Playlist Added"); child: Adw.ButtonContent { label: _("Playlist Added"); icon-name: "check-plain-symbolic"; }; halign: center; }; } } Adw.WrapBox top_wrapbox { halign: center; child-spacing: 10; line-spacing: 10; justify: spread; justify-last-line: true; } Gtk.Separator {} $NocturneSongQueue song_queue {} Gtk.Label thanks_label { label: _("Thank you for using Nocturne!"); wrap-mode: word_char; wrap: true; styles [ "dimmed" ] } }; }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/playlists.blp000066400000000000000000000066551523240030000224150ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistsPage: Adw.NavigationPage { title: _("Playlists"); tag: "playlists"; child: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Adw.ToggleGroup toggle_group_el { active-name: "list"; Adw.Toggle { icon-name: "view-list-bullet-symbolic"; tooltip: _("List"); name: "list"; } Adw.Toggle { icon-name: "grid-filled-symbolic"; tooltip: _("Grid"); name: "grid"; } } title-widget: Gtk.SearchEntry search_entry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Playlists"); search-changed => $on_search(); accessibility { label: _("Search playlists"); } }; [end] Gtk.Button { styles [ "flat" ] icon-name: "list-add-symbolic"; tooltip-text: _("Create Playlist"); action-name: "app.create_playlist"; } } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; Gtk.Box { orientation: vertical; spacing: 5; Gtk.Stack { transition-type: crossfade; visible-child-name: bind toggle_group_el.active-name; interpolate-size: true; vhomogeneous: false; Gtk.StackPage { name: "list"; child: Gtk.ListBox list_el { styles [ "boxed-list", "m10" ] selection-mode: none; valign: start; }; } Gtk.StackPage { name: "grid"; child: Adw.WrapBox wrapbox_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; justify: spread; justify-last-line: true; styles [ "m10" ] }; } } Gtk.Stack end_stack { Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "end"; child: Gtk.Label { styles [ "dimmed" ] label: _("Reached End of List"); halign: center; wrap: true; wrap-mode: word_char; justify: center; }; } } } }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Playlists Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/radios.blp000066400000000000000000000027351523240030000216450ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneRadiosPage: Adw.NavigationPage { title: _("Radios"); tag: "radios"; child: Adw.ToolbarView { [top] Adw.HeaderBar { title-widget: Gtk.SearchEntry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Radios"); search-changed => $on_search(); accessibility { label: _("Search radios"); } }; [end] Gtk.Button { styles [ "flat" ] icon-name: "list-add-symbolic"; tooltip-text: _("Add Radio"); action-name: "app.add_radio"; } } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; Gtk.ListBox list_el { styles [ "boxed-list", "m10" ] selection-mode: none; valign: start; } }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Radios Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/setup.blp000066400000000000000000000041751523240030000215240ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSetupPage: Adw.NavigationPage { title: _("Setup"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Gtk.Stack main_stack { Gtk.StackPage { name: "confirmation"; child: Adw.StatusPage { icon-name: "folder-download-symbolic"; title: _("Navidrome Download"); description: _("Nocturne will download Navidrome automatically"); child: Gtk.Button { label: _("Download (~20โ€ฏmb)"); tooltip-text: _("Download"); clicked => $download_clicked(); halign: center; }; }; } Gtk.StackPage { name: "downloading"; child: Adw.StatusPage downloading_status_page { icon-name: "folder-download-symbolic"; title: _("Downloading"); child: Gtk.ProgressBar progressbar_el { pulse-step: 0.1; halign: center; }; }; } Gtk.StackPage { name: "error"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("An Error Occurred"); description: _("Please try again later"); }; } Gtk.StackPage { name: "success"; child: Adw.StatusPage { icon-name: "check-plain-symbolic"; title: _("Download Successful"); description: _("Before continuing make sure you create an admin account and also make sure all your tracks are inside your music directory"); child: Gtk.Box { orientation: vertical; spacing: 10; halign: center; Gtk.LinkButton { uri: "http://127.0.0.1:4534"; label: _("Instance Webpage"); activate-link => $link_visited(); } Gtk.Button continue_button { label: _("Continue"); tooltip-text: _("Continue"); sensitive: false; clicked => $continue_clicked(); } }; }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/songs_all.blp000066400000000000000000000062431523240030000223430ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongsAllPage: Adw.NavigationPage { title: _("Songs"); tag: "songs-all"; child: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Adw.ToggleGroup toggle_group_el { active-name: "list"; Adw.Toggle { icon-name: "view-list-bullet-symbolic"; tooltip: _("List"); name: "list"; } Adw.Toggle { icon-name: "grid-filled-symbolic"; tooltip: _("Grid"); name: "grid"; } } title-widget: Gtk.SearchEntry search_entry { hexpand: true; key-capture-widget: main_stack; placeholder-text: _("Songs"); search-changed => $on_search(); accessibility { label: _("Search songs"); } }; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; Gtk.Box { orientation: vertical; spacing: 5; Gtk.Stack { transition-type: crossfade; visible-child-name: bind toggle_group_el.active-name; interpolate-size: true; vhomogeneous: false; Gtk.StackPage { name: "list"; child: $NocturneSongQueue list_el { styles [ "boxed-list", "m10" ] valign: start; }; } Gtk.StackPage { name: "grid"; child: Adw.WrapBox wrapbox_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; justify: fill; justify-last-line: true; styles [ "m10" ] }; } } Gtk.Stack end_stack { Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "end"; child: Gtk.Label { styles [ "dimmed" ] label: _("Reached End of List"); halign: center; wrap: true; wrap-mode: word_char; justify: center; }; } } } }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Songs Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/starred.blp000066400000000000000000000051111523240030000220170ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneStarredPage: Adw.NavigationPage { child: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Adw.ToggleGroup toggle_group_el { active-name: "list"; Adw.Toggle { icon-name: "view-list-bullet-symbolic"; tooltip: _("List"); name: "list"; } Adw.Toggle { icon-name: "grid-filled-symbolic"; tooltip: _("Grid"); name: "grid"; } } title-widget: Gtk.SearchEntry search_entry_el { hexpand: true; key-capture-widget: main_stack; placeholder-text: bind template.title; search-changed => $on_search(); accessibility { label: _("Search favorite songs"); } }; } content: Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp { maximum-size: 2000; tightening-threshold: 800; Gtk.Box { orientation: vertical; spacing: 5; Gtk.Stack { transition-type: crossfade; visible-child-name: bind toggle_group_el.active-name; interpolate-size: true; vhomogeneous: false; Gtk.StackPage { name: "list"; child: $NocturneSongQueue list_el { styles [ "boxed-list", "m10" ] valign: start; }; } Gtk.StackPage { name: "grid"; child: Adw.WrapBox wrapbox_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; justify: spread; justify-last-line: true; styles [ "m10" ] }; } } } }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage failed_status_el { icon-name: "sad-computer-symbolic"; title: _("No Songs Found"); }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/pages/welcome.blp000066400000000000000000000011031523240030000220030ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneWelcomePage: Adw.NavigationPage { title: _("Welcome"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Adw.StatusPage { icon-name: "com.jeffser.Nocturne"; title: _("Welcome to Nocturne"); description: _("Select an instance type to get started"); child: Gtk.ListBox listbox_el { styles [ "boxed-list-separate" ] halign: center; selection-mode: none; }; }; }; }Jeffser-Nocturne-b442fee/src/ui/playing/000077500000000000000000000000001523240030000202225ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/playing/control_page.blp000066400000000000000000000201641523240030000234000ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingControlPage: Adw.NavigationPage { title: _("Playing"); tag: "playing"; child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar header_bar { show-title: false; show-end-title-buttons: false; show-start-title-buttons: false; } content: Gtk.ScrolledWindow { propagate-natural-height: true; propagate-natural-width: false; hscrollbar-policy: never; child: Adw.Clamp clamp { maximum-size: 720; styles [ "p0" ] child: Gtk.Box main_container { orientation: vertical; vexpand: true; valign: center; spacing: 15; styles [ "p10" ] $NocturnePlayingCoverArt cover_art_el { margin-bottom: 10; } Gtk.Box { orientation: vertical; spacing: 10; Gtk.Label title_el { wrap: true; wrap-mode: word_char; lines: 2; ellipsize: end; justify: center; styles [ "title-1" ] } Adw.WrapBox { child-spacing: 5; line-spacing: 5; justify: fill; Gtk.Button radio_homepage_el { halign: center; hexpand: true; action-name: "app.visit_url"; action-target: "''"; child: Adw.ButtonContent { icon-name: "external-link-symbolic"; }; styles [ "flat", "dimmed", "p0" ] accessibility { label: _("Visit current radio homepage"); } } Gtk.Button artist_el { halign: center; hexpand: true; action-target: "''"; tooltip-text: _("Show Artist"); child: Gtk.Label { ellipsize: end; }; styles [ "flat", "dimmed", "p0" ] } Gtk.Button album_el { halign: center; hexpand: true; action-target: "''"; tooltip-text: _("Show Album"); child: Gtk.Label { ellipsize: end; }; styles [ "flat", "dimmed", "p0" ] } } Gtk.Scale progress_el { orientation: horizontal; change-value => $progress_bar_changed(); adjustment: Gtk.Adjustment { lower: 0; upper: 1; value: 0; }; styles [ "p0" ] accessibility { label: _("Current song progress bar"); } } Gtk.Box { Gtk.Label positive_progress_el { styles [ "numeric", "dimmed" ] xalign: 0; justify: left; hexpand: true; } Gtk.Label negative_progress_el { styles [ "numeric", "dimmed" ] xalign: 1; justify: right; hexpand: true; } } Gtk.Box { orientation: horizontal; spacing: 15; hexpand: true; halign: center; Gtk.Button { icon-name: "media-skip-backward-symbolic"; valign: center; action-name:"app.player_previous"; tooltip-text: _("Previous"); styles [ "circular" ] } Gtk.Stack state_stack_el { transition-type: crossfade; Gtk.StackPage { name: "play"; child: Gtk.Button { icon-name: "media-playback-start-symbolic"; valign: center; action-name:"app.player_play"; tooltip-text: _("Play"); styles [ "circular", "accent", "color" ] }; } Gtk.StackPage { name: "pause"; child: Gtk.Button { icon-name: "media-playback-pause-symbolic"; valign: center; action-name: "app.player_pause"; tooltip-text: _("Pause"); styles [ "circular", "accent", "color" ] }; } } Gtk.Button { icon-name: "media-skip-forward-symbolic"; valign: center; action-name: "app.player_next"; tooltip-text: _("Next"); styles [ "circular" ] } } Gtk.Box { orientation: horizontal; halign: center; styles [ "toolbar" ] $NocturnePlaybackModeButton {} $NocturneVolumeButton {} Gtk.MenuButton rating_button { icon-name: "starred-symbolic"; tooltip-text: _("Rate"); popover: Gtk.Popover { child: Gtk.Box rating_container { hexpand: true; halign: center; Gtk.Button { name: "1"; tooltip-text: _("1 Star"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "2"; tooltip-text: _("2 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "3"; tooltip-text: _("3 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "4"; tooltip-text: _("4 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } Gtk.Button { name: "5"; tooltip-text: _("5 Stars"); icon-name: "non-starred-symbolic"; clicked => $change_rating(); styles [ "flat" ] } }; }; } Gtk.Button star_el { icon-name: "non-starred-symbolic"; tooltip-text: _("Star"); action-name: "app.toggle_star"; action-target: "''"; } Gtk.Button show_sidebar_el { icon-name: "sidebar-show-right-symbolic"; tooltip-text: _("Toggle Sidebar"); clicked => $show_content_clicked(); } } } }; }; }; }; }Jeffser-Nocturne-b442fee/src/ui/playing/cover_art.blp000066400000000000000000000030501523240030000227030ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingCoverArt: Gtk.Box{ orientation: vertical; spacing: 5; halign: center; Gtk.Box { Gtk.Image previous_icon_el { icon-name: "media-skip-backward-symbolic"; visible: false; } Gtk.Overlay { halign: center; overflow: hidden; styles [ "card" ] [overlay] $NocturneSpectrum spectrum_el { hexpand: true; vexpand: true; overflow: hidden; } child: Adw.ViewStack view_stack_el { hhomogeneous: false; vhomogeneous: false; enable-transitions: true; Adw.ViewStackPage { title: _("Audio"); name: "audio"; icon-name: "sound-symbolic"; child: Gtk.Picture cover_el { paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Song Cover"); } }; } Adw.ViewStackPage { title: _("Video"); name: "video"; icon-name: "video-clip-symbolic"; child: Gtk.Picture video_el { paintable: Adw.SpinnerPaintable { widget: video_el; }; accessibility { label: _("Video"); } }; } }; } Gtk.Image next_icon_el { icon-name: "media-skip-forward-symbolic"; visible: false; } } Adw.ViewSwitcher view_switcher_el { stack: view_stack_el; halign: center; policy: wide; } }Jeffser-Nocturne-b442fee/src/ui/playing/equalizer_page.blp000066400000000000000000000033071523240030000237210ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneEqualizerPage: Gtk.ScrolledWindow { max-content-width: 500; max-content-height: 400; child: Adw.Clamp { maximum-size: 800; tightening-threshold: 600; child: Adw.Clamp { vexpand: true; orientation: vertical; maximum-size: 500; tightening-threshold: 400; child: Gtk.Box { orientation: vertical; spacing: 10; Gtk.MenuButton { label: _("Presets"); halign: center; menu-model: menu { section { item { label: _("Flat"); action: "app.set_equalizer_preset"; target: "flat"; } } section { item { label: _("Vocal Clarity"); action: "app.set_equalizer_preset"; target: "clarity"; } item { label: _("Smooth Jazz"); action: "app.set_equalizer_preset"; target: "jazz"; } item { label: _("Rock"); action: "app.set_equalizer_preset"; target: "rock"; } item { label: _("Classic"); action: "app.set_equalizer_preset"; target: "classic"; } item { label: _("Acoustic"); action: "app.set_equalizer_preset"; target: "acoustic"; } } }; } Gtk.Box container { orientation: horizontal; halign: center; vexpand: true; } }; }; }; }Jeffser-Nocturne-b442fee/src/ui/playing/footer.blp000066400000000000000000000062431523240030000222240ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingFooter: Gtk.Overlay { [overlay] Gtk.ProgressBar ro_progress_el { valign: end; styles [ "osd" ] accessibility { label: _("Current song progress indicator"); } } child: Adw.BreakpointBin { width-request: 360; height-request: 32; Adw.Breakpoint { condition ("max-width: 480sp") setters { previous_el.visible: false; next_el.visible: false; } } child: Gtk.Box { valign: start; orientation: horizontal; spacing: 10; styles [ "p10" ] Gtk.Box { orientation: horizontal; spacing: 10; hexpand: true; Gtk.Image cover_el { styles [ "card" ] accessibility { label: _("Current song cover art"); } height-request: 48; width-request: 48; overflow: hidden; valign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; } Gtk.Box detail_container { orientation: vertical; spacing: 5; valign: center; hexpand: true; Gtk.Label title_el { styles [ "title-4" ] xalign: 0; halign: start; ellipsize: end; } Gtk.Label artist_el { styles [ "dimmed" ] xalign: 0; halign: start; ellipsize: end; } Gtk.Scale progress_el { styles [ "p0" ] adjustment: Gtk.Adjustment { lower: 0; upper: 1; value: 0; }; visible: false; change-value => $progress_bar_changed(); } } } Gtk.Button previous_el { icon-name: "media-skip-backward-symbolic"; valign: center; action-name: "app.player_previous"; tooltip-text: _("Previous"); styles [ "circular" ] } Gtk.Stack state_stack_el { transition-type: crossfade; Gtk.StackPage { name: "play"; child: Gtk.Button { icon-name: "media-playback-start-symbolic"; valign: center; action-name: "app.player_play"; tooltip-text: _("Play"); styles [ "circular", "accent", "color" ] }; } Gtk.StackPage { name: "pause"; child: Gtk.Button { icon-name: "media-playback-pause-symbolic"; valign: center; action-name: "app.player_pause"; tooltip-text: _("Pause"); styles [ "circular", "accent", "color" ] }; } } Gtk.Button next_el { icon-name: "media-skip-forward-symbolic"; valign: center; action-name: "app.player_next"; tooltip-text: _("Next"); styles [ "circular" ] } }; }; }Jeffser-Nocturne-b442fee/src/ui/playing/lyrics_page.blp000066400000000000000000000106001523240030000232170ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingLyricsPage: Gtk.Stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "plain"; child: Gtk.Overlay { [overlay] Gtk.Button { styles [ "circular", "accent" ] margin-bottom: 10; margin-end: 10; valign: end; halign: end; tooltip-text: _("Go Back"); icon-name: "step-back-symbolic"; clicked => $go_to_main(); } child: Gtk.ScrolledWindow { propagate-natural-width: false; propagate-natural-height: true; max-content-width: 500; max-content-height: 400; child: Adw.Clamp { maximum-size: 800; tightening-threshold: 600; child: Gtk.Label plain_label_el { wrap: true; wrap-mode: word_char; justify: center; halign: center; valign: center; styles [ "title-4", "p10", "document" ] }; }; }; }; } Gtk.StackPage { name: "lrc"; child: Gtk.Overlay { [overlay] Gtk.MenuButton { styles [ "circular", "accent" ] margin-bottom: 10; margin-end: 10; valign: end; halign: end; tooltip-text: _("Lyrics Options"); icon-name: "view-more-symbolic"; popover: Gtk.Popover lrc_menu_popover { child: Adw.Spinner {}; }; } [overlay] Gtk.Button { styles [ "circular", "accent" ] visible: false; margin-bottom: 10; margin-end: 10; valign: end; halign: end; tooltip-text: _("Go Back"); icon-name: "step-back-symbolic"; clicked => $go_to_main(); } child: Gtk.ScrolledWindow scrolledwindow { propagate-natural-width: true; propagate-natural-height: true; max-content-width: 500; max-content-height: 400; child: Adw.Clamp { maximum-size: 800; tightening-threshold: 600; child: Gtk.ListBox lrc_list_el { styles [ "lrc_list" ] selection-mode: single; row-selected => $on_lrc_selection(); }; }; }; }; } Gtk.StackPage { name: "not-found"; child: Adw.StatusPage { title: _("No Lyrics Found"); icon-name: "sad-computer-symbolic"; child: Gtk.Button { tooltip-text: _("Go Back"); halign: center; child: Adw.ButtonContent { icon-name: "step-back-symbolic"; label: _("Go Back"); }; clicked => $go_to_main(); }; }; } Gtk.StackPage { name: "instrumental"; child: Adw.StatusPage { title: _("Instrumental"); icon-name: "music-note-symbolic"; child: Gtk.Button { tooltip-text: _("Go Back"); halign: center; child: Adw.ButtonContent { icon-name: "step-back-symbolic"; label: _("Go Back"); }; clicked => $go_to_main(); }; }; } Gtk.StackPage { name: "radio"; child: Adw.StatusPage { title: _("Radio"); icon-name: "sound-symbolic"; }; } Gtk.StackPage { name: "not-found-locally"; child: Adw.StatusPage { title: _("Not Downloaded"); icon-name: "folder-download-symbolic"; description: _("The lyrics for this song have not been downloaded yet, they might be available for download"); child: Gtk.Box { orientation: vertical; spacing: 10; halign: center; Gtk.Button { styles [ "accent" ] halign: center; tooltip-text: _("Download"); child: Adw.ButtonContent { icon-name: "folder-download-symbolic"; label: _("Download"); }; clicked => $lyric_download_requested(); } Gtk.Button { styles [ "flat" ] halign: center; tooltip-text: _("Load Lyrics File"); child: Adw.ButtonContent { icon-name: "paper-symbolic"; label: _("Load Lyrics File"); }; clicked => $lyric_load_requested(); } }; }; } }Jeffser-Nocturne-b442fee/src/ui/playing/playback_mode_button.blp000066400000000000000000000003351523240030000251070ustar00rootroot00000000000000using Gtk 4.0; template $NocturnePlaybackModeButton: Gtk.MenuButton { icon-name: "media-playlist-consecutive-symbolic"; tooltip-text: _("Consecutive"); popover: Gtk.Popover { child: Gtk.Box container {}; }; }Jeffser-Nocturne-b442fee/src/ui/playing/popout_window.blp000066400000000000000000000067321523240030000236460ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePopoutWindow: Adw.ApplicationWindow { title: _("Pop-out Window"); width-request: 360; height-request: 120; default-width: 430; default-height: 740; hide-on-close: true; close-request => $close_request(); notify::fullscreened => $fullscreen_toggled(); styles [ "popout-window" ] Adw.Breakpoint { condition ("min-width: 1000sp") setters { split_view.max-sidebar-width: 2440; toolbarview.extend-content-to-top-edge: true; sidebar_extra_container.visible: true; header_view_switcher.visible: false; } apply => $big_mode_apply(); unapply => $big_mode_unapply(); } Adw.Breakpoint breakpoint_el { condition ("max-width: 620sp") setters { split_view.collapsed: true; header.title-widget: null; header.show-title: false; switch_bar.reveal: true; } } Adw.Breakpoint { condition ("max-height: 130sp") setters { main_stack.visible-child-name: "footer"; } } content: Adw.ToastOverlay toast_overlay{ Gtk.Stack main_stack { transition-type: crossfade; hhomogeneous: false; Gtk.StackPage { name: "content"; child: Adw.NavigationSplitView split_view { sidebar-width-fraction: 0.5; max-sidebar-width: 320; sidebar: $NocturnePlayingControlPage playing_page { extra-widget: Adw.Bin sidebar_extra_container { halign: center; visible: false; Adw.ViewSwitcher { stack: stack; policy: wide; visible: bind split_view.collapsed inverted; } }; }; content: Adw.NavigationPage { title: _("Song Details"); child: Adw.ToolbarView toolbarview { [top] Adw.HeaderBar header { title-widget: Adw.ViewSwitcher header_view_switcher { stack: stack; policy: wide; }; } [bottom] Adw.ViewSwitcherBar switch_bar { stack: stack; } content: Adw.ViewStack stack { enable-transitions: true; vhomogeneous: true; hhomogeneous: true; Adw.ViewStackPage { name: "queue"; title: _("Queue"); icon-name: "playlist-symbolic"; child: $NocturnePlayingQueuePage queue_page {}; } Adw.ViewStackPage { name: "lyrics"; title: _("Lyrics"); icon-name: "text-justify-center-symbolic"; child: $NocturnePlayingLyricsPage lyrics_page {}; } Adw.ViewStackPage { name: "equalizer"; title: _("Equalizer"); icon-name: "sound-symbolic"; child: $NocturneEqualizerPage equalizer_page {}; } }; }; }; }; } Gtk.StackPage { name: "footer"; child: Gtk.WindowHandle { child: Gtk.Overlay { child: $NocturneSpectrum footer_spectrum_el { opacity: 0.5; overflow: hidden; }; [overlay] $NocturnePlayingFooter footer { valign: center; } }; }; } } }; } Jeffser-Nocturne-b442fee/src/ui/playing/queue_page.blp000066400000000000000000000014461523240030000230460ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingQueuePage: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; max-content-width: 500; max-content-height: 400; child: Adw.Clamp { maximum-size: 800; tightening-threshold: 600; child: Gtk.Box { orientation: vertical; spacing: 5; Adw.SwitchRow autoplay_row_el { [prefix] Adw.Spinner autoplay_spinner_el { tooltip-text: _("Generating Next Queue"); } activatable: false; title: _("Autoplay"); subtitle: _("Generate a new queue when the current one ends"); } Adw.Bin list_bin_el { child: $NocturneSongQueue song_list_el { styles [ "p10" ] }; } }; }; }Jeffser-Nocturne-b442fee/src/ui/playing/volume_button.blp000066400000000000000000000013651523240030000236300ustar00rootroot00000000000000using Gtk 4.0; template $NocturneVolumeButton: Gtk.MenuButton { icon-name: "speaker-2-symbolic"; tooltip-text: _("Volume"); popover: Gtk.Popover { child: Gtk.Box { orientation: horizontal; Gtk.Image { icon-name: "speaker-0-symbolic"; accessibility { label: _("Speaker Mute"); } } Gtk.Scale volume_el { adjustment: Gtk.Adjustment { lower: 0; upper: 1; value: 0.25; }; orientation: horizontal; value-changed => $on_volume_changed(); width-request: 150; } Gtk.Image { icon-name: "speaker-3-symbolic"; accessibility { label: _("Speaker Full Volume"); } } }; }; }Jeffser-Nocturne-b442fee/src/ui/playlist/000077500000000000000000000000001523240030000204205ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/playlist/button.blp000066400000000000000000000031751523240030000224400ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistButton: Gtk.Box { Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } orientation: vertical; spacing: 5; halign: center; Gtk.Overlay { styles [ "album-button" ] [overlay] Gtk.Button play_el { icon-name: "media-playback-start-symbolic"; styles [ "osd", "circular" ] tooltip-text: _("Play Playlist"); action-name: "app.play_playlist"; valign: end; halign: start; } child: Gtk.Button cover_button_el { styles [ "force_p0", "flat" ] action-name: "app.show_playlist"; halign: center; tooltip-text: _("Show Playlist"); child: Gtk.Image cover_el { styles [ "card" ] icon-size: large; overflow: hidden; hexpand: true; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Playlist cover art"); } }; }; } Gtk.Button name_el { styles [ "flat", "p0" ] accessibility { label: _("Enter playlist page"); } child: Gtk.Box { orientation: vertical; spacing: 5; Gtk.Label name_label_el { styles [ "title-3" ] ellipsize: end; max-width-chars: 1; } Gtk.Label song_count_label_el { styles [ "dimmed" ] ellipsize: end; max-width-chars: 1; } }; action-name: "app.show_playlist"; } }Jeffser-Nocturne-b442fee/src/ui/playlist/dialog.blp000066400000000000000000000020561523240030000223610ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistDialog: Adw.Dialog { content-width: 400; title: _("Playlist"); child: Adw.ToolbarView { [top] Adw.HeaderBar { title-widget: Gtk.SearchEntry search_entry_el { placeholder-text: _("Search (or create new playlist)"); hexpand: true; search-changed => $search_changed(); }; } content: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Adw.PreferencesGroup preferences_group_el { title: _("Adding Music to Playlist"); margin-start: 10; margin-end: 10; margin-bottom: 10; margin-top: 10; Gtk.ListBox list_el { styles [ "boxed-list" ] selection-mode: none; Adw.ButtonRow add_button_el { activatable: true; start-icon-name: "list-add-symbolic"; visible: false; tooltip-text: _("Create New Playlist and Add Songs"); } } }; }; }; }Jeffser-Nocturne-b442fee/src/ui/playlist/page.blp000066400000000000000000000050371523240030000220400ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistPage: Adw.NavigationPage { title: _("Playlist"); child: Adw.ToolbarView { extend-content-to-top-edge: true; [top] Adw.HeaderBar { show-title: false; } content: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; edge-reached => $scroll_edge_reached(); child: Adw.Clamp clamp_el { tightening-threshold: 800; maximum-size: 2000; child: Gtk.Box { styles [ "p10" ] orientation: vertical; spacing: 10; Adw.WrapBox { // Information Container orientation: horizontal; line-spacing: 10; child-spacing: 10; justify: fill; wrap-policy: minimum; Gtk.Image cover_el { styles [ "card" ] icon-size: large; width-request: 240; height-request: 240; overflow: hidden; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Playlist cover art"); } } Gtk.Box { orientation: vertical; spacing: 10; valign: center; Gtk.Label name_el { styles [ "title-1" ] xalign: 0; wrap: true; wrap-mode: word_char; lines: 2; halign: start; ellipsize: end; valign: center; visible: false; } Gtk.Label song_count_el { styles [ "dimmed" ] halign: start; ellipsize: end; valign: center; visible: false; } Gtk.Label duration_el { styles [ "dimmed", "numeric" ] halign: start; ellipsize: end; valign: center; visible: false; } Adw.WrapBox context_wrap_el { orientation: horizontal; line-spacing: 10; child-spacing: 10; } } } $NocturneSongQueue song_list_el {} }; }; }; }; } Jeffser-Nocturne-b442fee/src/ui/playlist/row.blp000066400000000000000000000016651523240030000217360ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistRow: Adw.ActionRow { action-name: "app.show_playlist"; activatable: true; use-markup: false; Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } [prefix] Gtk.Image cover_el { styles [ "card" ] height-request: 48; width-request: 48; margin-top: 10; margin-bottom: 10; overflow: hidden; valign: center; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Playlist cover art"); } } [suffix] Gtk.MenuButton menu_button_el { styles [ "circular", "flat", ] icon-name: "view-more-symbolic"; valign: center; notify::active => $on_context_button_active(); popover: Gtk.Popover { child: Adw.Spinner {}; }; tooltip-text: _("Options"); } }Jeffser-Nocturne-b442fee/src/ui/playlist/selector_row.blp000066400000000000000000000010161523240030000236240ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlaylistSelectorRow: Adw.ActionRow { action-name: "app.add_songs_to_playlist"; activatable: true; use-markup: false; [prefix] Gtk.Image cover_el { styles [ "card" ] height-request: 48; width-request: 48; margin-top: 10; margin-bottom: 10; overflow: hidden; valign: center; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Playlist cover art"); } } }Jeffser-Nocturne-b442fee/src/ui/preferences.blp000066400000000000000000000272231523240030000215650ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePreferencesDialog: Adw.PreferencesDialog { search-enabled: true; Adw.PreferencesPage { title: _("General"); icon-name: "preferences-system-symbolic"; Adw.PreferencesGroup { title: _("Behavior"); Adw.SwitchRow restore_el { title: _("Restore Queue on Launch"); } Adw.SwitchRow hide_on_close_el { title: _("Hide on Close"); } Adw.SwitchRow simulate_wbwl_el { title: _("Simulate Word by Word Lyrics"); subtitle: _("Used when only line by line lyrics are available"); } Adw.SwitchRow auto_download_lyrics_el { title: _("Auto Download Lyrics"); subtitle: _("Used when the current instance does not provide lyrics"); } Adw.SwitchRow use_gain_el { title: _("Use Track and Album Gain"); subtitle: _("Adjust the volume based on gain metadata"); } Adw.ComboRow default_page_el { title: _("Default Page on Login"); model: Gtk.StringList {}; notify::selected => $default_page_changed(); } Adw.ComboRow bitrate_el { title: _("Maximum Bitrate"); model: Gtk.StringList {}; notify:: selected => $max_bitrate_changed(); } } Adw.PreferencesGroup gnome_search_group_el { title: _("Gnome Search"); description: _("Search for your music directly from the system search bar while Nocturne is running. You can enable or disable this feature completely in System Settings"); Adw.SwitchRow gnome_search_artists_el { title: _("Include Artists"); } Adw.SwitchRow gnome_search_albums_el { title: _("Include Albums"); } Adw.SwitchRow gnome_search_songs_el { title: _("Include Songs"); } Adw.SwitchRow gnome_search_playlists_el { title: _("Include Playlists"); } } Adw.PreferencesGroup session_group_el { title: _("Session"); Adw.SwitchRow discord_rpc_el{ title: _("Discord Rich Presence"); subtitle: _("Share the currently playing song on Discord"); } Adw.SwitchRow discord_coverart_share_el { title: _("Share Cover Art With Discord"); subtitle: _("If the instance is not reachable by Discord servers, CoverArtArchive will be used instead"); visible: bind discord_rpc_el.active; subtitle-lines: 2; } Adw.ActionRow { title: _("ListenBrainz Link"); subtitle: _("Keep track of your playback history"); [suffix] Gtk.Stack listenbrainz_stack_el { Gtk.StackPage { name: "link"; child: Gtk.Button { label: _("Link"); tooltip-text: _("Link"); valign: center; clicked => $listenbrainz_link_requested(); }; } Gtk.StackPage { name: "unlink"; child: Gtk.Button { label: _("Unlink"); tooltip-text: _("Unlink"); valign: center; clicked => $listenbrainz_unlink_requested(); styles [ "destructive-action" ] }; } } } Adw.ActionRow instance_el { title: _("User"); visible: false; activatable: true; action-name: "app.visit_url"; action-target: "''"; use-markup: false; [prefix] Adw.Avatar instance_avatar_el { size: 32; } [suffix] Gtk.Image instance_icon_el { valign: center; icon-name: "external-link-symbolic"; accessibility { label: "External link"; } } } Adw.ButtonRow { title: _("Delete Image Cache"); styles [ "destructive-action" ] activated => $delete_image_cache_requested(); } Adw.ButtonRow { title: _("Logout"); styles [ "destructive-action" ] action-name: "app.logout"; } } } Adw.PreferencesPage { title: _("Customization"); icon-name: "paintbrush-symbolic"; Adw.PreferencesGroup { title: _("Interface"); Adw.SwitchRow context_button_el { title: _("Show Context Button in Rows"); subtitle: _("The menu can also be shown by right clicking / long pressing"); } Adw.SwitchRow context_label_el { title: _("Show Labels in Buttons of Pages"); } Adw.SwitchRow footer_big_mode_el { title: _("Show Progressbar in Player Footer"); } Adw.SwitchRow translucent_player_el { title: _("Make Background Translucent in Player"); subtitle: _("Does not affect sidebar player"); } Adw.SwitchRow use_sidebar_player_el { title: _("Use Sidebar Player When Possible"); subtitle: _("Sidebar player is only available when the window is wide enough"); } Adw.SwitchRow hide_singles_el { title: _("Filter Singles"); subtitle: _("Hides albums with only one song (does not affect homepage)"); } Adw.ActionRow { title: _("Button Size"); subtitle: _("Affects albums, artists, playlists and songs"); [suffix] Adw.ToggleGroup button_size_el { valign: center; Adw.Toggle { tooltip: _("Small"); label: _("Small"); name: "small"; } Adw.Toggle { tooltip: _("Big"); label: _("Big"); name: "big"; } } } } Adw.PreferencesGroup { title: _("Dynamic Background"); description: _("Background generated based on the album art of the current song"); Adw.ActionRow { title: _("Use in Main Window"); [suffix] Adw.ToggleGroup global_dynamic_bg_el { valign: center; Adw.Toggle { tooltip: _("Off"); label: _("Off"); name: ""; } Adw.Toggle { tooltip: _("Gradient"); label: _("Gradient"); name: "gradient"; } Adw.Toggle { tooltip: _("Blur"); label: _("Blur"); name: "blur"; } } } Adw.ActionRow { title: _("Use in Player"); [suffix] Adw.ToggleGroup player_dynamic_bg_el { valign: center; Adw.Toggle { tooltip: _("Off"); label: _("Off"); name: ""; } Adw.Toggle { tooltip: _("Gradient"); label: _("Gradient"); name: "gradient"; } Adw.Toggle { tooltip: _("Blur"); label: _("Blur"); name: "blur"; } } } Adw.ActionRow { title: _("Use in Pop-out Window"); [suffix] Adw.ToggleGroup popout_dynamic_bg_el { valign: center; Adw.Toggle { tooltip: _("Off"); label: _("Off"); name: ""; } Adw.Toggle { tooltip: _("Gradient"); label: _("Gradient"); name: "gradient"; } Adw.Toggle { tooltip: _("Blur"); label: _("Blur"); name: "blur"; } } } Adw.SwitchRow dynamic_accent_el { title: _("Use as Accent Color"); } } Adw.PreferencesGroup { title: _("Homepage"); description: _("Reload homepage to see changes"); Adw.ActionRow { title: _("Welcome Message"); subtitle: _("Where to get user information"); [suffix] Adw.ToggleGroup home_mode_el { valign: center; notify::active-name => $home_mode_changed(); Adw.Toggle { tooltip: _("Off"); label: _("Off"); name: ""; } Adw.Toggle { tooltip: _("Instance"); label: _("Instance"); name: "instance"; } Adw.Toggle { tooltip: _("System"); label: _("System"); name: "system"; } } } Adw.SpinRow hp_frequent_albums_el { title: _("Frequent Albums"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; step-increment: 1; }; } Adw.SpinRow hp_songs_el { title: _("Songs"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; step-increment: 1; }; } Adw.SpinRow hp_albums_el { title: _("Albums"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; step-increment: 1; }; } Adw.SpinRow hp_artists_el { title: _("Artists"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; step-increment: 1; }; } Adw.SpinRow hp_playlists_el { title: _("Playlists"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; step-increment: 1; }; } } Adw.PreferencesGroup sidebar_group { title: _("Sidebar"); } } Adw.PreferencesPage { title: _("Visualizer"); icon-name: "sound-symbolic"; Adw.PreferencesGroup { title: _("Preferences"); Adw.SwitchRow visualizer_el { title: _("Show Visualizer in Player"); subtitle: _("Show an audio spectrum visualizer on top of the album art in the player"); } } Adw.PreferencesGroup { title: _("Appearance"); sensitive: bind visualizer_el.active; Adw.SpinRow visualizer_bar_n_el { title: _("Number of Bars"); adjustment: Gtk.Adjustment { lower: 4; upper: 64; step-increment: 2; }; snap-to-ticks: true; digits: 0; } Adw.ActionRow { title: _("Visualizer Type"); [suffix] Adw.ToggleGroup visualizer_type_el { valign: center; Adw.Toggle { tooltip: _("Wave"); label: _("Wave"); name: "wave"; } Adw.Toggle { tooltip: _("Bars"); label: _("Bars"); name: "bars"; } Adw.Toggle { tooltip: _("Particles"); label: _("Particles"); name: "particles"; } } } Adw.ActionRow { title: _("Fill Mode"); [suffix] Adw.ToggleGroup visualizer_fill_el { valign: center; Adw.Toggle { tooltip: _("Solid"); label: _("Solid"); name: "solid"; } Adw.Toggle { tooltip: _("Translucent"); label: _("Translucent"); name: "translucent"; } Adw.Toggle { tooltip: _("Border"); label: _("Border"); name: "border"; } } } } Adw.PreferencesGroup { title: _("Color"); sensitive: bind visualizer_el.active; Adw.SwitchRow visualizer_auto_color_el { title: _("Use Dynamic Color"); } Adw.SwitchRow visualizer_invert_auto_color_el { title: _("Invert Color"); visible: bind visualizer_auto_color_el.active; } Adw.ActionRow { title: _("Manual Color"); visible: bind visualizer_auto_color_el.active inverted; [suffix] Gtk.ColorDialogButton visualizer_manual_color_el { valign: center; dialog: Gtk.ColorDialog { title: _("Visualizer Color"); with-alpha: false; }; notify::rgba => $visualizer_manual_color_changed(); } } } } } Jeffser-Nocturne-b442fee/src/ui/shortcuts-dialog.blp000066400000000000000000000026531523240030000225570ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; Adw.ShortcutsDialog shortcuts_dialog { Adw.ShortcutsSection { title: _("General"); Adw.ShortcutsItem { title: _("Search"); action-name: "app.search"; } Adw.ShortcutsItem { title: _("Reload Page"); action-name: "app.reload"; } Adw.ShortcutsItem { title: _("Show Shortcuts"); action-name: "app.shortcuts"; } Adw.ShortcutsItem { title: _("Show Preferences"); action-name: "app.preferences"; } Adw.ShortcutsItem { title: _("Quit"); action-name: "app.quit"; } } Adw.ShortcutsSection { title: _("Playback"); Adw.ShortcutsItem { title: _("Toggle Playback"); action-name: "app.player_toggle"; } Adw.ShortcutsItem { title: _("Next Song"); action-name: "app.player_next"; } Adw.ShortcutsItem { title: _("Previous Song"); action-name: "app.player_previous"; } Adw.ShortcutsItem { title: _("Raise Volume"); action-name: "app.player_raise_volume"; } Adw.ShortcutsItem { title: _("Lower Volume"); action-name: "app.player_lower_volume"; } } Adw.ShortcutsSection { title: _("Window"); Adw.ShortcutsItem { title: _("Toggle Fullscreen"); action-name: "app.toggle_fullscreen"; } Adw.ShortcutsItem { title: _("Open Pop-Out Window"); action-name: "app.open_popout_window"; } } }Jeffser-Nocturne-b442fee/src/ui/song/000077500000000000000000000000001523240030000175255ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/ui/song/button.blp000066400000000000000000000034451523240030000215450ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongButton: Gtk.Box { orientation: vertical; halign: center; Gtk.Button cover_button_el { styles [ "force_p0", "flat", "active_appearance_button" ] action-name: "app.play_song"; halign: center; tooltip-text: _("Play Song"); child: Gtk.Box { orientation: vertical; spacing: 5; Gtk.Image cover_el { styles [ "card" ] icon-size: large; overflow: hidden; hexpand: true; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Song cover art"); } } Gtk.CenterBox { hexpand: true; margin-bottom: 5; margin-start: 5; margin-end: 5; start-widget: Gtk.Image play_indicator_el { valign: center; visible: false; margin-end: 5; icon-name: "sound-symbolic"; accessibility { label: _("Play Indicator"); } }; center-widget: Gtk.Label name_el { styles [ "title-3" ] ellipsize: end; max-width-chars: 1; hexpand: true; }; } }; Gtk.GestureClick { button: 3; released => $show_popover_image(); } Gtk.GestureLongPress { pressed => $show_popover_image(); } } Gtk.Button artist_el { Gtk.GestureClick { button: 3; released => $show_popover_artist(); } Gtk.GestureLongPress { pressed => $show_popover_artist(); } styles [ "dimmed", "flat", "p0" ] accessibility { label: _("Enter artist page"); } child: Gtk.Label { ellipsize: end; max-width-chars: 1; }; } }Jeffser-Nocturne-b442fee/src/ui/song/details_dialog.blp000066400000000000000000000023161523240030000231720ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongDetailsDialog: Adw.Dialog { title: _("Song Details"); content-width: 420; child: Adw.ToolbarView { [top] Adw.HeaderBar { show-title: false; } extend-content-to-top-edge: true; content: Gtk.ScrolledWindow { propagate-natural-width: true; propagate-natural-height: true; child: Gtk.Box { orientation: vertical; spacing: 24; margin-start: 12; margin-end: 12; margin-bottom: 12; margin-top: 12; Gtk.Picture cover_el { height-request: 240; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; styles [ "card" ] accessibility { label: _("Song Cover"); } } Gtk.Label title_el { justify: center; halign: center; hexpand: true; wrap: true; wrap-mode: word_char; styles [ "title-1" ] } Gtk.ListBox listbox_el { selection-mode: none; styles [ "boxed-list", ] } }; }; }; }Jeffser-Nocturne-b442fee/src/ui/song/queue.blp000066400000000000000000000071101523240030000213470ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongQueue: Gtk.Box { orientation: vertical; Gtk.Button header_button { styles [ "flat", "title-2", "active_appearance_button" ] halign: start; sensitive: false; child: Adw.ButtonContent { label: bind template.header-label; icon-name: bind template.header-icon-name; }; tooltip-text: bind template.header-label; visible: bind $handle_visible_bind(template.header-label); action-name: bind $handle_action_name_bind(template.header-page-tag); action-target: bind $handle_action_target_bind(template.header-page-tag); margin-bottom: 10; } Gtk.Revealer toolbar_revealer_el { transition-type: slide_up; child: Adw.WrapBox { styles [ "osd", "toolbar" ] line-spacing: 5; child-spacing: 5; margin-bottom: 10; Gtk.Button { styles [ "circular" ] icon-name: "window-close-symbolic"; tooltip-text: _("Close"); clicked => $close_selector(); } Gtk.Button select_all_el { child: Adw.ButtonContent { icon-name: "check-plain-symbolic"; label: _("Select All"); }; tooltip-text: _("Select All"); clicked => $select_all(); } Gtk.Button play_el { child: Adw.ButtonContent { icon-name: "media-playback-start-symbolic"; label: _("Play"); }; tooltip-text: _("Play"); clicked => $play_selected(); } Gtk.Button play_next_el { child: Adw.ButtonContent { icon-name: "list-high-priority-symbolic"; label: _("Play Next"); }; tooltip-text: _("Play Next"); clicked => $play_next_selected(); } Gtk.Button play_later_el { child: Adw.ButtonContent { icon-name: "list-low-priority-symbolic"; label: _("Play Later"); }; tooltip-text: _("Play Later"); clicked => $play_later_selected(); } Gtk.Button { child: Adw.ButtonContent { icon-name: "playlist-symbolic"; label: _("Add To Playlist"); }; tooltip-text: _("Add To Playlist"); clicked => $add_to_playlist_selected(); } Gtk.Button download_el { child: Adw.ButtonContent { icon-name: "folder-download-symbolic"; label: _("Download"); }; tooltip-text: _("Download"); clicked => $download_selected(); } Gtk.Button remove_el { styles [ "error" ] child: Adw.ButtonContent { icon-name: "user-trash-symbolic"; label: _("Remove"); }; tooltip-text: _("Remove"); clicked => $remove_selected(); } Gtk.Button delete_download_el { styles [ "error" ] child: Adw.ButtonContent { icon-name: "user-trash-symbolic"; label: _("Delete Download"); }; tooltip-text: _("Delete Download"); clicked => $delete_download_selected(); } }; } Gtk.Stack main_stack { transition-type: crossfade; vhomogeneous: false; overflow: visible; Gtk.StackPage { name: "loading"; child: Adw.Spinner {}; } Gtk.StackPage { name: "content"; child: Gtk.ListBox list_el { styles [ "boxed-list" ] selection-mode: none; valign: start; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { title: _("No Songs Found"); }; } } }Jeffser-Nocturne-b442fee/src/ui/song/row.blp000066400000000000000000000055731523240030000210450ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongRow: Adw.ActionRow { activatable: true; action-name: "app.play_song"; use-markup: false; Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } Gtk.DropTarget { formats: "NocturneSongRow"; drop => $on_drop(); actions: move; } child: Gtk.Box { styles [ "p10", ] spacing: 10; margin-top: 5; orientation: horizontal; Gtk.Image icon_el { valign: center; visible: false; styles [ "dimmed" ] Gtk.DragSource { drag-begin => $on_drag_begin(); prepare => $on_drag_prepare(); actions: move; } accessibility { label: _("Drag icon"); } } Gtk.Box { orientation: vertical; valign: center; vexpand: true; hexpand: true; spacing: 2; Gtk.Label title_el { styles [ "heading", ] ellipsize: end; halign: start; } Adw.WrapBox subtitle_wrapbox { halign: start; line-spacing: 2; child-spacing: 2; wrap-policy: minimum; Gtk.Button external_file_el { action-name: "app.show_external_file_warning"; tooltip-text: _("External File"); icon-name: "paper-symbolic"; visible: false; styles [ "force_p0", "flat", "dimmed" ] } Gtk.Label duration_el { styles [ "subtitle", "numeric" ] visible: false; ellipsize: end; } Adw.Bin artist_container_el {} } } Gtk.Stack suffixes_stack_el { transition-type: crossfade; Gtk.StackPage { name: "normal"; child: Gtk.Box { orientation: horizontal; spacing: 10; Gtk.Button star_el { styles [ "circular", "flat" ] child: Adw.Spinner {}; action-name: "app.toggle_star"; valign: center; accessibility { label: _("Toggle star"); } } Gtk.MenuButton menu_button_el { styles [ "circular", "flat", ] icon-name: "view-more-symbolic"; valign: center; notify::active => $on_context_button_active(); popover: Gtk.Popover { child: Adw.Spinner {}; }; tooltip-text: _("Options"); } }; } Gtk.StackPage { name: "select"; child: Gtk.CheckButton check_el { halign: center; toggled => $check_toggled(); accessibility { label: _("Selection box"); } }; } } }; } Jeffser-Nocturne-b442fee/src/ui/song/small_row.blp000066400000000000000000000021411523240030000222210ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongSmallRow: Gtk.Button { Gtk.GestureClick { button: 3; released => $show_popover(); } Gtk.GestureLongPress { pressed => $show_popover(); } styles [ "card", "p0" ] action-name: "app.play_song"; child: Gtk.Box { orientation: horizontal; spacing: 5; Gtk.Image cover_el { styles [ "card" ] height-request: 48; width-request: 48; margin-top: 5; margin-bottom: 5; overflow: hidden; valign: center; halign: center; paintable: Adw.SpinnerPaintable { widget: cover_el; }; accessibility { label: _("Song cover art"); } } Gtk.Box { orientation: vertical; valign: center; Gtk.Label title_el { styles [ "heading" ] wrap: true; wrap-mode: word_char; xalign: 0; } Gtk.Label subtitle_el { styles [ "caption", "dimmed" ] wrap: true; wrap-mode: word_char; xalign: 0; } } }; }Jeffser-Nocturne-b442fee/src/ui/window.blp000066400000000000000000000220721523240030000205700ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneWindow: Adw.ApplicationWindow { title: "Nocturne"; width-request: 360; height-request: 560; close-request => $close_request(); Adw.Breakpoint { condition ("max-width: 840sp") apply => $big_breakpoint_toggled(); unapply => $big_breakpoint_toggled(); } Adw.Breakpoint breakpoint_el { condition ("max-width: 620sp") setters { main_split_view.collapsed: true; main_split_view.show-content: true; main_sidebar.mode: page; sheet_split_view.collapsed: true; sheet_header.title-widget: null; sheet_header.show-title: false; sheet_switch_bar.reveal: true; main_bottom_sheet.full-width: true; } } content: Adw.ToastOverlay toast_overlay { Gtk.Stack main_stack { transition-type: crossfade; Gtk.StackPage { name: "loading"; child: Adw.ToolbarView { [top] Adw.HeaderBar { show-title: false; } content: Gtk.Box { orientation: vertical; halign: center; valign: center; spacing: 10; styles [ "p10" ] Adw.Spinner { width-request: 240; height-request: 240; } Gtk.Label loading_label_el { wrap: true; wrap-mode: word_char; } }; }; } Gtk.StackPage { name: "welcome"; child: $NocturneWelcomePage welcome_page {}; } Gtk.StackPage { name: "setup"; child: $NocturneSetupPage setup_page {}; } Gtk.StackPage { name: "playback"; child: $NocturnePlaybackPage {}; } Gtk.StackPage { name: "content"; child: Adw.BottomSheet main_bottom_sheet { name: "main-bottom-sheet"; full-width: false; show-drag-handle: false; reveal-bottom-bar: false; accessible-role: navigation; Gtk.DropTarget { formats: "GFile"; drop => $on_drop(); actions: copy; } content: Adw.NavigationSplitView main_split_view { max-sidebar-width: 240; show-content: true; sidebar: Adw.NavigationPage { title: "Nocturne"; child: Adw.ToolbarView { [top] Adw.HeaderBar sidebar_headerbar { [start] Gtk.Button { icon-name: "media-playlist-shuffle-symbolic"; tooltip-text: _("Play Random Queue"); action-name: "app.play_random_queue"; } [end] Gtk.MenuButton { primary: true; icon-name: "open-menu-symbolic"; tooltip-text: _("Menu"); menu-model: primary_menu; } [end] $NocturneDownloadsQueueButton downloads_button_el {} } content: Adw.Sidebar main_sidebar { activated => $on_sidebar_activated(); }; }; }; content: Adw.NavigationPage { child: Adw.OverlaySplitView player_sidebar_splitview { max-sidebar-width: 480; min-sidebar-width: 360; show-sidebar: false; sidebar-position: end; content: Adw.NavigationView main_navigationview { $NocturneHomePage {} $NocturneArtistsPage {} $NocturneStarredPage { tag: "artists-starred"; title:_("Favorite Artists"); } $NocturnePlaylistsPage {} $NocturneSongsAllPage {} $NocturneStarredPage { tag: "songs-starred"; title:_("Favorite Songs"); } $NocturneAlbumsAllPage {} $NocturneRadiosPage {} $NocturneAlbumsPage { tag: "albums-random"; title: _("Random Albums"); } $NocturneAlbumsPage { tag: "albums-starred"; title: _("Favorite Albums"); } $NocturneAlbumsPage { tag: "albums-newest"; title: _("Newest Albums"); } $NocturneAlbumsPage { tag: "albums-recent"; title: _("Recent Albums"); } $NocturneAlbumsPage { tag: "albums-frequent"; title: _("Frequent Albums"); } }; sidebar: Adw.ToolbarView { styles [ "main_sidebar" ] extend-content-to-top-edge: true; [top] Adw.HeaderBar {} [bottom] Adw.ViewSwitcherBar { stack: sidebar_stack; reveal: true; } content: Adw.ViewStack sidebar_stack { enable-transitions: true; vhomogeneous: true; hhomogeneous: true; Adw.ViewStackPage { name: "player"; title: _("Player"); icon-name: "music-note-symbolic"; child: $NocturnePlayingControlPage sidebar_playing_page {}; } Adw.ViewStackPage { name: "queue"; title: _("Queue"); icon-name: "playlist-symbolic"; child: $NocturnePlayingQueuePage sidebar_queue_page { styles [ "queue_page_padding_top" ] }; } Adw.ViewStackPage { name: "lyrics"; title: _("Lyrics"); icon-name: "text-justify-center-symbolic"; child: $NocturnePlayingLyricsPage sidebar_lyrics_page {}; } Adw.ViewStackPage { name: "equalizer"; title: _("Equalizer"); icon-name: "sound-symbolic"; child: $NocturneEqualizerPage {}; } }; }; }; }; }; bottom-bar: $NocturnePlayingFooter footer {}; sheet: Adw.NavigationSplitView sheet_split_view { sidebar-width-fraction: 0.5; max-sidebar-width: 420; sidebar: $NocturnePlayingControlPage playing_page {}; content: Adw.NavigationPage { title: _("Song Details"); child: Adw.ToolbarView { [top] Adw.HeaderBar sheet_header { show-end-title-buttons: false; show-start-title-buttons: false; title-widget: Adw.ViewSwitcher { stack: sheet_stack; policy: wide; }; } [bottom] Adw.ViewSwitcherBar sheet_switch_bar { stack: sheet_stack; } Adw.ViewStack sheet_stack { enable-transitions: true; vhomogeneous: true; hhomogeneous: true; Adw.ViewStackPage { name: "queue"; title: _("Queue"); icon-name: "playlist-symbolic"; child: $NocturnePlayingQueuePage queue_page {}; } Adw.ViewStackPage { name: "lyrics"; title: _("Lyrics"); icon-name: "text-justify-center-symbolic"; child: $NocturnePlayingLyricsPage lyrics_page {}; } Adw.ViewStackPage { name: "equalizer"; title: _("Equalizer"); icon-name: "sound-symbolic"; child: $NocturneEqualizerPage {}; } } }; }; }; }; } } }; } menu primary_menu { section { item { label: _("Preferences"); action: "app.preferences"; } item { label: _("Open Pop-Out Window"); action: "app.open_popout_window"; } item { label: _("Toggle Fullscreen"); action: "app.toggle_fullscreen"; } item { label: _("See Last Nocturne Playback"); action: "app.launch_playback"; } item { label: _("Keyboard Shortcuts"); action: "app.shortcuts"; } item { label: _("About Nocturne"); action: "app.about"; } } }Jeffser-Nocturne-b442fee/src/widgets/000077500000000000000000000000001523240030000176105ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/__init__.py000066400000000000000000000012311523240030000217160ustar00rootroot00000000000000# __init__.py from .playing import PlayingFooter, PlayingControlPage, PopoutWindow, PlayingCoverArt from .pages import HomePage, LoginDialog, ArtistsPage, PlaylistsPage, StarredPage, SongsAllPage, AlbumsPage, AlbumsAllPage, RadiosPage, WelcomePage, SetupPage, PlaybackPage from .album import AlbumButton, AlbumPage, AlbumRow from .artist import ArtistButton, ArtistPage, ArtistRow from .playlist import PlaylistButton, PlaylistPage, PlaylistRow, PlaylistDialog, PlaylistSelectorRow from .song import SongRow, SongQueue, SongSmallRow, SongDetailsDialog, SongButton from .containers import Carousel, Wrapbox, PageDialog from .lyrics import LyricsDialog, prepare_lrc Jeffser-Nocturne-b442fee/src/widgets/album/000077500000000000000000000000001523240030000207105ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/album/__init__.py000066400000000000000000000001451523240030000230210ustar00rootroot00000000000000# __init__.py from .button import AlbumButton from .page import AlbumPage from .row import AlbumRow Jeffser-Nocturne-b442fee/src/widgets/album/button.py000066400000000000000000000135601523240030000226020ustar00rootroot00000000000000# button.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_ALBUM, CONTEXT_ARTIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/album/button.ui') class AlbumButton(Gtk.Box): __gtype_name__ = 'NocturneAlbumButton' play_el = Gtk.Template.Child() star_el = Gtk.Template.Child() cover_button_el = Gtk.Template.Child() cover_el = Gtk.Template.Child() name_button_el = Gtk.Template.Child() name_el = Gtk.Template.Child() year_el = Gtk.Template.Child() artist_el = Gtk.Template.Child() def __init__(self, id:str, show_year:bool=False): self.id = id integration = get_current_integration() integration.verifyAlbum(self.id) super().__init__() self.play_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.cover_button_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.star_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.name_button_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::button-size", lambda *_: GLib.idle_add(self.update_size)) self.show_year = show_year integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'artist', self.update_artist) integration.connect_to_model(self.id, 'artistId', self.update_artist_id) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model(self.id, 'starred', self.update_starred) integration.connect_to_model(self.id, 'year', self.update_year) def update_size(self): isBig = self.settings.get_value('button-size').unpack() == 'big' size = 240 if isBig else 180 pixel_size = size if self.cover_el.get_paintable() is not None else -1 self.cover_el.set_size_request(size, size) self.cover_el.set_pixel_size(pixel_size) if isBig: self.name_el.remove_css_class('title-4') self.name_el.add_css_class('title-3') else: self.name_el.remove_css_class('title-3') self.name_el.add_css_class('title-4') def update_cover(self, paintable:Gdk.Paintable=None): if paintable: self.cover_el.set_from_paintable(paintable) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-queue-symbolic") self.update_size() def update_name(self, name:str): self.name_el.set_label(name) self.name_button_el.set_tooltip_text(name) self.cover_button_el.set_tooltip_text(name) self.set_name(name) def update_artist(self, artist:str): self.artist_el.get_child().set_label(artist) self.artist_el.set_tooltip_text(artist) def update_year(self, year:int): if(year > 0): if self.show_year: self.year_el.set_label(str(year)) self.year_el.set_visible(True) else: self.year_el.set_visible(False) def update_artist_id(self, artistId:str): if artistId: self.artist_el.set_action_name("app.show_artist") self.artist_el.set_action_target_value(GLib.Variant.new_string(artistId)) else: self.artist_el.set_action_name("") def update_starred(self, starred:bool): if starred: self.star_el.add_css_class('accent') self.star_el.remove_css_class('dim-label') self.star_el.set_icon_name('heart-filled-symbolic') self.star_el.set_tooltip_text(_('Favorite')) else: self.star_el.remove_css_class('accent') self.star_el.add_css_class('dim-label') self.star_el.set_icon_name('heart-outline-thick-symbolic') self.star_el.set_tooltip_text(_('Not Favorite')) @Gtk.Template.Callback() def show_popover_image(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] context = CONTEXT_ALBUM.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] popover = Gtk.Popover( child=ContextContainer(context, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self.cover_button_el) popover.popup() @Gtk.Template.Callback() def show_popover_name(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] context = CONTEXT_ALBUM.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] popover = Gtk.Popover( child=ContextContainer(context, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self.name_button_el) popover.popup() @Gtk.Template.Callback() def show_popover_artist(self, *args): integration = get_current_integration() artist_id = integration.loaded_models.get(self.id).get_property('artistId') if artist_id: rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=ContextContainer(CONTEXT_ARTIST, artist_id), pointing_to=rect, has_arrow=False ) popover.set_parent(self.artist_el) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/album/meson.build000066400000000000000000000003511523240030000230510ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'album' widgets = [ '__init__.py', 'button.py', 'page.py', 'row.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/album/page.py000066400000000000000000000177761523240030000222200ustar00rootroot00000000000000# page.py from gi.repository import Gtk, Adw, GLib from ..song import SongRow from ...integrations import get_current_integration, models from ...constants import CONTEXT_ALBUM from ..containers import get_context_buttons_list import threading, io from colorthief import ColorThief class DiscIndicator(Gtk.ListBoxRow): __gtype_name__ = 'NocturneDiscIndicator' def __init__(self, discNumber:int): self.discNumber = discNumber super().__init__( child=Gtk.Label( label=_("Disc {}").format(self.discNumber), xalign=0, halign=Gtk.Align.START, css_classes=["title-2"] ), css_classes=["p10"], activatable=False ) @Gtk.Template(resource_path='/com/jeffser/Nocturne/album/page.ui') class AlbumPage(Adw.NavigationPage): __gtype_name__ = 'NocturneAlbumPage' clamp_el = Gtk.Template.Child() cover_el = Gtk.Template.Child() name_el = Gtk.Template.Child() artist_el = Gtk.Template.Child() star_el = Gtk.Template.Child() song_list_el = Gtk.Template.Child() rating_container = Gtk.Template.Child() context_wrap_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyAlbum(self.id, True) super().__init__() self.star_el.set_action_target_value(GLib.Variant.new_string(self.id)) context = CONTEXT_ALBUM.copy() del context['show-artist'] if 'no-downloads' in integration.limitations: del context['download'] context_buttons = get_context_buttons_list(context, self.id) for btn in context_buttons: if btn.get_name() != 'show-artist': self.context_wrap_el.append(btn) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'artist', self.update_artist) integration.connect_to_model(self.id, 'artistId', self.update_artist_id) integration.connect_to_model(self.id, 'starred', self.update_starred) integration.connect_to_model(self.id, 'song', self.update_song_list) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model(self.id, 'userRating', self.update_rating) self.song_list_el.list_el.set_sort_func(self.song_list_sort_func) def song_list_sort_func(self, r1, r2): integration = get_current_integration() trackN1 = 0 discN1 = 0 trackN2 = 0 discN2 = 0 if isinstance(r1, DiscIndicator): trackN1 = -1 discN1 = r1.discNumber else: if model1 := integration.loaded_models.get(r1.id): trackN1 = model1.get_property('track') discN1 = model1.get_property('discNumber') if isinstance(r2, DiscIndicator): trackN2 = -1 discN2 = r2.discNumber else: if model2 := integration.loaded_models.get(r2.id): trackN2 = model2.get_property('track') discN2 = model2.get_property('discNumber') if discN1 == discN2: return trackN1 - trackN2 else: return discN1 - discN2 def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(240) self.update_background(paintable.save_to_png_bytes().get_data()) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-queue-symbolic") self.cover_el.set_pixel_size(-1) def update_background(self, raw_bytes:bytes): def run(): img_io = io.BytesIO(raw_bytes) color = ColorThief(img_io).get_color(quality=10) css = f""" clamp {{ transition: background .2s; background: linear-gradient(180deg, color-mix(in srgb, rgb({','.join([str(c) for c in color])}) 50%, transparent), transparent 30%); background-size: 100% 1000px; background-repeat: no-repeat; }} """ provider = Gtk.CssProvider() provider.load_from_data(css.encode()) GLib.idle_add(self.clamp_el.get_style_context().add_provider, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) if raw_bytes: threading.Thread(target=run, daemon=True).start() def update_rating(self, rating:int): for i, el in enumerate(list(self.rating_container)): el.set_icon_name("starred-symbolic" if rating >= i+1 else "non-starred-symbolic") def update_name(self, name:str): self.name_el.set_label(name) self.name_el.set_visible(name) self.set_title(name or _('Album')) self.set_name(name) def update_artist(self, artist:str): self.artist_el.set_label(artist) self.artist_el.set_visible(artist) self.artist_el.set_tooltip_text(artist) def update_artist_id(self, artistId:str): self.artist_el.set_action_target_value(GLib.Variant.new_string(artistId)) def update_starred(self, starred:bool): if starred: self.star_el.add_css_class('accent') self.star_el.remove_css_class('dim-label') self.star_el.set_icon_name('heart-filled-symbolic') self.star_el.set_tooltip_text(_('Favorite')) else: self.star_el.remove_css_class('accent') self.star_el.add_css_class('dim-label') self.star_el.set_icon_name('heart-outline-thick-symbolic') self.star_el.set_tooltip_text(_('Not Favorite')) def update_song_list(self, song_list:list): def run(): integration = get_current_integration() GLib.idle_add(self.song_list_el.list_el.remove_all) GLib.idle_add(self.song_list_el.main_stack.set_visible_child_name, 'content' if len(song_list) > 0 else 'no-content') song_ids = [s.get('id') for s in song_list] discs = [] for song_id in song_ids: if not song_id in integration.loaded_models: integration.verifySong(song_id, use_threading=False) model = integration.loaded_models.get(song_id) discNumber = model.get_property('discNumber') if discNumber > 0 and discNumber not in discs: discs.append(discNumber) GLib.idle_add(self.song_list_el.list_el.append, SongRow(song_id)) for disc in discs: GLib.idle_add(self.song_list_el.list_el.append, DiscIndicator(disc)) GLib.idle_add(self.song_list_el.list_el.invalidate_sort) GLib.idle_add(self.connect_rows) if len(list(self.song_list_el.list_el)) != len(song_list): threading.Thread(target=run, daemon=True).start() def connect_rows(self): song_ids = [] def set_action(row): row.set_action_name(None) row.set_action_target_value(GLib.Variant('a{sv}', { 'songId': GLib.Variant('s', row.id), 'songs': GLib.Variant('as', song_ids), 'originId': GLib.Variant('s', self.id) })) row.set_action_name('app.play_song_from_list') for row in list(self.song_list_el.list_el): if isinstance(row, SongRow): song_ids.append(row.id) for row in list(self.song_list_el.list_el): if isinstance(row, SongRow): GLib.idle_add(set_action, row) @Gtk.Template.Callback() def change_rating(self, button): integration = get_current_integration() target_value = GLib.Variant('a{sv}', { 'model_id': GLib.Variant('s', self.id), 'rating': GLib.Variant('i', int(button.get_name())) }) self.get_root().activate_action("app.set_rating", target_value) def reload(self): pass Jeffser-Nocturne-b442fee/src/widgets/album/row.py000066400000000000000000000047521523240030000221010ustar00rootroot00000000000000# row.py from gi.repository import Gtk, Adw, GLib, Gio, Gdk from ...integrations import get_current_integration from ...constants import CONTEXT_ALBUM from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/album/row.ui') class AlbumRow(Adw.ActionRow): __gtype_name__ = 'NocturneAlbumRow' cover_el = Gtk.Template.Child() menu_button_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyAlbum(self.id) super().__init__() self.set_action_target_value(GLib.Variant.new_string(self.id)) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'artist', self.update_artist) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") settings.bind( "show-context-button", self.menu_button_el, "visible", Gio.SettingsBindFlags.DEFAULT ) def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(48) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-queue-symbolic") self.cover_el.set_pixel_size(-1) def update_name(self, name:str): self.set_title(name) self.set_name(name) def update_artist(self, artist:str): self.set_subtitle(artist or "") @Gtk.Template.Callback() def on_context_button_active(self, button, gparam): context = CONTEXT_ALBUM.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] button.get_popover().set_child(ContextContainer(context, self.id)) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] context = CONTEXT_ALBUM.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] popover = Gtk.Popover( child=ContextContainer(context, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/artist/000077500000000000000000000000001523240030000211165ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/artist/__init__.py000066400000000000000000000001501523240030000232230ustar00rootroot00000000000000# __init__.py from .button import ArtistButton from .page import ArtistPage from .row import ArtistRow Jeffser-Nocturne-b442fee/src/widgets/artist/button.py000066400000000000000000000051011523240030000230000ustar00rootroot00000000000000# button.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_ARTIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/artist/button.ui') class ArtistButton(Gtk.Button): __gtype_name__ = 'NocturneArtistButton' avatar_el = Gtk.Template.Child() name_el = Gtk.Template.Child() album_count_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyArtist(self.id) super().__init__( action_target=GLib.Variant.new_string(self.id) ) self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::button-size", lambda *_: self.update_size()) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'albumCount', self.update_album_count) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) def update_size(self): isBig = self.settings.get_value('button-size').unpack() == 'big' size = 240 if isBig else 180 self.avatar_el.set_size(size) if isBig: self.name_el.remove_css_class('title-4') self.name_el.add_css_class('title-3') else: self.name_el.remove_css_class('title-3') self.name_el.add_css_class('title-4') def update_cover(self, paintable): if paintable: self.avatar_el.set_custom_image(paintable) elif isinstance(self.avatar_el.get_custom_image(), Adw.SpinnerPaintable): self.avatar_el.set_custom_image(None) self.update_size() def update_name(self, name:str): self.avatar_el.set_tooltip_text(name) self.set_tooltip_text(name) self.name_el.set_label(name) self.set_name(name) def update_album_count(self, albumCount:int): self.album_count_el.set_label(ngettext("{} Album", "{} Albums", albumCount).format(albumCount)) self.album_count_el.set_visible(albumCount) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=ContextContainer(CONTEXT_ARTIST, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/artist/meson.build000066400000000000000000000003521523240030000232600ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'artist' widgets = [ '__init__.py', 'button.py', 'page.py', 'row.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/artist/page.py000066400000000000000000000144401523240030000224070ustar00rootroot00000000000000# page.py from gi.repository import Gtk, Adw, GLib, Pango from ...integrations import get_current_integration from ...constants import CONTEXT_ARTIST from ..containers import get_context_buttons_list from ..song import SongSmallRow from ..album import AlbumButton from .button import ArtistButton import threading, io from colorthief import ColorThief @Gtk.Template(resource_path='/com/jeffser/Nocturne/artist/page.ui') class ArtistPage(Adw.NavigationPage): __gtype_name__ = 'NocturneArtistPage' clamp_el = Gtk.Template.Child() avatar_el = Gtk.Template.Child() name_el = Gtk.Template.Child() biography_el = Gtk.Template.Child() star_el = Gtk.Template.Child() top_songs_wrapbox = Gtk.Template.Child() album_wrapbox = Gtk.Template.Child() artist_carousel = Gtk.Template.Child() rating_container = Gtk.Template.Child() context_wrap_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyArtist(self.id, True) super().__init__() self.star_el.set_action_target_value(GLib.Variant.new_string(self.id)) context_buttons = get_context_buttons_list(CONTEXT_ARTIST, self.id) for btn in context_buttons: self.context_wrap_el.append(btn) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'biography', self.update_biography) integration.connect_to_model(self.id, 'starred', self.update_starred) integration.connect_to_model(self.id, 'album', self.update_album_list) integration.connect_to_model(self.id, 'similarArtist', self.update_artist_list) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model(self.id, 'userRating', self.update_rating) self.top_songs_wrapbox.list_el.set_justify(Adw.JustifyMode.FILL) self.top_songs_wrapbox.list_el.set_justify_last_line(True) self.top_songs_wrapbox.list_el.set_child_spacing(5) self.top_songs_wrapbox.list_el.set_line_spacing(5) threading.Thread(target=self.update_top_songs, daemon=True).start() def update_top_songs(self): # call in different thread integration = get_current_integration() top_songs = integration.getTopSongs(self.id) self.top_songs_wrapbox.set_visible(len(top_songs) > 5) if len(top_songs) > 5: song_widgets = [SongSmallRow(song_id, show_album_name=True) for song_id in top_songs] self.top_songs_wrapbox.set_widgets(song_widgets) def update_rating(self, rating:int): for i, el in enumerate(list(self.rating_container)): el.set_icon_name("starred-symbolic" if rating >= i+1 else "non-starred-symbolic") def update_cover(self, paintable): if paintable: self.avatar_el.set_custom_image(paintable) self.update_background(paintable.save_to_png_bytes().get_data()) elif isinstance(self.avatar_el.get_custom_image(), Adw.SpinnerPaintable): self.avatar_el.set_custom_image(None) def update_background(self, raw_bytes:bytes): def run(): img_io = io.BytesIO(raw_bytes) color = ColorThief(img_io).get_color(quality=10) css = f""" clamp {{ transition: background .2s; background: linear-gradient(180deg, color-mix(in srgb, rgb({','.join([str(c) for c in color])}) 50%, transparent), transparent 30%); background-size: 100% 1000px; background-repeat: no-repeat; }} """ provider = Gtk.CssProvider() provider.load_from_data(css.encode()) GLib.idle_add(self.clamp_el.get_style_context().add_provider, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) if raw_bytes: threading.Thread(target=run, daemon=True).start() def update_name(self, name:str): self.avatar_el.set_tooltip_text(name) self.name_el.set_label(name) self.name_el.set_visible(name) self.set_title(name or _('Author')) self.set_name(name) def update_biography(self, biography:str): self.biography_el.set_label(biography.strip()) self.biography_el.get_parent().set_visible(biography.strip()) def update_starred(self, starred:bool): if starred: self.star_el.add_css_class('accent') self.star_el.remove_css_class('dim-label') self.star_el.set_icon_name('heart-filled-symbolic') self.star_el.set_tooltip_text(_('Favorite')) else: self.star_el.remove_css_class('accent') self.star_el.add_css_class('dim-label') self.star_el.set_icon_name('heart-outline-thick-symbolic') self.star_el.set_tooltip_text(_('Not Favorite')) def update_album_list(self, album_list:list): if album_list: albums = [a.get('id') for a in album_list if isinstance(a, dict)] album_buttons = [] for album in albums: button = AlbumButton(album, show_year=True) button.artist_el.set_visible(False) button.set_halign(Gtk.Align.CENTER) button.name_el.remove_css_class('title-3') album_buttons.append(button) self.album_wrapbox.set_widgets(album_buttons) def update_artist_list(self, artist_list:list): artists = [a.get('id') for a in artist_list] self.artist_carousel.set_widgets([ArtistButton(id) for id in artists]) # -- Callbacks -- @Gtk.Template.Callback() def on_biography_clicked(self, button): if button.get_child().get_ellipsize() == Pango.EllipsizeMode.NONE: button.get_child().set_ellipsize(Pango.EllipsizeMode.END) else: button.get_child().set_ellipsize(Pango.EllipsizeMode.NONE) @Gtk.Template.Callback() def change_rating(self, button): integration = get_current_integration() target_value = GLib.Variant('a{sv}', { 'model_id': GLib.Variant('s', self.id), 'rating': GLib.Variant('i', int(button.get_name())) }) self.get_root().activate_action("app.set_rating", target_value) def reload(self): pass Jeffser-Nocturne-b442fee/src/widgets/artist/row.py000066400000000000000000000043501523240030000223010ustar00rootroot00000000000000# row.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_ARTIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/artist/row.ui') class ArtistRow(Adw.ActionRow): __gtype_name__ = 'NocturneArtistRow' avatar_el = Gtk.Template.Child() menu_button_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyArtist(self.id) super().__init__() self.set_action_target_value(GLib.Variant.new_string(self.id)) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model(self.id, 'albumCount', self.update_album_count) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") settings.bind( "show-context-button", self.menu_button_el, "visible", Gio.SettingsBindFlags.DEFAULT ) def update_cover(self, paintable): if paintable: self.avatar_el.set_custom_image(paintable) elif isinstance(self.avatar_el.get_custom_image(), Adw.SpinnerPaintable): self.avatar_el.set_custom_image(None) def update_name(self, name:str): self.avatar_el.set_tooltip_text(name) self.set_title(name) self.set_name(name) def update_album_count(self, albumCount:int): self.set_subtitle(ngettext("{} Album", "{} Albums", albumCount).format(albumCount)) @Gtk.Template.Callback() def on_context_button_active(self, button, gparam): button.get_popover().set_child(ContextContainer(CONTEXT_ARTIST, self.id)) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=ContextContainer(CONTEXT_ARTIST, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/containers/000077500000000000000000000000001523240030000217555ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/containers/__init__.py000066400000000000000000000004161523240030000240670ustar00rootroot00000000000000# __init__.py from .carousel import Carousel from .wrapbox import Wrapbox from .context import ContextContainer, get_context_buttons_list from .page_dialog import PageDialog from .downloads_queue_button import DownloadsQueueButton from .download_row import DownloadRow Jeffser-Nocturne-b442fee/src/widgets/containers/carousel.py000066400000000000000000000065231523240030000241520ustar00rootroot00000000000000# carousel.py from gi.repository import GObject, Gtk, GLib, Gdk, Gio @Gtk.Template(resource_path='/com/jeffser/Nocturne/containers/carousel.ui') class Carousel(Gtk.Box): __gtype_name__ = 'NocturneCarousel' header_button = Gtk.Template.Child() list_el = Gtk.Template.Child() pan_container_el = Gtk.Template.Child() pan_start_el = Gtk.Template.Child() pan_end_el = Gtk.Template.Child() header_label = GObject.Property(type=str, default="") header_icon_name = GObject.Property(type=str, default="") header_page_tag = GObject.Property(type=str, default="") def remove_all(self): for page in list(self.list_el): self.list_el.remove(page) def set_widgets(self, widgets:list): def scroll_to_middle(): if self.list_el.get_n_pages() > 0: middle_index = int((self.list_el.get_n_pages()-1)/2) page = self.list_el.get_nth_page(max(0, middle_index)) if page: self.list_el.scroll_to(page, True) GLib.idle_add(self.set_visible, len(widgets) > 0) if self.list_el.get_n_pages() > 0: GLib.idle_add(self.remove_all) for i, page in enumerate(widgets): GLib.idle_add(self.list_el.append, page) GLib.timeout_add(200, scroll_to_middle) GLib.idle_add(self.pan_container_el.set_visible, len(widgets) >= 2) @Gtk.Template.Callback() def on_scroll(self, controller, dx, dy): position = self.list_el.get_position() if position == int(position): event = controller.get_current_event() state = event.get_modifier_state() if (state & Gdk.ModifierType.SHIFT_MASK) or dx != 0: direction = dy or dx next_position = int(max(0, min(position + direction, self.list_el.get_n_pages()))) next_page = self.list_el.get_nth_page(next_position) if next_page: self.list_el.scroll_to(next_page, True) return Gdk.EVENT_PROPAGATE def pan(self, to_end:bool): if first_page := self.list_el.get_nth_page(0): visible_pages_n = int(self.list_el.get_width() / first_page.get_width()) if to_end: next_position = int(self.list_el.get_position() + visible_pages_n) else: next_position = int(self.list_el.get_position() - visible_pages_n) next_position = max(min(next_position, self.list_el.get_n_pages() - 1), 0) self.list_el.scroll_to(self.list_el.get_nth_page(next_position), True) @Gtk.Template.Callback() def pan_start(self, button): self.pan(False) @Gtk.Template.Callback() def pan_end(self, button): self.pan(True) @Gtk.Template.Callback() def page_changed(self, carousel, index): self.pan_start_el.set_sensitive(index != 0) self.pan_end_el.set_sensitive(index != carousel.get_n_pages() - 1) @Gtk.Template.Callback() def handle_visible_bind(self, carousel, value) -> bool: return bool(value) @Gtk.Template.Callback() def handle_action_name_bind(self, carousel, value) -> str: return str("app.replace_root_page") if value else str("") @Gtk.Template.Callback() def handle_action_target_bind(self, carousel, value) -> GLib.Variant: return GLib.Variant.new_string(value or "") Jeffser-Nocturne-b442fee/src/widgets/containers/context.py000066400000000000000000000101521523240030000240120ustar00rootroot00000000000000# context.py from gi.repository import Gtk, Adw, GLib, Gio class ContextButton(Gtk.Button): __gtype_name__ = 'NocturneContextButton' def __init__(self, name:str, icon_name:str, css:list, connect_style:bool): super().__init__( css_classes=css, child=Adw.ButtonContent( label=name, icon_name=icon_name, halign=Gtk.Align.FILL if connect_style else Gtk.Align.START ), tooltip_text=name ) if connect_style: self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect('changed::show-context-button-label', self.label_visibility_toggled) self.label_visibility_toggled(self.settings, 'show-context-button-label') def label_visibility_toggled(self, settings, key): labelVisible = settings.get_value(key).unpack() label = list(list(self.get_child())[0])[1] label.set_visible(labelVisible) if labelVisible: self.remove_css_class('circular') self.remove_css_class('flat') else: self.add_css_class('circular') self.add_css_class('flat') def get_context_buttons_list(options:dict, model_id:str, cb_handler:callable=None, connect_style:bool=True) -> list: if cb_handler is None: cb_handler = lambda btn, callback: callback() if callback else None buttons = [] for data in options.values(): if data.get('special') == 'song-rating': buttons.append(Gtk.Separator( margin_top=5, margin_bottom=5 )) container = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, css_classes=["linked"] ) for i in range(1,6): btn = Gtk.Button( tooltip_text=_("{} Stars").format(i) if i != 1 else _("1 Star"), icon_name="starred-symbolic" if data.get('value', 0) >= i else "non-starred-symbolic", css_classes=["flat"] ) target_value = GLib.Variant('a{sv}', { 'model_id': GLib.Variant('s', model_id), 'rating': GLib.Variant('i', i) }) btn.set_action_name('app.set_rating') btn.set_action_target_value(target_value) btn.connect('clicked', cb_handler, data.get('connection')) container.append(btn) buttons.append(container) else: btn = ContextButton(data.get('name', ""), data.get('icon-name', ""), data.get('css', []), connect_style) if data.get('sensitive', True): btn.connect('clicked', cb_handler, data.get('connection')) if data.get('action-name') and data.get('sensitive', True): btn.set_action_name(data.get('action-name')) if data.get('action-target'): btn.set_action_target_value(GLib.Variant.new_string(data.get('action-target'))) elif model_id: btn.set_action_target_value(GLib.Variant.new_string(model_id)) btn.set_sensitive(data.get('sensitive', True)) buttons.append(btn) return buttons class ContextContainer(Gtk.Box): __gtype_name__ = 'NocturneContextContainer' def __init__(self, options:dict, model_id:str=None): #options: #name : { # icon-name:str # css:list # connection:callable # action-name:str # action-target:str # sensitive:bool #} super().__init__( orientation=Gtk.Orientation.VERTICAL ) buttons = get_context_buttons_list(options, model_id, cb_handler=self.callback_handler, connect_style=False) for btn in buttons: btn.add_css_class('flat') btn.set_tooltip_text('') self.append(btn) def callback_handler(self, button, callback): popover = button.get_ancestor(Gtk.Popover) if popover: popover.popdown() if callback: callback() Jeffser-Nocturne-b442fee/src/widgets/containers/download_row.py000066400000000000000000000033771523240030000250370ustar00rootroot00000000000000# download_row.py from gi.repository import Gtk, GObject from ...integrations import get_current_integration @Gtk.Template(resource_path='/com/jeffser/Nocturne/containers/download_row.ui') class DownloadRow(Gtk.ListBoxRow): __gtype_name__ = 'NocturneDownloadRow' title_label = Gtk.Template.Child() done_label = Gtk.Template.Child() progressbar = Gtk.Template.Child() done_button = Gtk.Template.Child() def __init__(self, model): self.model = model integration = get_current_integration() integration.verifySong(self.model.get_property('songId')) super().__init__() title = integration.loaded_models.get(self.model.get_property('songId')).get_property('title') self.title_label.set_label(title) self.set_tooltip_text(title) self.model.bind_property( "progress", self.progressbar, "fraction", GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE ) @Gtk.Template.Callback() def progressbar_frac_changed(self, progressbar, ud): if progressbar.get_fraction() == 1: self.done_button.set_visible(True) self.done_label.set_visible(True) progressbar.set_visible(False) @Gtk.Template.Callback() def remove_from_queue(self, button): integration = get_current_integration() download_queue = integration.loaded_models.get('currentSong').get_property('downloadQueueModel') found, position = download_queue.find_with_equal_func( self.model, lambda item_a, item_b, ud: item_a.get_property('songId') == item_b.get_property('songId'), 0 ) if found and position >= 0: download_queue.remove(position) Jeffser-Nocturne-b442fee/src/widgets/containers/downloads_queue_button.py000066400000000000000000000025711523240030000271250ustar00rootroot00000000000000# downloads_queue_button.py from gi.repository import Gtk, GLib from ...integrations import get_current_integration from .download_row import DownloadRow @Gtk.Template(resource_path='/com/jeffser/Nocturne/containers/downloads_queue_button.ui') class DownloadsQueueButton(Gtk.MenuButton): __gtype_name__ = 'NocturneDownloadsQueueButton' download_list_el = Gtk.Template.Child() def setup(self): integration = get_current_integration() model = integration.loaded_models.get('currentSong').get_property('downloadQueueModel') self.download_list_el.get_parent().set_overflow(Gtk.Overflow.VISIBLE) self.download_list_el.bind_model( model, lambda model: DownloadRow(model) ) model.connect('notify::n-items', lambda model, ud: self.downloadQueueModelChanged(model)) self.downloadQueueModelChanged(model) def downloadQueueModelChanged(self, model): self.set_visible(len(model) > 0) if len(model) > 0: self.add_css_class('accent') @Gtk.Template.Callback() def clear_done_downloads(self, button): for row in list(self.download_list_el): if row.done_button.get_visible(): GLib.idle_add(row.remove_from_queue, row.done_button) @Gtk.Template.Callback() def button_activated(self, button, ud): self.remove_css_class('accent') Jeffser-Nocturne-b442fee/src/widgets/containers/meson.build000066400000000000000000000004771523240030000241270ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'containers' widgets = [ '__init__.py', 'carousel.py', 'wrapbox.py', 'context.py', 'page_dialog.py', 'downloads_queue_button.py', 'download_row.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/containers/page_dialog.py000066400000000000000000000005741523240030000245700ustar00rootroot00000000000000# page_dialog.py from gi.repository import Gtk, Adw @Gtk.Template(resource_path='/com/jeffser/Nocturne/containers/page_dialog.ui') class PageDialog(Adw.Dialog): __gtype_name__ = 'NocturnePageDialog' navigation_view = Gtk.Template.Child() def __init__(self, page:Adw.NavigationPage): super().__init__() self.navigation_view.replace([page]) Jeffser-Nocturne-b442fee/src/widgets/containers/wrapbox.py000066400000000000000000000023501523240030000240110ustar00rootroot00000000000000# wrapbox.py from gi.repository import GObject, Gtk, GLib @Gtk.Template(resource_path='/com/jeffser/Nocturne/containers/wrapbox.ui') class Wrapbox(Gtk.Box): __gtype_name__ = 'NocturneWrapbox' header_button = Gtk.Template.Child() list_el = Gtk.Template.Child() header_label = GObject.Property(type=str, default="") header_icon_name = GObject.Property(type=str, default="") header_page_tag = GObject.Property(type=str, default="") def remove_all(self): for child in list(self.list_el): self.list_el.remove(child) def set_widgets(self, widgets:list): if len(list(self.list_el)): GLib.idle_add(self.remove_all) GLib.idle_add(self.set_visible, len(widgets) > 0) for page in widgets: GLib.idle_add(self.list_el.append, page) @Gtk.Template.Callback() def handle_visible_bind(self, carousel, value) -> bool: return bool(value) @Gtk.Template.Callback() def handle_action_name_bind(self, carousel, value) -> str: return str("app.replace_root_page") if value else str("") @Gtk.Template.Callback() def handle_action_target_bind(self, carousel, value) -> GLib.Variant: return GLib.Variant.new_string(value or "") Jeffser-Nocturne-b442fee/src/widgets/lyrics/000077500000000000000000000000001523240030000211155ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/lyrics/__init__.py000066400000000000000000000001211523240030000232200ustar00rootroot00000000000000# __init__.py from .helpers import prepare_lrc from .dialog import LyricsDialog Jeffser-Nocturne-b442fee/src/widgets/lyrics/dialog.py000066400000000000000000000200611523240030000227250ustar00rootroot00000000000000# dialog.py from gi.repository import Gtk, Adw, GLib, Gio, Gst from ...integrations import get_current_integration from ...constants import get_display_time from .helpers import prepare_lrc import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/lyrics/edit_row.ui') class LyricEditRow(Adw.EntryRow): __gtype_name__ = 'NocturneLyricEditRow' ts_button = Gtk.Template.Child() def __init__(self, ms, content, invalid_ms:bool=False): self.invalid_ms = invalid_ms super().__init__( text=content ) if self.invalid_ms: self.add_css_class('error') self.ms = ms self.show_timestamp() @Gtk.Template.Callback() def go_to_timestamp(self, button): integration = get_current_integration() GLib.idle_add(button.get_ancestor(Gtk.Popover).popdown) nanoseconds = int(self.ms/1000 * Gst.SECOND) self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) @Gtk.Template.Callback() def set_current_timestamp(self, button=None): integration = get_current_integration() ps = integration.loaded_models.get('currentSong').get_property('positionSeconds') self.ms = int(ps * 1000) self.invalid_ms = False if button: GLib.idle_add(button.get_ancestor(Gtk.Popover).popdown) GLib.idle_add(self.show_timestamp) GLib.idle_add(self.get_ancestor(Gtk.ListBox).invalidate_sort) @Gtk.Template.Callback() def remove(self, button): GLib.idle_add(button.get_ancestor(Gtk.Popover).popdown) GLib.idle_add(self.get_ancestor(Gtk.ListBox).remove, self) GLib.idle_add(self.get_ancestor(Adw.Dialog).update_visibility) def show_timestamp(self): self.set_title(_("No Timestamp") if self.invalid_ms else get_display_time(self.ms / 1000, True)) if not self.invalid_ms: self.remove_css_class('error') @Gtk.Template(resource_path='/com/jeffser/Nocturne/lyrics/dialog.ui') class LyricsDialog(Adw.Dialog): __gtype_name__ = 'NocturneLyricsDialog' main_stack = Gtk.Template.Child() lrc_list_el = Gtk.Template.Child() progress_el = Gtk.Template.Child() state_stack_el = Gtk.Template.Child() position_spin = Gtk.Template.Child() is_seeking = False focused_row = None def __init__(self): integration = get_current_integration() self.id = integration.loaded_models.get('currentSong').get_property('songId') super().__init__() self.lrc_list_el.set_sort_func(lambda r1, r2: r1.ms - r2.ms) integration.connect_to_model('currentSong', 'positionSeconds', self.position_changed) integration.connect_to_model('currentSong', 'buttonState', self.state_stack_el.set_visible_child_name) integration.connect_to_model(self.id, 'title', self.set_title) integration.connect_to_model(self.id, 'duration', self.update_duration) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.playback_mode_backup = settings.get_value('playback-mode').unpack() settings.set_string('playback-mode', 'repeat-one') threading.Thread(target=self.retrieve_lyrics, daemon=True).start() def update_duration(self, duration): self.progress_el.get_adjustment().set_upper(duration) self.position_spin.get_adjustment().set_upper(duration) def retrieve_lyrics(self): GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') GLib.idle_add(self.lrc_list_el.remove_all) integration = get_current_integration() lyrics_type, raw_content = integration.getLyrics(self.id, True) if lyrics_type == 'lrc': content = prepare_lrc(raw_content) for line in content: row = LyricEditRow( ms=line.get('ms'), content=line.get('content', '') ) GLib.idle_add(self.lrc_list_el.append, row) elif lyrics_type == 'plain': for index, line in enumerate(content.split('\n')): row = LyricEditRow( ms=index*10000000, content=line, invalid_ms=True ) GLib.idle_add(self.lrc_list_el.append, row) GLib.idle_add(self.lrc_list_el.invalidate_sort) GLib.idle_add(self.update_visibility) def position_changed(self, position_seconds:float): self.progress_el.get_adjustment().set_value(position_seconds) if self.get_focus() not in list(self.position_spin): self.position_spin.set_value(position_seconds) if len(list(self.lrc_list_el)) > 0: ms = int(position_seconds * 1000)+100 best_match = list(self.lrc_list_el)[0] for row in list(self.lrc_list_el): if row.ms <= ms: best_match = row else: break if self.focused_row != best_match: if self.focused_row: self.focused_row.ts_button.remove_css_class('suggested-action') self.focused_row = best_match self.focused_row.ts_button.add_css_class('suggested-action') def update_visibility(self): self.main_stack.set_visible_child_name('content' if len(list(self.lrc_list_el)) > 0 else 'empty') @Gtk.Template.Callback() def seek_start(self, gesture, n_press, x, y): self.is_seeking = True @Gtk.Template.Callback() def seek_end(self, gesture): self.is_seeking = False @Gtk.Template.Callback() def progress_bar_changed(self, adjustment): if self.is_seeking: nanoseconds = int(adjustment.get_value() * Gst.SECOND) self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) @Gtk.Template.Callback() def add_line(self, button): integration = get_current_integration() ps = integration.loaded_models.get('currentSong').get_property('positionSeconds') row = LyricEditRow( ms=int(ps * 1000), content="" ) self.lrc_list_el.append(row) GLib.idle_add(self.lrc_list_el.invalidate_sort) GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def position_spin_changed(self, spinbutton): if spinbutton.get_sensitive() and self.get_root(): nanoseconds = int(spinbutton.get_value() * Gst.SECOND) self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) @Gtk.Template.Callback() def state_stack_changed(self, stack, ud): self.position_spin.set_sensitive(stack.get_visible_child_name() == "play") @Gtk.Template.Callback() def cancel_clicked(self, button): Gio.Settings(schema_id="com.jeffser.Nocturne").set_string('playback-mode', self.playback_mode_backup) self.close() @Gtk.Template.Callback() def save_clicked(self, button): lines = [] for row in list(self.lrc_list_el): # ms, content lines.append((row.get_title(), row.get_text())) content = '\n'.join(['[{}] {}'.format(ms, content) for ms, content in lines]) integration = get_current_integration() integration.saveLyrics(self.id, content, 'lrc') window.lyrics_page.song_changed(self.id) __show_custom_toast(window, self.id, "title", _("Lyrics Saved")) Gio.Settings(schema_id="com.jeffser.Nocturne").set_string('playback-mode', self.playback_mode_backup) self.close() @Gtk.Template.Callback() def set_next_timestamp(self, button): next_index = list(self.lrc_list_el).index(self.focused_row) + 1 if next_index < len(list(self.lrc_list_el)): GLib.idle_add(list(self.lrc_list_el)[next_index].set_current_timestamp) Jeffser-Nocturne-b442fee/src/widgets/lyrics/helpers.py000066400000000000000000000017061523240030000231350ustar00rootroot00000000000000# helpers.py from ...integrations import get_current_integration def prepare_lrc(lrc_str:str) -> list: lrc_lines = [] for line in lrc_str.split('\n'): if line.startswith('['): timestamp, content = line[1:].split(']')[:2] minutes_str, rest = timestamp.split(':')[:2] diveded_second = rest.split('.') if len(diveded_second) == 1: seconds_str = diveded_second[0] ms_str = "0" else: seconds_str, ms_str = diveded_second[:2] try: minutes = int(minutes_str) seconds = int(seconds_str) ms = int(ms_str) if len(ms_str) == 2: ms *= 10 timing = (minutes * 60000) + (seconds * 1000) + ms lrc_lines.append({'ms': timing, 'content': content.strip()}) except ValueError: pass return lrc_lines Jeffser-Nocturne-b442fee/src/widgets/lyrics/meson.build000066400000000000000000000003411523240030000232550ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'lyrics' widgets = [ '__init__.py', 'dialog.py', 'helpers.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/meson.build000066400000000000000000000005031523240030000217500ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' widgets = [ '__init__.py', ] install_data(widgets, install_dir: moduledir) subdir('playing') subdir('pages') subdir('album') subdir('artist') subdir('playlist') subdir('song') subdir('containers') subdir('lyrics')Jeffser-Nocturne-b442fee/src/widgets/pages/000077500000000000000000000000001523240030000207075ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/pages/__init__.py000066400000000000000000000006311523240030000230200ustar00rootroot00000000000000# __init__.py from .home import HomePage from .login import LoginDialog from .artists import ArtistsPage from .playlists import PlaylistsPage from .starred import StarredPage from .songs_all import SongsAllPage from .albums import AlbumsPage from .albums_all import AlbumsAllPage from .radios import RadiosPage from .welcome import WelcomePage from .setup import SetupPage from .playback import PlaybackPage Jeffser-Nocturne-b442fee/src/widgets/pages/albums.py000066400000000000000000000061341523240030000225500ustar00rootroot00000000000000# albums.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..album import AlbumButton import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/albums.ui') class AlbumsPage(Adw.NavigationPage): __gtype_name__ = 'NocturneAlbumsPage' list_el = Gtk.Template.Child() main_stack = Gtk.Template.Child() end_stack = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() offset = 0 loading = False skipped_albums = 0 def __init__(self): super().__init__() self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.load_albums, daemon=True).start() def reload(self): GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') self.offset = 0 self.loading = False GLib.idle_add(self.reset) GLib.idle_add(self.end_stack.set_visible_child_name, 'loading') threading.Thread(target=self.load_albums, daemon=True).start() def reset(self): self.list_el.remove_all() def load_albums(self): if self.loading: return self.loading = True integration = get_current_integration() hide_singles = Gio.Settings(schema_id="com.jeffser.Nocturne").get_value('hide-singles').unpack() self.skipped_albums = 0 albums = integration.getAlbumList( list_type=self.get_tag().split('-')[1], size=20, offset=self.offset ) for album_id in albums: should_skip = False if hide_singles: if model := integration.loaded_models.get(album_id): if model.get_property('songCount') <= 1: self.skipped_albums += 1 should_skip = True results = [button for button in list(self.list_el.list_el) if getattr(button, 'id', None) == album_id] if len(results) > 0: GLib.idle_add(results[0].set_visible, not should_skip) elif not should_skip: button = AlbumButton(album_id) GLib.idle_add(self.list_el.list_el.append, button) GLib.idle_add(self.end_stack.set_visible_child_name, 'end' if len(albums) - self.skipped_albums < 20 else 'loading') self.offset += 20 self.loading = False GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.end_stack.get_visible_child_name() == 'loading': threading.Thread(target=self.load_albums, daemon=True).start() def update_visibility(self): for row in list(self.list_el.list_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/albums_all.py000066400000000000000000000103711523240030000233760ustar00rootroot00000000000000# albums_all.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..album import AlbumRow, AlbumButton import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/albums_all.ui') class AlbumsAllPage(Adw.NavigationPage): __gtype_name__ = 'NocturneAlbumsAllPage' toggle_group_el = Gtk.Template.Child() search_entry = Gtk.Template.Child() main_stack = Gtk.Template.Child() list_el = Gtk.Template.Child() wrapbox_el = Gtk.Template.Child() end_stack = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() offset = 0 searching = False skipped_albums = 0 def __init__(self): super().__init__() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.bind( "default-view-mode-albums", self.toggle_group_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.search, daemon=True).start() def reload(self): GLib.idle_add(self.on_search, self.search_entry) def reset(self): self.list_el.remove_all() for el in list(self.wrapbox_el): self.wrapbox_el.remove(el) def search(self): if self.searching: return self.searching = True integration = get_current_integration() hide_singles = self.settings.get_value('hide-singles').unpack() self.skipped_albums = 0 query = None while query != self.search_entry.get_text(): # In case query changes whilst calling integration query = self.search_entry.get_text() search_results = integration.search(query=query, albumCount=30, albumOffset=self.offset) for album_id in search_results.get('album'): should_skip = False if hide_singles: if model := integration.loaded_models.get(album_id): if model.get_property('songCount') <= 1: self.skipped_albums += 1 should_skip = True results_list = [row for row in list(self.list_el) if row.id == album_id] if len(results_list) > 0: GLib.idle_add(results_list[0].set_visible, not should_skip) elif not should_skip: row = AlbumRow(album_id) GLib.idle_add(self.list_el.append, row) results_wrapbox = [button for button in list(self.wrapbox_el) if button.id == album_id] if len(results_wrapbox) > 0: GLib.idle_add(results_wrapbox[0].set_visible, not should_skip) elif not should_skip: button = AlbumButton(album_id) GLib.idle_add(self.wrapbox_el.append, button) GLib.idle_add(self.end_stack.set_visible_child_name, 'end' if len(search_results.get('album')) - self.skipped_albums < 30 else 'loading') self.offset += 30 self.searching = False if len(search_results.get('album')) >= 30 and self.skipped_albums: self.search() else: GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def on_search(self, search_entry): self.offset = 0 self.searching = False print('on_search') for widget in list(self.list_el) + list(self.wrapbox_el): widget.set_visible(False) self.end_stack.set_visible_child_name('loading') threading.Thread(target=self.search, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.end_stack.get_visible_child_name() == 'loading': threading.Thread(target=self.search, daemon=True).start() def update_visibility(self): for row in list(self.list_el) + list(self.wrapbox_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/artists.py000066400000000000000000000071271523240030000227610ustar00rootroot00000000000000# artist.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..artist import ArtistRow, ArtistButton import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/artists.ui') class ArtistsPage(Adw.NavigationPage): __gtype_name__ = 'NocturneArtistsPage' toggle_group_el = Gtk.Template.Child() search_entry = Gtk.Template.Child() main_stack = Gtk.Template.Child() list_el = Gtk.Template.Child() wrapbox_el = Gtk.Template.Child() end_stack = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() offset = 0 searching = False def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "default-view-mode-artists", self.toggle_group_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.search, daemon=True).start() def reload(self): if len(list(self.list_el)) + len(list(self.wrapbox_el)) == 0: GLib.idle_add(self.on_search, self.search_entry) def reset(self): self.list_el.remove_all() for el in list(self.wrapbox_el): self.wrapbox_el.remove(el) def search(self): if self.searching: return self.searching = True integration = get_current_integration() query = None while query != self.search_entry.get_text(): # In case query changes whilst calling integration query = self.search_entry.get_text() search_results = integration.search(query=query, artistCount=30, artistOffset=self.offset) for artist_id in search_results.get('artist'): results_list = [row for row in list(self.list_el) if row.id == artist_id] if len(results_list) > 0: GLib.idle_add(results_list[0].set_visible, True) else: row = ArtistRow(artist_id) GLib.idle_add(self.list_el.append, row) results_wrapbox = [button for button in list(self.wrapbox_el) if button.id == artist_id] if len(results_wrapbox) > 0: GLib.idle_add(results_wrapbox[0].set_visible, True) else: button = ArtistButton(artist_id) GLib.idle_add(self.wrapbox_el.append, button) GLib.idle_add(self.end_stack.set_visible_child_name, 'end' if len(search_results.get('artist')) < 30 else 'loading') self.offset += 30 self.searching = False GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def on_search(self, search_entry): self.offset = 0 for row in list(self.list_el) + list(self.wrapbox_el): row.set_visible(False) threading.Thread(target=self.search, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.end_stack.get_visible_child_name() == 'loading': threading.Thread(target=self.search, daemon=True).start() def update_visibility(self): for row in list(self.list_el) + list(self.wrapbox_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/home.py000066400000000000000000000216531523240030000222200ustar00rootroot00000000000000# home.py from gi.repository import Gtk, Adw, GLib, Gio, Gdk from ...integrations import get_current_integration from ...constants import DATA_DIR from ..album import AlbumButton from ..artist import ArtistButton from ..playlist import PlaylistButton from ..song import SongSmallRow import threading, os, io from colorthief import ColorThief @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/home.ui') class HomePage(Adw.NavigationPage): __gtype_name__ = 'NocturneHomePage' header_bar = Gtk.Template.Child() search_toggle = Gtk.Template.Child() search_bar = Gtk.Template.Child() search_entry = Gtk.Template.Child() main_stack = Gtk.Template.Child() main_clamp = Gtk.Template.Child() main_container = Gtk.Template.Child() frequent_album_carousel = Gtk.Template.Child() welcome_container = Gtk.Template.Child() welcome_avatar = Gtk.Template.Child() welcome_username_label = Gtk.Template.Child() song_wrapbox = Gtk.Template.Child() album_carousel = Gtk.Template.Child() artist_carousel = Gtk.Template.Child() playlist_carousel = Gtk.Template.Child() def __init__(self): super().__init__() self.css_provider = Gtk.CssProvider() self.main_clamp.get_style_context().add_provider( self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.max_frequent_albums = self.settings.get_value('n-frequent-albums-home').unpack() self.max_songs = self.settings.get_value('n-songs-home').unpack() self.max_albums = self.settings.get_value('n-albums-home').unpack() self.max_artists = self.settings.get_value('n-artists-home').unpack() self.max_playlists = self.settings.get_value('n-playlists-home').unpack() self.searching = False list(self.search_bar)[0].set_transition_type(Gtk.RevealerTransitionType.CROSSFADE) self.song_wrapbox.list_el.set_margin_start(10) self.song_wrapbox.list_el.set_margin_end(10) self.song_wrapbox.list_el.set_justify(Adw.JustifyMode.FILL) self.song_wrapbox.list_el.set_justify_last_line(True) self.song_wrapbox.list_el.set_child_spacing(5) self.song_wrapbox.list_el.set_line_spacing(5) def get_default_results(self) -> dict: if integration := get_current_integration(): frequent_albums = integration.getAlbumList(list_type="frequent", size=self.max_frequent_albums) if self.max_frequent_albums > 0 else [] songs = integration.getRandomSongs(size=self.max_songs) if self.max_songs > 0 else [] albums = integration.getAlbumList(list_type="random", size=self.max_albums) if self.max_albums > 0 else [] artists = integration.getArtists(size=self.max_artists) if self.max_artists > 0 else [] playlists = integration.getPlaylists()[:self.max_playlists] return { 'frequent-album': frequent_albums, 'song': songs, 'album': albums, 'artist': artists, 'playlist': playlists } return {} def update_pfp_gradient(self, paintable): raw_bytes = paintable.save_to_png_bytes().get_data() if not raw_bytes: self.css_provider.load_from_data('clamp {background: none;}'.encode()) img_io = io.BytesIO(raw_bytes) color = ColorThief(img_io).get_color(quality=10) css = f""" clamp {{ transition: background .2s; background: linear-gradient(180deg, color-mix(in srgb, rgb({','.join([str(c) for c in color])}) 25%, transparent), transparent 30%); background-size: 100% 1000px; background-repeat: no-repeat; }} """ self.css_provider.load_from_data(css.encode()) def update_welcome_visibility(self): welcome_mode = self.settings.get_value("welcome-mode-home").unpack() welcome_username = self.settings.get_value("welcome-user-home").unpack() self.welcome_avatar.set_custom_image(None) self.css_provider.load_from_data('clamp {background: none;}'.encode()) if welcome_mode and welcome_username: self.welcome_container.set_visible(True) try: pfp_destination_path = os.path.join(DATA_DIR, 'pfp') if os.path.isfile(pfp_destination_path): if paintable := Gdk.Texture.new_from_filename(pfp_destination_path): self.welcome_avatar.set_custom_image(paintable) threading.Thread(target=self.update_pfp_gradient, args=(paintable,), daemon=True).start() except: self.welcome_avatar.set_custom_image(None) self.welcome_username_label.set_label(welcome_username) else: self.welcome_container.set_visible(False) def reload(self): self.max_frequent_albums = self.settings.get_value('n-frequent-albums-home').unpack() self.max_songs = self.settings.get_value('n-songs-home').unpack() self.max_albums = self.settings.get_value('n-albums-home').unpack() self.max_artists = self.settings.get_value('n-artists-home').unpack() self.max_playlists = self.settings.get_value('n-playlists-home').unpack() threading.Thread(target=self.search, daemon=True).start() GLib.idle_add(self.search_mode_toggled, self.search_toggle) GLib.idle_add(self.update_welcome_visibility) def reset(self): threading.Thread(target=self.frequent_album_carousel.set_widgets, args=([],), daemon=True).start() threading.Thread(target=self.song_wrapbox.set_widgets, args=([],), daemon=True).start() threading.Thread(target=self.album_carousel.set_widgets, args=([],), daemon=True).start() threading.Thread(target=self.artist_carousel.set_widgets, args=([],), daemon=True).start() threading.Thread(target=self.playlist_carousel.set_widgets, args=([],), daemon=True).start() def search(self): if self.searching: return self.searching = True GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') if integration := get_current_integration(): if self.search_entry.get_text(): query = None while query != self.search_entry.get_text(): # In case query changes whilst calling integration query = self.search_entry.get_text() search_results = integration.search( query=query, songCount=self.max_songs, artistCount=self.max_artists, albumCount=self.max_albums, playlistCount=self.max_playlists ) if self.settings.get_value('hide-singles').unpack(): if album_results := search_results.get('album'): for albumId in album_results.copy(): if model := integration.loaded_models.get(albumId): if model.get_property('songCount') <= 1: search_results['album'].remove(albumId) else: search_results = self.get_default_results() threading.Thread( target=self.frequent_album_carousel.set_widgets, args=([AlbumButton(id) for id in search_results.get('frequent-album') or []],), daemon=True ).start() threading.Thread( target=self.song_wrapbox.set_widgets, args=([SongSmallRow(id) for id in search_results.get('song') or []],), daemon=True ).start() threading.Thread( target=self.album_carousel.set_widgets, args=([AlbumButton(id) for id in search_results.get('album') or []],), daemon=True ).start() threading.Thread( target=self.artist_carousel.set_widgets, args=([ArtistButton(id) for id in search_results.get('artist') or []],), daemon=True ).start() threading.Thread( target=self.playlist_carousel.set_widgets, args=([PlaylistButton(id) for id in search_results.get('playlist') or []],), daemon=True ).start() has_results = any([len(search_results.get(key) or []) > 0 for key in list(search_results)]) else: has_results = False GLib.idle_add(self.main_stack.set_visible_child_name, 'content' if has_results else 'no-content') self.searching = False @Gtk.Template.Callback() def search_mode_toggled(self, button): self.main_container.set_margin_top(0 if button.get_active() else self.header_bar.get_height() or 46) @Gtk.Template.Callback() def on_search(self, entry): threading.Thread(target=self.search, daemon=True).start() Jeffser-Nocturne-b442fee/src/widgets/pages/login.py000066400000000000000000000203351523240030000223740ustar00rootroot00000000000000# login.py from gi.repository import Gtk, Adw, Gio, GLib from ...integrations import secret from ..containers import ContextContainer import threading, time @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/login.ui') class LoginDialog(Adw.Dialog): __gtype_name__ = 'NocturneLoginDialog' toast_overlay = Gtk.Template.Child() link_button_el = Gtk.Template.Child() server_status_el = Gtk.Template.Child() extra_menu_el = Gtk.Template.Child() status_page = Gtk.Template.Child() url_el = Gtk.Template.Child() url_options_el = Gtk.Template.Child() trust_server_el = Gtk.Template.Child() directory_el = Gtk.Template.Child() user_el = Gtk.Template.Child() password_el = Gtk.Template.Child() login_button_el = Gtk.Template.Child() quick_connect_button_el = Gtk.Template.Child() def __init__(self, integration): super().__init__() self.integration = integration if not self.integration.get_property('libraryDir'): settings = Gio.Settings(schema_id="com.jeffser.Nocturne") if selected_local_folder := settings.get_value("integration-library-dir").unpack(): self.integration.set_property('libraryDir', selected_local_folder) # Metadata metadata = self.integration.login_page_metadata self.status_page.set_icon_name(metadata.get('icon-name')) self.status_page.set_title(metadata.get('title') or _("Login")) self.status_page.set_description(metadata.get('description') or '') # Additional Link additional_link_metadata = metadata.get('additional-link', {}) self.link_button_el.get_parent().set_visible(additional_link_metadata.get('url')) self.link_button_el.set_label(additional_link_metadata.get('label') or additional_link_metadata.get('url') or '') self.link_button_el.set_uri(additional_link_metadata.get('url') or '') # Server Status if 'status' in metadata.get('entries', []): self.server_status_el.set_visible(True) self.integration.connect( 'notify::serverRunning', lambda *p, integ=self.integration: self.server_status_el.set_subtitle(_("Running") if integ.get_property('serverRunning') else _("Not Running")) ) else: self.server_status_el.set_visible(False) # Url self.url_el.set_visible('url' in metadata.get('entries', [])) self.url_el.set_text(self.integration.get_property('url')) # Url Extra Options self.url_options_el.set_visible('trust-server' in metadata.get('entries', [])) # Change line if more options are added self.trust_server_el.set_visible('trust-server' in metadata.get('entries', [])) # User self.user_el.set_visible('user' in metadata.get('entries', [])) self.user_el.set_text(self.integration.get_property('user')) # Password self.password_el.set_visible('password' in metadata.get('entries', [])) self.password_el.set_text('') # Directory self.directory_el.set_visible('library-dir' in metadata.get('entries', [])) self.directory_el.set_subtitle(self.integration.get_property('libraryDir')) # Login Button self.login_button_el.set_label(metadata.get('login-label') or _("Login")) # Quick Connect (Jellyfin) self.quick_connect_button_el.set_visible(self.integration.__gtype_name__ == 'NocturneIntegrationJellyfin') # Extra Menu self.extra_menu_el.set_visible('extra-menu' in metadata) self.extra_menu_el.set_tooltip_text(metadata.get('extra-menu', {}).get('title', _("Extra Menu"))) self.extra_menu_el.get_popover().set_child(ContextContainer(metadata.get('extra-menu', {}).get('context', {}), '')) GLib.idle_add(self.update_login_sensitivity) @Gtk.Template.Callback() def update_login_sensitivity(self, *args): sensitive = True entries = self.integration.login_page_metadata.get('entries', []) if 'status' in entries: sensitive = sensitive and self.integration.get_property('serverRunning') if 'url' in entries: sensitive = sensitive and self.url_el.get_text() if 'user' in entries: sensitive = sensitive and self.user_el.get_text() if 'password' in entries: sensitive = sensitive and self.password_el.get_text() if 'library-dir' in entries: sensitive = sensitive and self.directory_el.get_subtitle() self.login_button_el.set_sensitive(sensitive) @Gtk.Template.Callback() def library_changed(self, row, gparam): if row.get_visible() and 'library-dir' in self.integration.login_page_metadata.get('entries'): self.integration.set_property('libraryDir', row.get_subtitle()) self.integration.terminate_instance() threading.Thread(target=self.integration.start_instance, daemon=True).start() @Gtk.Template.Callback() def open_local_directory(self, row): def response(dialog, result): if folder := dialog.select_folder_finish(result): row.set_subtitle(folder.get_path()) current_path = row.get_subtitle() dialog = Gtk.FileDialog( title=_("Music Library"), initial_folder=Gio.File.new_for_path(current_path) if current_path else None ) dialog.select_folder(self.get_root(), None, response) @Gtk.Template.Callback() def server_restart_requested(self, row): row.set_sensitive(False) self.integration.terminate_instance() if self.integration.start_instance(): row.set_subtitle(_("Restarted")) GLib.timeout_add(1000, row.set_subtitle, _("Running")) else: row.set_subtitle(_("Error")) row.set_sensitive(True) @Gtk.Template.Callback() def login_button_clicked(self, button=None): self.login_button_el.set_sensitive(False) url = self.url_el.get_text().strip() if not url.startswith('http'): url = 'http://' + url self.integration.set_property('url', url) self.integration.set_property('trustServer', self.trust_server_el.get_active()) self.integration.set_property('user', self.user_el.get_text().strip()) secret.store_password(self.password_el.get_text().strip()) self.integration.set_property('libraryDir', self.directory_el.get_subtitle()) threading.Thread(target=self.get_root().get_application().try_login, args=(self.integration,), daemon=True).start() @Gtk.Template.Callback() def quick_connect_button_clicked(self, button): if self.integration.__gtype_name__ != 'NocturneIntegrationJellyfin': return def wait_confirmation(data, dialog): waited_turns = 0 is_authenticated = False while not is_authenticated and dialog.get_root(): is_authenticated = self.integration.checkQuickConnect(data.get('Secret')) if is_authenticated: GLib.idle_add(dialog.close) threading.Thread(target=self.get_root().get_application().try_login, args=(self.integration,), daemon=True).start() break time.sleep(5) waited_turns += 1 if waited_turns >= 5: GLib.idle_add(dialog.close) break def run(): data = self.integration.initiateQuickConnect() dialog = Adw.AlertDialog( heading=_("Quick Connect"), body=data.get("Code") or _("Error getting code"), extra_child=Gtk.LinkButton( label=_("Quick Connect Page"), uri="{}/web/#/quickconnect".format(self.url_el.get_text()) ) ) dialog.add_response( "cancel", _("Cancel") ) dialog.set_close_response("cancel") GLib.idle_add(dialog.choose, self.get_root(), None, lambda *_: None ) GLib.idle_add(threading.Thread(target=wait_confirmation, args=(data, dialog), daemon=True).start) threading.Thread(target=run, daemon=True).start() Jeffser-Nocturne-b442fee/src/widgets/pages/meson.build000066400000000000000000000006001523240030000230450ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'pages' widgets = [ '__init__.py', 'home.py', 'login.py', 'artists.py', 'playlists.py', 'starred.py', 'songs_all.py', 'albums.py', 'albums_all.py', 'radios.py', 'welcome.py', 'setup.py', 'playback.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/pages/playback.py000066400000000000000000000053721523240030000230560ustar00rootroot00000000000000# playback.py from gi.repository import Gtk, Adw, GLib, Pango from ...integrations import get_current_integration from ..song import SongRow, SongButton from datetime import datetime @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/playback.ui') class PlaybackPage(Adw.NavigationPage): __gtype_name__ = 'NocturnePlaybackPage' spectrum = Gtk.Template.Child() top_wrapbox = Gtk.Template.Child() song_queue = Gtk.Template.Child() playlist_stack = Gtk.Template.Child() thanks_label = Gtk.Template.Child() def setup(self, songs:list, month:datetime): self.songs = songs self.month = month self.spectrum.setup() for item in list(self.top_wrapbox): self.top_wrapbox.remove(item) self.song_queue.list_el.remove_all() self.thanks_label.set_visible(len(songs) >= 50) # Add thank you message if 50 or more songs are included self.playlist_stack.set_visible_child_name("active") target_value = GLib.Variant('a{sv}', { 'new_playlist': GLib.Variant('s', _("Nocturne Playback ~ {}").format(self.month.strftime("%B %Y"))), 'songs': GLib.Variant('as', [song[0] for song in self.songs]) }) self.playlist_stack.get_child_by_name('active').set_action_target_value(target_value) self.playlist_stack.get_child_by_name('active').set_action_name("app.add_songs_to_playlist") integration = get_current_integration() for rank, song in enumerate(self.songs[:5]): container = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=5 ) container.append(Gtk.Label( css_classes=["title-1"], label="#{}".format(rank+1) )) container.append(SongButton(song[0])) container.append(Gtk.Label( css_classes=["dimmed"], label=_("{} Plays").format(song[1]) )) self.top_wrapbox.append(container) for rank, song in enumerate(self.songs[5:]): row = SongRow(song[0]) row.subtitle_wrapbox.prepend(Gtk.Label( ellipsize=Pango.EllipsizeMode.END, label="#{} ยท {} ยท".format(rank+6, _("{} Plays").format(song[1])), css_classes=["subtitle"] )) self.song_queue.list_el.append(row) self.song_queue.main_stack.set_visible_child_name('content') @Gtk.Template.Callback() def go_home(self, button): if root := self.get_root(): root.main_stack.set_visible_child_name('content') root.replace_root_page('home') @Gtk.Template.Callback() def save_playlist(self, button): self.playlist_stack.set_visible_child_name("inactive") Jeffser-Nocturne-b442fee/src/widgets/pages/playlists.py000066400000000000000000000071701523240030000233120ustar00rootroot00000000000000# playlists.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..playlist import PlaylistRow, PlaylistButton import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/playlists.ui') class PlaylistsPage(Adw.NavigationPage): __gtype_name__ = 'NocturnePlaylistsPage' toggle_group_el = Gtk.Template.Child() main_stack = Gtk.Template.Child() list_el = Gtk.Template.Child() wrapbox_el = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() end_stack = Gtk.Template.Child() search_entry = Gtk.Template.Child() offset = 0 searching = False def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "default-view-mode-playlists", self.toggle_group_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.search, daemon=True).start() def reload(self): if len(list(self.list_el)) + len(list(self.wrapbox_el)) == 0: GLib.idle_add(self.on_search, self.search_entry) def reset(self): self.list_el.remove_all() for el in list(self.wrapbox_el): self.wrapbox_el.remove(el) def search(self): if self.searching: return self.searching = True integration = get_current_integration() query = None while query != self.search_entry.get_text(): # In case query changes whilst calling integration query = self.search_entry.get_text() search_results = integration.search(query=query, playlistCount=30, playlistOffset=self.offset) for playlist_id in search_results.get('playlist'): results_list = [row for row in list(self.list_el) if row.id == playlist_id] if len(results_list) > 0: GLib.idle_add(results_list[0].set_visible, True) else: row = PlaylistRow(playlist_id) GLib.idle_add(self.list_el.append, row) results_wrapbox = [button for button in list(self.wrapbox_el) if button.id == playlist_id] if len(results_wrapbox) > 0: GLib.idle_add(results_wrapbox[0].set_visible, True) else: button = PlaylistButton(playlist_id) GLib.idle_add(self.wrapbox_el.append, button) GLib.idle_add(self.end_stack.set_visible_child_name, 'end' if len(search_results.get('playlist')) < 30 else 'loading') self.offset += 30 self.searching = False GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def on_search(self, search_entry): self.offset = 0 for widget in list(self.list_el) + list(self.wrapbox_el): widget.set_visible(False) threading.Thread(target=self.search, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.end_stack.get_visible_child_name() == 'loading': threading.Thread(target=self.search, daemon=True).start() def update_visibility(self): for row in list(self.list_el) + list(self.wrapbox_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/radios.py000066400000000000000000000025661523240030000225530ustar00rootroot00000000000000# radios.py from gi.repository import Gtk, Adw, GLib from ...integrations import get_current_integration from ..song import SongRow import re @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/radios.ui') class RadiosPage(Adw.NavigationPage): __gtype_name__ = 'NocturneRadiosPage' main_stack = Gtk.Template.Child() list_el = Gtk.Template.Child() def reload(self): # call in different thread GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') integration = get_current_integration() radios = integration.getInternetRadioStations() GLib.idle_add(self.reset) for id in radios: GLib.idle_add(self.list_el.append, SongRow(id)) GLib.idle_add(self.update_visibility) def reset(self): self.list_el.remove_all() @Gtk.Template.Callback() def on_search(self, search_entry): query = search_entry.get_text() for child in list(self.list_el): child.set_visible(child.get_name() != 'GtkListBoxRow' and re.search(query, child.get_name(), re.IGNORECASE)) GLib.idle_add(self.update_visibility) def update_visibility(self): for row in list(self.list_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/setup.py000066400000000000000000000102471523240030000224250ustar00rootroot00000000000000# setup.py # page used to download the managed Navidrome instance from gi.repository import Gtk, Adw, GLib from ...constants import BASE_NAVIDROME_DIR, CACHE_DIR from . import LoginDialog import os, requests, tarfile, threading, platform @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/setup.ui') class SetupPage(Adw.NavigationPage): __gtype_name__ = 'NocturneSetupPage' main_stack = Gtk.Template.Child() downloading_status_page = Gtk.Template.Child() progressbar_el = Gtk.Template.Child() continue_button = Gtk.Template.Child() integration = None def set_integration(self, integration): self.integration = integration def get_latest_url(self) -> str: url = "https://api.github.com/repos/navidrome/navidrome/releases/latest" try: response = requests.get(url, timeout=10) response.raise_for_status() latest_tag = response.json().get('tag_name') except Exception as e: print(e) latest_tag = "v0.60.3" latest_tag = latest_tag.removeprefix("v") arch = { "x86_64": "amd", "amd64": "amd", "arm64": "arm", "aarch64": "arm", }.get(platform.machine().lower()) return "https://github.com/navidrome/navidrome/releases/download/v{tag}/navidrome_{tag}_linux_{architecture}64.tar.gz".format(tag=latest_tag, architecture=arch) def verify_instance(self): # Call in different thread if self.integration.start_instance(): GLib.idle_add(self.main_stack.set_visible_child_name, 'success') else: GLib.idle_add(self.main_stack.set_visible_child_name, 'error') def download_worker(self): try: url = self.get_latest_url() print("Downloading Navidrome from: ", url) response = requests.get(url, stream=True) response.raise_for_status() total_size = response.headers.get('content-length') total_size = int(total_size) if total_size else None downloaded = 0 download_path = os.path.join(CACHE_DIR, "navidrome.tar.gz") with open(download_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) if total_size: GLib.idle_add(self.progressbar_el.set_fraction, downloaded / total_size) GLib.idle_add(self.downloading_status_page.set_description, '{}%'.format(round(downloaded / total_size * 100))) GLib.idle_add(self.progressbar_el.pulse) GLib.idle_add(self.downloading_status_page.set_description, _("Installing")) with tarfile.open(download_path, "r:gz") as tar: tar.extractall(path=BASE_NAVIDROME_DIR, filter='fully_trusted') if os.path.isfile(download_path): os.remove(download_path) if self.integration.get_property('libraryDir'): threading.Thread(target=self.verify_instance).start() else: def response(dialog, result): if folder := dialog.select_folder_finish(result): self.integration.set_property('libraryDir', folder.get_path()) threading.Thread(target=self.verify_instance).start() dialog = Gtk.FileDialog(title=_("Music Library")) dialog.select_folder(self.get_root(), None, response) except Exception as e: print(e) self.main_stack.set_visible_child_name('error') @Gtk.Template.Callback() def download_clicked(self, button): self.main_stack.set_visible_child_name('downloading') threading.Thread(target=self.download_worker, daemon=True).start() @Gtk.Template.Callback() def link_visited(self, button): self.continue_button.set_sensitive(True) @Gtk.Template.Callback() def continue_clicked(self, button): self.get_root().main_stack.set_visible_child_name('welcome') LoginDialog(self.integration).present(self.get_root()) Jeffser-Nocturne-b442fee/src/widgets/pages/songs_all.py000066400000000000000000000072601523240030000232470ustar00rootroot00000000000000# songs_all.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..song import SongRow, SongButton import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/songs_all.ui') class SongsAllPage(Adw.NavigationPage): __gtype_name__ = 'NocturneSongsAllPage' toggle_group_el = Gtk.Template.Child() search_entry = Gtk.Template.Child() main_stack = Gtk.Template.Child() list_el = Gtk.Template.Child() wrapbox_el = Gtk.Template.Child() end_stack = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() offset = 0 searching = False def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "default-view-mode-songs", self.toggle_group_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.search, args=(60,), daemon=True).start() def reload(self): if len(list(self.list_el.list_el)) + len(list(self.wrapbox_el)) == 0: GLib.idle_add(self.on_search, self.search_entry) def reset(self): self.list_el.list_el.remove_all() for el in list(self.wrapbox_el): self.wrapbox_el.remove(el) def search(self, count=30): if self.searching: return self.searching = True query = self.search_entry.get_text() integration = get_current_integration() search_results = integration.search( query=query, songCount=count, songOffset=self.offset ) for song_id in search_results.get('song'): results_list = [row for row in list(self.list_el.list_el) if row.id == song_id] if len(results_list) > 0: GLib.idle_add(results_list[0].set_visible, True) else: row = SongRow(song_id) GLib.idle_add(self.list_el.list_el.append, row) results_wrapbox = [button for button in list(self.wrapbox_el) if button.id == song_id] if len(results_wrapbox) > 0: GLib.idle_add(results_wrapbox[0].set_visible, True) else: button = SongButton(song_id) GLib.idle_add(self.wrapbox_el.append, button) GLib.idle_add(self.end_stack.set_visible_child_name, 'end' if len(search_results.get('song')) < count else 'loading') GLib.idle_add(self.list_el.main_stack.set_visible_child_name, 'content' if len(list(self.list_el.list_el)) else 'no-content') self.offset += count self.searching = False GLib.idle_add(self.update_visibility) @Gtk.Template.Callback() def on_search(self, search_entry): self.offset = 0 for row in list(self.list_el.list_el) + list(self.wrapbox_el): row.set_visible(False) threading.Thread(target=self.search, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.end_stack.get_visible_child_name() == 'loading': threading.Thread(target=self.search, daemon=True).start() def update_visibility(self): for row in list(self.list_el.list_el) + list(self.wrapbox_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/starred.py000066400000000000000000000112551523240030000227310ustar00rootroot00000000000000# starred.py # handles both starred artists and songs from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..song import SongRow, SongButton from ..artist import ArtistRow, ArtistButton import re, threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/starred.ui') class StarredPage(Adw.NavigationPage): __gtype_name__ = 'NocturneStarredPage' list_el = Gtk.Template.Child() wrapbox_el = Gtk.Template.Child() search_entry_el = Gtk.Template.Child() main_stack = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() toggle_group_el = Gtk.Template.Child() failed_status_el = Gtk.Template.Child() item_ids = [] offset = 0 searching = False def __init__(self): super().__init__() self.scrolledwindow.get_vadjustment().connect('notify::upper', lambda va, ud: GLib.timeout_add(1000, self.check_scrollbar, va)) self.connect("notify::tag", self.on_tag_assigned) def on_tag_assigned(self, obj, pspec): pref_string = f"default-view-mode-{self.get_tag().split('-')[0]}" Gio.Settings(schema_id="com.jeffser.Nocturne").bind( pref_string, self.toggle_group_el, "active-name", Gio.SettingsBindFlags.DEFAULT ) if self.get_tag() == 'favorite-artists': GLib.idle_add(self.search_entry_el.update_property( [Gtk.AccessibleProperty.LABEL], [_("Search favorite artists")] )) GLib.idle_add(self.failed_status_el.set_title(_("No Artists Found"))) def search(self): if self.searching: return self.searching = True integration = get_current_integration() ids_to_show = [] if query := self.search_entry_el.get_text(): for item_id in self.item_ids: if model := integration.loaded_models.get(item_id): if 'songs' in self.get_tag(): checker = model.get_property('title') + model.get_property('artist') elif 'artists' in self.get_tag(): checker = model.get_property('name') if re.search(query, checker, re.IGNORECASE): ids_to_show.append(item_id) else: ids_to_show = self.item_ids ids_to_show = ids_to_show[:self.offset+30] missing_ids = ids_to_show.copy() for widget in list(self.list_el.list_el) + list(self.wrapbox_el): GLib.idle_add(widget.set_visible, widget.id in ids_to_show) if widget.id in missing_ids: missing_ids.remove(widget.id) for item_id in missing_ids: if 'songs' in self.get_tag(): GLib.idle_add(self.list_el.list_el.append, SongRow(item_id)) GLib.idle_add(self.wrapbox_el.append, SongButton(item_id)) elif 'artists' in self.get_tag(): GLib.idle_add(self.list_el.list_el.append, ArtistRow(item_id)) GLib.idle_add(self.wrapbox_el.append, ArtistButton(item_id)) self.offset += 30 GLib.idle_add(self.update_visibility) self.searching = False def check_scrollbar(self, adjustment): if adjustment.get_upper() <= adjustment.get_page_size(): threading.Thread(target=self.search).start() def reset(self): self.offset = 0 GLib.idle_add(self.list_el.list_el.remove_all) for el in list(self.wrapbox_el): GLib.idle_add(self.wrapbox_el.remove, el) integration = get_current_integration() self.item_ids = integration.getStarred((self.get_tag().split('-')[0]).removesuffix("s")) def reload(self): GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') def run(): self.reset() self.search() threading.Thread(target=run, daemon=True).start() @Gtk.Template.Callback() def on_search(self, search_entry): self.offset = 0 threading.Thread(target=self.search, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM and self.offset < len(self.item_ids): threading.Thread(target=self.search, daemon=True).start() def update_visibility(self): for row in list(self.list_el.list_el): if row.get_visible(): self.main_stack.set_visible_child_name('content') self.list_el.main_stack.set_visible_child_name('content') return self.main_stack.set_visible_child_name('no-content') self.list_el.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-b442fee/src/widgets/pages/welcome.py000066400000000000000000000034101523240030000227120ustar00rootroot00000000000000# welcome.py from gi.repository import Gtk, Adw from . import LoginDialog from ...integrations import Local, Navidrome, NavidromeIntegrated, Bandcamp, Jellyfin, Offline import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/welcome.ui') class WelcomePage(Adw.NavigationPage): __gtype_name__ = 'NocturneWelcomePage' listbox_el = Gtk.Template.Child() def __init__(self): super().__init__() integrations = [Navidrome, NavidromeIntegrated, Jellyfin, Bandcamp, Local, Offline] for integration in integrations: metadata = integration.button_metadata row = Adw.ActionRow( title=metadata.get('title', _("Integration")), subtitle=metadata.get('subtitle', ""), tooltip_text=metadata.get('title', _("Integration")), activatable=True ) if icon_name := integration.login_page_metadata.get('icon-name'): row.add_prefix(Gtk.Image( icon_name=icon_name, valign=Gtk.Align.CENTER )) row.add_suffix(Gtk.Image( icon_name="go-next-symbolic", valign=Gtk.Align.CENTER )) row.connect('activated', self.option_selected, integration) self.listbox_el.append(row) def option_selected(self, row, integration): integration = integration() if integration.check_if_ready(row): if integration.login_page_metadata.get('entries', []): dialog = LoginDialog(integration) dialog.present(self.get_root()) else: threading.Thread(target=self.get_root().get_application().try_login, args=(integration,), daemon=True).start() Jeffser-Nocturne-b442fee/src/widgets/playing/000077500000000000000000000000001523240030000212535ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/playing/__init__.py000066400000000000000000000007021523240030000233630ustar00rootroot00000000000000# __init__.py from .footer import PlayingFooter from .control_page import PlayingControlPage from .queue_page import PlayingQueuePage from .lyrics_page import PlayingLyricsPage from .player import Player from .popout_window import PopoutWindow from .volume_button import VolumeButton from .playback_mode_button import PlaybackModeButton from .spectrum import Spectrum from .equalizer_page import EqualizerPage from .cover_art import PlayingCoverArt Jeffser-Nocturne-b442fee/src/widgets/playing/control_page.py000066400000000000000000000226521523240030000243100ustar00rootroot00000000000000# control_page.py from gi.repository import Gtk, Adw, GLib, Gst, GObject from ...integrations import get_current_integration from ...constants import get_display_time import threading from urllib.parse import urlparse @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/control_page.ui') class PlayingControlPage(Adw.NavigationPage): __gtype_name__ = 'NocturnePlayingControlPage' extra_widget = GObject.Property(type=Gtk.Widget) header_bar = Gtk.Template.Child() clamp = Gtk.Template.Child() main_container = Gtk.Template.Child() cover_art_el = Gtk.Template.Child() title_el = Gtk.Template.Child() radio_homepage_el = Gtk.Template.Child() artist_el = Gtk.Template.Child() album_el = Gtk.Template.Child() progress_el = Gtk.Template.Child() positive_progress_el = Gtk.Template.Child() negative_progress_el = Gtk.Template.Child() star_el = Gtk.Template.Child() show_sidebar_el = Gtk.Template.Child() state_stack_el = Gtk.Template.Child() rating_button = Gtk.Template.Child() rating_container = Gtk.Template.Child() breakpoint_state = False def setup(self): integration = get_current_integration() integration.connect_to_model('currentSong', 'positionSeconds', self.update_position) integration.connect_to_model('currentSong', 'buttonState', self.state_stack_el.set_visible_child_name) integration.connect_to_model('currentSong', 'displaySongTitle', self.display_title_changed) integration.connect_to_model('currentSong', 'displaySongArtist', self.display_artist_changed) self.cover_art_el.setup() self.setup_sidebar_button_connection() if stack := self.get_ancestor(Gtk.Stack): stack.get_parent().set_overflow(Gtk.Overflow.HIDDEN) if stack2 := stack.get_parent().get_parent(): stack2.get_parent().set_overflow(Gtk.Overflow.HIDDEN) if extra_widget := self.get_property('extra-widget'): self.main_container.prepend(extra_widget) # Connect to Current Song connections = { 'id': lambda model_id: self.star_el.set_action_target_value(GLib.Variant('s', model_id or '')), 'radioStreamUrl': self.update_radioStreamUrl, 'artistId': self.update_artistId, 'album': self.update_album, 'albumId': self.update_albumId, 'isExternalFile': self.update_isExternalFile, 'duration': self.progress_el.get_adjustment().set_upper, 'userRating': self.update_userRating, 'starred': self.update_starred } for parameter, callback in connections.items(): integration.connect_to_current_song(parameter, callback) def update_position(self, positionSeconds:int): integration = get_current_integration() current_song = integration.loaded_models.get('currentSong') if current_song: song = integration.loaded_models.get(current_song.get_property('songId')) if song: label_positive = get_display_time(positionSeconds) label_negative = get_display_time(song.get_property('duration') - positionSeconds) self.positive_progress_el.set_label(label_positive) self.negative_progress_el.set_label('-{}'.format(label_negative)) if not integration.loaded_models.get('currentSong').get_property('seeking'): self.progress_el.get_adjustment().set_value(positionSeconds) def breakpoint_toggled(self, active:bool): self.breakpoint_state = active if isinstance(self.get_parent(), Adw.NavigationView) and not self.get_parent().get_vhomogeneous(): self.get_parent().set_vhomogeneous(True) def setup_sidebar_button_connection(self): if root := self.get_root(): root.breakpoint_el.connect('apply', lambda *_: self.breakpoint_toggled(True)) root.breakpoint_el.connect('unapply', lambda *_: self.breakpoint_toggled(False)) if width := root.get_width(): self.breakpoint_toggled(width <= 620) @Gtk.Template.Callback() def progress_bar_changed(self, scale_el, scroll_type, value): value = scale_el.get_adjustment().get_value() integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('seeking', True) def change_time(val): integration.loaded_models.get('currentSong').set_property('seeking', False) nanoseconds = int(val * Gst.SECOND) self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) GLib.timeout_add(500, lambda v=value: change_time(v) if v == scale_el.get_adjustment().get_value() else None) @Gtk.Template.Callback() def show_content_clicked(self, button): if view := self.get_ancestor(Adw.NavigationSplitView): if self.breakpoint_state: view.set_show_content(True) else: view.set_collapsed(not view.get_collapsed()) @Gtk.Template.Callback() def change_rating(self, button): integration = get_current_integration() target_value = GLib.Variant('a{sv}', { 'model_id': GLib.Variant('s', integration.loaded_models.get('currentSong').get_property('songId')), 'rating': GLib.Variant('i', int(button.get_name())) }) self.get_root().activate_action("app.set_rating", target_value) def change_bottom_sheet_state(self, playing:bool): bottom_sheet = self.get_ancestor(Adw.BottomSheet) if bottom_sheet: bottom_sheet.set_can_open(playing) if not playing: bottom_sheet.set_open(False) bottom_sheet.set_reveal_bottom_bar(playing) if root := self.get_root(): if application := root.get_application(): if playing: application.inhibit_suspend() else: application.uninhibit_suspend() if popout_window := application.popout_window: popout_window.close() def update_radioStreamUrl(self, radioStreamUrl:str): self.radio_homepage_el.set_visible(radioStreamUrl) self.artist_el.set_visible(not radioStreamUrl) self.positive_progress_el.set_visible(not radioStreamUrl) self.negative_progress_el.set_visible(not radioStreamUrl) self.progress_el.set_visible(not radioStreamUrl) self.rating_button.set_visible(not radioStreamUrl) self.star_el.set_visible(not radioStreamUrl) homepage_url = "" if radioStreamUrl: stream_url = urlparse(radioStreamUrl) homepage_url = '{}://{}'.format(stream_url.scheme, stream_url.netloc) self.radio_homepage_el.set_tooltip_text(homepage_url) self.radio_homepage_el.set_action_target_value(GLib.Variant.new_string(homepage_url)) self.radio_homepage_el.get_child().set_label(urlparse(radioStreamUrl).netloc) def update_artistId(self, artistId:str): if artistId: self.artist_el.set_action_target_value(GLib.Variant.new_string(artistId)) self.artist_el.set_action_name("app.show_artist") self.artist_el.set_sensitive(True) else: self.artist_el.set_action_name("") self.artist_el.set_sensitive(False) def update_album(self, album:str): self.album_el.get_child().set_label(album) self.album_el.set_tooltip_text(album) self.album_el.set_visible(album) def update_albumId(self, albumId:str): if albumId: self.album_el.set_action_target_value(GLib.Variant.new_string(albumId)) self.album_el.set_action_name("app.show_album") self.album_el.set_sensitive(True) else: self.album_el.set_action_name("") self.album_el.set_sensitive(False) def update_isExternalFile(self, isExternalFile:bool): self.album_el.get_ancestor(Adw.WrapBox).set_sensitive(not isExternalFile) self.rating_container.set_visible(self.rating_container.get_visible() and not isExternalFile) self.star_el.set_visible(self.star_el.get_visible() and not isExternalFile) def update_userRating(self, userRating:int): for i, el in enumerate(list(self.rating_container)): el.set_icon_name("starred-symbolic" if userRating >= i+1 else "non-starred-symbolic") self.rating_button.set_icon_name("starred-symbolic" if userRating > 0 else "non-starred-symbolic") def update_starred(self, starred:bool): if starred: self.star_el.add_css_class('accent') self.star_el.remove_css_class('dim-label') self.star_el.set_icon_name('heart-filled-symbolic') self.star_el.set_tooltip_text(_('Favorite')) else: self.star_el.remove_css_class('accent') self.star_el.add_css_class('dim-label') self.star_el.set_icon_name('heart-outline-thick-symbolic') self.star_el.set_tooltip_text(_('Not Favorite')) def display_title_changed(self, display_title:str): self.title_el.set_label(display_title) self.title_el.set_tooltip_text(display_title) def display_artist_changed(self, display_artist:str): self.artist_el.get_child().set_label(display_artist) self.artist_el.set_tooltip_text(display_artist) Jeffser-Nocturne-b442fee/src/widgets/playing/cover_art.py000066400000000000000000000125621523240030000236170ustar00rootroot00000000000000# coverArt.py from gi.repository import GObject, Adw, Gtk, Gdk, GLib from ...integrations import get_current_integration import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/cover_art.ui') class PlayingCoverArt(Gtk.Box, Adw.Swipeable): __gtype_name__ = 'NocturnePlayingCoverArt' spectrum_el = Gtk.Template.Child() view_stack_el = Gtk.Template.Child() cover_el = Gtk.Template.Child() video_el = Gtk.Template.Child() view_switcher_el = Gtk.Template.Child() previous_icon_el = Gtk.Template.Child() next_icon_el = Gtk.Template.Child() swipeProgress = GObject.Property(type=float, default=0.0) def __init__(self, **kwargs): super().__init__(**kwargs) self.animation_target = Adw.CallbackAnimationTarget.new(lambda val: self.set_property('swipeProgress', val)) self.animation = Adw.TimedAnimation.new( self, 0, 0, 200, self.animation_target ) self.swipe_tracker = Adw.SwipeTracker.new(self) self.swipe_tracker.set_enabled(True) self.swipe_tracker.set_allow_mouse_drag(True) self.swipe_tracker.connect('update-swipe', self.on_swipe_update) self.swipe_tracker.connect('end-swipe', self.on_swipe_end) self.connect('notify::swipeProgress', self.swipe_progress_changed) # Swipeable and SwipeTracker code ---- def do_get_distance(self): return float(self.get_width() / 1.5) def do_get_progress(self): return float(self.get_property('swipeProgress')) def do_get_cancel_progress(self): return float(0.0) def do_get_snap_points(self): return [-1.0, -0.9, 0.9, 1.0], 4 def on_swipe_update(self, tracker, progress): self.set_property('swipeProgress', progress) self.queue_allocate() def on_swipe_end(self, tracker, velocity, to): progress = self.get_property('swipeProgress') progress = self.get_property('swipeProgress') if progress != 0: self.animation.set_value_from(progress) self.animation.play() # Handle changing the song if self.get_property('swipeProgress') > 0.9: GLib.timeout_add(200, lambda: self.get_root().activate_action("app.player_next") and False) elif self.get_property('swipeProgress') < -0.9: GLib.timeout_add(200, lambda: self.get_root().activate_action("app.player_previous") and False) def swipe_progress_changed(self, widget, gparam): progress = self.get_property('swipeProgress') # NEXT if progress > 0: self.next_icon_el.set_margin_start(int(progress*10)) self.next_icon_el.set_pixel_size(max(int(progress*32), 1)) self.next_icon_el.set_visible(True) else: self.next_icon_el.set_margin_start(0) self.next_icon_el.set_pixel_size(1) self.next_icon_el.set_visible(False) if progress > 0.9: self.next_icon_el.add_css_class('accent') self.next_icon_el.remove_css_class('dimmed') else: self.next_icon_el.add_css_class('dimmed') self.next_icon_el.remove_css_class('accent') # PREVIOUS if progress < 0: self.previous_icon_el.set_margin_end(int(abs(progress)*10)) self.previous_icon_el.set_pixel_size(max(int(abs(progress)*32), 1)) self.previous_icon_el.set_visible(True) else: self.previous_icon_el.set_margin_end(0) self.previous_icon_el.set_pixel_size(1) self.previous_icon_el.set_visible(False) if progress < -0.9: self.previous_icon_el.add_css_class('accent') self.previous_icon_el.remove_css_class('dimmed') else: self.previous_icon_el.add_css_class('dimmed') self.previous_icon_el.remove_css_class('accent') # ------------------------------------ def setup(self): integration = get_current_integration() integration.connect_to_current_song('gdkPaintableBig', self.update_cover_art) integration.connect_to_model('currentSong', 'videoId', self.video_changed) self.spectrum_el.setup() if root := self.get_root(): if app := root.get_application(): if player := app.player: if video_sink := player.gst.get_property('video-sink'): self.video_el.set_paintable(video_sink.get_property('paintable')) def update_cover_art(self, paintable): if paintable: self.cover_el.remove_css_class('p50') else: icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) paintable = icon_theme.lookup_icon( 'music-note-symbolic', None, 64, 1, Gtk.TextDirection.NONE, 0 ) self.cover_el.add_css_class('p50') self.cover_el.set_paintable(paintable) def video_changed(self, videoId:str): integration = get_current_integration() songId = integration.loaded_models.get('currentSong').get_property('songId') video_available = videoId and videoId == songId and self.video_el.get_paintable() self.view_switcher_el.set_visible(video_available) self.view_stack_el.set_visible_child_name('video' if video_available else 'audio') Jeffser-Nocturne-b442fee/src/widgets/playing/equalizer_page.py000066400000000000000000000026411523240030000246250ustar00rootroot00000000000000# equalizer_page.py from gi.repository import Gtk, Gio @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/equalizer_page.ui') class EqualizerPage(Gtk.ScrolledWindow): __gtype_name__ = 'NocturneEqualizerPage' container = Gtk.Template.Child() def __init__(self): super().__init__() settings = Gio.Settings(schema_id="com.jeffser.Nocturne") for i in range(6): band = Gtk.Scale( inverted=True, orientation=Gtk.Orientation.VERTICAL, draw_value=True, adjustment=Gtk.Adjustment( lower=-12.0, upper=12.0, value=0 ) ) settings.bind( "eq-band-{}".format(i), band.get_adjustment(), "value", Gio.SettingsBindFlags.DEFAULT ) band.add_mark( value=6, position=Gtk.PositionType.TOP, markup="6dB" if i == 0 else None ) band.add_mark( value=0, position=Gtk.PositionType.TOP, markup="0dB" if i == 0 else None ) band.add_mark( value=-6, position=Gtk.PositionType.TOP, markup="-6dB" if i == 0 else None ) self.container.append(band) Jeffser-Nocturne-b442fee/src/widgets/playing/footer.py000066400000000000000000000114531523240030000231270ustar00rootroot00000000000000# footer.py from gi.repository import Gtk, GLib, GObject, Gst, Gio from ...integrations import get_current_integration import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/footer.ui') class PlayingFooter(Gtk.Overlay): __gtype_name__ = 'NocturnePlayingFooter' cover_el = Gtk.Template.Child() title_el = Gtk.Template.Child() artist_el = Gtk.Template.Child() progress_el = Gtk.Template.Child() ro_progress_el = Gtk.Template.Child() state_stack_el = Gtk.Template.Child() detail_container = Gtk.Template.Child() forceHugeMode = GObject.Property(type=bool, default=False) def setup(self): # Called after login self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") integration = get_current_integration() integration.connect_to_model('currentSong', 'songId', lambda song_id: self.update_progress_el_visibility()) integration.connect_to_model('currentSong', 'positionSeconds', self.position_changed) integration.connect_to_model('currentSong', 'buttonState', self.state_stack_el.set_visible_child_name) integration.connect_to_model('currentSong', 'displaySongTitle', self.display_title_changed) integration.connect_to_model('currentSong', 'displaySongArtist', self.display_artist_changed) self.settings.connect("changed::use-big-footer", self.big_mode_toggled) self.big_mode_toggled(self.settings, 'use-big-footer') connections = { 'duration': self.progress_el.get_adjustment().set_upper, 'gdkPaintable': self.update_coverArt } for parameter, callback in connections.items(): integration.connect_to_current_song(parameter, callback) def big_mode_toggled(self, settings, key): if self.get_property('forceHugeMode'): big_mode = True size = 100 self.cover_el.set_size_request(size, size) self.cover_el.set_pixel_size(size) self.title_el.set_wrap(True) self.title_el.set_lines(2) self.detail_container.set_spacing(10) else: big_mode = settings.get_value(key).unpack() size = 70 if big_mode else 48 self.cover_el.set_size_request(size, size) self.cover_el.set_pixel_size(size) self.title_el.set_wrap(False) self.title_el.set_lines(1) self.detail_container.set_spacing(5) self.ro_progress_el.set_visible(not big_mode) self.update_progress_el_visibility() def update_progress_el_visibility(self): mode_status = self.settings.get_value('use-big-footer').unpack() force_status = self.get_property('forceHugeMode') integration = get_current_integration() songId = integration.loaded_models.get('currentSong').get_property('songId') isRadio = False if model := integration.loaded_models.get(songId): isRadio = bool(model.get_property('radioStreamUrl')) self.progress_el.set_visible(not isRadio and (mode_status or force_status)) def display_title_changed(self, display_title:str): self.title_el.set_label(display_title) def display_artist_changed(self, display_artist:str): self.artist_el.set_label(display_artist) self.artist_el.set_visible(self.artist_el.get_label()) def position_changed(self, positionSeconds:float): integration = get_current_integration() if not integration.loaded_models.get('currentSong').get_property('seeking'): duration = self.progress_el.get_adjustment().get_upper() self.ro_progress_el.set_fraction(0 if duration == 0 else positionSeconds / duration) self.progress_el.get_adjustment().set_value(positionSeconds) def update_coverArt(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(self.cover_el.get_size_request()[0]) else: self.cover_el.set_from_icon_name('music-note-symbolic') self.cover_el.set_pixel_size(-1) @Gtk.Template.Callback() def progress_bar_changed(self, scale_el, scroll_type, value): value = scale_el.get_adjustment().get_value() integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('seeking', True) def change_time(val): integration.loaded_models.get('currentSong').set_property('seeking', False) nanoseconds = int(val * Gst.SECOND) self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, nanoseconds ) GLib.timeout_add(500, lambda v=value: change_time(v) if v == scale_el.get_adjustment().get_value() else None) Jeffser-Nocturne-b442fee/src/widgets/playing/lyrics_page.py000066400000000000000000000235671523240030000241430ustar00rootroot00000000000000# lyrics_page.py from gi.repository import Gtk, GObject, GLib, Gio, Pango, Gst from ...integrations import get_current_integration from ..lyrics.helpers import prepare_lrc from ...constants import DATA_DIR, CONTEXT_LYRICS from ..containers import ContextContainer import threading, os, re class LyricRow(Gtk.ListBoxRow): __gtype_name__ = 'NocturneLyricRow' ms = GObject.Property(type=int) def __init__(self, content:str, ms:int, duration_ms:int): super().__init__( ms=ms, child=Gtk.Label( wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, justify=Gtk.Justification.CENTER, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER ) ) pattern = r"<(\d{2}):(\d{2})\.(\d{2,3})>\s*([^<]*)" matches = re.findall(pattern, content) self.cues = {} if len(matches) > 0: for m, s, ms_part, text in matches: if text: minutes = int(m) seconds = int(s) fraction = int(ms_part) if len(ms_part) == 2: fraction *= 10 total_ms = (minutes * 60000) + (seconds * 1000) + fraction self.cues[total_ms] = text if len(self.cues) == 0: if content: if duration_ms == 0: self.cues[ms] = content else: words = content.split(' ') weights = [] for word in words: word = word.replace('.', '...') # Add more time word = word.replace(',', '..') # Add more time word = ''.join(c for c in word if c not in 'ยก!ยฟ?\'"') # Ignore characters weights.append(len(word)) ms_per_letter = int(duration_ms / sum(weights)) last_timestamp = 0 for i, word in enumerate(words): if i == 0: timestamp = ms else: timestamp = int(last_timestamp + weights[i-1] * ms_per_letter) last_timestamp = timestamp self.cues[timestamp] = word + ' ' else: self.cues[ms] = "๐ŸŽต" if duration_ms > 1000 else "" self.sorted_ts = sorted(self.cues.keys()) self.alphas = {ts: 0.3 for ts in self.sorted_ts} self.last_ms = 0 self.add_tick_callback(self.on_frame_tick) self.on_frame_tick(self, None, True) def on_frame_tick(self, widget, frame_clock, force:bool=False): lerp_speed = 0.1 changed = False parts = [] for ts in self.sorted_ts: word = GLib.markup_escape_text(self.cues[ts]) target = 1.0 if self.last_ms >= ts else 0.3 current = self.alphas[ts] if abs(current - target) > 0.001: self.alphas[ts] = current + (target - current) * lerp_speed changed = True alpha_val = int(self.alphas[ts] * 65536) # Pango uses that number as max for some reason parts.append(f"{word}") if changed or force: self.get_child().set_markup("".join(parts)) return self.get_root() @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/lyrics_page.ui') class PlayingLyricsPage(Gtk.Stack): __gtype_name__ = 'NocturnePlayingLyricsPage' plain_label_el = Gtk.Template.Child() lrc_list_el = Gtk.Template.Child() scrolledwindow = Gtk.Template.Child() lrc_menu_popover = Gtk.Template.Child() code_is_selecting = False # used so that `on_lrc_selection` is only executed when manually selecting last_best_match = None def __init__(self): super().__init__() context = CONTEXT_LYRICS.copy() context["remove"]["connection"] = self.go_to_main self.lrc_menu_popover.set_child(ContextContainer(context)) def setup(self): # Called after login integration = get_current_integration() integration.connect_to_model('currentSong', 'songId', self.song_changed) integration.connect_to_model('currentSong', 'positionSeconds', self.position_changed) def update_lyrics(self, song_id:str, attempt_download:bool): # We verify if the song requested and the current song are the same # We have to do this both before and after downloading the lyrics settings = Gio.Settings(schema_id="com.jeffser.Nocturne") integration = get_current_integration() if integration.loaded_models.get('currentSong').get_property('songId') != song_id: return lyrics_type, raw_content = integration.getLyrics(song_id, attempt_download) if integration.loaded_models.get('currentSong').get_property('songId') != song_id: return if settings.get_value('auto-download-lyrics').unpack() and lyrics_type == 'not-found-locally': thread = threading.Thread(target=self.update_lyrics, args=(song_id, True), daemon=True) GLib.timeout_add(3000, thread.start) return GLib.idle_add(self.set_visible_child_name, lyrics_type) if lyrics_type == 'plain': GLib.idle_add(self.plain_label_el.set_label, raw_content) elif lyrics_type == 'lrc': content = prepare_lrc(raw_content) GLib.idle_add(self.lrc_list_el.remove_all) if content[0].get('content'): content.insert(0, {'content': '', 'ms': 0}) simulate_wbwl = settings.get_value('simulate-word-by-word-lyrics').unpack() for i, line in enumerate(content): if simulate_wbwl: duration_ms = 1000 if i + 1 < len(content): duration_ms = content[i+1].get('ms') - line.get('ms') else: integration = get_current_integration() if model := integration.loaded_models.get(song_id): duration_ms = model.get_property('duration') * 1000 - line.get('ms') else: duration_ms = 0 row = LyricRow( ms=line.get('ms'), content=line.get('content', ''), duration_ms=duration_ms ) GLib.idle_add(self.lrc_list_el.append, row) def song_changed(self, model_id:str, lrclib_download:bool=False): self.set_visible_child_name('loading') threading.Thread(target=self.update_lyrics, args=(model_id, lrclib_download), daemon=True).start() def position_changed(self, position_seconds:float): if self.get_visible_child_name() == 'lrc': ms = int(position_seconds * 1000)+100 best_match = None for row in list(self.lrc_list_el): if row.ms <= ms: best_match = row else: break if best_match: if best_match == self.lrc_list_el.get_selected_row(): best_match.last_ms = ms else: self.code_is_selecting = True self.lrc_list_el.select_row(best_match) self.code_is_selecting = False @Gtk.Template.Callback() def on_lrc_selection(self, list_el, position): row = list_el.get_selected_row() if self.last_best_match: self.last_best_match.last_ms = -1 self.last_best_match = row if row: if not self.code_is_selecting: self.get_root().get_application().player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, int(row.ms/1000 * Gst.SECOND) ) vadj = self.scrolledwindow.get_vadjustment() row_alloc = row.get_allocation() viewport_height = self.scrolledwindow.get_height() row_y = row_alloc.y row_height = row_alloc.height target = row_y - (viewport_height / 2) + (row_height / 2) lower = vadj.get_lower() upper = vadj.get_upper() - vadj.get_page_size() target = max(lower, min(target, upper)) vadj.set_value(target) @Gtk.Template.Callback() def lyric_download_requested(self, button): integration = get_current_integration() self.song_changed(integration.loaded_models.get('currentSong').get_property('songId'), True) def copy_lyrics_lrc(self, dialog, task): if source_file := dialog.open_finish(task): with open(source_file.get_path(), 'r') as f: integration = get_current_integration() if songId := integration.loaded_models.get('currentSong').get_property('songId'): if content := f.read(): integration.saveLyrics(songId, content, 'lrc') self.song_changed(songId) @Gtk.Template.Callback() def lyric_load_requested(self, button): integration = get_current_integration() file_filter = Gtk.FileFilter( name=_("LRC File") ) file_filter.add_mime_type('text/x-lrc') file_filter.add_mime_type('application/x-lrc') file_filter.add_suffix('lrc') file_filter_list = Gio.ListStore.new(Gtk.FileFilter) file_filter_list.append(file_filter) Gtk.FileDialog( filters=file_filter_list ).open(self.get_root(), None, self.copy_lyrics_lrc) @Gtk.Template.Callback() def go_to_main(self, button=None): self.set_visible_child_name('not-found-locally') integration = get_current_integration() integration.deleteLyrics(integration.loaded_models.get('currentSong').get_property('songId')) Jeffser-Nocturne-b442fee/src/widgets/playing/meson.build000066400000000000000000000006401523240030000234150ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'playing' widgets = [ '__init__.py', 'footer.py', 'control_page.py', 'queue_page.py', 'lyrics_page.py', 'player.py', 'popout_window.py', 'volume_button.py', 'playback_mode_button.py', 'spectrum.py', 'equalizer_page.py', 'cover_art.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/playing/playback_mode_button.py000066400000000000000000000026061523240030000260160ustar00rootroot00000000000000# playback_mode_button.py from gi.repository import Gtk, Gio from ...constants import PLAYBACK_MODES @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/playback_mode_button.ui') class PlaybackModeButton(Gtk.MenuButton): __gtype_name__ = 'NocturnePlaybackModeButton' container = Gtk.Template.Child() def __init__(self): super().__init__() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect('changed::playback-mode', lambda settings, key: self.mode_changed(settings.get_value(key).unpack())) self.mode_changed(self.settings.get_value('playback-mode').unpack()) for name, metadata in PLAYBACK_MODES.items(): button = Gtk.Button( css_classes=['flat'], icon_name=metadata.get('icon-name'), tooltip_text=metadata.get('display-name'), name=name ) button.connect('clicked', lambda btn: self.mode_changed(btn.get_name())) self.container.append(button) def mode_changed(self, name:str): self.set_icon_name(PLAYBACK_MODES.get(name, {}).get('icon-name')) self.set_tooltip_text(PLAYBACK_MODES.get(name, {}).get('display-name')) self.get_popover().popdown() if self.settings.get_value('playback-mode').unpack() != name: self.settings.set_string('playback-mode', name) Jeffser-Nocturne-b442fee/src/widgets/playing/player.py000066400000000000000000000656541523240030000231410ustar00rootroot00000000000000# player.py from gi.repository import Adw, GLib, GObject, Gst, Gio from mpris_server.adapters import MprisAdapter from mpris_server.events import EventAdapter from mpris_server.server import Server from mpris_server import Metadata, ValidMetadata, Track, Position, Volume, Rate, PlayState, DbusObj, MetadataObj, ActivePlaylist, PlaylistEntry, MprisInterface from ...integrations import get_current_integration from ...integrations.discord_rpc import DiscordRPC from urllib.parse import urlparse import threading, io, base64, logging from PIL import Image, ImageFilter from colorthief import ColorThief logger = logging.getLogger(__name__) Gst.init(None) class PlayerAdapter(MprisAdapter): # Implementations from https://github.com/alexdelorenzo/mpris_server/blob/master/src/mpris_server/adapters.py def __init__(self, player): self.player = player self.last_cover_art = {'id': '', 'url': ''} super().__init__() # -- RootAdapter -- def get_desktop_entry(self) -> str: return "com.jeffser.Nocturne" def can_fullscreen(self) -> bool: return False def can_quit(self) -> bool: return True def can_raise(self) -> bool: return True def has_tracklist(self) -> bool: return False def quit(self): integration = get_current_integration() if integration: integration.loaded_models.get('currentSong').set_property('songId', None) def set_fullscreen(self, value:bool): # def can_fullscreen returns false pass def set_raise(self, value:bool): # TODO idk maybe raise the window and open the sheet? pass # -- PlayerAdapter -- def metadata(self) -> ValidMetadata: integration = get_current_integration() if not integration: return MetadataObj() current_song_model = integration.loaded_models.get('currentSong') song = integration.loaded_models.get(current_song_model.get_property('songId')) if not song: return MetadataObj() if self.last_cover_art.get('id') != song.get_property('id'): self.last_cover_art['id'] = song.get_property('id') self.last_cover_art['url'] = integration.getCoverArtUrl(song.get_property('id')) if song.get_property('radioStreamUrl'): if not (title := current_song_model.get_property("displaySongTitle")): title = song.get_property('title') if not (artist := current_song_model.get_property("displaySongArtist")): artist = song.get_property('title') if artist == title: #fall back to stream URL for artist so MPRIS widgets don't show duplicate labels artist = urlparse(song.get_property('radioStreamUrl')).netloc artists = [artist] album = song.get_property('title') else: album = song.get_property('album') artists = ([a.get('name') for a in song.get_property('artists')] or [song.get_property('artist')]) title = song.get_property('title') return MetadataObj( album=album, art_url=self.last_cover_art.get('url'), artists=artists, as_text=[song.get_property('title')], length=song.get_property('duration')*1000000, title=title, track_id='/com/jeffser/Nocturne/track/{}'.format(song.get_property('id')), track_number=0 ) def can_control(self) -> bool: return True def can_go_next(self) -> bool: return True def can_go_previous(self) -> bool: return True def can_pause(self) -> bool: return True def can_play(self) -> bool: return True def can_seek(self) -> bool: return True def get_current_position(self) -> Position: # Unused # Microseconds success, position = self.player.gst.query_position(Gst.Format.TIME) return Position(position/1000) # Microsecond def get_rate(self) -> Rate: return Rate(1) def get_maximum_rate(self) -> Rate: return Rate(1) def get_minimum_rate(self) -> Rate: return Rate(1) def get_next_track(self) -> Track: pass def get_playstate(self) -> PlayState: success, state, pending = self.player.gst.get_state(0) return PlayState.PLAYING if state == Gst.State.PLAYING else PlayState.PAUSED def get_previous_track(self) -> Track: pass def get_shuffle(self) -> bool: # Shuffle isn't a thing in Nocturne the queue is what it is for the most part return False def get_volume(self) -> Volume: return Volume(self.player.gst.get_property("volume")) def is_mute(self) -> bool: return self.player.gst.get_property("volume") == 0 def is_playlist(self) -> bool: # Again, the queue is what it is, I'm not sure if I can get this info return False def is_repeating(self) -> bool: return self.player.settings.get_value('playback-mode').unpack() == 'repeat-one' def next(self): self.player.handle_song_change_request("next") def open_uri(self, uri:str): # ? pass def pause(self): self.player.gst.set_state(Gst.State.PAUSED) def play(self): self.player.gst.set_state(Gst.State.PLAYING) def previous(self): self.player.handle_song_change_request("previous") def resume(self): self.player.gst.set_state(Gst.State.PLAYING) def seek(self, time:Position, track_id: DbusObj | None = None): self.player.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, time*1000 ) self.player.emit_changes(self.player.mpris.player, changes=['Position']) def set_maximum_rate(self, value:Rate): # Idk pass def set_minimum_rate(self, value:Rate): # Idk pass def set_mute(self, value:bool): # TODO I'm not sure what to do when unmuting, should I save previous volume? pass def set_rate(self, value:Rate): # Idk pass def set_repeating(self, value:bool): self.player.settings.set_string('playback-mode', 'repeat-one' if value else 'consecutive') def set_shuffle(self, value:bool): # TODO not sure how I could implement this pass def set_volume(self, value:Volume): self.player.settings.set_double('volume', value) def stop(self): self.player.gst.set_state(Gst.State.NULL) def activate_playlist(self, id:DbusObj): pass def get_active_playlist(self) -> ActivePlaylist: #TODO pass def get_playlists(self, index:int, max_count:int, order:str, reverse:bool) -> list[PlaylistEntry]: #TODO return [] def add_track(self, uri:str, after_track:DbusObj, set_as_current:bool): pass def can_edit_tracks(self) -> bool: return False def get_tracks(self) -> list[DbusObj]: return [] def get_tracks_metadata(self, track_ids:list[DbusObj]) -> list[Metadata]: return [] def go_to(self, track_id:DbusObj): pass def remove_track(self, track_id:DbusObj): pass class Player(EventAdapter): __gtype_name__ = 'NocturnePlayer' def __init__(self, application): self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.set_double("volume", self.settings.get_value("volume").unpack()) self.application = application self.gst = Gst.ElementFactory.make("playbin", "music-player") self.gst.connect("source-setup", self.on_source_setup) self.gst.set_property("video-sink", Gst.ElementFactory.make("gtk4paintablesink", "video-sink")) self.gst.connect("video-changed", self.video_changed) self.bin = Gst.Bin.new("audio-filter-bin") # Equalizer self.equalizer = Gst.ElementFactory.make("equalizer-nbands", "equalizer") self.bin.add(self.equalizer) self.equalizer.set_property("num-bands", 6) for i in range(self.equalizer.get_property("num-bands")): band = self.equalizer.get_child_by_index(i) self.settings.bind( "eq-band-{}".format(i), band, "gain", Gio.SettingsBindFlags.DEFAULT ) # ReplayGain self.rg_volume = Gst.ElementFactory.make("rgvolume", "replaygain") self.rg_limiter = Gst.ElementFactory.make("rglimiter", "limiter") self.bin.add(self.rg_volume) self.bin.add(self.rg_limiter) self.rg_volume.set_property("album-mode", True) # Spectrum self.spectrum = Gst.ElementFactory.make("spectrum", "spectrum-analyzer") self.bin.add(self.spectrum) self.settings.bind( "visualizer-bar-n", self.spectrum, "bands", Gio.SettingsBindFlags.DEFAULT ) self.spectrum.set_property("threshold", -60) self.spectrum.set_property("post-messages", True) self.spectrum.set_property("message-magnitude", True) self.spectrum.set_property("multi-channel", True) self.spectrum.set_property("interval", 50000000) # Links self.equalizer.link(self.rg_volume) self.rg_volume.link(self.rg_limiter) self.rg_limiter.link(self.spectrum) sink_pad = Gst.GhostPad.new("sink", self.equalizer.get_static_pad("sink")) src_pad = Gst.GhostPad.new("src", self.spectrum.get_static_pad("src")) self.bin.add_pad(sink_pad) self.bin.add_pad(src_pad) self.gst.set_property("audio-filter", self.bin) self.gst.set_property("buffer-duration", 5 * Gst.SECOND) self.gst.set_property("buffer-size", 10 * 1024 * 1024) # 10MB I think self.updating_volume = False self.settings.connect("changed::volume", self.settings_volume_changed) self.gst.connect("notify::volume", self.gst_volume_changed) self.bus = self.gst.get_bus() self.bus.add_signal_watch() self.bus.connect("message::eos", lambda bus, msg: self.handle_song_change_request("end") if msg.src == self.gst else None) self.bus.connect("message::error", lambda bus, msg: logger.error(msg.parse_error()[0])) self.bus.connect("message::state-changed", self.handle_message_state_changed) self.bus.connect("message::tag", self.handle_message_tag) self.bus.connect("message::element", self.handle_message_element) self.adapter = PlayerAdapter(self) self.mpris = Server("com.jeffser.Nocturne", adapter=self.adapter) super().__init__(root=self.mpris.root, player=self.mpris.player) self.interface = MprisInterface("Nocturne", self.adapter) self.mpris_published = False try: self.mpris.publish() self.mpris_published = True except Exception as e: print("Failed to publish MPRIS:", e) GLib.timeout_add(64, self.update_stream_progress) self.last_song = '' self.pause_next_change = False self.last_gst_state_type = -1 self.discord_rpc = DiscordRPC(self) self.settings.connect('changed::discord-rpc-enabled', lambda *_: self.discord_rpc.update()) self.settings.connect('changed::discord-rpc-client-id', lambda *_: self.discord_rpc.update()) integration = get_current_integration() # Connect to Current Song connections = { 'title': self.update_title, 'radioStreamUrl': self.update_radioStreamUrl, 'artists': self.update_artists, 'trackGain': self.update_trackGain, 'albumGain': self.update_albumGain, 'gdkPaintable': lambda paintable: threading.Thread(target=self.update_palette, args=(paintable,), daemon=True).start() } for parameter, callback in connections.items(): integration.connect_to_current_song(parameter, callback) integration.connect_to_model('currentSong', 'songId', self.song_changed) integration.connect_to_model('currentSong', 'songId', lambda *_: self.discord_rpc.update()) integration.connect_to_model('currentSong', 'displaySongTitle', lambda *_: self.discord_rpc.update()) integration.connect_to_model('currentSong', 'displaySongArtist', lambda *_: self.discord_rpc.update()) def settings_volume_changed(self, settings, key): if not self.updating_volume: self.updating_volume = True try: value = settings.get_value(key).unpack() ** 3 self.gst.set_property('volume', value) finally: self.updating_volume = False def gst_volume_changed(self, gst, gp): if not self.updating_volume: self.updating_volume = True try: value = gst.get_property('volume') value = value ** (1/3) if value > 0 else 0.0 self.settings.set_double('volume', value) finally: self.updating_volume = False def on_source_setup(self, playbin, source): try: if GObject.type_is_a(source, Gst.ElementFactory.find("souphttpsrc").get_element_type()): if integration := get_current_integration(): source.set_property("ssl-strict", not integration.get_property('trustServer')) except: pass def video_changed(self, playbin): integration = get_current_integration() if playbin.get_property('n-video') or 0 > 0: songId = integration.loaded_models.get('currentSong').get_property('songId') integration.loaded_models.get('currentSong').set_property('videoId', songId) else: integration.loaded_models.get('currentSong').set_property('videoId', "") # --- def handle_song_change_request(self, action:str): # action can be next, previous or end (song ended) integration = get_current_integration() current_song_id = integration.loaded_models.get('currentSong').songId if current_song_id: position = integration.loaded_models.get('currentSong').get_property('positionSeconds') song = integration.loaded_models.get(current_song_id) duration = song.get_property('duration') if song else 0 if action == "end" or (duration > 0 and position >= (duration / 2)): threading.Thread(target=integration.scrobble, args=(current_song_id,), daemon=True).start() mode = self.settings.get_value('playback-mode').unpack() if action != "end" and mode == "repeat-one": mode = "consecutive" if action == "previous" and integration.loaded_models.get('currentSong').get_property('positionSeconds') > 5: self.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0 ) return id_list = [so.get_string() for so in integration.loaded_models.get('currentSong').get_property('queueModel')] integration.loaded_models.get('currentSong').set_property('magnitudes', {}) if len(id_list) > 0: if not current_song_id: # fallback in case nothing was playing integration.loaded_models.get('currentSong').set_property('songId', id_list[0]) elif mode in ('consecutive', 'repeat-all'): try: next_index = id_list.index(current_song_id) + (1 if action in ("next", "end") else -1) except ValueError: # index was not found next_index = 0 if mode == 'consecutive': if next_index < 0: integration.loaded_models.get('currentSong').set_property('songId', id_list[0]) elif next_index < len(id_list): integration.loaded_models.get('currentSong').set_property('songId', id_list[next_index]) elif self.settings.get_value('auto-play').unpack(): self.auto_play() elif mode == 'repeat-all': if next_index < len(id_list) and next_index >= 0: integration.loaded_models.get('currentSong').set_property('songId', id_list[next_index]) else: integration.loaded_models.get('currentSong').set_property('songId', id_list[0]) self.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0 ) elif mode == 'repeat-one': self.gst.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0 ) else: integration.loaded_models.get('currentSong').set_property('songId', None) def auto_play(self): if integration := get_current_integration(): generated_queue = integration.loaded_models.get('currentSong').get_property('generatedQueue') if generated_queue.get_property('n-items') == 0: self.application.get_active_window().activate_action( "app.generate_auto_play_queue", GLib.Variant('b', True) ) else: self.application.get_active_window().activate_action( "app.play_songs", GLib.Variant("as", [so.get_string() for so in list(generated_queue)]) ) def handle_spectrum(self, struct): serialized = struct.serialize_full(Gst.SerializeFlags.NONE) channels_str = serialized.split('< < ')[1].split(' > >;')[0].replace('(float)', '').split(' >, < ') channels = [] for c in channels_str: channels.append([float(m.strip()) for m in c.split(', ')[:int(self.spectrum.get_property('bands')/2)]]) integration = get_current_integration() timestamp = struct.get_uint64('stream-time')[1] / 1000000000 magnitudes = [(60-abs(m)) / 60 * self.settings.get_value("volume").unpack() for m in channels[0] + list(reversed(channels[1]))] if timestamp and magnitudes: if not integration.loaded_models.get('currentSong').get_property('magnitudes'): integration.loaded_models.get('currentSong').set_property('magnitudes', {}) integration.loaded_models.get('currentSong').magnitudes[timestamp] = magnitudes def handle_message_element(self, bus, message): if message.src == self.spectrum: struct = message.get_structure() if struct and struct.get_name() == "spectrum" and self.settings.get_value('show-visualizer').unpack(): threading.Thread(target=self.handle_spectrum, args=(struct,), daemon=True).start() def handle_message_state_changed(self, bus, message): if message.src == self.gst: old_state, new_state, pending_state = message.parse_state_changed() if pending_state == Gst.State.VOID_PENDING and new_state != Gst.State.READY: integration = get_current_integration() if not integration.loaded_models.get('currentSong').get_property('seeking'): is_playing = new_state == Gst.State.PLAYING integration.loaded_models.get("currentSong").set_property("buttonState", 'pause' if is_playing else 'play') self.emit_changes(self.mpris.player, changes=['Metadata', 'PlaybackStatus']) self.discord_rpc.update() def handle_message_tag(self, bus, message): integration = get_current_integration() if model := integration.loaded_models.get('currentSong'): if song_model := integration.loaded_models.get(model.get_property('songId')): if song_model.get_property('radioStreamUrl'): # is radio if tag_list := message.parse_tag(): success_title, title = tag_list.get_string(Gst.TAG_TITLE) if success_title and title and title != 'null': current_title = model.get_property('displaySongTitle') if current_title != title: model.set_property('displaySongTitle', title.strip()) success_artist, artist = tag_list.get_string(Gst.TAG_ARTIST) if success_artist and artist and artist != 'null': current_artist = model.get_property('displaySongArtist') if current_artist != artist: model.set_property('displaySongArtist', artist.strip()) if success_title or success_artist: self.emit_changes(self.mpris.player, changes=['Metadata', 'PlaybackStatus']) def update_stream_progress(self): if integration := get_current_integration(): if integration.loaded_models.get('currentSong').get_property('seeking'): return True success, position = self.gst.query_position(Gst.Format.TIME) current_song = integration.loaded_models.get('currentSong') if success: seconds = position / Gst.SECOND current_song.set_property('positionSeconds', seconds) return True def restore_play_queue(self): integration = get_current_integration() songs = self.application.external_songs if songs: for song in songs: integration.loaded_models[song.id] = song song_list = [s.id for s in songs] current_id = song_list[0] else: current_id, song_list = integration.getPlayQueue() for song in song_list: integration.verifySong(song) if len(song_list) > 0: if len(self.application.external_songs) == 0: self.pause_next_change = True self.application.get_active_window().activate_action( "app.play_songs", GLib.Variant("as", song_list) ) self.application.external_songs = [] def update_palette(self, paintable): # Load Image if not paintable: return raw_bytes = paintable.save_to_png_bytes().get_data() if not raw_bytes: return img_io = io.BytesIO(raw_bytes) # Generate Palette palette = ColorThief(img_io).get_palette(quality=10, color_count=2) # Blur Image with Image.open(img_io) as img: small_img = img.resize((24, 24)) blurred_img = small_img.filter(ImageFilter.GaussianBlur(radius=2)) if blurred_img.mode != "RGBA": blurred_img = blurred_img.convert("RGBA") blurred_img.putalpha(int(255 * 0.3)) buffer = io.BytesIO() blurred_img.save(buffer, format="PNG") blur_str = "data:image/png;base64,{}".format(base64.b64encode(buffer.getvalue()).decode("utf-8")) # Make and Load CSS css = f""" window.dynamic-accent {{ --accent-color: oklab(from rgb({','.join([str(c) for c in palette[0]])}) var(--standalone-color-oklab)); }} window.popout-window.dynamic-bg-blur, window.dynamic-bg-blur bottom-sheet#main-bottom-sheet sheet > stack, window.dynamic-bg-blur:not(.global-dynamic-bg-gradient):not(.global-dynamic-bg-blur) overlay-split-view .main_sidebar, window.global-dynamic-bg-blur {{ background-image: url("{blur_str}"); }} window.popout-window.dynamic-bg-gradient, window.dynamic-bg-gradient bottom-sheet#main-bottom-sheet sheet > stack, window.dynamic-bg-gradient:not(.global-dynamic-bg-gradient):not(.global-dynamic-bg-blur) overlay-split-view .main_sidebar, window.global-dynamic-bg-gradient {{ background-image: linear-gradient( to bottom, rgba({','.join([str(c) for c in palette[0]])},0.4), rgba({','.join([str(c) for c in palette[1]])},0.4) ); }} """ self.application.css_provider.load_from_string(css) integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('accentColor', palette[0]) def update_title(self, title:str): integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('displaySongTitle', title) def update_radioStreamUrl(self, radioStreamUrl:str): pass #Maybe add the radio stream URL to the currentSong or something? def update_artists(self, artists:list): integration = get_current_integration() integration.loaded_models.get('currentSong').set_property('displaySongArtist', artists[0].get('name') if len(artists) > 0 else '') def update_trackGain(self, trackGain:float): if self.settings.get_value('use-gain').unpack(): self.rg_volume.set_property('fallback-gain', trackGain) self.rg_volume.set_property('album-mode', False) def update_albumGain(self, albumGain:float): if self.settings.get_value('use-gain').unpack() and self.rg_volume.get_property('fallback-gain') == 0.0: self.rg_volume.set_property('fallback-gain', albumGain) self.rg_volume.set_property('album-mode', True) def song_changed(self, song_id:str): integration = get_current_integration() if song_id: if song_id != self.last_song: self.last_song = song_id threading.Thread(target=integration.scrobble, args=(song_id,), kwargs={'submission': False}, daemon=True).start() # Volume warning if self.gst.get_property('volume') == 0: if active_window := self.application.props.active_window: active_window.toast_overlay.add_toast(Adw.Toast( title=_("Warning: Song changed but volume is set to 0") )) if stream_url := integration.get_stream_url(song_id): self.gst.set_state(Gst.State.READY) self.gst.set_property('uri', stream_url) if self.pause_next_change: self.gst.set_state(Gst.State.PAUSED) self.pause_next_change = False else: self.gst.set_state(Gst.State.PLAYING) # Fix duration if model := integration.loaded_models.get(song_id): if not model.get_property('duration'): self.gst.get_state(Gst.CLOCK_TIME_NONE) success, duration = self.gst.query_duration(Gst.Format.TIME) if success: model.set_property('duration', duration / Gst.SECOND) else: self.gst.set_state(Gst.State.NULL) else: self.gst.set_state(Gst.State.NULL) Jeffser-Nocturne-b442fee/src/widgets/playing/popout_window.py000066400000000000000000000103231523240030000245410ustar00rootroot00000000000000# popout_window.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ...constants import get_display_time import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/popout_window.ui') class PopoutWindow(Adw.ApplicationWindow): __gtype_name__ = 'NocturnePopoutWindow' toolbarview = Gtk.Template.Child() header_view_switcher = Gtk.Template.Child() breakpoint_el = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() playing_page = Gtk.Template.Child() lyrics_page = Gtk.Template.Child() queue_page = Gtk.Template.Child() footer = Gtk.Template.Child() split_view = Gtk.Template.Child() footer_spectrum_el = Gtk.Template.Child() fullscreen_btn = None def __init__(self, application, fullscreened): super().__init__( application=application, fullscreened=fullscreened ) integration = get_current_integration() current_song_id = integration.loaded_models.get('currentSong').get_property('songId') self.playing_page.last_song_id = current_song_id GLib.idle_add(self.playing_page.setup) GLib.idle_add(self.lyrics_page.setup) GLib.idle_add(self.footer.setup) GLib.idle_add(self.queue_page.setup) self.playing_page.header_bar.get_ancestor(Adw.ToolbarView).set_extend_content_to_top_edge(False) self.playing_page.header_bar.set_show_start_title_buttons(True) self.playing_page.header_bar.set_show_end_title_buttons(True) self.footer.set_property('forceHugeMode', True) self.fullscreen_btn = Gtk.Button( icon_name="view-fullscreen-symbolic", tooltip_text=_("Toggle Fullscreen") ) self.fullscreen_btn.connect('clicked', self.toggle_fullscreen) self.playing_page.header_bar.pack_start(self.fullscreen_btn) self.footer_spectrum_el.setup() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect('changed::popout-dynamic-bg-mode', self.dynamic_bg_mode_changed) self.dynamic_bg_mode_changed(self.settings, 'popout-dynamic-bg-mode') self.settings.connect('changed::use-dynamic-accent', self.css_toggled, 'dynamic-accent') self.css_toggled(self.settings, 'use-dynamic-accent', 'dynamic-accent') self.playing_page.cover_art_el.view_stack_el.connect('notify::visible-child-name', self.cover_art_mode_toggled) def cover_art_mode_toggled(self, view_stack, gparam): mode = view_stack.get_visible_child_name() self.playing_page.clamp.set_maximum_size(1920 if mode == 'video' else 720) def css_toggled(self, settings, key, css_class): if settings.get_value(key).unpack(): self.add_css_class(css_class) else: self.remove_css_class(css_class) def dynamic_bg_mode_changed(self, settings, key): value = settings.get_value(key).unpack() self.remove_css_class('dynamic-bg-gradient') self.remove_css_class('dynamic-bg-blur') if value: self.add_css_class('dynamic-bg-{}'.format(value)) @Gtk.Template.Callback() def close_request(self, window): if application := self.get_application(): application.uninhibit_idle() if self.get_application().main_window.get_hide_on_close(): self.get_application().main_window.present() def toggle_fullscreen(self, button): if self.is_fullscreen(): self.unfullscreen() else: self.fullscreen() @Gtk.Template.Callback() def big_mode_apply(self, breakpoint_el): self.add_css_class('big-mode') @Gtk.Template.Callback() def big_mode_unapply(self, breakpoint_el): self.remove_css_class('big-mode') @Gtk.Template.Callback() def fullscreen_toggled(self, window, gparam): fullscreen = window.is_fullscreen() if self.fullscreen_btn: self.fullscreen_btn.set_icon_name('view-unfullscreen-symbolic' if fullscreen else 'view-fullscreen-symbolic') if application := self.get_application(): if fullscreen: application.inhibit_idle(self) else: application.uninhibit_idle() Jeffser-Nocturne-b442fee/src/widgets/playing/queue_page.py000066400000000000000000000035151523240030000237510ustar00rootroot00000000000000# queue_page.py from gi.repository import Gtk, GLib, Gio from ..song import SongRow from ...integrations import get_current_integration import threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/queue_page.ui') class PlayingQueuePage(Gtk.ScrolledWindow): __gtype_name__ = 'NocturnePlayingQueuePage' song_list_el = Gtk.Template.Child() autoplay_row_el = Gtk.Template.Child() autoplay_spinner_el = Gtk.Template.Child() list_bin_el = Gtk.Template.Child() def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( 'auto-play', self.autoplay_row_el, 'active', Gio.SettingsBindFlags.DEFAULT ) self.song_list_el.main_stack.set_visible_child_name('content') def setup(self): integration = get_current_integration() integration.connect_to_model('currentSong', 'generatingQueue', self.autoplay_spinner_el.get_parent().set_visible) global_queue = integration.loaded_models.get('currentSong').get_property('queueModel') if len(list(self.song_list_el.list_el)) == 0: self.queue_changed(global_queue, 0, 0, global_queue.get_property('n-items')) global_queue.connect('items-changed', self.queue_changed) def queue_changed(self, global_queue, position:int, removed:int, added:int): for _ in range(removed): if row := self.song_list_el.list_el.get_row_at_index(position): self.song_list_el.list_el.remove(row) for i in range(added): if item := global_queue.get_item(position + i): row = SongRow( item.get_string(), draggable=True, removable=True ) self.song_list_el.list_el.insert(row, position + i) Jeffser-Nocturne-b442fee/src/widgets/playing/spectrum.py000066400000000000000000000135271523240030000234770ustar00rootroot00000000000000# spectrum.py from gi.repository import Gtk, GLib, Gio from ...integrations import get_current_integration from colorthief import ColorThief import io, threading, math class Spectrum(Gtk.DrawingArea): __gtype_name__ = 'NocturneSpectrum' def setup(self): integration = get_current_integration() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") default_bar_n = self.settings.get_value('visualizer-bar-n').unpack() self.target_magnitudes = [0] * default_bar_n self.current_magnitudes = [0] * default_bar_n self.stopped = False self.accent_color = [0.2, 0.6, 0.9] self.speed = 0.03 self.set_draw_func(self.on_draw) integration.connect_to_model('currentSong', 'buttonState', self.playback_changed) integration.connect_to_model('currentSong', 'positionSeconds', self.on_timestamp_changed) integration.connect_to_model('currentSong', 'accentColor', self.update_color) self.settings.bind( "show-visualizer", self, "visible", Gio.SettingsBindFlags.DEFAULT ) GLib.timeout_add(16, self.on_tick) def on_timestamp_changed(self, timestamp:float): if self.stopped: self.target_magnitudes = [0] * self.settings.get_value('visualizer-bar-n').unpack() elif integration := get_current_integration(): if magnitudes_dict := integration.loaded_models.get('currentSong').get_property('magnitudes'): if next_timestamp := min((k for k in magnitudes_dict if k >= timestamp), default=None): self.target_magnitudes = magnitudes_dict.get(next_timestamp) def on_tick(self): if self.get_visible(): if len(self.target_magnitudes) != len(self.current_magnitudes): self.current_magnitudes = [0] * len(self.target_magnitudes) for i in range(len(self.target_magnitudes)): speed_modifier = self.speed * max(0.25, min(1, 10*abs(self.target_magnitudes[i] - self.current_magnitudes[i]))) if self.target_magnitudes[i] >= self.current_magnitudes[i]: self.current_magnitudes[i] = min(self.target_magnitudes[i], self.current_magnitudes[i] + speed_modifier) else: self.current_magnitudes[i] = max(0, self.current_magnitudes[i] - speed_modifier) self.queue_draw() return True def on_draw(self, drawing_area, cr, width, height): if not self.current_magnitudes or min(width, height) < 24: return n = len(self.current_magnitudes) dx = width / (n - 1) fill_mode = self.settings.get_value("visualizer-fill-mode").unpack() visualizer_type = self.settings.get_value('visualizer-type').unpack() if visualizer_type == "wave" and not fill_mode == "border": cr.move_to(0, height) opacity = 0.75 if fill_mode == "translucent" else 1 if self.settings.get_value("visualizer-auto-color").unpack(): if self.settings.get_value("visualizer-auto-color-invert").unpack(): cr.set_source_rgba(*[1-c for c in self.accent_color], opacity) else: cr.set_source_rgba(*self.accent_color, opacity) else: try: rgb_str = self.settings.get_value("visualizer-manual-color").unpack() cr.set_source_rgba(*[float(c) for c in rgb_str.split(',')], opacity) except: cr.set_source_rgba(*self.accent_color, opacity) if visualizer_type == "wave": cr.line_to(0, height * (1 - self.current_magnitudes[0])) for i in range(n - 1): x1 = i * dx y1 = height * (1 - self.current_magnitudes[i]) x2 = (i + 1) * dx y2 = height * (1 - self.current_magnitudes[i + 1]) # Control points for smoothness xc = (x1 + x2) / 2 cr.curve_to(xc, y1, xc, y2, x2, y2) if fill_mode == "border": cr.set_line_width(3.0) cr.stroke() else: cr.line_to(width, height) cr.close_path() cr.fill_preserve() elif visualizer_type in ("bars", "particles"): gap = 2 if visualizer_type == "bars" else 4 bar_w = dx - gap radius = 6 if visualizer_type == "bars" else bar_w for i in range(n): bar_x = (i * dx) - (bar_w / 2) if visualizer_type == "particles": bar_h = bar_w bar_y = max(0, min(height * (1 - self.current_magnitudes[i]), height - bar_h)) else: bar_h = self.current_magnitudes[i] * height bar_y = height - bar_h if bar_h > 1: r = min(radius, bar_w / 2, bar_h / 2) cr.new_sub_path() cr.arc(bar_x + bar_w - r, bar_y + r, r, -math.pi/2, 0) cr.arc(bar_x + bar_w - r, bar_y + bar_h - r, r, 0, math.pi/2) cr.arc(bar_x + r, bar_y + bar_h - r, r, math.pi/2, math.pi) cr.arc(bar_x + r, bar_y + r, r, math.pi, 3*math.pi/2) if fill_mode == "border": cr.set_line_width(3.0) cr.close_path() cr.stroke() else: cr.close_path() cr.fill() def update_color(self, accent_color): if accent_color: self.accent_color = [min(c/255, 1) for c in accent_color] else: self.accent_color = [0.2, 0.6, 0.9] def playback_changed(self, playbackState:str): if playbackState == "play": self.stopped = True else: self.stopped = False Jeffser-Nocturne-b442fee/src/widgets/playing/volume_button.py000066400000000000000000000016261523240030000245340ustar00rootroot00000000000000# volume_button.py from gi.repository import Gtk, Gio @Gtk.Template(resource_path='/com/jeffser/Nocturne/playing/volume_button.ui') class VolumeButton(Gtk.MenuButton): __gtype_name__ = 'NocturneVolumeButton' volume_el = Gtk.Template.Child() def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "volume", self.volume_el.get_adjustment(), "value", Gio.SettingsBindFlags.DEFAULT ) @Gtk.Template.Callback() def on_volume_changed(self, scale_el): value = scale_el.get_value() if value == 0: self.set_icon_name("speaker-0-symbolic") elif value < 0.33: self.set_icon_name("speaker-1-symbolic") elif value < 0.66: self.set_icon_name("speaker-2-symbolic") else: self.set_icon_name("speaker-3-symbolic") Jeffser-Nocturne-b442fee/src/widgets/playlist/000077500000000000000000000000001523240030000214515ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/playlist/__init__.py000066400000000000000000000002771523240030000235700ustar00rootroot00000000000000# __init__.py from .button import PlaylistButton from .page import PlaylistPage from .row import PlaylistRow from .dialog import PlaylistDialog from .selector_row import PlaylistSelectorRow Jeffser-Nocturne-b442fee/src/widgets/playlist/button.py000066400000000000000000000063121523240030000233400ustar00rootroot00000000000000# button.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_PLAYLIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/playlist/button.ui') class PlaylistButton(Gtk.Box): __gtype_name__ = 'NocturnePlaylistButton' play_el = Gtk.Template.Child() cover_button_el = Gtk.Template.Child() cover_el = Gtk.Template.Child() name_el = Gtk.Template.Child() name_label_el = Gtk.Template.Child() song_count_label_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyPlaylist(self.id) super().__init__() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::button-size", lambda *_: self.update_size()) self.play_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.cover_button_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.name_el.set_action_target_value(GLib.Variant.new_string(self.id)) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'songCount', self.update_song_count) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) def update_size(self): isBig = self.settings.get_value('button-size').unpack() == 'big' size = 240 if isBig else 180 pixel_size = size if self.cover_el.get_paintable() is not None else -1 self.cover_el.set_size_request(size, size) self.cover_el.set_pixel_size(pixel_size) if isBig: self.name_label_el.remove_css_class('title-4') self.name_label_el.add_css_class('title-3') else: self.name_label_el.remove_css_class('title-3') self.name_label_el.add_css_class('title-4') def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-note-symbolic") self.update_size() def update_name(self, name:str): self.name_el.set_tooltip_text(name) self.name_label_el.set_label(name) self.cover_button_el.set_tooltip_text(name) self.set_name(name) def update_song_count(self, songCount:int): self.song_count_label_el.set_label(ngettext("{} Song", "{} Songs", songCount).format(songCount)) self.song_count_label_el.set_visible(songCount) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] context = CONTEXT_PLAYLIST.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] popover = Gtk.Popover( child=ContextContainer(context, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/playlist/dialog.py000066400000000000000000000042721523240030000232670ustar00rootroot00000000000000# dialog.py from gi.repository import Gtk, Adw, GLib from .selector_row import PlaylistSelectorRow from ...integrations import get_current_integration import re @Gtk.Template(resource_path='/com/jeffser/Nocturne/playlist/dialog.ui') class PlaylistDialog(Adw.Dialog): __gtype_name__ = 'NocturnePlaylistDialog' search_entry_el = Gtk.Template.Child() preferences_group_el = Gtk.Template.Child() list_el = Gtk.Template.Child() add_button_el = Gtk.Template.Child() def __init__(self, song_list:list): self.song_list = song_list integration = get_current_integration() super().__init__() for id in integration.getPlaylists(): row = PlaylistSelectorRow(id) target_value = GLib.Variant('a{sv}', { 'playlist': GLib.Variant('s', id), 'songs': GLib.Variant('as', self.song_list) }) row.set_action_target_value(target_value) self.list_el.append(row) if len(self.song_list) > 1: self.preferences_group_el.set_description(_("{} Songs").format(len(self.song_list))) else: integration.verifySong(self.song_list[0], force_update=True, use_threading=False) self.preferences_group_el.set_description(integration.loaded_models.get(self.song_list[0]).get_property('title')) self.search_changed(self.search_entry_el) @Gtk.Template.Callback() def search_changed(self, entry): query = entry.get_text() rows = list(self.list_el)[1:] exact_match = False for child in rows: child.set_visible(re.search(query, child.get_name(), re.IGNORECASE)) if child.get_name().lower() == query.lower(): exact_match = True self.add_button_el.set_visible((not exact_match and query) or len(rows) == 0) target_value = GLib.Variant('a{sv}', { 'new_playlist': GLib.Variant('s', query), 'songs': GLib.Variant('as', self.song_list) }) self.add_button_el.set_action_target_value(target_value) self.add_button_el.set_action_name('app.add_songs_to_playlist') self.add_button_el.set_title(query or _("New Playlist")) Jeffser-Nocturne-b442fee/src/widgets/playlist/meson.build000066400000000000000000000004201523240030000236070ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'playlist' widgets = [ '__init__.py', 'button.py', 'page.py', 'row.py', 'dialog.py', 'selector_row.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/playlist/page.py000066400000000000000000000117371523240030000227500ustar00rootroot00000000000000# page.py from gi.repository import Gtk, Adw, GLib from ...integrations import get_current_integration from ...constants import CONTEXT_PLAYLIST, get_display_time from ..containers import get_context_buttons_list from ..song import SongRow import threading, io from colorthief import ColorThief @Gtk.Template(resource_path='/com/jeffser/Nocturne/playlist/page.ui') class PlaylistPage(Adw.NavigationPage): __gtype_name__ = 'NocturnePlaylistPage' clamp_el = Gtk.Template.Child() cover_el = Gtk.Template.Child() name_el = Gtk.Template.Child() song_count_el = Gtk.Template.Child() duration_el = Gtk.Template.Child() song_list_el = Gtk.Template.Child() context_wrap_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyPlaylist(self.id, True) super().__init__() context = CONTEXT_PLAYLIST.copy() del context['edit'] del context['delete'] if 'no-downloads' in get_current_integration().limitations: del context['download'] context_buttons = get_context_buttons_list(context, self.id) for btn in context_buttons: self.context_wrap_el.append(btn) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'songCount', self.update_song_count) integration.connect_to_model(self.id, 'duration', self.update_duration) integration.connect_to_model(self.id, 'entry', self.update_song_list) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) self.song_list_el.playlist_id = self.id self.song_ids = [] self.current_offset = 0 self.loading = False def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(240) self.update_background(paintable.save_to_png_bytes().get_data()) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-note-symbolic") self.cover_el.set_pixel_size(-1) def update_background(self, raw_bytes:bytes): def run(): img_io = io.BytesIO(raw_bytes) color = ColorThief(img_io).get_color(quality=10) css = f""" clamp {{ transition: background .2s; background: linear-gradient(180deg, color-mix(in srgb, rgb({','.join([str(c) for c in color])}) 50%, transparent), transparent 30%); background-size: 100% 1000px; background-repeat: no-repeat; }} """ provider = Gtk.CssProvider() provider.load_from_data(css.encode()) GLib.idle_add(self.clamp_el.get_style_context().add_provider, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) if raw_bytes: threading.Thread(target=run, daemon=True).start() def update_name(self, name:str): self.name_el.set_label(name) self.name_el.set_visible(name) self.set_title(name or _('Playlist')) self.set_name(name) def load_song_rows(self): # Pagination of Playlist in order to load hundreds if self.current_offset < len(self.song_ids) and not self.loading: self.loading = True for songId in self.song_ids[self.current_offset:self.current_offset+50]: row = SongRow(songId, removable=True) row.set_action_name('app.play_song_from_list') row.set_action_target_value(GLib.Variant('a{sv}', { 'songId': GLib.Variant('s', songId), 'songs': GLib.Variant('as', self.song_ids), 'originId': GLib.Variant('s', self.id) })) GLib.idle_add(self.song_list_el.list_el.append, row) self.current_offset += 50 GLib.idle_add(setattr, self, 'loading', False) def update_song_list(self, song_list:list): self.song_list_el.list_el.remove_all() self.song_list_el.main_stack.set_visible_child_name('content' if len(song_list) > 0 else 'no-content') self.song_ids = [s.get('id') for s in song_list] self.current_offset = 0 threading.Thread(target=self.load_song_rows, daemon=True).start() @Gtk.Template.Callback() def scroll_edge_reached(self, scrolledwindow, pos): if pos == Gtk.PositionType.BOTTOM: threading.Thread(target=self.load_song_rows, daemon=True).start() def update_song_count(self, songCount:int): self.song_count_el.set_label(ngettext("{} Song", "{} Songs", songCount).format(songCount)) self.song_count_el.set_visible(songCount) def update_duration(self, duration:int): self.duration_el.set_label(get_display_time(duration)) self.duration_el.set_visible(duration) def reload(self): pass Jeffser-Nocturne-b442fee/src/widgets/playlist/row.py000066400000000000000000000050731523240030000226370ustar00rootroot00000000000000# row.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_PLAYLIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/playlist/row.ui') class PlaylistRow(Adw.ActionRow): __gtype_name__ = 'NocturnePlaylistRow' cover_el = Gtk.Template.Child() menu_button_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyPlaylist(self.id) super().__init__() self.set_action_target_value(GLib.Variant.new_string(self.id)) integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'songCount', self.update_song_count) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") settings.bind( "show-context-button", self.menu_button_el, "visible", Gio.SettingsBindFlags.DEFAULT ) def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(48) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-note-symbolic") self.cover_el.set_pixel_size(-1) def update_name(self, name:str): self.set_title(name) self.set_name(name) def update_song_count(self, songCount:int): self.set_subtitle(ngettext("{} Song", "{} Songs", songCount).format(songCount)) @Gtk.Template.Callback() def on_context_button_active(self, button, gparam): context = CONTEXT_PLAYLIST.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] button.get_popover().set_child(ContextContainer(context, self.id)) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] context = CONTEXT_PLAYLIST.copy() if 'no-downloads' in get_current_integration().limitations: del context['download'] popover = Gtk.Popover( child=ContextContainer(context, self.id), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/playlist/selector_row.py000066400000000000000000000025431523240030000245360ustar00rootroot00000000000000# selector_row.py from gi.repository import Gtk, Adw from ...integrations import get_current_integration @Gtk.Template(resource_path='/com/jeffser/Nocturne/playlist/selector_row.ui') class PlaylistSelectorRow(Adw.ActionRow): __gtype_name__ = 'NocturnePlaylistSelectorRow' cover_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifyPlaylist(self.id) super().__init__() integration.connect_to_model(self.id, 'name', self.update_name) integration.connect_to_model(self.id, 'songCount', self.update_song_count) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(48) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-note-symbolic") self.cover_el.set_pixel_size(-1) def update_name(self, name:str): self.set_title(name) self.set_name(name) self.set_tooltip_text(_("Add Songs to '{}'").format(name)) def update_song_count(self, songCount:int): self.set_subtitle(ngettext("{} Song", "{} Songs", songCount).format(songCount)) Jeffser-Nocturne-b442fee/src/widgets/song/000077500000000000000000000000001523240030000205565ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/src/widgets/song/__init__.py000066400000000000000000000002661523240030000226730ustar00rootroot00000000000000# __init__.py from .row import SongRow from .queue import SongQueue from .small_row import SongSmallRow from .details_dialog import SongDetailsDialog from .button import SongButton Jeffser-Nocturne-b442fee/src/widgets/song/button.py000066400000000000000000000124641523240030000224520ustar00rootroot00000000000000# button.py from gi.repository import Gtk, Adw, GLib, Gdk, Gio from ...integrations import get_current_integration from ...constants import CONTEXT_SONG, CONTEXT_ARTIST from ..containers import ContextContainer @Gtk.Template(resource_path='/com/jeffser/Nocturne/song/button.ui') class SongButton(Gtk.Box): __gtype_name__ = 'NocturneSongButton' cover_el = Gtk.Template.Child() cover_button_el = Gtk.Template.Child() play_indicator_el = Gtk.Template.Child() name_el = Gtk.Template.Child() artist_el = Gtk.Template.Child() def __init__(self, id:str): self.id = id integration = get_current_integration() integration.verifySong(self.id) super().__init__() self.cover_button_el.set_action_target_value(GLib.Variant.new_string(self.id)) self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::button-size", lambda *_: self.update_size()) integration.connect_to_model(self.id, 'title', self.update_name) integration.connect_to_model(self.id, 'artists', self.update_artists) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model('currentSong', 'songId', self.current_song_changed) def update_size(self): isBig = self.settings.get_value('button-size').unpack() == 'big' size = 240 if isBig else 180 pixel_size = size if self.cover_el.get_paintable() is not None else -1 self.cover_el.set_size_request(size, size) self.cover_el.set_pixel_size(pixel_size) if isBig: self.name_el.remove_css_class('title-4') self.name_el.add_css_class('title-3') else: self.name_el.remove_css_class('title-3') self.name_el.add_css_class('title-4') def update_name(self, name:str): self.name_el.set_label(name) self.cover_button_el.set_tooltip_text(name) self.set_name(name) def update_artists(self, artists:list): if artists: if artist_name := artists[0].get('name'): self.artist_el.get_child().set_label(artist_name) self.artist_el.set_tooltip_text(artist_name) if artist_id := artists[0].get('id'): self.artist_el.set_action_target_value(GLib.Variant('s', artist_id)) self.artist_el.set_action_name('app.show_artist') return self.artist_el.get_child().set_label("") self.artist_el.set_tooltip_text("") def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-queue-symbolic") self.update_size() def current_song_changed(self, songId): self.play_indicator_el.set_visible(self.id == songId) if self.id == songId: self.cover_button_el.set_action_name(None) self.cover_button_el.add_css_class('accent') else: self.cover_button_el.set_action_name("app.play_song") self.cover_button_el.remove_css_class('accent') def generate_context_menu(self) -> ContextContainer: integration = get_current_integration() context_dict = CONTEXT_SONG.copy() del context_dict["edit-radio"] del context_dict["delete-radio"] del context_dict["remove"] del context_dict["select"] context_dict["play-next"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id context_dict["play-later"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id context_dict['rating']['value'] = integration.loaded_models.get(self.id).get_property('userRating') if integration.__gtype_name__ == 'NocturneIntegrationOffline': context_dict["delete-download"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id else: del context_dict["delete-download"] if 'no-downloads' in integration.limitations: del context_dict["download"] return ContextContainer(context_dict, self.id) @Gtk.Template.Callback() def show_popover_image(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=self.generate_context_menu(), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() @Gtk.Template.Callback() def show_popover_artist(self, *args): integration = get_current_integration() artist_id = integration.loaded_models.get(self.id).get_property('artistId') if artist_id: rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=ContextContainer(CONTEXT_ARTIST, artist_id), pointing_to=rect, has_arrow=False ) popover.set_parent(self.artist_el) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/song/details_dialog.py000066400000000000000000000103071523240030000240750ustar00rootroot00000000000000# details_dialog.py from gi.repository import Gtk, Adw, GLib from ...integrations import get_current_integration from ...constants import get_display_time @Gtk.Template(resource_path='/com/jeffser/Nocturne/song/details_dialog.ui') class SongDetailsDialog(Adw.Dialog): __gtype_name__ = 'NocturneSongDetailsDialog' cover_el = Gtk.Template.Child() title_el = Gtk.Template.Child() listbox_el = Gtk.Template.Child() def __init__(self, model_id:str): self.id = model_id integration = get_current_integration() super().__init__() value_transformations = { "size": lambda size: "{} MB".format(round(size * 0.000001, 2)), "starred": lambda starred: _("Yes") if starred else _("No"), "duration": lambda duration: get_display_time(duration), "bitRate": lambda bitRate: "{} kbps".format(bitRate), "bitDepth": lambda bitDepth: "{}-bit".format(bitDepth), "samplingRate": lambda samplingRate: "{} kHz".format(samplingRate / 1000), "musicBrainzId": lambda mbid: mbid or _("Not Found") } property_order = [ "album", "artist", "artists", "musicBrainzId", "year", "duration", "starred", "discNumber", "track", "size", "contentType", "path", "suffix", "bitRate", "bitDepth", "samplingRate", "channelCount", "trackGain", "albumGain", "bpm", "genres" ] order_map = {name: i for i, name in enumerate(property_order)} song_details = integration.getSongDetails(self.id) self.title_el.set_label(song_details.get_property('title')) paintable = None if model := integration.loaded_models.get(self.id): paintable = model.get_property('gdkPaintable') self.cover_el.set_paintable(paintable) self.cover_el.set_visible(paintable) property_list = song_details.list_properties() property_list.sort(key=lambda x: order_map.get(x.get_name(), -1)) for prop in property_list: if nick := prop.get_nick(): raw_value = song_details.get_property(prop.get_name()) if isinstance(raw_value, list): expander = Adw.ExpanderRow( title=nick ) for item in raw_value: row = Adw.ActionRow( title=item.get('name', str(item)), use_markup=False ) if model_id := item.get('id'): row.set_action_name('app.show_artist') row.set_action_target_value(GLib.Variant('s', model_id)) row.set_activatable(True) expander.add_row(row) self.listbox_el.append(expander) else: row = Adw.ActionRow( title=nick, subtitle=value_transformations.get(prop.get_name(), lambda val: str(val))(raw_value), css_classes=['property'], use_markup=False ) if prop.get_name() == 'album': if model_id := song_details.get_property('albumId'): row.set_action_name('app.show_album') row.set_action_target_value(GLib.Variant('s', model_id)) row.set_activatable(True) elif prop.get_name() == 'artist': if model_id := song_details.get_property('artistId'): row.set_action_name('app.show_artist') row.set_action_target_value(GLib.Variant('s', model_id)) row.set_activatable(True) elif prop.get_name() == 'musicBrainzId': if mbid := song_details.get_property('musicBrainzId'): row.set_action_name('') row_set_action_target_value(GLib.Variant('s', 'https://musicbrainz.org/recording/{}'.format(mbid))) row.set_activatable(True) self.listbox_el.append(row) Jeffser-Nocturne-b442fee/src/widgets/song/meson.build000066400000000000000000000004221523240030000227160ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'song' widgets = [ '__init__.py', 'row.py', 'queue.py', 'small_row.py', 'details_dialog.py', 'button.py', ] install_data(widgets, install_dir: moduledir)Jeffser-Nocturne-b442fee/src/widgets/song/queue.py000066400000000000000000000153231523240030000222600ustar00rootroot00000000000000# queue.py from gi.repository import GObject, Gtk, GLib from ...integrations import get_current_integration @Gtk.Template(resource_path='/com/jeffser/Nocturne/song/queue.ui') class SongQueue(Gtk.Box): __gtype_name__ = 'NocturneSongQueue' header_button = Gtk.Template.Child() main_stack = Gtk.Template.Child() toolbar_revealer_el = Gtk.Template.Child() list_el = Gtk.Template.Child() remove_el = Gtk.Template.Child() play_el = Gtk.Template.Child() play_next_el = Gtk.Template.Child() play_later_el = Gtk.Template.Child() download_el = Gtk.Template.Child() delete_download_el = Gtk.Template.Child() playlist_id:str = "" header_label = GObject.Property(type=str, default="") header_icon_name = GObject.Property(type=str, default="") header_page_tag = GObject.Property(type=str, default="") def set_selected_mode(self, select:bool=False, selected_row:Gtk.Widget=None): integration = get_current_integration() for row in list(self.list_el): if row.__gtype_name__ != 'NocturneDiscIndicator': row.suffixes_stack_el.set_visible_child_name('select' if select else 'normal') row.check_el.set_active(row == selected_row) row.set_activatable(not select and row.id != integration.loaded_models.get('currentSong').get_property('songId')) if select: self.remove_el.set_visible(selected_row.removable) self.play_el.set_visible(not selected_row.draggable) self.play_next_el.set_visible(not selected_row.draggable) self.play_later_el.set_visible(not selected_row.draggable) self.delete_download_el.set_visible(integration.__gtype_name__ == 'NocturneIntegrationOffline') self.download_el.set_visible('no-downloads' not in integration.limitations) self.toolbar_revealer_el.set_reveal_child(select) def get_selected_rows(self) -> list: return [row for row in list(self.list_el) if row.__gtype_name__ != 'NocturneDiscIndicator' and row.check_el.get_active()] def get_selected_indexes(self) -> list: return [i for i, row in enumerate(list(self.list_el)) if row.__gtype_name__ != 'NocturneDiscIndicator' and row.check_el.get_active()] def get_all_ids(self) -> list: return [row.id for row in list(self.list_el) if row.__gtype_name__ != 'NocturneDiscIndicator'] @Gtk.Template.Callback() def close_selector(self, button=None): self.set_selected_mode() @Gtk.Template.Callback() def delete_download_selected(self, button): selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] target_value = GLib.Variant('as', selected_ids) self.get_root().activate_action("app.delete_downloads", target_value) self.close_selector() @Gtk.Template.Callback() def download_selected(self, button): selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] target_value = GLib.Variant('as', selected_ids) self.get_root().activate_action("app.download_songs", target_value) self.close_selector() @Gtk.Template.Callback() def remove_selected(self, button): if self.playlist_id: # is playlist indexes = self.get_selected_indexes() target_value = GLib.Variant('a{sv}', { 'playlist': GLib.Variant('s', self.playlist_id), 'indexes': GLib.Variant('as', [str(i) for i in indexes]) }) self.get_root().activate_action("app.remove_songs_from_playlist", target_value) for index in indexes: self.list_el.remove(list(self.list_el)[index]) self.main_stack.set_visible_child_name('content' if len(list(self.list_el)) > 0 else 'no-content') else: integration = get_current_integration() queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') all_ids = [so.get_string() for so in list(queue_model)] selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] current_song_id = integration.loaded_models.get('currentSong').get_property('songId') if current_song_id in selected_ids: # handle changing song if len(selected_rows) == len(all_ids): new_id = None else: new_id = [s for s in all_ids if s not in selected_ids][0] integration.loaded_models.get('currentSong').set_property('songId', new_id) indexes_to_be_removed = [] for i, song_id in enumerate(all_ids): if song_id in selected_ids: indexes_to_be_removed.append(i) for index in reversed(indexes_to_be_removed): queue_model.remove(index) self.close_selector() @Gtk.Template.Callback() def select_all(self, button): for row in list(self.list_el): row.check_el.set_active(True) @Gtk.Template.Callback() def play_selected(self, button): selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] target_value = GLib.Variant('as', selected_ids) self.get_root().activate_action("app.play_songs", target_value) self.close_selector() @Gtk.Template.Callback() def play_next_selected(self, button): selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] target_value = GLib.Variant('as', selected_ids) self.get_root().activate_action("app.play_songs_next", target_value) self.close_selector() @Gtk.Template.Callback() def play_later_selected(self, button): selected_rows = self.get_selected_rows() selected_ids = [r.id for r in selected_rows] target_value = GLib.Variant('as', selected_ids) self.get_root().activate_action("app.play_songs_later", target_value) self.close_selector() @Gtk.Template.Callback() def add_to_playlist_selected(self, button): selected_rows = self.get_selected_rows() target_value = GLib.Variant('as', [r.id for r in selected_rows]) self.get_root().activate_action("app.prompt_add_songs_to_playlist", target_value) self.close_selector() @Gtk.Template.Callback() def handle_visible_bind(self, queue, value) -> bool: return bool(value) @Gtk.Template.Callback() def handle_action_name_bind(self, carousel, value) -> str: return str("app.replace_root_page") if value else str("") @Gtk.Template.Callback() def handle_action_target_bind(self, carousel, value) -> GLib.Variant: return GLib.Variant.new_string(value or "") Jeffser-Nocturne-b442fee/src/widgets/song/row.py000066400000000000000000000307041523240030000217430ustar00rootroot00000000000000# row.py from gi.repository import Gtk, Adw, Gdk, GLib, Pango, Gio from .queue import SongQueue from ...integrations import get_current_integration from ..containers import ContextContainer from ...constants import CONTEXT_SONG, get_display_time from urllib.parse import urlparse @Gtk.Template(resource_path='/com/jeffser/Nocturne/song/row.ui') class SongRow(Adw.ActionRow): __gtype_name__ = 'NocturneSongRow' icon_el = Gtk.Template.Child() title_el = Gtk.Template.Child() duration_el = Gtk.Template.Child() artist_container_el = Gtk.Template.Child() external_file_el = Gtk.Template.Child() suffixes_stack_el = Gtk.Template.Child() star_el = Gtk.Template.Child() check_el = Gtk.Template.Child() menu_button_el = Gtk.Template.Child() subtitle_wrapbox = Gtk.Template.Child() def __init__(self, id:str, draggable:bool=False, removable:bool=False): self.id = id self.draggable = draggable self.removable = removable # used in queue integration = get_current_integration() integration.verifySong(self.id) super().__init__( action_target=GLib.Variant.new_string(self.id) ) self.star_el.set_action_target_value(GLib.Variant.new_string(self.id)) integration.connect_to_model(self.id, 'title', self.update_title) integration.connect_to_model(self.id, 'artists', self.update_artists) integration.connect_to_model(self.id, 'duration', self.update_duration) integration.connect_to_model(self.id, 'starred', self.update_starred) integration.connect_to_model(self.id, 'radioStreamUrl', self.update_radioStreamUrl) # for radios integration.connect_to_model(self.id, 'isExternalFile', self.update_is_external) integration.connect_to_model(self.id, 'deleted', self.delete_status_changed) integration.connect_to_model('currentSong', 'songId', self.current_song_changed) Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "show-context-button", self.menu_button_el, "visible", Gio.SettingsBindFlags.DEFAULT ) def delete_status_changed(self, status:bool): if status: if listbox := self.get_ancestor(Gtk.ListBox): listbox.remove(self) def update_is_external(self, isExternalFile:bool): self.external_file_el.set_visible(isExternalFile) integration = get_current_integration() self.star_el.set_visible(not isExternalFile and not integration.loaded_models.get(self.id).get_property('radioStreamUrl')) def generate_context_menu(self) -> ContextContainer: integration = get_current_integration() model = integration.loaded_models.get(self.id) context_dict = CONTEXT_SONG.copy() context_dict["select"]["connection"] = self.select_clicked context_dict["play-next"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id context_dict["play-later"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id context_dict['rating']['value'] = integration.loaded_models.get(self.id).get_property('userRating') if not model or not (model.get_property('radioStreamUrl') and not self.draggable): del context_dict["edit-radio"] del context_dict["delete-radio"] if "edit" in context_dict and 'no-edit-radio' in integration.limitations: del context_dict["edit-radio"] if 'no-downloads' in integration.limitations or not model or model.get_property('radioStreamUrl'): del context_dict["download"] if integration.__gtype_name__ == 'NocturneIntegrationOffline': context_dict["delete-download"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id else: del context_dict["delete-download"] if not model or model.get_property('radioStreamUrl') or model.get_property('isExternalFile'): del context_dict["add-to-playlist"] del context_dict["show-album"] del context_dict["show-artist"] if not self.draggable: del context_dict["select"] if self.removable: context_dict["remove"]["connection"] = self.remove_selected else: del context_dict["remove"] return ContextContainer(context_dict, self.id) def update_title(self, title:str): self.title_el.set_label(title) self.title_el.set_tooltip_text(title) self.set_name(title) def update_duration(self, duration:int): if duration == -1: self.duration_el.set_label(_("Radio")) else: self.duration_el.set_label(get_display_time(duration)) self.duration_el.set_visible(duration != 0) def update_artists(self, artists:list): integration = get_current_integration() model = integration.loaded_models.get(self.id) if len(artists) == 1: button = Gtk.Button( action_name = 'app.show_artist', action_target = GLib.Variant.new_string(artists[0].get('id')), child = Gtk.Label( ellipsize=Pango.EllipsizeMode.END, label=artists[0].get('name'), css_classes=['subtitle'] ), css_classes = ['p0', 'flat'], tooltip_text=artists[0].get('name') ) self.artist_container_el.set_child(button) self.artist_container_el.set_sensitive(not model.get_property('isExternalFile')) if len(artists) >= 5: menu = Gio.Menu() for artist in artists: item = Gio.MenuItem.new( label=artist.get('name') ) if not model.get_property('isExternalFile'): item.set_action_and_target_value( 'app.show_artist', GLib.Variant.new_string(artist.get('id')) ) menu.append_item(item) button = Gtk.MenuButton( child = Gtk.Label( ellipsize=Pango.EllipsizeMode.END, label=_("Multiple Artists"), css_classes=['subtitle'] ), css_classes = ['p0', 'flat'], menu_model = menu, tooltip_text=_("Multiple Artists") ) self.artist_container_el.set_child(button) else: container = Adw.WrapBox( line_spacing=2, child_spacing=2, wrap_policy=Adw.WrapPolicy.MINIMUM ) for artist in artists: button = Gtk.Button( action_name = 'app.show_artist', action_target = GLib.Variant.new_string(artist.get('id')), child = Gtk.Label( ellipsize=Pango.EllipsizeMode.END, label=artist.get('name'), css_classes=['subtitle'] ), css_classes = ['p0', 'flat'], tooltip_text=artist.get('name') ) container.append(button) self.artist_container_el.set_child(container) self.artist_container_el.set_sensitive(not model.get_property('isExternalFile')) self.artist_container_el.set_visible(self.artist_container_el.get_child()) def update_starred(self, starred:bool): if starred: self.star_el.add_css_class('accent') self.star_el.remove_css_class('dim-label') self.star_el.set_icon_name('heart-filled-symbolic') self.star_el.set_tooltip_text(_('Favorite')) else: self.star_el.remove_css_class('accent') self.star_el.add_css_class('dim-label') self.star_el.set_icon_name('heart-outline-thick-symbolic') self.star_el.set_tooltip_text(_('Not Favorite')) def update_radioStreamUrl(self, radioStreamUrl:str): if not radioStreamUrl: return if radioStreamUrl := urlparse(radioStreamUrl): homepage_url = '{}://{}'.format(radioStreamUrl.scheme, radioStreamUrl.netloc) button = Gtk.Button( action_name = 'app.visit_url', action_target = GLib.Variant.new_string(homepage_url), child = Gtk.Label( ellipsize=Pango.EllipsizeMode.END, label=urlparse(homepage_url).netloc, css_classes=['subtitle'] ), css_classes = ['p0', 'flat'], tooltip_text=homepage_url ) self.artist_container_el.set_child(button) def current_song_changed(self, songId:str): self.set_activatable(songId != self.id) if songId == self.id: self.icon_el.set_from_icon_name('sound-symbolic') self.icon_el.set_visible(True) self.add_css_class('accent') else: self.remove_css_class('accent') if self.draggable: self.icon_el.set_from_icon_name('list-drag-handle-symbolic') self.icon_el.set_visible(True) else: self.icon_el.set_from_icon_name(None) self.icon_el.set_visible(False) # -- Callbacks -- @Gtk.Template.Callback() def on_drop(self, drop_target, row, x, y): if self != row and self.draggable: index_source = list(row.get_ancestor(Gtk.ListBox)).index(row) index_target = list(self.get_ancestor(Gtk.ListBox)).index(self) if y > self.get_height() / 2: # bottom index_target += 1 integration = get_current_integration() queue_model = integration.loaded_models.get('currentSong').get_property('queueModel') queue_model.splice(index_source, 1, []) queue_model.splice(index_target, 0, [Gtk.StringObject.new(row.id)]) @Gtk.Template.Callback() def on_drag_begin(self, drag_source, drag): if self.draggable: paintable = Gtk.WidgetPaintable.new(self) drag_source.set_icon(paintable, 0, 0) @Gtk.Template.Callback() def on_drag_prepare(self, drag_source, x, y): if self.draggable: return Gdk.ContentProvider.new_for_value(self) def select_clicked(self): queue = self.get_ancestor(SongQueue) queue.set_selected_mode( select=True, selected_row=self ) def remove_selected(self): queue = self.get_ancestor(SongQueue) if queue.playlist_id: #is playlist target_value = GLib.Variant('a{sv}', { 'playlist': GLib.Variant('s', queue.playlist_id), 'indexes': GLib.Variant('as', str(list(queue.list_el).index(self))) }) self.get_root().activate_action("app.remove_songs_from_playlist", target_value) queue.list_el.remove(self) else: integration = get_current_integration() if self.id == integration.loaded_models.get('currentSong').get_property('songId'): all_ids = queue.get_all_ids() if len(all_ids) > 1: next_index = all_ids.index(self.id) + 1 if len(all_ids) <= next_index: next_index = 0 integration.loaded_models.get('currentSong').set_property('songId', all_ids[next_index]) else: integration.loaded_models.get('currentSong').set_property('songId', None) queue.list_el.remove(self) @Gtk.Template.Callback() def check_toggled(self, checkbutton): if not checkbutton.get_active(): queue = self.get_ancestor(SongQueue) if len(queue.get_selected_rows()) == 0: queue.set_selected_mode() @Gtk.Template.Callback() def on_context_button_active(self, button, gparam): button.get_popover().set_child(self.generate_context_menu()) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=self.generate_context_menu(), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/widgets/song/small_row.py000066400000000000000000000071771523240030000231430ustar00rootroot00000000000000# small_row.py from gi.repository import Gtk, Adw, Gdk, GLib from ...integrations import get_current_integration from ..containers import ContextContainer from ...constants import CONTEXT_SONG @Gtk.Template(resource_path='/com/jeffser/Nocturne/song/small_row.ui') class SongSmallRow(Gtk.Button): __gtype_name__ = 'NocturneSongSmallRow' cover_el = Gtk.Template.Child() title_el = Gtk.Template.Child() subtitle_el = Gtk.Template.Child() def __init__(self, id:str, show_album_name:bool=False): self.id = id self.show_album_name = show_album_name integration = get_current_integration() integration.verifySong(self.id) super().__init__( action_target=GLib.Variant.new_string(self.id) ) integration.connect_to_model(self.id, 'title', self.update_title) integration.connect_to_model(self.id, 'artists', self.update_artists) integration.connect_to_model(self.id, 'album', self.update_album) integration.connect_to_model(self.id, 'gdkPaintable', self.update_cover) integration.connect_to_model(self.id, 'deleted', self.delete_status_changed) def delete_status_changed(self, status:bool): if status: if wrapbox := self.get_ancestor(Adw.WrapBox): wrapbox.remove(self) def update_cover(self, paintable): if paintable: self.cover_el.set_from_paintable(paintable) self.cover_el.set_pixel_size(48) elif isinstance(self.cover_el.get_paintable(), Adw.SpinnerPaintable): self.cover_el.set_from_icon_name("music-note-symbolic") self.cover_el.set_pixel_size(-1) def update_title(self, title:str): self.title_el.set_label(title) self.set_tooltip_text(title) self.set_name(title) def update_artists(self, artists:list): if not self.show_album_name: if len(artists) > 0: self.subtitle_el.set_label(artists[0].get('name')) else: self.subtitle_el.set_label("") def update_album(self, album:str): if self.show_album_name: self.subtitle_el.set_label(album) def generate_context_menu(self) -> ContextContainer: integration = get_current_integration() context_dict = CONTEXT_SONG.copy() del context_dict["edit-radio"] del context_dict["delete-radio"] del context_dict["remove"] del context_dict["select"] context_dict['rating']['value'] = integration.loaded_models.get(self.id).get_property('userRating') context_dict["play-next"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id context_dict["play-later"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id if integration.__gtype_name__ == 'NocturneIntegrationOffline': context_dict["delete-download"]["sensitive"] = integration.loaded_models.get('currentSong').get_property('songId') != self.id else: del context_dict["delete-download"] if 'no-downloads' in integration.limitations: del context_dict["download"] return ContextContainer(context_dict, self.id) @Gtk.Template.Callback() def show_popover(self, *args): rect = Gdk.Rectangle() if len(args) == 4: rect.x, rect.y = args[2], args[3] else: rect.x, rect.y = args[1], args[2] popover = Gtk.Popover( child=self.generate_context_menu(), pointing_to=rect, has_arrow=False ) popover.set_parent(self) popover.popup() Jeffser-Nocturne-b442fee/src/window.py000066400000000000000000000454721523240030000200370ustar00rootroot00000000000000# window.py # # Copyright 2026 Jeffry Samuel # # 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 . # # SPDX-License-Identifier: GPL-3.0-or-later from gi.repository import Gtk, Adw, GLib, Gio, GObject, Pango from . import actions from .integrations import get_current_integration, models from .constants import SIDEBAR_MENU import threading from datetime import datetime, timedelta class SidebarItem(Adw.SidebarItem): __gtype_name__ = 'NocturneSidebarItem' page_tag = GObject.Property(type=str) playlist_id = GObject.Property(type=str) # optional @Gtk.Template(resource_path='/com/jeffser/Nocturne/window.ui') class NocturneWindow(Adw.ApplicationWindow): __gtype_name__ = 'NocturneWindow' sidebar_headerbar = Gtk.Template.Child() loading_label_el = Gtk.Template.Child() breakpoint_el = Gtk.Template.Child() main_navigationview = Gtk.Template.Child() main_bottom_sheet = Gtk.Template.Child() main_split_view = Gtk.Template.Child() sheet_split_view = Gtk.Template.Child() playing_page = Gtk.Template.Child() queue_page = Gtk.Template.Child() lyrics_page = Gtk.Template.Child() player_sidebar_splitview = Gtk.Template.Child() sidebar_playing_page = Gtk.Template.Child() sidebar_queue_page = Gtk.Template.Child() sidebar_lyrics_page = Gtk.Template.Child() main_sidebar = Gtk.Template.Child() main_stack = Gtk.Template.Child() footer = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() downloads_button_el = Gtk.Template.Child() @Gtk.Template.Callback() def close_request(self, window): if not self.get_hide_on_close(): if integration := get_current_integration(): id_list = [so.get_string() for so in integration.loaded_models.get('currentSong').get_property('queueModel')] current_song = integration.loaded_models.get('currentSong') queue_origin = current_song.get_property('queueOrigin') song_id = current_song.get_property('songId') timestamp = current_song.get_property('positionSeconds') if model := integration.loaded_models.get(queue_origin): if isinstance(model, models.Playlist): integration.savePlaylistResume( queue_origin_id=queue_origin, song_id=song_id, current_timestamp=timestamp ) integration.savePlayQueue(id_list, song_id, timestamp * 1000) integration.terminate_instance() self.settings.set_int('default-width', self.get_width()) self.settings.set_int('default-height', self.get_height()) if app := self.get_application(): if player := app.player: GLib.idle_add(player.discord_rpc.close) GLib.idle_add(player.mpris.quit) GLib.idle_add(app.quit) @Gtk.Template.Callback() def on_sidebar_activated(self, sidebar, index): page_tag = sidebar.get_selected_item().get_property('page_tag') if playlist_id := sidebar.get_selected_item().get_property('playlist_id'): self.activate_action("app.replace_root_page", GLib.Variant('s', 'playlists')) self.activate_action("app.show_playlist", GLib.Variant('s', playlist_id)) else: self.replace_root_page(page_tag) def replace_root_page(self, page_tag:str): if page := self.main_navigationview.find_page(page_tag): self.main_bottom_sheet.set_open(False) self.main_split_view.set_show_content(True) threading.Thread(target=page.reload, daemon=True).start() self.main_navigationview.replace([page]) def create_action(self, callback:callable, shortcuts:list=[], parameter_type:str="s"): def call_action(cb, va): if va is None: cb(self) else: cb(self, va.unpack()) self.get_application().create_action( name=callback.__name__, callback=lambda at, va, cb=callback: threading.Thread(target=call_action, args=(cb, va), daemon=True).start(), shortcuts=shortcuts, parameter_type=GLib.VariantType.new(parameter_type) if parameter_type else None ) def setup_sidebar(self): self.main_sidebar.remove_all() disabled_sections = self.settings.get_value('sidebar-disabled-sections').unpack() disabled_items = self.settings.get_value('sidebar-disabled-items').unpack() for section_id, section_data in SIDEBAR_MENU.items(): if section_id in disabled_sections: if row_title := section_data.get("title"): root_section = self.main_sidebar.get_sections()[0] row = SidebarItem( title=row_title, tooltip=row_title, icon_name=section_data.get("icon-name"), page_tag=section_data.get("page-tag") ) root_section.append(row) else: section_el = Adw.SidebarSection( title=section_data.get('title') ) append_section = False for item_id, item_data in section_data.get('items').items(): if item_id not in disabled_items: if item_id == 'playlists-group': threading.Thread(target=self.update_playlist_section_of_sidebar, args=(section_el,), daemon=True).start() else: if row_title := item_data.get("title"): row = SidebarItem( title=row_title, tooltip=row_title, icon_name=item_data.get("icon-name"), page_tag=item_data.get("page-tag") ) section_el.append(row) append_section = True if append_section: self.main_sidebar.append(section_el) def update_loading_message(self, integration): message = integration.get_property("loadingMessage") self.loading_label_el.set_visible(message) self.loading_label_el.set_label(message) if not message: threading.Thread(target=self.main_navigationview.get_visible_page().reload, daemon=True).start() def update_playlist_section_of_sidebar(self, section_el): integration = get_current_integration() integration.connect('notify::loadingMessage', lambda integration, ud: self.update_loading_message(integration)) if integration.get_property('loadingMessage'): self.update_loading_message(integration) def update_row_paintable(row, paintable): picture = Gtk.Picture( paintable=paintable, margin_top=5, margin_bottom=5 ) row.set_prefix(picture) for playlistId in integration.getPlaylists()[:4]: if model := integration.loaded_models.get(playlistId): item = SidebarItem( page_tag="playlists", playlist_id=playlistId ) GLib.idle_add(section_el.append, item) integration.connect_to_model(playlistId, "name", lambda name, row=item: row.set_title(name)) integration.connect_to_model(playlistId, "name", lambda name, row=item: row.set_tooltip(name)) integration.connect_to_model(playlistId, "songCount", lambda n, row=item: row.set_subtitle(('{} Songs' if n > 1 else '{} Song').format(n))) #integration.connect_to_model(playlistId, "gdkPaintable", lambda paintable, row=item: update_row_paintable(row, paintable)) #TODO: enable this once Adw 1.10 is out https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/method.SidebarItem.set_prefix.html def setup(self): # call using glib_add self.footer.setup() self.playing_page.setup() self.lyrics_page.setup() self.queue_page.setup() self.sidebar_playing_page.setup() self.sidebar_lyrics_page.setup() self.sidebar_queue_page.setup() self.downloads_button_el.setup() self.setup_sidebar() self.sidebar_playing_page.show_sidebar_el.set_visible(False) threading.Thread(target=self.notify_playback).start() integration = get_current_integration() integration.connect_to_model('currentSong', 'songId', self.song_changed) def song_changed(self, songId:str): playing = bool(songId) if application := self.get_root().get_application(): if playing: application.inhibit_suspend() else: application.uninhibit_suspend() if popout_window := application.popout_window: popout_window.close() self.big_breakpoint_toggled() def notify_playback(self): # Notifies user if Nocturne Playback is ready for this month prev_month = datetime.now().replace(day=1) - timedelta(days=1) if prev_month.strftime("%m-%Y") != self.settings.get_value('last-playback-checked').unpack(): integration = get_current_integration() top_songs = [] for songId, plays in integration.getPlaybackScrobble(prev_month.strftime("%m-%Y")): integration.verifySong(songId, use_threading=False) if songId in integration.loaded_models: top_songs.append((songId, plays)) if len(top_songs) > 5: def response(dialog, task): if dialog.choose_finish(task) == "show": self.get_root().activate_action("app.launch_playback") dialog = Adw.AlertDialog( heading=_("Nocturne Playback"), body=_("Your Playback queue for {} is available!").format(prev_month.strftime("%B %Y")) ) dialog.add_response("show", _("Show")) dialog.add_response("later", _("Later")) dialog.set_response_appearance("show", Adw.ResponseAppearance.SUGGESTED) dialog.set_default_response("show") GLib.idle_add(dialog.choose, self, None, response) self.settings.set_string('last-playback-checked', prev_month.strftime("%m-%Y")) def __init__(self, **kwargs): super().__init__(**kwargs) self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.create_action(actions.reload, shortcuts=['R'], parameter_type=None) self.create_action(actions.show_error) self.create_action(actions.search, shortcuts=['F'], parameter_type=None) self.create_action(actions.launch_playback, parameter_type=None) self.create_action(actions.generate_auto_play_queue, parameter_type="b") self.create_action(actions.set_equalizer_preset) self.create_action(actions.replace_root_page) self.create_action(actions.visit_url) self.create_action(actions.toggle_star) self.create_action(actions.logout, parameter_type=None) self.create_action(actions.show_external_file_warning, parameter_type=None) self.create_action(actions.update_navidrome_server, parameter_type=None) self.create_action(actions.delete_navidrome_server, parameter_type=None) self.create_action(actions.open_popout_window, shortcuts=['P'], parameter_type=None) self.create_action(actions.toggle_fullscreen, shortcuts=['F11'], parameter_type=None) self.create_action(actions.set_rating, parameter_type="a{sv}") # dict with string keys and any values self.create_action(actions.player_toggle, shortcuts=['K'], parameter_type=None) self.create_action(actions.player_play, parameter_type=None) self.create_action(actions.player_pause, parameter_type=None) self.create_action(actions.player_next, shortcuts=['Right'], parameter_type=None) self.create_action(actions.player_previous, shortcuts=['Left'], parameter_type=None) self.create_action(actions.player_raise_volume, shortcuts=['Up'], parameter_type=None) self.create_action(actions.player_lower_volume, shortcuts=['Down'], parameter_type=None) self.create_action(actions.play_radio) self.create_action(actions.add_radio, parameter_type=None) self.create_action(actions.update_radio) self.create_action(actions.delete_radio) self.create_action(actions.play_song) self.create_action(actions.play_song_from_list, parameter_type="a{sv}") self.create_action(actions.play_song_next) self.create_action(actions.play_song_later) self.create_action(actions.play_songs, parameter_type="as") self.create_action(actions.play_songs_next, parameter_type="as") self.create_action(actions.play_songs_later, parameter_type="as") self.create_action(actions.edit_lyrics, parameter_type=None) self.create_action(actions.play_random_queue, parameter_type=None) self.create_action(actions.show_song_details) self.create_action(actions.show_album) self.create_action(actions.show_album_from_song) self.create_action(actions.play_album) self.create_action(actions.play_album_next) self.create_action(actions.play_album_later) self.create_action(actions.play_album_shuffle) self.create_action(actions.show_playlist) self.create_action(actions.resume_playlist) self.create_action(actions.play_playlist) self.create_action(actions.play_playlist_next) self.create_action(actions.play_playlist_later) self.create_action(actions.play_playlist_shuffle) self.create_action(actions.update_playlist) self.create_action(actions.create_playlist, parameter_type=None) self.create_action(actions.remove_songs_from_playlist, parameter_type="a{sv}") self.create_action(actions.prompt_add_songs_to_playlist, parameter_type="as") self.create_action(actions.add_songs_to_playlist, parameter_type="a{sv}") self.create_action(actions.prompt_add_song_to_playlist) self.create_action(actions.prompt_add_album_to_playlist) self.create_action(actions.delete_playlist) self.create_action(actions.show_artist) self.create_action(actions.show_artist_from_song) self.create_action(actions.show_artist_from_album) self.create_action(actions.play_shuffle_artist) self.create_action(actions.play_radio_artist) self.create_action(actions.download_song) self.create_action(actions.download_songs, parameter_type="as") self.create_action(actions.download_album) self.create_action(actions.download_playlist) self.create_action(actions.delete_download) self.create_action(actions.delete_downloads, parameter_type="as") self.set_property('default-width', self.settings.get_value('default-width').unpack()) self.set_property('default-height', self.settings.get_value('default-height').unpack()) self.set_property('hide-on-close', self.settings.get_value('hide-on-close').unpack()) self.settings.bind( "hide-on-close", self, "hide-on-close", Gio.SettingsBindFlags.DEFAULT ) list(list(self.sidebar_headerbar)[0])[0].get_center_widget().get_child().set_ellipsize(Pango.EllipsizeMode.NONE) css_settings = { 'use-dynamic-accent': 'dynamic-accent', 'player-blur-bg': 'player-translucent' } for key, class_name in css_settings.items(): self.settings.connect('changed::{}'.format(key), self.css_toggled, class_name) self.css_toggled(self.settings, key, class_name) self.settings.connect('changed::global-dynamic-bg-mode', self.dynamic_bg_mode_changed, 'global-') self.dynamic_bg_mode_changed(self.settings, 'global-dynamic-bg-mode', 'global-') self.settings.connect('changed::player-dynamic-bg-mode', self.dynamic_bg_mode_changed, '') self.dynamic_bg_mode_changed(self.settings, 'player-dynamic-bg-mode', '') self.settings.connect('changed::use-sidebar-player', lambda *_: self.big_breakpoint_toggled()) def css_toggled(self, settings, key, css_class): if settings.get_value(key).unpack(): self.add_css_class(css_class) else: self.remove_css_class(css_class) def dynamic_bg_mode_changed(self, settings, key, prefix): value = settings.get_value(key).unpack() self.remove_css_class('{}dynamic-bg-gradient'.format(prefix)) self.remove_css_class('{}dynamic-bg-blur'.format(prefix)) if value: self.add_css_class('{}dynamic-bg-{}'.format(prefix, value)) @Gtk.Template.Callback() def on_drop(self, drop_target, file, x, y): self.get_application().do_open([file]) @Gtk.Template.Callback() def big_breakpoint_toggled(self, bp=None): if integration := get_current_integration(): song_playing = bool(integration.loaded_models.get('currentSong').get_property('songId')) else: song_playing = False is_small = self.get_width() <= 840 if is_small: self.player_sidebar_splitview.set_show_sidebar(False) self.main_bottom_sheet.set_reveal_bottom_bar(song_playing) self.main_bottom_sheet.set_can_open(song_playing) else: show_sidebar = self.settings.get_value('use-sidebar-player').unpack() self.player_sidebar_splitview.set_show_sidebar(show_sidebar and song_playing) self.main_bottom_sheet.set_reveal_bottom_bar(not show_sidebar and song_playing) self.main_bottom_sheet.set_can_open(not show_sidebar and song_playing) if show_sidebar: self.main_bottom_sheet.set_open(False) if not song_playing: self.main_bottom_sheet.set_open(False) if self.main_bottom_sheet.get_reveal_bottom_bar(): self.add_css_class('showing-footer') else: self.remove_css_class('showing-footer') Jeffser-Nocturne-b442fee/stubs/000077500000000000000000000000001523240030000165135ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/stubs/gi/000077500000000000000000000000001523240030000171125ustar00rootroot00000000000000Jeffser-Nocturne-b442fee/stubs/gi/__init__.pyi000066400000000000000000000004601523240030000213740ustar00rootroot00000000000000from typing import Any # Module-level stubs for `gi` so calls like `gi.require_version(...)` are recognized. def require_version(name: str, version: str) -> None: ... def require_foreign(name: str) -> None: ... # Expose `repository` as a package submodule (types are in repository.pyi) repository: Any Jeffser-Nocturne-b442fee/stubs/gi/repository.pyi000066400000000000000000000004321523240030000220530ustar00rootroot00000000000000from typing import Any # Minimal stub for gi.repository names used in this project. Adw: Any Gtk: Any GLib: Any GObject: Any Gdk: Any Gio: Any GdkPixbuf: Any Gst: Any Pango: Any def require_version(name: str, version: str) -> None: ... def require_foreign(name: str) -> None: ... Jeffser-Nocturne-b442fee/translation_tool.sh000077500000000000000000000023121523240030000213030ustar00rootroot00000000000000#!/usr/bin/env bash #find data src -type f \( -name "*.py" -o -name "*.blp" -o -name "*.in" -o -name "*.xml" \) | sort > po/POTFILES.in set -euo pipefail cd "$(dirname "$0")" # Always regenerate the template first xgettext --output=po/nocturne.pot --files-from=po/POTFILES.in --from-code=UTF-8 --add-comments --keyword=_ --keyword=C_:1c,2 --keyword=ngettext:1,2 echo -e "\n---๐ŸŒ™ Nocturne Translation Tool ๐ŸŒ™---\n" echo "Available languages:" ls po/*.po 2>/dev/null | sed -E 's|po/([^.]+)\.po|\1|' | tr '\n' ' ' echo echo read -rp "Enter your language code (e.g., es, ru, pt_BR): " lang po_file="po/${lang}.po" if [[ -f "$po_file" ]]; then echo -e "\nUpdating $lang..." msgmerge --no-fuzzy-matching --update --backup=none "$po_file" po/nocturne.pot echo "โœ… $lang updated successfully." else echo -e "\nโŒ No translation found for '$lang'." read -rp "Would you like to create a new translation for '$lang'? (y/n): " answer if [[ "$answer" =~ ^[Yy]$ ]]; then echo "" msginit --locale="$lang" --input=po/nocturne.pot --output-file="$po_file" echo -e "\nโœ… New translation created at $po_file." else echo -e "\nโŒ Operation cancelled." exit 1 fi fi