pax_global_header00006660000000000000000000000064152220741140014510gustar00rootroot0000000000000052 comment=469023d6c96450d3954364c961819ed0858d4d81 Jeffser-Nocturne-469023d/000077500000000000000000000000001522207411400151445ustar00rootroot00000000000000Jeffser-Nocturne-469023d/.env000066400000000000000000000000601522207411400157310ustar00rootroot00000000000000PYTHONPATH=${PYTHONPATH}:${workspaceFolder}/src Jeffser-Nocturne-469023d/.github/000077500000000000000000000000001522207411400165045ustar00rootroot00000000000000Jeffser-Nocturne-469023d/.github/FUNDING.yml000066400000000000000000000015371522207411400203270ustar00rootroot00000000000000# 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-469023d/.github/ISSUE_TEMPLATE/000077500000000000000000000000001522207411400206675ustar00rootroot00000000000000Jeffser-Nocturne-469023d/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014041522207411400233600ustar00rootroot00000000000000--- 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-469023d/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341522207411400244130ustar00rootroot00000000000000--- 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-469023d/.gitignore000066400000000000000000000000651522207411400171350ustar00rootroot00000000000000*.ui __pycache__ gschemas.compiled nocturne.gresourceJeffser-Nocturne-469023d/.vscode/000077500000000000000000000000001522207411400165055ustar00rootroot00000000000000Jeffser-Nocturne-469023d/.vscode/launch.json000066400000000000000000000005511522207411400206530ustar00rootroot00000000000000{ "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-469023d/.vscode/settings.json000066400000000000000000000005571522207411400212470ustar00rootroot00000000000000{ "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-469023d/.vscode/tasks.json000066400000000000000000000010421522207411400205220ustar00rootroot00000000000000{ "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-469023d/CODE_OF_CONDUCT.md000066400000000000000000000004341522207411400177440ustar00rootroot00000000000000Nocturne 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-469023d/CONTRIBUTING.md000066400000000000000000000010131522207411400173700ustar00rootroot00000000000000# 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-469023d/COPYING000066400000000000000000001045201522207411400162010ustar00rootroot00000000000000 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-469023d/Nocturne.doap000066400000000000000000000024351522207411400176120ustar00rootroot00000000000000 Nocturne An Adwaita Navidrome / Subsonic client Python GTK 4 Libadwaita Jeffry Samuel jeffser jeffser Jeffser-Nocturne-469023d/README.md000066400000000000000000000114501522207411400164240ustar00rootroot00000000000000

Nocturne

Nocturne is a Navidrome / Jellyfin 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 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) Jeffser-Nocturne-469023d/SECURITY.md000066400000000000000000000032731522207411400167420ustar00rootroot00000000000000# 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-469023d/com.jeffser.Nocturne.yml000066400000000000000000000150541522207411400216710ustar00rootroot00000000000000id: 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' - '--filesystem=xdg-music:ro' 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: nocturne builddir: true buildsystem: meson sources: - type: dir path: . Jeffser-Nocturne-469023d/data/000077500000000000000000000000001522207411400160555ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/com.jeffser.Nocturne.SearchProvider.desktop.in000066400000000000000000000003561522207411400267750ustar00rootroot00000000000000[Desktop Entry] Type=Service 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-469023d/data/com.jeffser.Nocturne.SearchProvider.service.in000066400000000000000000000001371522207411400267610ustar00rootroot00000000000000[D-BUS Service] Name=com.jeffser.Nocturne.SearchProvider Exec=@bindir@/nocturne_search_providerJeffser-Nocturne-469023d/data/com.jeffser.Nocturne.desktop.in000066400000000000000000000011311522207411400240460ustar00rootroot00000000000000[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-469023d/data/com.jeffser.Nocturne.gschema.xml000066400000000000000000000122071522207411400242040ustar00rootroot00000000000000 true false false true 'home' 0 true true true true false false true true true false false false false 'big' '' 'blur' 'blur' true 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 '' 'list' Jeffser-Nocturne-469023d/data/com.jeffser.Nocturne.metainfo.xml.in000066400000000000000000000550451522207411400250130ustar00rootroot00000000000000 com.jeffser.Nocturne CC0-1.0 GPL-3.0-or-later Nocturne Bring your music library together

A modern Navidrome / Jellyfin client

Features

  • Exploration by songs, artists, albums, radios and playlists
  • Playlist management
  • Compatibility with Jellyfin, OpenSubsonic and local files
  • 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 navidrome jellyfin music gtk 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-469023d/data/com.jeffser.Nocturne.search-provider.ini000066400000000000000000000002161522207411400256460ustar00rootroot00000000000000[Shell Search Provider] DesktopId=com.jeffser.Nocturne.desktop BusName=com.jeffser.Nocturne.SearchProvider ObjectPath=@object_path@ Version=2 Jeffser-Nocturne-469023d/data/com.jeffser.Nocturne.service.in000066400000000000000000000001301522207411400240330ustar00rootroot00000000000000[D-BUS Service] Name=com.jeffser.Nocturne Exec=@bindir@/nocturne --gapplication-service Jeffser-Nocturne-469023d/data/icons/000077500000000000000000000000001522207411400171705ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/000077500000000000000000000000001522207411400206275ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/scalable/000077500000000000000000000000001522207411400223755ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/scalable/apps/000077500000000000000000000000001522207411400233405ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/scalable/apps/com.jeffser.Nocturne.Devel.svg000066400000000000000000000236641522207411400311270ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/data/icons/hicolor/scalable/apps/com.jeffser.Nocturne.svg000066400000000000000000000111531522207411400300570ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/data/icons/hicolor/symbolic/000077500000000000000000000000001522207411400224505ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/symbolic/apps/000077500000000000000000000000001522207411400234135ustar00rootroot00000000000000Jeffser-Nocturne-469023d/data/icons/hicolor/symbolic/apps/com.jeffser.Nocturne-symbolic.svg000066400000000000000000000010461522207411400317510ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/data/icons/meson.build000066400000000000000000000006271522207411400213370ustar00rootroot00000000000000application_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-469023d/data/meson.build000066400000000000000000000047511522207411400202260ustar00rootroot00000000000000desktop_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-469023d/meson.build000066400000000000000000000005431522207411400173100ustar00rootroot00000000000000project('nocturne', version: '1.3.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-469023d/nocturne-uninstalled000077500000000000000000000046471522207411400212620ustar00rootroot00000000000000#!/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-469023d/po/000077500000000000000000000000001522207411400155625ustar00rootroot00000000000000Jeffser-Nocturne-469023d/po/LINGUAS000066400000000000000000000000441522207411400166050ustar00rootroot00000000000000ca de es eu hr ru te tr zh_CN zh_TW Jeffser-Nocturne-469023d/po/POTFILES.in000066400000000000000000000072241522207411400173440ustar00rootroot00000000000000data/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/songs_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/songs_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-469023d/po/ca.po000066400000000000000000001401731522207411400165130ustar00rootroot00000000000000# 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-06-19 02:17+0200\n" "PO-Revision-Date: 2026-06-19 12:37+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 Navidrome / Jellyfin client" msgstr "Un client modern de Navidrome i Jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Caracterรญstiques" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 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:14 msgid "Playlist management" msgstr "Gestiรณ de llistes de reproducciรณ" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "Compatible amb Jellyfin, OpenSubsonic i fitxers locals" #: 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:91 src/ui/window.blp:190 #: src/ui/window.blp:242 msgid "Lyrics" msgstr "Lletra" #: 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: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รบsica" #: 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 "Errada" #: src/actions.py:139 msgid "No details provided" msgstr "No s'ha proporcionat cap detall" #: src/actions.py:141 src/actions.py:290 src/preferences.py:433 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "Tanca" #: src/actions.py:182 #, 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:287 src/ui/song/row.blp:69 msgid "External File" msgstr "Fitxer extern" #: 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 "" "Aquesta pista s'ha carregat des d'un fitxer extern, per tant, tindrร  menys " "funcionalitats que una pista de la biblioteca" #: src/actions.py:308 msgid "Navidrome server deleted successfully" msgstr "El servidor de Navidrome s'ha suprimit correctament" #: src/actions.py:317 msgid "Delete Navidrome Server" msgstr "Suprimeix el servidor de Navidrome" #: src/actions.py:318 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Voleu suprimir el servidor de Navidrome integrat?" #: 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 "Cancelยทla" #: src/actions.py:321 msgid "Keep Data" msgstr "Mantรฉn les dades" #: src/actions.py:322 msgid "Delete Everything" msgstr "Suprimeix-ho tot" #: src/actions.py:411 msgid "No Name" msgstr "Sense nom" #: src/actions.py:430 msgid "Radio updated successfully" msgstr "La rร dio s'ha actualitzat correctament" #: src/actions.py:430 msgid "Radio added successfully" msgstr "La radio s'ha afegit correctament" #: src/actions.py:441 msgid "Error updating radio" msgstr "S'ha produรฏt un error en actualitzar la rร dio" #: src/actions.py:441 msgid "Error adding radio" msgstr "S'ha produรฏt un error en afegir la rร dio" #: src/actions.py:450 src/actions.py:833 msgid "Name" msgstr "Nom" #: src/actions.py:455 msgid "Stream Url" msgstr "URL de l'emissiรณ" #: src/actions.py:462 msgid "Update Radio Station" msgstr "Actualitza l'emissora de rร dio" #: src/actions.py:462 msgid "Add Radio Station" msgstr "Afegeix una emissora de rร dio" #: src/actions.py:466 src/preferences.py:387 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Desa" #: src/actions.py:482 msgid "Radio deleted successfully" msgstr "La rร dio s'ha suprimit correctament" #: src/actions.py:490 msgid "Error deleting radio" msgstr "S'ha produรฏt un error en suprimir la rร dio" #: src/actions.py:496 msgid "Delete Radio Station" msgstr "Suprimeix l'emissora de rร dio" #: src/actions.py:497 src/actions.py:942 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "Esteu segur que voleu suprimir '{}'?" #: src/actions.py:500 src/actions.py:945 src/constants.py:375 #: src/constants.py:423 msgid "Delete" msgstr "Suprimeix" #: src/actions.py:534 src/actions.py:566 src/actions.py:572 src/actions.py:674 #: src/actions.py:762 msgid "Playing Next" msgstr "Es reproduirร  a continuaciรณ" #: src/actions.py:546 src/actions.py:585 src/actions.py:591 src/actions.py:691 #: src/actions.py:779 msgid "Playing Later" msgstr "Es reproduirร  mรฉs tard" #: src/actions.py:566 src/actions.py:585 src/actions.py:1059 #: src/actions.py:1194 src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} canรงons" #: src/actions.py:621 msgid "Lyrics Saved" msgstr "S'ha desat la lletra" #: src/actions.py:814 msgid "Playlist updated successfully" msgstr "La llista s'ha actualitzat correctament" #: src/actions.py:814 msgid "Playlist created successfully" msgstr "La llista s'ha creat correctament" #: src/actions.py:824 msgid "Error updating playlist" msgstr "S'ha produรฏt un error en actualitzar la llista" #: src/actions.py:824 msgid "Error creating playlist" msgstr "S'ha produรฏt un error en crear la llista" #: src/actions.py:838 msgid "Update Playlist" msgstr "Actualitza la llista" #: src/actions.py:838 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Crea una llista" #: src/actions.py:842 msgid "Update" msgstr "Actualitza" #: src/actions.py:842 msgid "Create" msgstr "Crea" #: src/actions.py:861 #, 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:892 src/actions.py:912 #, 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:916 src/actions.py:1064 #, 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:929 msgid "Playlist Deleted" msgstr "La llista s'ha eliminat" #: src/actions.py:941 msgid "Delete Playlist" msgstr "Suprimeix la llista" #: src/actions.py:990 msgid "No songs found" msgstr "No s'ha trobat cap canรงรณ" #: src/actions.py:1033 src/actions.py:1060 src/actions.py:1097 #: src/actions.py:1133 msgid "Download Started" msgstr "S'ha iniciat la baixada" #: src/actions.py:1040 src/actions.py:1071 src/actions.py:1078 #: src/actions.py:1113 src/actions.py:1149 msgid "Already Downloaded" msgstr "Ja s'ha baixat" #: src/actions.py:1065 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "{} canรงรณ ({})" msgstr[1] "{} canรงons ({})" #: src/actions.py:1077 msgid "Download Skipped" msgstr "S'ha omรจs la baixada" #: src/actions.py:1101 src/actions.py:1137 #, 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:1176 src/actions.py:1195 msgid "Deleted" msgstr "Eliminada" #: src/constants.py:185 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "En ordre" #: src/constants.py:189 msgid "Repeat All" msgstr "Repeteix-ho tot" #: src/constants.py:193 msgid "Repeat One" msgstr "Repeteix-ne una" #: src/constants.py:198 #, python-brace-format msgid "Low ({})" msgstr "Baixa ({})" #: src/constants.py:199 #, python-brace-format msgid "Medium ({})" msgstr "Mitjana ({})" #: src/constants.py:200 #, python-brace-format msgid "High ({})" msgstr "Alta ({})" #: src/constants.py:201 #, python-brace-format msgid "Ultra ({})" msgstr "Mร xima ({})" #: src/constants.py:202 msgid "Original File" msgstr "Fitxer original" #. Item #: src/constants.py:209 src/ui/pages/home.blp:5 msgid "Home" msgstr "Inici" #. 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 "Artistes" #. 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 "ร€lbums" #. Item #: src/constants.py:224 src/constants.py:259 src/constants.py:279 #: src/window.py:149 msgid "All" msgstr "Tots" #. Item #: src/constants.py:229 msgid "Random" msgstr "Aleatoris" #. Item #: src/constants.py:234 src/constants.py:264 msgid "Favorites" msgstr "Preferits" #. Item #: src/constants.py:239 msgid "Recently Added" msgstr "Afegits recentment" #. Item #: src/constants.py:244 msgid "Recently Played" msgstr "Reproduรฏts recentment" #. Item #: src/constants.py:249 msgid "Most Played" msgstr "Mรฉs reproduรฏts" #. 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 "Canรงons" #. Item #: src/constants.py:269 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Rร dios" #. 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 "Llistes" #: 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 "Reprodueix" #: src/constants.py:294 src/constants.py:327 src/constants.py:350 msgid "Shuffle" msgstr "Barreja" #: 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 "Reprodueix a continuaciรณ" #: 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 "Reprodueix mรฉs tard" #: 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 "Afegeix a la llista" #: 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 "Baixa" #: src/constants.py:319 src/constants.py:418 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Mostra l'artista" #: src/constants.py:332 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Rร dio" #: src/constants.py:345 msgid "Resume" msgstr "Reprรจn" #: src/constants.py:370 src/constants.py:398 msgid "Edit" msgstr "Edita" #: src/constants.py:384 msgid "Select" msgstr "Selecciona" #: src/constants.py:413 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Mostra l'ร lbum" #: src/constants.py:429 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Suprimeix" #: src/constants.py:434 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "Esborra la baixada" #: src/constants.py:440 src/main.py:134 msgid "Show Details" msgstr "Mostra els detalls" #: src/constants.py:451 msgid "Visit Webpage" msgstr "Visita la pร gina web" #: src/constants.py:457 msgid "Update Server" msgstr "Actualitza el servidor" #: src/constants.py:462 msgid "Delete Server" msgstr "Suprimeix el servidor" #: src/constants.py:471 msgid "Edit Lyrics" msgstr "Edita la lletra" #: src/constants.py:476 msgid "Remove Lyrics" msgstr "Suprimeix la lletra" #: src/integrations/base.py:148 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:232 msgid "Could not log in" msgstr "No s'ha pogut iniciar la sessiรณ" #: src/integrations/jellyfin.py:968 msgid "Unknown" msgstr "Desconegut" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:586 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:280 msgid "No Album" msgstr "Cap ร lbum" #: src/integrations/local.py:602 src/integrations/local.py:611 msgid "Offline Mode" msgstr "Mode sense connexiรณ" #: src/integrations/local.py:603 msgid "Access your downloads" msgstr "Accedeix a les teves baixades" #: src/integrations/models.py:138 src/ui/album/page.blp:5 #: src/widgets/album/page.py:129 msgid "Album" msgstr "ร€lbum" #: src/integrations/models.py:140 msgid "Display Artist" msgstr "Artista principal" #: src/integrations/models.py:142 msgid "MusicBrainz ID" msgstr "ID del MusicBrainz" #: src/integrations/models.py:143 msgid "Track Number" msgstr "Nรบmero de pista" #: src/integrations/models.py:144 msgid "Year" msgstr "Any" #: src/integrations/models.py:145 msgid "Size" msgstr "Mida" #: src/integrations/models.py:146 msgid "Suffix" msgstr "Extensiรณ" #: 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 "Preferit" #: src/integrations/models.py:148 msgid "Duration" msgstr "Durada" #: src/integrations/models.py:150 msgid "Bit Rate" msgstr "Taxa de bits" #: src/integrations/models.py:151 msgid "Bit Depth" msgstr "Profunditat de bits" #: src/integrations/models.py:152 msgid "Sampling Rate" msgstr "Freqรผรจncia de mostreig" #: src/integrations/models.py:153 msgid "Channel Count" msgstr "Nombre de canals" #: src/integrations/models.py:155 msgid "Path" msgstr "Camรญ" #: src/integrations/models.py:156 msgid "Disc Number" msgstr "Nรบmero de disc" #: src/integrations/models.py:157 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:158 msgid "Genres" msgstr "Gรจneres" #. list #: src/integrations/models.py:160 msgid "Track Gain" msgstr "Guany de la pista" #: src/integrations/models.py:161 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 / Navidrome instance" msgstr "Utilitzeu una instร ncia ja existent de Navidrome / Subsonic" #: src/integrations/navidrome.py:141 msgid "Server returned an error" msgstr "El servidor ha retornat un error" #: src/integrations/navidrome.py:141 msgid "Unknown Error" msgstr "Error desconegut" #: src/integrations/navidrome.py:591 src/integrations/navidrome.py:600 msgid "Managed Server" msgstr "Servidor gestionat" #: src/integrations/navidrome.py:592 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:595 msgid "Manage Server" msgstr "Gestiona el servidor" #: src/integrations/navidrome.py:601 msgid "Create and use a Navidrome instance" msgstr "Creeu i utilitzeu una instร ncia de Navidrome" #: src/main.py:70 msgid "Music is Playing" msgstr "Hi ha mรบsica en reproducciรณ" #: src/main.py:83 msgid "Fullscreen Player Active" msgstr "El reproductor a pantalla completa estร  actiu" #: src/main.py:131 msgid "Login Failed" msgstr "Ha fallat l'inici de sessiรณ" #: src/preferences.py:375 msgid "Settings Page" msgstr "Configuraciรณ" #: src/preferences.py:378 msgid "User Token" msgstr "Testimoni d'usuari" #: src/preferences.py:382 msgid "Link ListenBrainz" msgstr "Enllaรงa amb ListenBrainz" #: src/preferences.py:383 msgid "Connect your ListenBrainz account with a user token" msgstr "Connecteu el compte de ListenBrainz amb un testimoni d'usuari" #: src/preferences.py:423 msgid "Flatpak Sandbox Warning" msgstr "Avรญs sobre l'aรฏllament de 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 "" "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:21 msgid "Play Album" msgstr "Reprodueix l'ร lbum" #: 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 "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:117 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:126 msgid "2 Stars" msgstr "2 estrelles" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:135 msgid "3 Stars" msgstr "3 estrelles" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:144 msgid "4 Stars" msgstr "4 estrelles" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:153 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:127 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/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 "Llista" #: src/ui/containers/carousel.blp:38 msgid "Pan to the Left" msgstr "Desplaรงa cap a l'esquerra" #: src/ui/containers/carousel.blp:64 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:240 #: src/ui/playing/footer.blp:118 msgid "Pause" msgstr "Pausa" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:175 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:68 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:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "Inici de sessiรณ" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "Estat del servidor" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "No s'estร  executant" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "Reinicia la instร ncia" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Gestiona el servidor de Navidrome" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URL" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "Mรฉs opcions" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Confia en els certificats del servidor" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Directori de la biblioteca" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Nom d'usuari" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Contrasenya" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 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:207 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:124 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:210 src/ui/playing/footer.blp:88 msgid "Previous" msgstr "Anterior" #: src/ui/playing/control_page.blp:254 src/ui/playing/footer.blp:132 msgid "Next" msgstr "Segรผent" #: src/ui/playing/control_page.blp:274 msgid "Star" msgstr "Destacat" #: src/ui/playing/control_page.blp:281 msgid "Toggle Sidebar" msgstr "Mostra o amaga la barra lateral" #: src/ui/playing/cover_art.blp:28 msgid "Audio" msgstr "ร€udio" #: src/ui/playing/cover_art.blp:36 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Carร tula de la canรงรณ" #: src/ui/playing/cover_art.blp:42 src/ui/playing/cover_art.blp:50 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:34 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:65 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:214 msgid "Song Details" msgstr "Detalls de la canรงรณ" #: src/ui/playing/popout_window.blp:85 src/ui/window.blp:180 #: src/ui/window.blp:236 msgid "Queue" msgstr "Cua" #: src/ui/playing/popout_window.blp:97 src/ui/window.blp:196 #: src/ui/window.blp:248 msgid "Equalizer" msgstr "Equalitzador" #: src/ui/playing/queue_page.blp:20 msgid "Generating Next Queue" msgstr "S'estร  generant la cua segรผent" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Reproducciรณ automร tica" #: src/ui/playing/queue_page.blp:25 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:28 msgid "Play Playlist" msgstr "Reprodueix la llista" #: 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:84 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 "Use Track and Album Gain" msgstr "Utilitza el guany de la pista i de l'ร lbum" #: src/ui/preferences.blp:23 msgid "Adjust the volume based on gain metadata" msgstr "Ajusteu el volum d'acord amb les metadades de guany" #: src/ui/preferences.blp:26 msgid "Default Page on Login" msgstr "Pร gina per defecte en iniciar la sessiรณ" #: src/ui/preferences.blp:31 msgid "Maximum Bitrate" msgstr "Taxa mร xima de bits" #: src/ui/preferences.blp:37 msgid "Session" msgstr "Sessiรณ" #: src/ui/preferences.blp:40 msgid "Discord Rich Presence" msgstr "Estat d'activitat a Discord" #: src/ui/preferences.blp:41 msgid "Share the currently playing song on Discord" msgstr "Compartiu la canรงรณ actual a Discord" #: src/ui/preferences.blp:45 msgid "Share Cover Art With Discord" msgstr "Compartiu la carร tula a Discord" #: src/ui/preferences.blp:46 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:52 msgid "ListenBrainz Link" msgstr "Enllaรงa amb ListenBrainz" #: src/ui/preferences.blp:53 msgid "Keep track of your playback history" msgstr "Manteniu un registre de l'historial de reproducciรณ" #: src/ui/preferences.blp:59 src/ui/preferences.blp:60 msgid "Link" msgstr "Enllaรงa" #: src/ui/preferences.blp:68 src/ui/preferences.blp:69 msgid "Unlink" msgstr "Desenllaรงa" #: src/ui/preferences.blp:81 msgid "User" msgstr "Usuari" #: src/ui/preferences.blp:101 msgid "Logout" msgstr "Desconnecta" #: src/ui/preferences.blp:111 msgid "Customization" msgstr "Personalitzaciรณ" #: src/ui/preferences.blp:114 msgid "Interface" msgstr "Interficie" #: src/ui/preferences.blp:116 msgid "Show Context Button in Rows" msgstr "Mostra el botรณ contextual a cada fila" #: src/ui/preferences.blp:117 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:120 msgid "Show Labels in Buttons of Pages" msgstr "Mostra les etiquetes als botons de les pร gines" #: src/ui/preferences.blp:123 msgid "Show Progressbar in Player Footer" msgstr "Mostra la barra de progrรฉs al reproductor" #: src/ui/preferences.blp:126 msgid "Make Background Translucent in Player" msgstr "Fes translรบcid el fons del reproductor" #: src/ui/preferences.blp:129 msgid "Use Sidebar Player When Possible" msgstr "Utilitza el reproductor de la barra lateral si รฉs possible" #: src/ui/preferences.blp:130 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:133 msgid "Show Pan Buttons on Carousels" msgstr "Mostra els botons de desplaรงament als carrusels" #: src/ui/preferences.blp:134 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" #: src/ui/preferences.blp:137 msgid "Button Size" msgstr "Mida del botรณ" #: src/ui/preferences.blp:138 msgid "Affects albums, artists, playlists and songs" msgstr "S'aplica a ร lbums, artistes, llistes i canรงons" #: src/ui/preferences.blp:144 src/ui/preferences.blp:145 msgid "Small" msgstr "Petit" #: src/ui/preferences.blp:149 src/ui/preferences.blp:150 msgid "Big" msgstr "Gran" #: src/ui/preferences.blp:158 msgid "Dynamic Background" msgstr "Fons dinร mic" #: src/ui/preferences.blp:159 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:162 msgid "Use in Main Window" msgstr "Utilitza'l a la finestra principal" #: src/ui/preferences.blp:163 msgid "Experimental" msgstr "Experimental" #: 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 "Apagat" #: 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 "Degradat" #: 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 "Difuminat" #: src/ui/preferences.blp:187 msgid "Use in Player" msgstr "Utilitza'l al reproductor" #: src/ui/preferences.blp:211 msgid "Use in Pop-out Window" msgstr "Utilitza'l a la finestra emergent" #: src/ui/preferences.blp:235 msgid "Use as Accent Color" msgstr "Utilitza'l com a color de realรงat" #: src/ui/preferences.blp:240 msgid "Homepage" msgstr "Pร gina d'inici" #: src/ui/preferences.blp:241 msgid "Reload homepage to see changes" msgstr "Restaureu la pร gina d'inici per veure els canvis" #: src/ui/preferences.blp:277 msgid "Sidebar" msgstr "Barra lateral" #: src/ui/preferences.blp:281 msgid "Visualizer" msgstr "Visualitzador" #: src/ui/preferences.blp:285 src/ui/window.blp:265 msgid "Preferences" msgstr "Parร metres" #: src/ui/preferences.blp:287 msgid "Show Visualizer in Player" msgstr "Mostra el visualitzador al reproductor" #: src/ui/preferences.blp:288 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:293 msgid "Appearance" msgstr "Aparenรงa" #: src/ui/preferences.blp:297 msgid "Number of Bars" msgstr "Nรบmero de barres" #: src/ui/preferences.blp:308 msgid "Visualizer Type" msgstr "Tipus de visualitzador" #: src/ui/preferences.blp:314 src/ui/preferences.blp:315 msgid "Wave" msgstr "Ones" #: src/ui/preferences.blp:319 src/ui/preferences.blp:320 msgid "Bars" msgstr "Barres" #: src/ui/preferences.blp:324 src/ui/preferences.blp:325 msgid "Particles" msgstr "Partรญcules" #: src/ui/preferences.blp:332 msgid "Fill Mode" msgstr "Mode d'emplenament" #: src/ui/preferences.blp:338 src/ui/preferences.blp:339 msgid "Solid" msgstr "Sรฒlid" #: src/ui/preferences.blp:343 src/ui/preferences.blp:344 msgid "Translucent" msgstr "Translรบcid" #: src/ui/preferences.blp:348 src/ui/preferences.blp:349 msgid "Border" msgstr "Contorn" #: src/ui/preferences.blp:357 msgid "Color" msgstr "Color" #: src/ui/preferences.blp:361 msgid "Use Dynamic Color" msgstr "Utilitza un color dinร mic" #: src/ui/preferences.blp:364 msgid "Invert Color" msgstr "Inverteix el color" #: src/ui/preferences.blp:368 msgid "Manual Color" msgstr "Color manual" #: src/ui/preferences.blp:374 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:275 #: src/widgets/playing/popout_window.py:53 msgid "Toggle Fullscreen" msgstr "Canvia a pantalla completa" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:270 msgid "Open Pop-Out Window" msgstr "Obre una finestra emergent" #: src/ui/song/button.blp:16 msgid "Play Song" msgstr "Reprodueix la canรงรณ" #: 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/row.blp:41 msgid "Drag icon" msgstr "Arrossega una icona" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "Casella de selecciรณ" #: src/ui/window.blp:99 msgid "Play Random Queue" msgstr "Reprodueix una cua aleatรฒria" #: src/ui/window.blp:107 msgid "Menu" msgstr "Menรบ" #: src/ui/window.blp:135 msgid "Random Albums" msgstr "ร€lbums aleatoris" #: src/ui/window.blp:139 msgid "Favorite Albums" msgstr "ร€lbums preferits" #: src/ui/window.blp:143 msgid "Newest Albums" msgstr "ร€lbums nous" #: src/ui/window.blp:147 msgid "Recent Albums" msgstr "ร€lbums recents" #: src/ui/window.blp:151 msgid "Frequent Albums" msgstr "ร€lbums freqรผents" #: src/ui/window.blp:174 msgid "Player" msgstr "Reproductor" #: src/ui/window.blp:280 msgid "See Last Nocturne Playback" msgstr "Mostra la darrera reproducciรณ del Nocturne" #: src/ui/window.blp:285 msgid "Keyboard Shortcuts" msgstr "Dreceres de teclat" #: src/ui/window.blp:290 msgid "About Nocturne" msgstr "Quant al 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 "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:48 msgid "Top Songs" msgstr "Canรงons mรฉs populars" #: src/widgets/artist/page.py:58 msgid "Related Artists" msgstr "Artistes relacionats" #: src/widgets/artist/page.py:113 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/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "En funcionament" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "Menรบ extra" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Biblioteca de mรบsica local" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "S'ha reiniciat" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "Connexiรณ rร pida" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "S'ha produรฏt un error en obtenir el codi" #: src/widgets/pages/login.py:146 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:63 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:234 msgid "LRC File" msgstr "Fitxer LRC" #: src/widgets/playing/player.py:634 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:116 #: 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:208 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "La cua de reproducciรณ {} ja es troba disponible!" #: src/window.py:210 msgid "Show" msgstr "Mostra" #: src/window.py:211 msgid "Later" msgstr "Mรฉs tard" #~ 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-469023d/po/de.po000066400000000000000000001153451522207411400165230ustar00rootroot00000000000000# 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-469023d/po/es.po000066400000000000000000001564301522207411400165420ustar00rootroot00000000000000# 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-03 20:56-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 Navidrome / Jellyfin client" msgstr "Un cliente moderno de Navidrome / Jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Caracterรญsticas" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 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:14 msgid "Playlist management" msgstr "Gestiรณn de Playlists" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "Compatibilidad con Jellyfin, OpenSubsonic y archivos locales" #: 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:92 src/ui/window.blp:190 #: src/ui/window.blp:242 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 "navidrome" msgstr "navidrome" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "jellyfin" msgstr "jellyfin" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "music" msgstr "music" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 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:138 src/widgets/pages/login.py:107 msgid "Error" msgstr "Error" #: src/actions.py:139 msgid "No details provided" msgstr "No se proporcionaron detalles" #: src/actions.py:141 src/actions.py:290 src/preferences.py:467 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "Cerrar" #: src/actions.py:182 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "No hay suficientes datos para el Nocturne Playback ({})" #: src/actions.py:287 src/ui/song/row.blp:72 msgid "External File" msgstr "Archivo Externo" #: 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 "" "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:308 msgid "Navidrome server deleted successfully" msgstr "El servidor Navidrome se eliminรณ correctamente" #: src/actions.py:317 msgid "Delete Navidrome Server" msgstr "Eliminar Servidor Navidrome" #: src/actions.py:318 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:320 src/actions.py:465 src/actions.py:499 src/actions.py:841 #: src/actions.py:944 src/preferences.py:420 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "Cancelar" #: src/actions.py:321 msgid "Keep Data" msgstr "Mantener Datos" #: src/actions.py:322 msgid "Delete Everything" msgstr "Eliminar Todo" #: src/actions.py:411 msgid "No Name" msgstr "Sin Nombre" #: src/actions.py:430 msgid "Radio updated successfully" msgstr "Radio actualizada correctamente" #: src/actions.py:430 msgid "Radio added successfully" msgstr "Radio agregada correctamente" #: src/actions.py:441 msgid "Error updating radio" msgstr "Error actualizando radio" #: src/actions.py:441 msgid "Error adding radio" msgstr "Error agregando radio" #: src/actions.py:450 src/actions.py:833 msgid "Name" msgstr "Nombre" #: src/actions.py:455 msgid "Stream Url" msgstr "Url de Stream" #: src/actions.py:462 msgid "Update Radio Station" msgstr "Actualizar Estaciรณn de Radio" #: src/actions.py:462 msgid "Add Radio Station" msgstr "Agregar Estaciรณn de Radio" #: src/actions.py:466 src/preferences.py:421 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Guardar" #: src/actions.py:482 msgid "Radio deleted successfully" msgstr "Radio eliminada correctamente" #: src/actions.py:490 msgid "Error deleting radio" msgstr "Error eliminando radio" #: src/actions.py:496 msgid "Delete Radio Station" msgstr "Eliminar Estaciรณn de Radio" #: src/actions.py:497 src/actions.py:942 #, python-brace-format msgid "Are you sure you want to delete '{}'?" msgstr "ยฟEstรกs seguro de que quieres eliminar '{}'?" #: src/actions.py:500 src/actions.py:945 src/constants.py:375 #: src/constants.py:423 msgid "Delete" msgstr "Eliminar" #: src/actions.py:534 src/actions.py:566 src/actions.py:572 src/actions.py:674 #: src/actions.py:762 msgid "Playing Next" msgstr "Reproduciendo a Continuaciรณn" #: src/actions.py:546 src/actions.py:585 src/actions.py:591 src/actions.py:691 #: src/actions.py:779 msgid "Playing Later" msgstr "Reproduciendo Mรกs Tarde" #: src/actions.py:566 src/actions.py:585 src/actions.py:1059 #: src/actions.py:1194 src/widgets/playlist/dialog.py:31 #, python-brace-format msgid "{} Songs" msgstr "{} Canciones" #: src/actions.py:621 msgid "Lyrics Saved" msgstr "Letras Guardadas" #: src/actions.py:814 msgid "Playlist updated successfully" msgstr "Playlist actualizada correctamente" #: src/actions.py:814 msgid "Playlist created successfully" msgstr "Playlist creada correctamente" #: src/actions.py:824 msgid "Error updating playlist" msgstr "Error actualizando playlist" #: src/actions.py:824 msgid "Error creating playlist" msgstr "Error creando playlist" #: src/actions.py:838 msgid "Update Playlist" msgstr "Actualizar Playlist" #: src/actions.py:838 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Crear Playlist" #: src/actions.py:842 msgid "Update" msgstr "Actualizar" #: src/actions.py:842 msgid "Create" msgstr "Crear" #: src/actions.py:861 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:892 src/actions.py:912 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:916 src/actions.py:1064 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: src/actions.py:929 msgid "Playlist Deleted" msgstr "Playlist Eliminada" #: src/actions.py:941 msgid "Delete Playlist" msgstr "Eliminar Playlist" #: src/actions.py:990 msgid "No songs found" msgstr "No se encontraron canciones" #: src/actions.py:1033 src/actions.py:1060 src/actions.py:1097 #: src/actions.py:1133 msgid "Download Started" msgstr "Descarga Iniciada" #: src/actions.py:1040 src/actions.py:1071 src/actions.py:1078 #: src/actions.py:1113 src/actions.py:1149 msgid "Already Downloaded" msgstr "Ya Fue Descargado" #: src/actions.py:1065 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:1077 msgid "Download Skipped" msgstr "Descarga Saltada" #: src/actions.py:1101 src/actions.py:1137 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: src/actions.py:1176 src/actions.py:1195 msgid "Deleted" msgstr "Eliminado" #: src/constants.py:185 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Consecutivo" #: src/constants.py:189 msgid "Repeat All" msgstr "Repetir Todo" #: src/constants.py:193 msgid "Repeat One" msgstr "Repetir Una" #: src/constants.py:198 #, python-brace-format msgid "Low ({})" msgstr "Bajo ({})" #: src/constants.py:199 #, python-brace-format msgid "Medium ({})" msgstr "Medio ({})" #: src/constants.py:200 #, python-brace-format msgid "High ({})" msgstr "Alto ({})" #: src/constants.py:201 #, python-brace-format msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:202 msgid "Original File" msgstr "Archivo Original" #. Item #: src/constants.py:209 src/ui/pages/home.blp:5 msgid "Home" msgstr "Hogar" #. 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:275 src/widgets/pages/home.py:54 msgid "Artists" msgstr "Artistas" #. Section #: src/constants.py:221 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:267 #: src/widgets/artist/page.py:53 src/widgets/pages/home.py:49 msgid "Albums" msgstr "รlbumes" #. Item #: src/constants.py:224 src/constants.py:259 src/constants.py:279 #: src/window.py:149 msgid "All" msgstr "Todos" #. Item #: src/constants.py:229 msgid "Random" msgstr "Aleatorio" #. Item #: src/constants.py:234 src/constants.py:264 msgid "Favorites" msgstr "Favoritos" #. Item #: src/constants.py:239 msgid "Recently Added" msgstr "Agregado Recientemente" #. Item #: src/constants.py:244 msgid "Recently Played" msgstr "Reproducido Recientemente" #. Item #: src/constants.py:249 msgid "Most Played" msgstr "Mรกs reproducida" #. Section #: src/constants.py:256 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:259 #: src/widgets/album/page.py:45 src/widgets/pages/home.py:38 #: src/widgets/playlist/page.py:29 msgid "Songs" msgstr "Canciones" #. Item #: src/constants.py:269 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Radios" #. Section #: src/constants.py:276 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:283 #: src/widgets/pages/home.py:59 msgid "Playlists" msgstr "Playlists" #: src/constants.py:289 src/constants.py:340 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:172 src/ui/playing/footer.blp:103 #: src/ui/song/queue.blp:47 src/ui/song/queue.blp:49 msgid "Play" msgstr "Reproducir" #: src/constants.py:294 src/constants.py:327 src/constants.py:350 msgid "Shuffle" msgstr "Mezclar" #: 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 "Reproducir a Continuaciรณn" #: 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 "Reproducir Mรกs Tarde" #: 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 "Agregar a Playlist" #: 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 "Descargar" #: src/constants.py:319 src/constants.py:418 #: src/nocturne_search_provider.py.in:11 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Mostrar Artista" #: src/constants.py:332 src/ui/playing/lyrics_page.blp:146 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Radio" #: src/constants.py:345 msgid "Resume" msgstr "Reanudar" #: src/constants.py:370 src/constants.py:398 msgid "Edit" msgstr "Editar" #: src/constants.py:384 msgid "Select" msgstr "Seleccionar" #: src/constants.py:413 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Mostrar รlbum" #: src/constants.py:429 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Eliminar" #: src/constants.py:434 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "Eliminar Descarga" #: src/constants.py:440 src/main.py:156 msgid "Show Details" msgstr "Mostar Detalles" #: src/constants.py:451 msgid "Visit Webpage" msgstr "Visitar Pรกgina Web" #: src/constants.py:457 msgid "Update Server" msgstr "Actualizar Server" #: src/constants.py:462 msgid "Delete Server" msgstr "Eliminar Server" #: src/constants.py:471 msgid "Edit Lyrics" msgstr "Editar Letra" #: src/constants.py:476 msgid "Remove Lyrics" msgstr "Remover Letra" #: src/integrations/base.py:153 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:230 msgid "Could not log in" msgstr "No se pudo iniciar sesiรณn" #: src/integrations/jellyfin.py:1016 msgid "Unknown" msgstr "Desconocido" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:639 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:288 msgid "No Album" msgstr "Sin รlbum" #: src/integrations/local.py:655 src/integrations/local.py:664 msgid "Offline Mode" msgstr "Modo Offline" #: src/integrations/local.py:656 msgid "Access your downloads" msgstr "Accede a tus descargas" #: src/integrations/models.py:138 src/ui/album/page.blp:5 #: src/widgets/album/page.py:129 msgid "Album" msgstr "รlbum" #: src/integrations/models.py:140 msgid "Display Artist" msgstr "Artista Principal" #: src/integrations/models.py:142 msgid "MusicBrainz ID" msgstr "MusicBrainz ID" #: src/integrations/models.py:143 msgid "Track Number" msgstr "Nรบmero de Pieza" #: src/integrations/models.py:144 msgid "Year" msgstr "Aรฑo" #: src/integrations/models.py:145 msgid "Size" msgstr "Tamaรฑo" #: src/integrations/models.py:146 msgid "Suffix" msgstr "Sufijo" #: 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:180 src/widgets/song/row.py:183 msgid "Favorite" msgstr "Favorito" #: src/integrations/models.py:148 msgid "Duration" msgstr "Duraciรณn" #: src/integrations/models.py:150 msgid "Bit Rate" msgstr "Tasa de Bits" #: src/integrations/models.py:151 msgid "Bit Depth" msgstr "Profundidad de Bits" #: src/integrations/models.py:152 msgid "Sampling Rate" msgstr "Frecuencia de Muestreo" #: src/integrations/models.py:153 msgid "Channel Count" msgstr "Cantidad de Canales" #: src/integrations/models.py:155 msgid "Path" msgstr "Direcciรณn" #: src/integrations/models.py:156 msgid "Disc Number" msgstr "Nรบmero de Disco" #: src/integrations/models.py:157 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:158 msgid "Genres" msgstr "Generos" #. list #: src/integrations/models.py:160 msgid "Track Gain" msgstr "Ganancia de Pista" #: src/integrations/models.py:161 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 / Navidrome instance" msgstr "Utilice una instancia existente de OpenSubsonic/Navidrome" #: src/integrations/navidrome.py:149 msgid "Server returned an error" msgstr "El servidor devolviรณ un error" #: src/integrations/navidrome.py:149 msgid "Unknown Error" msgstr "Error desconocido" #: src/integrations/navidrome.py:663 src/integrations/navidrome.py:672 msgid "Managed Server" msgstr "Servidor Gestionado" #: src/integrations/navidrome.py:664 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:667 msgid "Manage Server" msgstr "Gestiona Servidor" #: src/integrations/navidrome.py:673 msgid "Create and use a Navidrome instance" msgstr "Crea y utiliza una instancia de Navidrome" #: src/main.py:92 msgid "Music is Playing" msgstr "Mรบsica en Reproducciรณn" #: src/main.py:105 msgid "Fullscreen Player Active" msgstr "Reproductor de Pantalla Completa Activo" #: src/main.py:153 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:409 msgid "Settings Page" msgstr "Pรกgina de Ajustes" #: src/preferences.py:412 msgid "User Token" msgstr "Token de Usuario" #: src/preferences.py:416 msgid "Link ListenBrainz" msgstr "Vincula ListenBrainz" #: src/preferences.py:417 msgid "Connect your ListenBrainz account with a user token" msgstr "Conecta tu cuenta de ListenBrainz con un token de usuario" #: src/preferences.py:457 msgid "Flatpak Sandbox Warning" msgstr "Advertencia de Flatpak Sandbox" #: src/preferences.py:458 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/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 "Lista" #: src/ui/containers/carousel.blp:38 msgid "Pan to the Left" msgstr "Desplazar Hacia la Izquierda" #: src/ui/containers/carousel.blp:64 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:118 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: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 "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 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:68 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:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "Inicio de Sesiรณn" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "Estado del Servidor" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "No en Funcionamiento" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "Reiniciar Instancia" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Gestionar Servidor Navidrome" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "Url" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "Mรกs Opciones" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Confiar en Certificados del Servidor" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Directorio de Librerรญa" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Usuario" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Contraseรฑa" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 msgid "Use Quick Connect" msgstr "Usar Conexiรณn Rรกpida" #: src/ui/pages/playback.blp:5 src/ui/shortcuts-dialog.blp:25 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:207 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/songs_starred.blp:96 #: src/ui/song/queue.blp:124 msgid "No Songs Found" msgstr "No se Encontraron Canciones" #: src/ui/pages/songs_starred.blp:5 src/ui/pages/songs_starred.blp:28 msgid "Favorite Songs" msgstr "Canciones Favoritas" #: src/ui/pages/songs_starred.blp:31 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:88 msgid "Previous" msgstr "Previo" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:132 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:28 msgid "Audio" msgstr "Audio" #: src/ui/playing/cover_art.blp:36 src/ui/song/details_dialog.blp:34 msgid "Song Cover" msgstr "Portada de Canciรณn" #: src/ui/playing/cover_art.blp:42 src/ui/playing/cover_art.blp:50 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:34 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:66 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:214 msgid "Song Details" msgstr "Detalles de Canciรณn" #: src/ui/playing/popout_window.blp:86 src/ui/window.blp:180 #: src/ui/window.blp:236 msgid "Queue" msgstr "Cola" #: src/ui/playing/popout_window.blp:98 src/ui/window.blp:196 #: src/ui/window.blp:248 msgid "Equalizer" msgstr "Ecualizador" #: src/ui/playing/queue_page.blp:20 msgid "Generating Next Queue" msgstr "Generando Siguiente Cola" #: src/ui/playing/queue_page.blp:24 msgid "Autoplay" msgstr "Reproducciรณn Automรกtica" #: src/ui/playing/queue_page.blp:25 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:84 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 "Use Track and Album Gain" msgstr "Utilizar Ganancia de Canciรณn y รlbum" #: src/ui/preferences.blp:23 msgid "Adjust the volume based on gain metadata" msgstr "Ajusta el volumen segรบn los metadatos de ganancia" #: src/ui/preferences.blp:26 msgid "Default Page on Login" msgstr "Pรกgina por Defecto en Inicio" #: src/ui/preferences.blp:31 msgid "Maximum Bitrate" msgstr "Bitrate Mรกximo" #: src/ui/preferences.blp:37 msgid "Gnome Search" msgstr "Bรบsqueda Gnome" #: src/ui/preferences.blp:38 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:41 msgid "Include Artists" msgstr "Incluir Artistas" #: src/ui/preferences.blp:44 msgid "Include Albums" msgstr "Incluir Albumes" #: src/ui/preferences.blp:47 msgid "Include Songs" msgstr "Incluir Canciones" #: src/ui/preferences.blp:50 msgid "Include Playlists" msgstr "Incluir Playlists" #: src/ui/preferences.blp:54 msgid "Session" msgstr "Sesiรณn" #: src/ui/preferences.blp:57 msgid "Discord Rich Presence" msgstr "Discord Rich Presence" #: src/ui/preferences.blp:58 msgid "Share the currently playing song on Discord" msgstr "Comparte la canciรณn que se estรก reproduciendo en Discord" #: src/ui/preferences.blp:62 msgid "Share Cover Art With Discord" msgstr "Compartir Arte con Discord" #: src/ui/preferences.blp:63 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:69 msgid "ListenBrainz Link" msgstr "Vincula ListenBrainz" #: src/ui/preferences.blp:70 msgid "Keep track of your playback history" msgstr "Mantรฉn un registro de tu historial de reproducciรณn" #: src/ui/preferences.blp:76 src/ui/preferences.blp:77 msgid "Link" msgstr "Conectar" #: src/ui/preferences.blp:85 src/ui/preferences.blp:86 msgid "Unlink" msgstr "Desconectar" #: src/ui/preferences.blp:98 msgid "User" msgstr "Usuario" #: src/ui/preferences.blp:118 msgid "Logout" msgstr "Cerrar Sesiรณn" #: src/ui/preferences.blp:128 msgid "Customization" msgstr "Personalizaciรณn" #: src/ui/preferences.blp:131 msgid "Interface" msgstr "Interfaz" #: src/ui/preferences.blp:133 msgid "Show Context Button in Rows" msgstr "Mostrar Botรณn de Contexto en las Filas" #: src/ui/preferences.blp:134 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:137 msgid "Show Labels in Buttons of Pages" msgstr "Mostrar Etiquetas en Botones de Pรกginas" #: src/ui/preferences.blp:140 msgid "Show Progressbar in Player Footer" msgstr "Mostrar Barra de Progreso en el Pie de Reproductor" #: src/ui/preferences.blp:143 msgid "Make Background Translucent in Player" msgstr "Hacer Fondo Translรบcido en el Reproductor" #: src/ui/preferences.blp:146 msgid "Use Sidebar Player When Possible" msgstr "Usar el Reproductor de Barra Lateral Cuando es Posible" #: src/ui/preferences.blp:147 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:150 msgid "Show Pan Buttons on Carousels" msgstr "Mostrar Botones de Desplazamiento en Carruseles" #: src/ui/preferences.blp:151 msgid "Only visible when a carousel has 5 or more items" msgstr "Solo visible cuando un carrusel tiene 5 o mรกs elementos" #: src/ui/preferences.blp:154 msgid "Button Size" msgstr "Tamaรฑo de Botones" #: src/ui/preferences.blp:155 msgid "Affects albums, artists, playlists and songs" msgstr "Afecta albumes, artistas, playlists y canciones" #: src/ui/preferences.blp:161 src/ui/preferences.blp:162 msgid "Small" msgstr "Pequeรฑo" #: src/ui/preferences.blp:166 src/ui/preferences.blp:167 msgid "Big" msgstr "Grande" #: src/ui/preferences.blp:175 msgid "Dynamic Background" msgstr "Fondo Dinรกmico" #: src/ui/preferences.blp:176 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:179 msgid "Use in Main Window" msgstr "Usar en la Ventana Principal" #: src/ui/preferences.blp:185 src/ui/preferences.blp:186 #: src/ui/preferences.blp:209 src/ui/preferences.blp:210 #: src/ui/preferences.blp:233 src/ui/preferences.blp:234 msgid "Off" msgstr "Apagado" #: src/ui/preferences.blp:190 src/ui/preferences.blp:191 #: src/ui/preferences.blp:214 src/ui/preferences.blp:215 #: src/ui/preferences.blp:238 src/ui/preferences.blp:239 msgid "Gradient" msgstr "Gradiante" #: src/ui/preferences.blp:195 src/ui/preferences.blp:196 #: src/ui/preferences.blp:219 src/ui/preferences.blp:220 #: src/ui/preferences.blp:243 src/ui/preferences.blp:244 msgid "Blur" msgstr "Blur" #: src/ui/preferences.blp:203 msgid "Use in Player" msgstr "Usar en Reproductor" #: src/ui/preferences.blp:227 msgid "Use in Pop-out Window" msgstr "Usar en Ventana Emergente" #: src/ui/preferences.blp:251 msgid "Use as Accent Color" msgstr "Usar Como Color de Acento" #: src/ui/preferences.blp:256 msgid "Homepage" msgstr "Hogar" #: src/ui/preferences.blp:257 msgid "Reload homepage to see changes" msgstr "Actualiza la pรกgina del hogar para ver cambios" #: src/ui/preferences.blp:293 msgid "Sidebar" msgstr "Barra Lateral" #: src/ui/preferences.blp:297 msgid "Visualizer" msgstr "Visualizador" #: src/ui/preferences.blp:301 src/ui/window.blp:265 msgid "Preferences" msgstr "Preferencias" #: src/ui/preferences.blp:303 msgid "Show Visualizer in Player" msgstr "Mostrar Visualizador en el Reproductor" #: src/ui/preferences.blp:304 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:309 msgid "Appearance" msgstr "Apariencia" #: src/ui/preferences.blp:313 msgid "Number of Bars" msgstr "Nรบmero de Barras" #: src/ui/preferences.blp:324 msgid "Visualizer Type" msgstr "Tipo de Visualizador" #: src/ui/preferences.blp:330 src/ui/preferences.blp:331 msgid "Wave" msgstr "Ola" #: src/ui/preferences.blp:335 src/ui/preferences.blp:336 msgid "Bars" msgstr "Barras" #: src/ui/preferences.blp:340 src/ui/preferences.blp:341 msgid "Particles" msgstr "Particulas" #: src/ui/preferences.blp:348 msgid "Fill Mode" msgstr "Modo de Relleno" #: src/ui/preferences.blp:354 src/ui/preferences.blp:355 msgid "Solid" msgstr "Solido" #: src/ui/preferences.blp:359 src/ui/preferences.blp:360 msgid "Translucent" msgstr "Translรบcido" #: src/ui/preferences.blp:364 src/ui/preferences.blp:365 msgid "Border" msgstr "Borde" #: src/ui/preferences.blp:373 msgid "Color" msgstr "Color" #: src/ui/preferences.blp:377 msgid "Use Dynamic Color" msgstr "Utilizar Color Dinรกmico" #: src/ui/preferences.blp:380 msgid "Invert Color" msgstr "Invertir Color" #: src/ui/preferences.blp:384 msgid "Manual Color" msgstr "Color Manual" #: src/ui/preferences.blp:390 msgid "Visualizer Color" msgstr "Color de Visualizador" #: src/ui/shortcuts-dialog.blp:12 msgid "Show Shortcuts" msgstr "Mostrar Atajos" #: src/ui/shortcuts-dialog.blp:16 msgid "Show Preferences" msgstr "Mostrar Preferencias" #: src/ui/shortcuts-dialog.blp:20 msgid "Quit" msgstr "Cerrar" #: src/ui/shortcuts-dialog.blp:27 msgid "Toggle Playback" msgstr "Alternar Reproducciรณn" #: src/ui/shortcuts-dialog.blp:31 msgid "Next Song" msgstr "Siguiente Canciรณn" #: src/ui/shortcuts-dialog.blp:35 msgid "Previous Song" msgstr "Canciรณn Previa" #: src/ui/shortcuts-dialog.blp:39 msgid "Raise Volume" msgstr "Aumentar Volumen" #: src/ui/shortcuts-dialog.blp:43 msgid "Lower Volume" msgstr "Restar Volumen" #: src/ui/shortcuts-dialog.blp:48 msgid "Window" msgstr "Ventana" #: src/ui/shortcuts-dialog.blp:50 src/ui/window.blp:275 #: src/widgets/playing/popout_window.py:53 msgid "Toggle Fullscreen" msgstr "Alternar Pantalla Completa" #: src/ui/shortcuts-dialog.blp:54 src/ui/window.blp:270 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/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:99 msgid "Play Random Queue" msgstr "Reproducir Cola Aleatoria" #: src/ui/window.blp:107 msgid "Menu" msgstr "Menรบ" #: src/ui/window.blp:135 msgid "Random Albums" msgstr "รlbumes Aleatorios" #: src/ui/window.blp:139 msgid "Favorite Albums" msgstr "รlbumes Favoritos" #: src/ui/window.blp:143 msgid "Newest Albums" msgstr "รlbumes Nuevos" #: src/ui/window.blp:147 msgid "Recent Albums" msgstr "รlbumes Recientes" #: src/ui/window.blp:151 msgid "Frequent Albums" msgstr "รlbumes Frecuentes" #: src/ui/window.blp:174 msgid "Player" msgstr "Reproductor" #: src/ui/window.blp:280 msgid "See Last Nocturne Playback" msgstr "Mostrar el รšltimo Nocturne Playback" #: src/ui/window.blp:285 msgid "Keyboard Shortcuts" msgstr "Atajos de Teclado" #: src/ui/window.blp:290 msgid "About Nocturne" msgstr "Sobre 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:185 #: 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:48 msgid "Top Songs" msgstr "Canciones Top" #: src/widgets/artist/page.py:58 msgid "Related Artists" msgstr "Artistas Relacionados" #: src/widgets/artist/page.py:113 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/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "En Funcionamiento" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "Menรบ Extra" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Libreria de Mรบsica Local" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "Reiniciado" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "Conexiรณn Rรกpida" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "Error obteniendo cรณdigo" #: src/widgets/pages/login.py:146 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:63 msgid "Installing" msgstr "Instalando" #: src/widgets/pages/welcome.py:20 src/widgets/pages/welcome.py:22 msgid "Integration" msgstr "Integraciรณn" #: src/widgets/playing/lyrics_page.py:234 msgid "LRC File" msgstr "Archivo LRC" #: src/widgets/playing/player.py:634 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:116 #: 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:208 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "ยกTu cola de reproducciรณn para {} estรก disponible!" #: src/window.py:210 msgid "Show" msgstr "Mostar" #: src/window.py:211 msgid "Later" msgstr "Mรกs Tarde" #~ 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-469023d/po/eu.po000066400000000000000000001336401522207411400165420ustar00rootroot00000000000000# 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-05-27 21:16+0200\n" "PO-Revision-Date: 2026-05-27 21:24+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 Navidrome / Jellyfin client" msgstr "Navidrome / Jellyfin bezero modernoa" #: data/com.jeffser.Nocturne.metainfo.xml.in:11 msgid "Features" msgstr "Eginbideak" #: data/com.jeffser.Nocturne.metainfo.xml.in:13 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:14 msgid "Playlist management" msgstr "Kudeatu erreprodukzio-zerrendak" #: data/com.jeffser.Nocturne.metainfo.xml.in:15 msgid "Compatibility with Jellyfin, OpenSubsonic and local files" msgstr "Jellyfin, OpenSubsonic eta fitxategi lokalekin bateragarria" #: 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:311 src/ui/window.blp:175 #: src/ui/window.blp:227 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: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 "musika" #: data/com.jeffser.Nocturne.metainfo.xml.in:90 msgid "gtk" msgstr "gtk" #: src/actions.py:173 msgid "Not enough data found for Nocturne Playback ({})" msgstr "Ez da behar beste datu aurkitu Nocturne erreprodukziorako ({})" #: src/actions.py:278 src/ui/song/row.blp:69 msgid "External File" msgstr "Kanpoko fitxategia" #: src/actions.py:279 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:281 src/preferences.py:426 src/ui/song/queue.blp:40 msgid "Close" msgstr "Itxi" #: src/actions.py:299 msgid "Navidrome server deleted successfully" msgstr "Navidrome zerbitzaria behar bezala ezabatu da" #: src/actions.py:308 msgid "Delete Navidrome Server" msgstr "Ezabatu Navidrome zerbitzaria" #: src/actions.py:309 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "Ziur Navidrome zerbitzaria ezabatu nahi duzula?" #: src/actions.py:311 src/actions.py:445 src/actions.py:479 src/actions.py:821 #: src/actions.py:924 src/preferences.py:379 src/ui/lyrics/dialog.blp:16 #: src/ui/lyrics/dialog.blp:17 src/widgets/pages/login.py:152 msgid "Cancel" msgstr "Utzi" #: src/actions.py:312 msgid "Keep Data" msgstr "Mantendu datuak" #: src/actions.py:313 msgid "Delete Everything" msgstr "Ezabatu dena" #: src/actions.py:391 msgid "No Name" msgstr "Izenik ez" #: src/actions.py:410 msgid "Radio updated successfully" msgstr "Irratia behar bezala eguneratu da" #: src/actions.py:410 msgid "Radio added successfully" msgstr "Irratia behar bezala gehitu da" #: src/actions.py:421 msgid "Error updating radio" msgstr "Errorea irratia eguneratzean" #: src/actions.py:421 msgid "Error adding radio" msgstr "Errorea irratia gehitzean" #: src/actions.py:430 src/actions.py:813 msgid "Name" msgstr "Izena" #: src/actions.py:435 msgid "Stream Url" msgstr "Jarioaren URLa" #: src/actions.py:442 msgid "Update Radio Station" msgstr "Eguneratu irrati-estazioa" #: src/actions.py:442 msgid "Add Radio Station" msgstr "Gehitu irrati-estazioa" #: src/actions.py:446 src/preferences.py:380 src/ui/lyrics/dialog.blp:23 #: src/ui/lyrics/dialog.blp:24 msgid "Save" msgstr "Gorde" #: src/actions.py:462 msgid "Radio deleted successfully" msgstr "Irratia behar bezala ezabatu da" #: src/actions.py:470 msgid "Error deleting radio" msgstr "Errorea irratia ezabatzean" #: src/actions.py:476 msgid "Delete Radio Station" msgstr "Ezabatu irrati-estazioa" #: src/actions.py:477 src/actions.py:922 msgid "Are you sure you want to delete '{}'?" msgstr "Ziur '{}' ezabatu nahi duzula?" #: src/actions.py:480 src/actions.py:925 src/constants.py:373 #: src/constants.py:426 msgid "Delete" msgstr "Ezabatu" #: src/actions.py:514 src/actions.py:546 src/actions.py:552 src/actions.py:654 #: src/actions.py:742 msgid "Playing Next" msgstr "Hurrena entzungai" #: src/actions.py:526 src/actions.py:565 src/actions.py:571 src/actions.py:671 #: src/actions.py:759 msgid "Playing Later" msgstr "Geroago entzungai" #: src/actions.py:546 src/actions.py:565 src/actions.py:1039 #: src/actions.py:1174 src/widgets/playlist/dialog.py:31 msgid "{} Songs" msgstr "{} abesti" #: src/actions.py:601 msgid "Lyrics Saved" msgstr "Hitzak gorde dira" #: src/actions.py:794 msgid "Playlist updated successfully" msgstr "Erreprodukzio-zerrenda behar bezala eguneratu da" #: src/actions.py:794 msgid "Playlist created successfully" msgstr "Erreprodukzio-zerrenda behar bezala sortu da" #: src/actions.py:804 msgid "Error updating playlist" msgstr "Errorea erreprodukzio-zerrenda eguneratzean" #: src/actions.py:804 msgid "Error creating playlist" msgstr "Errorea erreprodukzio-zerrenda sortzean" #: src/actions.py:818 msgid "Update Playlist" msgstr "Eguneratu erreprodukzio-zerrenda" #: src/actions.py:818 src/ui/pages/playlists.blp:41 msgid "Create Playlist" msgstr "Sortu erreprodukzio-zerrenda" #: src/actions.py:822 msgid "Update" msgstr "Eguneratu" #: src/actions.py:822 msgid "Create" msgstr "Sortu" #: src/actions.py:841 msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "Abesti {} kendu da" msgstr[1] "{} abesti kendu dira" #: src/actions.py:872 src/actions.py:892 msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "Abesti {} gehitu da" msgstr[1] "{} abesti gehitu dira" #: src/actions.py:896 src/actions.py:1044 msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "Abesti {} saltatu da" msgstr[1] "{} abesti saltatu dira" #: src/actions.py:909 msgid "Playlist Deleted" msgstr "Erreprodukzio-zerrenda ezabatu da" #: src/actions.py:921 msgid "Delete Playlist" msgstr "Ezabatu erreprodukzio-zerrenda" #: src/actions.py:970 msgid "No songs found" msgstr "Ez da abestirik aurkitu" #: src/actions.py:1013 src/actions.py:1040 src/actions.py:1077 #: src/actions.py:1113 msgid "Download Started" msgstr "Deskarga hasi da" #: src/actions.py:1020 src/actions.py:1051 src/actions.py:1058 #: src/actions.py:1093 src/actions.py:1129 msgid "Already Downloaded" msgstr "Dagoeneko deskargatua" #: src/actions.py:1045 msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "Abesti {} ({})" msgstr[1] "{} abesti ({})" #: src/actions.py:1057 msgid "Download Skipped" msgstr "Deskarga saltatu da" #: src/actions.py:1081 src/actions.py:1117 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:1156 src/actions.py:1175 msgid "Deleted" msgstr "Ezabatuta" #: src/constants.py:183 src/ui/playing/playback_mode_button.blp:5 msgid "Consecutive" msgstr "Jarraian" #: src/constants.py:187 msgid "Repeat All" msgstr "Errepikatu dena" #: src/constants.py:191 msgid "Repeat One" msgstr "Errepikatu bat" #: src/constants.py:196 msgid "Low ({})" msgstr "Baxua ({})" #: src/constants.py:197 msgid "Medium ({})" msgstr "Tartekoa ({})" #: src/constants.py:198 msgid "High ({})" msgstr "Altua ({})" #: src/constants.py:199 msgid "Ultra ({})" msgstr "Ultra ({})" #: src/constants.py:200 msgid "Original File" msgstr "Fitxategiaren jatorrizkoa" #. Item #: src/constants.py:207 src/ui/pages/home.blp:5 msgid "Home" msgstr "Hasiera" #. Item #. list #: src/constants.py:212 src/integrations/models.py:158 #: src/ui/pages/artists.blp:5 src/ui/pages/artists.blp:28 #: src/ui/preferences.blp:255 src/widgets/pages/home.py:54 msgid "Artists" msgstr "Artistak" #. Section #: src/constants.py:219 src/ui/pages/albums_all.blp:5 #: src/ui/pages/albums_all.blp:28 src/ui/preferences.blp:247 #: src/widgets/artist/page.py:55 src/widgets/pages/home.py:49 msgid "Albums" msgstr "Albumak" #. Item #: src/constants.py:222 src/constants.py:257 src/constants.py:277 #: src/window.py:145 msgid "All" msgstr "Guztiak" #. Item #: src/constants.py:227 msgid "Random" msgstr "Ausaz" #. Item #: src/constants.py:232 src/constants.py:262 msgid "Favorites" msgstr "Gogokoak" #. Item #: src/constants.py:237 msgid "Recently Added" msgstr "Gehitu berriak" #. Item #: src/constants.py:242 msgid "Recently Played" msgstr "Entzun berriak" #. Item #: src/constants.py:247 msgid "Most Played" msgstr "Entzunenak" #. Section #: src/constants.py:254 src/ui/pages/songs_all.blp:5 #: src/ui/pages/songs_all.blp:28 src/ui/preferences.blp:239 #: src/widgets/album/page.py:47 src/widgets/pages/home.py:38 #: src/widgets/playlist/page.py:31 msgid "Songs" msgstr "Abestiak" #. Item #: src/constants.py:267 src/ui/pages/radios.blp:5 src/ui/pages/radios.blp:13 msgid "Radios" msgstr "Irratiak" #. Section #: src/constants.py:274 src/ui/pages/playlists.blp:5 #: src/ui/pages/playlists.blp:28 src/ui/preferences.blp:263 #: src/widgets/pages/home.py:59 msgid "Playlists" msgstr "Erreprodukzio-zerrendak" #: src/constants.py:287 src/constants.py:338 src/ui/lyrics/dialog.blp:47 #: src/ui/playing/control_page.blp:225 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 "Erreproduzitu" #: src/constants.py:292 src/constants.py:325 src/constants.py:348 msgid "Shuffle" msgstr "Nahastu" #: src/constants.py:297 src/constants.py:353 src/constants.py:386 #: src/ui/song/queue.blp:55 src/ui/song/queue.blp:57 msgid "Play Next" msgstr "Erreproduzitu hurrena" #: src/constants.py:302 src/constants.py:358 src/constants.py:391 #: src/ui/song/queue.blp:63 src/ui/song/queue.blp:65 msgid "Play Later" msgstr "Erreproduzitu geroago" #: src/constants.py:307 src/constants.py:406 src/ui/song/queue.blp:71 #: src/ui/song/queue.blp:73 msgid "Add To Playlist" msgstr "Gehitu erreprodukzio-zerrendara" #: src/constants.py:312 src/constants.py:363 src/constants.py:411 #: 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 "Deskargatu" #: src/constants.py:317 src/constants.py:421 src/ui/playing/control_page.blp:83 msgid "Show Artist" msgstr "Erakutsi artista" #: src/constants.py:330 src/ui/playing/lyrics_page.blp:126 #: src/widgets/song/row.py:109 msgid "Radio" msgstr "Irratia" #: src/constants.py:343 msgid "Resume" msgstr "Berrekin" #: src/constants.py:368 src/constants.py:396 msgid "Edit" msgstr "Editatu" #: src/constants.py:382 msgid "Select" msgstr "Hautatu" #: src/constants.py:401 msgid "Edit Lyrics" msgstr "Editatu hitzak" #: src/constants.py:416 src/ui/album/button.blp:49 #: src/ui/playing/control_page.blp:98 msgid "Show Album" msgstr "Erakutsi albuma" #: src/constants.py:432 src/ui/song/queue.blp:90 src/ui/song/queue.blp:92 msgid "Remove" msgstr "Kendu" #: src/constants.py:437 src/ui/song/queue.blp:101 src/ui/song/queue.blp:103 msgid "Delete Download" msgstr "Ezabatu deskarga" #: src/constants.py:443 msgid "Show Details" msgstr "Erakutsi xehetasunak" #: src/constants.py:451 msgid "Visit Webpage" msgstr "Bisitatu webgunea" #: src/constants.py:457 msgid "Update Server" msgstr "Eguneratu zerbitzaria" #: src/constants.py:462 msgid "Delete Server" msgstr "Ezabatu zerbitzaria" #: 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:16 msgid "Connect to a Jellyfin server." msgstr "Konektatu Jellyfin zerbitzari batera." #: src/integrations/jellyfin.py:20 msgid "Jellyfin" msgstr "Jellyfin" #: src/integrations/jellyfin.py:21 msgid "Use an existing Jellyfin instance" msgstr "Erabili existitzen den Jellyfin zerbitzari bat" #: src/integrations/jellyfin.py:956 msgid "Unknown" msgstr "Ezezaguna" #: src/integrations/local.py:22 src/integrations/local.py:28 #: src/integrations/local.py:585 msgid "Local Files" msgstr "Fitxategi lokalak" #: 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 "" "Utzi Nocturneri fitxategi lokalak zuzenean kargatzen. Liburutegi " "handientzako zerbitzari dedikatu bat erabiltzea komeni da." #: src/integrations/local.py:25 src/ui/pages/setup.blp:67 #: src/ui/pages/setup.blp:68 msgid "Continue" msgstr "Jarraitu" #: src/integrations/local.py:29 msgid "Limited functionality" msgstr "Funtzionalitate mugatua" #. load songs, albums, artists #: src/integrations/local.py:70 msgid "Loading Songs" msgstr "Abestiak kargatzen" #: src/integrations/local.py:280 msgid "No Album" msgstr "Albumik ez" #: src/integrations/local.py:601 src/integrations/local.py:610 msgid "Offline Mode" msgstr "Offline modua" #: src/integrations/local.py:602 msgid "Access your downloads" msgstr "Atzitu zure deskargak" #: src/integrations/models.py:137 src/ui/album/page.blp:5 #: src/widgets/album/page.py:131 msgid "Album" msgstr "Albuma" #: src/integrations/models.py:139 msgid "Display Artist" msgstr "Artistaren izena" #: src/integrations/models.py:141 msgid "MusicBrainz ID" msgstr "MusicBrainz IDa" #: src/integrations/models.py:142 msgid "Track Number" msgstr "Pista zenbakia" #: src/integrations/models.py:143 msgid "Year" msgstr "Urtea" #: src/integrations/models.py:144 msgid "Size" msgstr "Tamaina" #: src/integrations/models.py:145 msgid "Suffix" msgstr "Atzizkia" #: src/integrations/models.py:146 src/widgets/album/button.py:78 #: src/widgets/album/page.py:147 src/widgets/artist/page.py:127 #: src/widgets/playing/control_page.py:176 src/widgets/song/row.py:163 msgid "Favorite" msgstr "Gogokoa" #: src/integrations/models.py:147 msgid "Duration" msgstr "Iraupena" #: src/integrations/models.py:149 msgid "Bit Rate" msgstr "Bit-tasa" #: src/integrations/models.py:150 msgid "Bit Depth" msgstr "Bit sakonera" #: src/integrations/models.py:151 msgid "Sampling Rate" msgstr "Laginketa-tasa" #: src/integrations/models.py:152 msgid "Channel Count" msgstr "Kanal kopurua" #: src/integrations/models.py:154 msgid "Path" msgstr "Bidea" #: src/integrations/models.py:155 msgid "Disc Number" msgstr "Disko zenbakia" #: src/integrations/models.py:156 msgid "BPM" msgstr "BPM" #: src/integrations/models.py:157 msgid "Genres" msgstr "Generoak" #. list #: src/integrations/models.py:159 msgid "Track Gain" msgstr "Pistaren irabazia" #: src/integrations/models.py:160 msgid "Album Gain" msgstr "Albumaren irabazia" #: src/integrations/navidrome.py:18 msgid "Connect to an OpenSubsonic server like Navidrome." msgstr "Konektatu OpenSubsonic zerbitzari batera (adb. Navidrome)." #: src/integrations/navidrome.py:22 msgid "External Server" msgstr "Kanpoko zerbitzaria" #: src/integrations/navidrome.py:23 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "Erabili existitzen den OpenSubsonic / Navidrome instantzia bat" #: src/integrations/navidrome.py:585 src/integrations/navidrome.py:594 msgid "Managed Server" msgstr "Zerbitzari kudeatua" #: src/integrations/navidrome.py:586 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" "Konektatu Nocturnek zuzenean kudea dezakeen Navidrome instantzia batera." #: src/integrations/navidrome.py:589 msgid "Manage Server" msgstr "Kudeatu zerbitzaria" #: src/integrations/navidrome.py:595 msgid "Create and use a Navidrome instance" msgstr "Sortu eta erabili Navidrome instantzia" #: src/main.py:70 msgid "Music is Playing" msgstr "Musika erreproduzitzen" #: src/main.py:83 msgid "Fullscreen Player Active" msgstr "Pantaila osoko erreproduzitzailea aktibo" #: src/main.py:129 msgid "Login Failed" msgstr "Saio hasierak huts egin du" #: src/preferences.py:368 msgid "Settings Page" msgstr "Ezarpenen orria" #: src/preferences.py:371 msgid "User Token" msgstr "Erabiltzaile-tokena" #: src/preferences.py:375 msgid "Link ListenBrainz" msgstr "Lotu ListenBrainz" #: src/preferences.py:376 msgid "Connect your ListenBrainz account with a user token" msgstr "Konektatu zure ListenBrainz kontua erabiltzaile-token bidez" #: src/preferences.py:416 msgid "Flatpak Sandbox Warning" msgstr "Flatpak ingurune isolatuaren abisua" #: src/preferences.py:417 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:21 msgid "Play Album" msgstr "Erreproduzitu albuma" #: 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 "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:88 msgid "Enter album page" msgstr "Sartu albumaren orrira" #: src/ui/album/button.blp:113 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:117 msgid "1 Star" msgstr "Izar 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 izar" #: src/ui/album/page.blp:113 src/ui/artist/page.blp:111 #: src/ui/playing/control_page.blp:135 msgid "3 Stars" msgstr "3 izar" #: src/ui/album/page.blp:122 src/ui/artist/page.blp:120 #: src/ui/playing/control_page.blp:144 msgid "4 Stars" msgstr "4 izar" #: src/ui/album/page.blp:131 src/ui/artist/page.blp:129 #: src/ui/playing/control_page.blp:153 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:127 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: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 "Zerrenda" #: src/ui/containers/carousel.blp:38 msgid "Pan to the Left" msgstr "Korritu ezkerrera" #: src/ui/containers/carousel.blp:64 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:240 #: src/ui/playing/footer.blp:118 src/ui/playing/popout_window.blp:142 msgid "Pause" msgstr "Pausatu" #: src/ui/lyrics/dialog.blp:98 src/ui/playing/control_page.blp:175 #: src/ui/playing/popout_window.blp:229 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:102 src/ui/pages/login.blp:103 #: src/widgets/pages/login.py:34 src/widgets/pages/login.py:68 msgid "Login" msgstr "Hasi saioa" #: src/ui/pages/login.blp:31 msgid "Server Status" msgstr "Zerbitzariaren egoera" #: src/ui/pages/login.blp:32 src/widgets/pages/login.py:42 msgid "Not Running" msgstr "Ez dago martxan" #: src/ui/pages/login.blp:33 msgid "Restart Instance" msgstr "Berrabiarazi instantzia" #: src/ui/pages/login.blp:45 msgid "Manage Navidrome Server" msgstr "Kudeatu Navidrome zerbitzaria" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "URLa" #: src/ui/pages/login.blp:62 msgid "More Options" msgstr "Aukera gehiago" #: src/ui/pages/login.blp:68 msgid "Trust Server Certificates" msgstr "Fidatu zerbitzariaren ziurtagiriez" #: src/ui/pages/login.blp:76 msgid "Library Directory" msgstr "Liburutegiaren direktorioa" #: src/ui/pages/login.blp:88 msgid "Username" msgstr "Erabiltzaile-izena" #: src/ui/pages/login.blp:92 msgid "Password" msgstr "Pasahitza" #: src/ui/pages/login.blp:112 src/ui/pages/login.blp:113 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:202 msgid "Nocturne Playback" msgstr "Nocturne - Erreprodukzioa" #: 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:95 #: src/ui/song/queue.blp:124 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:210 src/ui/playing/footer.blp:88 #: src/ui/playing/popout_window.blp:112 msgid "Previous" msgstr "Aurrekoa" #: src/ui/playing/control_page.blp:254 src/ui/playing/footer.blp:132 #: src/ui/playing/popout_window.blp:156 msgid "Next" msgstr "Hurrengoa" #: src/ui/playing/control_page.blp:274 msgid "Star" msgstr "Eman izarra" #: src/ui/playing/control_page.blp:281 msgid "Show Sidebar" msgstr "Erakutsi 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:34 msgid "Current song cover art" msgstr "Uneko abestiaren azala" #: 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 "Itzuli" #: src/ui/playing/lyrics_page.blp:92 msgid "No Lyrics Found" msgstr "Ez da hitzik aurkitu" #: src/ui/playing/lyrics_page.blp:109 msgid "Instrumental" msgstr "Instrumentala" #: src/ui/playing/lyrics_page.blp:134 msgid "Not Downloaded" msgstr "Ez da deskargatu" #: 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 "" "Abesti honen hitzak oraindik ez dira deskargatu, baliteke deskargagarri " "egotea" #: src/ui/playing/lyrics_page.blp:160 src/ui/playing/lyrics_page.blp:163 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:195 msgid "Toggle Sidebar" msgstr "Erakutsi / ezkutatu alboko barra" #: src/ui/playing/popout_window.blp:240 src/ui/window.blp:159 msgid "Player" msgstr "Erreproduzitzailea" #: src/ui/playing/popout_window.blp:257 src/ui/shortcuts-dialog.blp:50 #: src/ui/window.blp:260 src/widgets/playing/popout_window.py:70 msgid "Toggle Fullscreen" msgstr "Pantaila osoa gaitu / ezgaitu" #: src/ui/playing/popout_window.blp:285 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:199 msgid "Song Details" msgstr "Abestiaren xehetasunak" #: src/ui/playing/popout_window.blp:305 src/ui/window.blp:165 #: src/ui/window.blp:221 msgid "Queue" msgstr "Ilara" #: src/ui/playing/popout_window.blp:317 src/ui/window.blp:181 #: src/ui/window.blp:233 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:28 msgid "Play Playlist" msgstr "Erreproduzitu zerrenda" #: 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:86 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 "Default Page on Login" msgstr "Hasierako orri lehenetsia" #: src/ui/preferences.blp:27 msgid "Maximum Bitrate" msgstr "Gehienezko bit-tasa" #: src/ui/preferences.blp:33 msgid "Session" msgstr "Saioa" #: src/ui/preferences.blp:36 msgid "Discord Rich Presence" msgstr "Discorden presentzia aberastua" #: src/ui/preferences.blp:37 msgid "Share the currently playing song on Discord" msgstr "Partekatu uneko abestia Discorden" #: src/ui/preferences.blp:41 msgid "Share Cover Art With Discord" msgstr "Partekatu azala Discorden" #: src/ui/preferences.blp:42 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:48 msgid "ListenBrainz Link" msgstr "ListenBrainz esteka" #: src/ui/preferences.blp:49 msgid "Keep track of your playback history" msgstr "Erregistratu zure erreprodukzioen historia" #: src/ui/preferences.blp:55 src/ui/preferences.blp:56 msgid "Link" msgstr "Lotu" #: src/ui/preferences.blp:64 src/ui/preferences.blp:65 msgid "Unlink" msgstr "Askatu" #: src/ui/preferences.blp:77 msgid "User" msgstr "Erabiltzailea" #: src/ui/preferences.blp:97 msgid "Logout" msgstr "Amaitu saioa" #: src/ui/preferences.blp:107 msgid "Customization" msgstr "Pertsonalizazioa" #: src/ui/preferences.blp:110 msgid "Interface" msgstr "Interfazea" #: src/ui/preferences.blp:112 msgid "Show Context Button in Rows" msgstr "Erakutsi kontestu-botoia lerroetan" #: src/ui/preferences.blp:113 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:116 msgid "Show Labels in Buttons of Pages" msgstr "Erakutsi orrietako botoien etiketak" #: src/ui/preferences.blp:119 msgid "Show Progressbar in Player Footer" msgstr "Erakutsi aurrerapen-barra erreproduzitzailearen oinean" #: src/ui/preferences.blp:122 msgid "Make Background Translucent in Player" msgstr "Erreproduzitzailearen atzeko plano zeharrargia" #: src/ui/preferences.blp:125 msgid "Use Sidebar Player When Possible" msgstr "Erakutsi erreproduzitzailea alboko barran ahal dela" #: src/ui/preferences.blp:126 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:129 msgid "Show Pan Buttons on Carousels" msgstr "Erakutsi ezker / eskuin botoiak karruseletan" #: src/ui/preferences.blp:130 msgid "Only visible when a carousel has 5 or more items" msgstr "Karruselak 5 elementutik gora dituenean soilik ikusgai" #: src/ui/preferences.blp:133 msgid "Button Size" msgstr "Botoien tamaina" #: src/ui/preferences.blp:134 msgid "Affects albums, artists, playlists and songs" msgstr "Album, artista, erreprodukzio-zerrenda eta abestiei eragiten die" #: src/ui/preferences.blp:140 src/ui/preferences.blp:141 msgid "Small" msgstr "Txikia" #: src/ui/preferences.blp:145 src/ui/preferences.blp:146 msgid "Big" msgstr "Handia" #: src/ui/preferences.blp:154 msgid "Dynamic Background" msgstr "Atzeko plano dinamikoa" #: src/ui/preferences.blp:155 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:158 msgid "Use in Main Window" msgstr "Erabili leiho nagusian" #: src/ui/preferences.blp:159 msgid "Experimental" msgstr "Esperimentala" #: src/ui/preferences.blp:165 src/ui/preferences.blp:166 #: src/ui/preferences.blp:189 src/ui/preferences.blp:190 #: src/ui/preferences.blp:213 src/ui/preferences.blp:214 msgid "Off" msgstr "Ez" #: src/ui/preferences.blp:170 src/ui/preferences.blp:171 #: src/ui/preferences.blp:194 src/ui/preferences.blp:195 #: src/ui/preferences.blp:218 src/ui/preferences.blp:219 msgid "Gradient" msgstr "Gradientea" #: src/ui/preferences.blp:175 src/ui/preferences.blp:176 #: src/ui/preferences.blp:199 src/ui/preferences.blp:200 #: src/ui/preferences.blp:223 src/ui/preferences.blp:224 msgid "Blur" msgstr "Lausoa" #: src/ui/preferences.blp:183 msgid "Use in Player" msgstr "Erabili erreproduzitzailean" #: src/ui/preferences.blp:207 msgid "Use in Pop-out Window" msgstr "Erabili leiho gainerakorrean" #: src/ui/preferences.blp:231 msgid "Use as Accent Color" msgstr "Erabili nabarmentze kolore gisa" #: src/ui/preferences.blp:236 msgid "Homepage" msgstr "Hasiera orria" #: src/ui/preferences.blp:237 msgid "Reload homepage to see changes" msgstr "Birkargatu hasiera orria aldaketak ikusteko" #: src/ui/preferences.blp:273 msgid "Sidebar" msgstr "Alboko barra" #: src/ui/preferences.blp:277 msgid "Visualizer" msgstr "Bistaratzailea" #: src/ui/preferences.blp:281 src/ui/window.blp:250 msgid "Preferences" msgstr "Hobespenak" #: src/ui/preferences.blp:283 msgid "Show Visualizer in Player" msgstr "Erakutsi espektroa erreproduzitzailean" #: src/ui/preferences.blp:284 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:289 msgid "Appearance" msgstr "Itxura" #: src/ui/preferences.blp:293 msgid "Number of Bars" msgstr "Barra kopurua" #: src/ui/preferences.blp:304 msgid "Visualizer Type" msgstr "Bistaratzaile mota" #: src/ui/preferences.blp:310 src/ui/preferences.blp:311 msgid "Wave" msgstr "Uhina" #: src/ui/preferences.blp:315 src/ui/preferences.blp:316 msgid "Bars" msgstr "Barrak" #: src/ui/preferences.blp:320 src/ui/preferences.blp:321 msgid "Particles" msgstr "Partikulak" #: src/ui/preferences.blp:328 msgid "Fill Mode" msgstr "Betetze modua" #: src/ui/preferences.blp:334 src/ui/preferences.blp:335 msgid "Solid" msgstr "Solidoa" #: src/ui/preferences.blp:339 src/ui/preferences.blp:340 msgid "Translucent" msgstr "Zeharrargia" #: src/ui/preferences.blp:344 src/ui/preferences.blp:345 msgid "Border" msgstr "Ertzak" #: src/ui/preferences.blp:353 msgid "Color" msgstr "Kolorea" #: src/ui/preferences.blp:357 msgid "Use Dynamic Color" msgstr "Erabili kolore dinamikoa" #: src/ui/preferences.blp:360 msgid "Invert Color" msgstr "Alderantzi kolorea" #: src/ui/preferences.blp:364 msgid "Manual Color" msgstr "Kolore zehatza" #: src/ui/preferences.blp:370 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:54 src/ui/window.blp:255 msgid "Open Pop-Out Window" msgstr "Ireki leiho gainerakorra" #: src/ui/song/button.blp:16 msgid "Play Song" msgstr "Erreproduzitu abestia" #: 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/row.blp:41 msgid "Drag icon" msgstr "Arrastatze ikonoa" #: src/ui/song/row.blp:139 msgid "Selection box" msgstr "Hautapen kutxa" #: src/ui/window.blp:79 msgid "Play Random Queue" msgstr "Erreproduzitu ausazko ilara" #: src/ui/window.blp:92 msgid "Menu" msgstr "Menua" #: src/ui/window.blp:120 msgid "Random Albums" msgstr "Albumak ausaz" #: src/ui/window.blp:124 msgid "Favorite Albums" msgstr "Gogoko albumak" #: src/ui/window.blp:128 msgid "Newest Albums" msgstr "Album berrienak" #: src/ui/window.blp:132 msgid "Recent Albums" msgstr "Oraintsuko albumak" #: src/ui/window.blp:136 msgid "Frequent Albums" msgstr "Ohiko albumak" #: src/ui/window.blp:265 msgid "See Last Nocturne Playback" msgstr "Ikusi Nocturneko azken erreprodukzioa" #: src/ui/window.blp:270 msgid "Keyboard Shortcuts" msgstr "Teklatuko lasterbideak" #: src/ui/window.blp:275 msgid "About Nocturne" msgstr "Nocturneri buruz" #: src/widgets/album/button.py:83 src/widgets/album/page.py:152 #: src/widgets/artist/page.py:132 src/widgets/playing/control_page.py:181 #: src/widgets/song/row.py:168 msgid "Not Favorite" msgstr "Ez gogokoa" #: src/widgets/album/page.py:18 msgid "Disc {}" msgstr "{} diskoa" #: src/widgets/artist/button.py:57 src/widgets/artist/row.py:47 msgid "{} Album" msgid_plural "{} Albums" msgstr[0] "Album {}" msgstr[1] "{} album" #: src/widgets/artist/page.py:50 msgid "Top Songs" msgstr "Abesti entzunenak" #: src/widgets/artist/page.py:60 msgid "Related Artists" msgstr "Artista erlazionatuak" #: src/widgets/artist/page.py:115 msgid "Author" msgstr "Egilea" #: src/widgets/lyrics/dialog.py:54 msgid "No Timestamp" msgstr "Denbora-markarik ez" #: src/widgets/pages/login.py:42 src/widgets/pages/login.py:105 msgid "Running" msgstr "Martxan" #: src/widgets/pages/login.py:76 msgid "Extra Menu" msgstr "Menu estra" #: src/widgets/pages/login.py:94 msgid "Local Music Library" msgstr "Musika liburutegi lokala" #: src/widgets/pages/login.py:104 msgid "Restarted" msgstr "Berrabiarazia" #: src/widgets/pages/login.py:107 msgid "Error" msgstr "Errorea" #: src/widgets/pages/login.py:143 msgid "Quick Connect" msgstr "Konexio bizkorra" #: src/widgets/pages/login.py:144 msgid "Error getting code" msgstr "Errorea kodea lortzean" #: src/widgets/pages/login.py:146 msgid "Quick Connect Page" msgstr "Konexio bizkorrerako orria" #: src/widgets/pages/playback.py:29 msgid "Nocturne Playback ~ {}" msgstr "Nocturne erreprodukzioa ~ {}" #: src/widgets/pages/playback.py:48 src/widgets/pages/playback.py:57 msgid "{} Plays" msgstr "{} erreprodukzio" #: src/widgets/pages/setup.py:63 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:215 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:64 src/widgets/playlist/page.py:118 #: src/widgets/playlist/row.py:48 src/widgets/playlist/selector_row.py:37 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:34 msgid "Add Songs to '{}'" msgstr "Gehitu abestiak hona: '{}'" #: src/widgets/song/details_dialog.py:23 msgid "Yes" msgstr "Bai" #: src/widgets/song/details_dialog.py:23 msgid "No" msgstr "Ez" #: src/widgets/song/details_dialog.py:28 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:203 msgid "Your Playback queue for {} is available!" msgstr "{}(e)rako erreprodukzio-ilara prest dago!" #: src/window.py:205 msgid "Show" msgstr "Erakutsi" #: src/window.py:206 msgid "Later" msgstr "Geroago" #~ 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-469023d/po/hr.po000066400000000000000000001144401522207411400165370ustar00rootroot00000000000000# 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-469023d/po/meson.build000066400000000000000000000000511522207411400177200ustar00rootroot00000000000000i18n.gettext('nocturne', preset: 'glib') Jeffser-Nocturne-469023d/po/nocturne.pot000066400000000000000000001127031522207411400201470ustar00rootroot00000000000000# 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-03 20:56-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 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 "" #: 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:92 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:86 msgid "Mini Player" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:91 msgid "navidrome" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:92 msgid "jellyfin" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:93 msgid "music" msgstr "" #: data/com.jeffser.Nocturne.metainfo.xml.in:94 msgid "gtk" msgstr "" #: data/com.jeffser.Nocturne.SearchProvider.desktop.in:3 msgid "Nocturne Search Provider" msgstr "" #: 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:467 #: src/ui/song/queue.blp:40 msgid "Close" msgstr "" #: src/actions.py:182 #, python-brace-format msgid "Not enough data found for Nocturne Playback ({})" msgstr "" #: src/actions.py:287 src/ui/song/row.blp:72 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 "" #: src/actions.py:317 msgid "Delete Navidrome Server" msgstr "" #: src/actions.py:318 msgid "Are you sure you want to delete the integrated Navidrome server?" msgstr "" #: src/actions.py:320 src/actions.py:465 src/actions.py:499 src/actions.py:841 #: src/actions.py:944 src/preferences.py:420 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 "" #: 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:421 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 #, python-brace-format 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 #, python-brace-format 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 #, python-brace-format msgid "{} Song Removed" msgid_plural "{} Songs Removed" msgstr[0] "" msgstr[1] "" #: src/actions.py:892 src/actions.py:912 #, python-brace-format msgid "{} Song Added" msgid_plural "{} Songs Added" msgstr[0] "" msgstr[1] "" #: src/actions.py:916 src/actions.py:1064 #, python-brace-format msgid "{} Song Skipped" msgid_plural "{} Songs Skipped" msgstr[0] "" msgstr[1] "" #: 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 #, python-brace-format msgid "{} Song ({})" msgid_plural "{} Songs ({})" msgstr[0] "" msgstr[1] "" #: src/actions.py:1077 msgid "Download Skipped" msgstr "" #: src/actions.py:1101 src/actions.py:1137 #, python-brace-format msgid "Download Started ({} Song Skipped)" msgid_plural "Download Started ({} Songs Skipped)" msgstr[0] "" msgstr[1] "" #: 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 #, python-brace-format msgid "Low ({})" msgstr "" #: src/constants.py:199 #, python-brace-format msgid "Medium ({})" msgstr "" #: src/constants.py:200 #, python-brace-format msgid "High ({})" msgstr "" #: src/constants.py:201 #, python-brace-format 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:275 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:267 #: 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:259 #: 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:283 #: 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:172 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/nocturne_search_provider.py.in:11 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:156 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:153 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:230 msgid "Could not log in" msgstr "" #: src/integrations/jellyfin.py:1016 msgid "Unknown" msgstr "" #: src/integrations/local.py:20 src/integrations/local.py:26 #: src/integrations/local.py:639 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:288 msgid "No Album" msgstr "" #: src/integrations/local.py:655 src/integrations/local.py:664 msgid "Offline Mode" msgstr "" #: src/integrations/local.py:656 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 "" #: 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:180 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 "" #: 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 "" #: src/integrations/navidrome.py:23 msgid "External Server" msgstr "" #: src/integrations/navidrome.py:24 msgid "Use an existing OpenSubsonic / Navidrome instance" msgstr "" #: src/integrations/navidrome.py:149 msgid "Server returned an error" msgstr "" #: src/integrations/navidrome.py:149 msgid "Unknown Error" msgstr "" #: src/integrations/navidrome.py:663 src/integrations/navidrome.py:672 msgid "Managed Server" msgstr "" #: src/integrations/navidrome.py:664 msgid "Connect to a Navidrome instance directly managed by Nocturne." msgstr "" #: src/integrations/navidrome.py:667 msgid "Manage Server" msgstr "" #: src/integrations/navidrome.py:673 msgid "Create and use a Navidrome instance" msgstr "" #: src/main.py:92 msgid "Music is Playing" msgstr "" #: src/main.py:105 msgid "Fullscreen Player Active" msgstr "" #: src/main.py:153 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:409 msgid "Settings Page" msgstr "" #: src/preferences.py:412 msgid "User Token" msgstr "" #: src/preferences.py:416 msgid "Link ListenBrainz" msgstr "" #: src/preferences.py:417 msgid "Connect your ListenBrainz account with a user token" msgstr "" #: src/preferences.py:457 msgid "Flatpak Sandbox Warning" msgstr "" #: src/preferences.py:458 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/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:187 #: src/ui/playing/footer.blp:118 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: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 "" #: src/ui/pages/login.blp:55 msgid "Url" msgstr "" #: 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 "" #: 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/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 "" #: 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:88 msgid "Previous" msgstr "" #: src/ui/playing/control_page.blp:201 src/ui/playing/footer.blp:132 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: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 "" #: 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:66 src/ui/song/details_dialog.blp:5 #: src/ui/window.blp:214 msgid "Song Details" msgstr "" #: src/ui/playing/popout_window.blp:86 src/ui/window.blp:180 #: src/ui/window.blp:236 msgid "Queue" msgstr "" #: src/ui/playing/popout_window.blp:98 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: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 "Gnome Search" msgstr "" #: src/ui/preferences.blp:38 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:41 msgid "Include Artists" msgstr "" #: src/ui/preferences.blp:44 msgid "Include Albums" msgstr "" #: src/ui/preferences.blp:47 msgid "Include Songs" msgstr "" #: src/ui/preferences.blp:50 msgid "Include Playlists" msgstr "" #: src/ui/preferences.blp:54 msgid "Session" msgstr "" #: src/ui/preferences.blp:57 msgid "Discord Rich Presence" msgstr "" #: src/ui/preferences.blp:58 msgid "Share the currently playing song on Discord" msgstr "" #: src/ui/preferences.blp:62 msgid "Share Cover Art With Discord" msgstr "" #: src/ui/preferences.blp:63 msgid "" "If the instance is not reachable by Discord servers, CoverArtArchive will be " "used instead" msgstr "" #: src/ui/preferences.blp:69 msgid "ListenBrainz Link" msgstr "" #: src/ui/preferences.blp:70 msgid "Keep track of your playback history" msgstr "" #: src/ui/preferences.blp:76 src/ui/preferences.blp:77 msgid "Link" msgstr "" #: src/ui/preferences.blp:85 src/ui/preferences.blp:86 msgid "Unlink" msgstr "" #: src/ui/preferences.blp:98 msgid "User" msgstr "" #: src/ui/preferences.blp:118 msgid "Logout" msgstr "" #: src/ui/preferences.blp:128 msgid "Customization" msgstr "" #: src/ui/preferences.blp:131 msgid "Interface" msgstr "" #: src/ui/preferences.blp:133 msgid "Show Context Button in Rows" msgstr "" #: src/ui/preferences.blp:134 msgid "The menu can also be shown by right clicking / long pressing" msgstr "" #: src/ui/preferences.blp:137 msgid "Show Labels in Buttons of Pages" msgstr "" #: src/ui/preferences.blp:140 msgid "Show Progressbar in Player Footer" msgstr "" #: src/ui/preferences.blp:143 msgid "Make Background Translucent in Player" msgstr "" #: src/ui/preferences.blp:146 msgid "Use Sidebar Player When Possible" msgstr "" #: src/ui/preferences.blp:147 msgid "Sidebar player is only available when the window is wide enough" msgstr "" #: src/ui/preferences.blp:150 msgid "Show Pan Buttons on Carousels" msgstr "" #: src/ui/preferences.blp:151 msgid "Only visible when a carousel has 5 or more items" msgstr "" #: src/ui/preferences.blp:154 msgid "Button Size" msgstr "" #: src/ui/preferences.blp:155 msgid "Affects albums, artists, playlists and songs" msgstr "" #: src/ui/preferences.blp:161 src/ui/preferences.blp:162 msgid "Small" msgstr "" #: src/ui/preferences.blp:166 src/ui/preferences.blp:167 msgid "Big" msgstr "" #: src/ui/preferences.blp:175 msgid "Dynamic Background" msgstr "" #: src/ui/preferences.blp:176 msgid "Background generated based on the album art of the current song" msgstr "" #: src/ui/preferences.blp:179 msgid "Use in Main Window" msgstr "" #: src/ui/preferences.blp:185 src/ui/preferences.blp:186 #: src/ui/preferences.blp:209 src/ui/preferences.blp:210 #: src/ui/preferences.blp:233 src/ui/preferences.blp:234 msgid "Off" msgstr "" #: src/ui/preferences.blp:190 src/ui/preferences.blp:191 #: src/ui/preferences.blp:214 src/ui/preferences.blp:215 #: src/ui/preferences.blp:238 src/ui/preferences.blp:239 msgid "Gradient" msgstr "" #: src/ui/preferences.blp:195 src/ui/preferences.blp:196 #: src/ui/preferences.blp:219 src/ui/preferences.blp:220 #: src/ui/preferences.blp:243 src/ui/preferences.blp:244 msgid "Blur" msgstr "" #: src/ui/preferences.blp:203 msgid "Use in Player" msgstr "" #: src/ui/preferences.blp:227 msgid "Use in Pop-out Window" msgstr "" #: src/ui/preferences.blp:251 msgid "Use as Accent Color" msgstr "" #: src/ui/preferences.blp:256 msgid "Homepage" msgstr "" #: src/ui/preferences.blp:257 msgid "Reload homepage to see changes" msgstr "" #: src/ui/preferences.blp:293 msgid "Sidebar" msgstr "" #: src/ui/preferences.blp:297 msgid "Visualizer" msgstr "" #: src/ui/preferences.blp:301 src/ui/window.blp:265 msgid "Preferences" msgstr "" #: src/ui/preferences.blp:303 msgid "Show Visualizer in Player" msgstr "" #: src/ui/preferences.blp:304 msgid "Show an audio spectrum visualizer on top of the album art in the player" msgstr "" #: src/ui/preferences.blp:309 msgid "Appearance" msgstr "" #: src/ui/preferences.blp:313 msgid "Number of Bars" msgstr "" #: src/ui/preferences.blp:324 msgid "Visualizer Type" msgstr "" #: src/ui/preferences.blp:330 src/ui/preferences.blp:331 msgid "Wave" msgstr "" #: src/ui/preferences.blp:335 src/ui/preferences.blp:336 msgid "Bars" msgstr "" #: src/ui/preferences.blp:340 src/ui/preferences.blp:341 msgid "Particles" msgstr "" #: src/ui/preferences.blp:348 msgid "Fill Mode" msgstr "" #: src/ui/preferences.blp:354 src/ui/preferences.blp:355 msgid "Solid" msgstr "" #: src/ui/preferences.blp:359 src/ui/preferences.blp:360 msgid "Translucent" msgstr "" #: src/ui/preferences.blp:364 src/ui/preferences.blp:365 msgid "Border" msgstr "" #: src/ui/preferences.blp:373 msgid "Color" msgstr "" #: src/ui/preferences.blp:377 msgid "Use Dynamic Color" msgstr "" #: src/ui/preferences.blp:380 msgid "Invert Color" msgstr "" #: src/ui/preferences.blp:384 msgid "Manual Color" msgstr "" #: src/ui/preferences.blp:390 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: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:44 msgid "Drag icon" msgstr "" #: src/ui/song/row.blp:142 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 "" #: src/ui/window.blp:285 msgid "Keyboard Shortcuts" msgstr "" #: src/ui/window.blp:290 msgid "About Nocturne" msgstr "" #: src/widgets/album/button.py:95 src/widgets/album/page.py:150 #: src/widgets/artist/page.py:130 src/widgets/playing/control_page.py:185 #: 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: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 #, python-brace-format 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 #, 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: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 "" #: src/widgets/playing/player.py:634 msgid "Warning: Song changed but volume is set to 0" msgstr "" #: 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] "" 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:208 #, python-brace-format msgid "Your Playback queue for {} is available!" msgstr "" #: src/window.py:210 msgid "Show" msgstr "" #: src/window.py:211 msgid "Later" msgstr "" Jeffser-Nocturne-469023d/po/ru.po000066400000000000000000001622271522207411400165620ustar00rootroot00000000000000# 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-469023d/po/te.po000066400000000000000000001622351522207411400165430ustar00rootroot00000000000000# 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-469023d/po/tr.po000066400000000000000000001151411522207411400165520ustar00rootroot00000000000000# 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-469023d/po/zh_CN.po000066400000000000000000001170631522207411400171330ustar00rootroot00000000000000# 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-469023d/po/zh_TW.po000066400000000000000000001172131522207411400171620ustar00rootroot00000000000000# 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-469023d/src/000077500000000000000000000000001522207411400157335ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/__init__.py000066400000000000000000000000001522207411400200320ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/actions.py000066400000000000000000001343351522207411400177560ustar00rootroot00000000000000# 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): for dialog in window.get_dialogs(): dialog.close() 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': dialog.navigation_view.push(page) return Widgets.PageDialog(page).present(active_window) else: active_window.main_bottom_sheet.set_open(False) active_window.main_split_view.set_show_content(True) active_window.main_navigationview.push(page) def __show_custom_toast(window, model_id:str, title_property:str, subtitle:str, icon_name:str=None): 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: 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, ) if paintable := integration.getCoverArt(model_id): album_art.set_from_paintable(paintable) album_art.set_pixel_size(48) else: album_art.set_from_icon_name("music-note-symbolic") 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 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: queue_model.remove(i) elif song_id == current_song_id: current_song_index = i + 1 songs.reverse() 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: 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 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')) 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): def run(): 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) threading.Thread( target=__replace_queue, args=(window, [s[0] for s in top_songs]) ).start() 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') window.main_stack.set_visible_child_name('loading') threading.Thread(target=run, daemon=True).start() def generate_auto_play_queue(window, replace_on_finish:bool): def run(): 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 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)]) threading.Thread(target=run, daemon=True).start() 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] window.main_sidebar.set_selected(index) except Exception as e: pass 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', None) else: if integration.star(model.get_property('id')): model.set_property('starred', datetime.now(UTC).isoformat(timespec='microseconds').replace('+00:00', 'Z')) def logout(window): integration = get_current_integration() integration.terminate_instance() 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 window.get_application().player.mpris_published: window.get_application().player.mpris.unpublish() for dialog in window.get_dialogs(): 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')) dialog.choose(window, None, lambda *_: None, None) def update_navidrome_server(window): window.main_stack.set_visible_child_name('setup') if dialog := window.get_visible_dialog(): 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 ) GLib.idle_add(window.toast_overlay.add_toast, toast) GLib.idle_add(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) 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: popout_window.present() if popout_window.is_fullscreen(): popout_window.unfullscreen() else: popout_window.fullscreen() else: window.get_application().lookup_action("toggle_fullscreen").set_enabled(False) open_popout_window(window, True) GLib.timeout_add(1000, window.get_application().lookup_action("toggle_fullscreen").set_enabled, 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 threading.Thread(target=player.gst.set_state, args=(Gst.State.PAUSED,), daemon=True).start() else: # Play the song threading.Thread(target=player.gst.set_state, args=(Gst.State.PLAYING,), daemon=True).start() def player_play(window): player = window.get_application().player threading.Thread(target=player.gst.set_state, args=(Gst.State.PLAYING,), daemon=True).start() def player_pause(window): player = window.get_application().player threading.Thread(target=player.gst.set_state, args=(Gst.State.PAUSED,), daemon=True).start() 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: threading.Thread(target=__replace_queue, args=(window,[model_id],), daemon=True).start() def update_radio(window, id:str=""): integration = get_current_integration() model = integration.loaded_models.get(id) if id else None def response(dialog, task, name_el, stream_el, id:str): if dialog.choose_finish(task) == 'save': name = name_el.get_text().strip() or _("No Name") stream = stream_el.get_text().strip() if not stream.startswith('http'): stream = 'http://' + stream if name and (stream or not stream_el.get_visible()): integration = get_current_integration() if id: result = integration.updateInternetRadioStation( id, name, stream ) else: result = integration.createInternetRadioStation( name, stream ) if result: toast = Adw.Toast( title=_("Radio updated successfully") if id else _("Radio added successfully"), timeout=2 ) window.toast_overlay.add_toast(toast) if 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() return toast = Adw.Toast( title=_("Error updating radio") if id else _("Error adding radio"), timeout=2 ) 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 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, id) 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 response(dialog, task, id): if dialog.choose_finish(task) == 'delete': result = integration.deleteInternetRadioStation(id) if result: toast = Adw.Toast( title=_("Radio deleted successfully"), timeout=2 ) window.toast_overlay.add_toast(toast) del integration.loaded_models[id] threading.Thread(target=window.main_navigationview.get_visible_page().reload, daemon=True).start() else: toast = Adw.Toast( title=_("Error deleting radio"), timeout=2 ) window.toast_overlay.add_toast(toast) 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) dialog.choose(window, None, response, model_id) # -- 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: threading.Thread(target=__replace_queue, args=(window,[model_id],), daemon=True).start() def play_song_from_list(window, data:dict): song_id = data.get('songId') songs = data.get('songs', [song_id]) origin_id = data.get('originId', "") if song_id: threading.Thread( target=__replace_queue, args=(window, songs, song_id), kwargs={"origin_id": origin_id}, daemon=True ).start() def play_song_next(window, model_id:str): threading.Thread( target=__play_next, args=(window, [model_id]), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'title', _("Playing Next")), daemon=True ).start() def play_song_later(window, model_id:str): threading.Thread( target=__play_later, args=(window, [model_id]), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'title', _("Playing Later")), daemon=True ).start() def play_songs(window, song_list:list): threading.Thread( target=__replace_queue, args=(window, song_list), daemon=True ).start() def play_songs_next(window, song_list:list): threading.Thread( target=__play_next, args=(window, song_list), daemon=True ).start() if len(song_list)> 1: threading.Thread( target=__show_custom_toast, args=(window, None, _("{} Songs").format(len(song_list)), _("Playing Next"), "list-high-priority-symbolic"), daemon=True ).start() else: threading.Thread( target=__show_custom_toast, args=(window, song_list[0], "title", _("Playing Next")), daemon=True ).start() def play_songs_later(window, song_list:list): threading.Thread( target=__play_later, args=(window, song_list,), daemon=True ).start() if len(song_list) > 1: threading.Thread( target=__show_custom_toast, args=(window, None, _("{} Songs").format(len(song_list)), _("Playing Later"), "list-low-priority-symbolic"), daemon=True ).start() else: threading.Thread( target=__show_custom_toast, args=(window, song_list[0], "title", _("Playing Later")), daemon=True ).start() def edit_lyrics(window): Widgets.LyricsDialog().present(window.get_application().props.active_window) def save_lyrics(window, lyric_dict:dict): # lyric_dict KEYS # id:str # content:str integration = get_current_integration() model = integration.loaded_models.get(lyric_dict.get('id')) 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') ) lyrics_dir = os.path.join(DATA_DIR, 'lyrics') lrc_path = os.path.join(lyrics_dir, file_name_without_ext+'.lrc') with open(lrc_path, 'w') as f: f.write(lyric_dict.get('content')) window.lyrics_page.song_changed(lyric_dict.get('id')) threading.Thread( target=__show_custom_toast, args=(window, lyric_dict.get('id'), "title", _("Lyrics Saved")), daemon=True ).start() def play_random_queue(window): integration = get_current_integration() threading.Thread( target=__replace_queue, args=(window, integration.getRandomSongs(),), daemon=True ).start() def show_song_details(window, model_id): dialog = Widgets.SongDetailsDialog(model_id) 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() album = integration.loaded_models.get(model_id) if album: integration.verifyAlbum(album.get_property('id'), force_update=True, use_threading=False) threading.Thread( target=__replace_queue, args=(window, [s.get('id') for s in album.get_property('song')]), kwargs={"origin_id": model_id}, daemon=True ).start() def play_album_next(window, model_id:str): integration = get_current_integration() album = integration.loaded_models.get(model_id) if album: integration.verifyAlbum(album.get_property('id'), force_update=True, use_threading=False) threading.Thread( target=__play_next, args=(window, [s.get('id') for s in album.get_property('song')]), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'name', _("Playing Next")), daemon=True ).start() def play_album_later(window, model_id:str): integration = get_current_integration() album = integration.loaded_models.get(model_id) if album: integration.verifyAlbum(album.get_property('id'), force_update=True, use_threading=False) threading.Thread( target=__play_later, args=(window, [s.get('id') for s in album.get_property('song')]), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'name', _("Playing Later")), daemon=True ).start() def play_album_shuffle(window, model_id:str): integration = get_current_integration() album = integration.loaded_models.get(model_id) if album: integration.verifyAlbum(album.get_property('id'), force_update=True, use_threading=False) song_list = [s.get('id') for s in album.get_property('song')] random.shuffle(song_list) threading.Thread( target=__replace_queue, args=(window, song_list), kwargs={"origin_id": model_id}, daemon=True ).start() # -- 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) threading.Thread( target=__replace_queue, args=(window, [s.get('id') for s in playlist.get_property('entry')],), kwargs={"origin_id": model_id}, daemon=True ).start() def resume_playlist(window, model_id:str): integration = get_current_integration() current_id, timestamp = integration.getPlaylistResume(model_id) def run(): 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) if playlist := integration.loaded_models.get(model_id): threading.Thread(target=run).start() def play_playlist_next(window, model_id:str): integration = get_current_integration() playlist = integration.loaded_models.get(model_id) if playlist: integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) threading.Thread( target=__play_next, args=(window, [s.get('id') for s in playlist.get_property('entry')],), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'name', _("Playing Next")), daemon=True ).start() def play_playlist_later(window, model_id:str): integration = get_current_integration() playlist = integration.loaded_models.get(model_id) if playlist: integration.verifyPlaylist(playlist.get_property('id'), force_update=True, use_threading=False) threading.Thread( target=__play_later, args=(window, [s.get('id') for s in playlist.get_property('entry')]), daemon=True ).start() threading.Thread( target=__show_custom_toast, args=(window, model_id, 'name', _("Playing Later")), daemon=True ).start() def play_playlist_shuffle(window, model_id:str): integration = get_current_integration() playlist = integration.loaded_models.get(model_id) if playlist: 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) threading.Thread( target=__replace_queue, args=(window, song_list), kwargs={"origin_id": model_id}, daemon=True ).start() def update_playlist(window, model_id:str=None): integration = get_current_integration() model = integration.loaded_models.get(model_id) if model_id else None def response(dialog, task, name_el, id:str): if dialog.choose_finish(task) == 'create': name = name_el.get_text() if name: result = integration.createPlaylist( name, playlistId=id ) if result: if not id: threading.Thread(target=window.update_playlist_section_of_sidebar, daemon=True).start() toast = Adw.Toast( title=_("Playlist updated successfully") if id else _("Playlist created successfully"), timeout=2 ) window.toast_overlay.add_toast(toast) if id: model.set_property('name', name) else: threading.Thread(target=window.main_navigationview.get_visible_page().reload, daemon=True).start() return toast = Adw.Toast( title=_("Error updating playlist") if id else _("Error creating playlist"), timeout=2 ) 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: 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) dialog.choose(window, None, response, name_el, model_id) 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', []) integration = get_current_integration() result = integration.updatePlaylist( playlist_id, songIndexToRemove=song_list ) if result: threading.Thread( target=__show_custom_toast, args=(window, playlist_id, "name", ngettext("{} Song Removed", "{} Songs Removed", len(song_list)).format(len(song_list))), daemon=True ).start() def prompt_add_songs_to_playlist(window, song_list:list): dialog = Widgets.playlist.PlaylistDialog(song_list) dialog.present(window.get_application().props.active_window) def prompt_add_song_to_playlist(window, model_id:str): dialog = Widgets.playlist.PlaylistDialog([model_id]) 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) model = integration.loaded_models.get(model_id) dialog = Widgets.playlist.PlaylistDialog([s.get('id') for s in model.get_property('song')]) 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(): dialog.close() if data.get('new_playlist'): response = integration.createPlaylist( name=data.get('new_playlist'), songId=data.get('songs') ) if response: integration.verifyPlaylist(response, force_update=True, use_threading=False) message = ngettext("{} Song Added", "{} Songs Added", len(data.get("songs"))).format(len(data.get("songs"))) threading.Thread( target=__show_custom_toast, args=(window, response, "name", message), daemon=True ).start() threading.Thread(target=window.update_playlist_section_of_sidebar, daemon=True).start() elif data.get('playlist'): integration.verifyPlaylist(data.get('playlist'), force_update=True, use_threading=False) 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)) threading.Thread( target=__show_custom_toast, args=(window, data.get('playlist'), "name", ' | '.join(message)), daemon=True ).start() def delete_playlist(window, model_id:str): integration = get_current_integration() model = integration.loaded_models.get(model_id) def show_toast(model): __show_custom_toast(window, model.get_property('id'), "name", _("Playlist Deleted")) del integration.loaded_models[model.id] window.main_navigationview.get_visible_page().reload() def response(dialog, task, model): if dialog.choose_finish(task) == "delete": result = integration.deletePlaylist(model.get_property('id')) if result: threading.Thread(target=show_toast, args=(model,), daemon=True).start() threading.Thread(target=window.update_playlist_section_of_sidebar, daemon=True).start() 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) 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() if model := integration.loaded_models.get(model_id): if artist_id := model.get_property('artistId'): __show_page(window, Widgets.ArtistPage(artist_id)) def play_shuffle_artist(window, model_id:str): integration = get_current_integration() def run(): 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)))) threading.Thread(target=run, daemon=True).start() def play_radio_artist(window, model_id:str): integration = get_current_integration() def run(): 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) threading.Thread(target=run, daemon=True).start() # -- 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 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): def run(songId): is_downloading = __request_song_download(songId) if is_downloading: __show_custom_toast( window, songId, 'title', _("Download Started") ) else: __show_custom_toast( window, songId, 'title', _("Already Downloaded") ) threading.Thread(target=run, args=(model_id,), daemon=True).start() def download_songs(window, model_list:str): if len(model_list) == 1: download_song(window, model_list[0]) return def run(songIds): integration = get_current_integration() successful_starts = 0 for songId in songIds: successful_starts += 1 if __request_song_download(songId) else 0 if len(songIds) == successful_starts: __show_custom_toast( window, None, _("{} Songs").format(len(songIds)), _("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") ) threading.Thread(target=run, args=(model_list,), daemon=True).start() def download_album(window, model_id:str): def run(albumId): integration = get_current_integration() integration.verifyAlbum(albumId, force_update=True, use_threading=False) if model := integration.loaded_models.get(albumId): 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, albumId, '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, albumId, 'name', message ) else: __show_custom_toast( window, albumId, 'name', _("Already Downloaded") ) threading.Thread(target=run, args=(model_id,), daemon=True).start() def download_playlist(window, model_id:str): def run(playlistId): integration = get_current_integration() integration.verifyPlaylist(playlistId, force_update=True, use_threading=False) if model := integration.loaded_models.get(playlistId): 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, playlistId, '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, playlistId, 'name', message ) else: __show_custom_toast( window, playlistId, 'name', _("Already Downloaded") ) threading.Thread(target=run, args=(model_id,), daemon=True).start() 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): def run(): 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] threading.Thread(target=run, daemon=True).start() def delete_downloads(window, model_list:list): def run(): 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") ) threading.Thread(target=run, daemon=True).start() Jeffser-Nocturne-469023d/src/constants.py000066400000000000000000000352341522207411400203300ustar00rootroot00000000000000# constants.py import os, subprocess, json, shutil from tinytag import TinyTag DISCORD_APP_ID = "1504946005764739092" IN_FLATPAK = bool(os.getenv("FLATPAK_ID")) 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', '.opous', '.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") # Wrapped in a try/catch for non-Linux platforms where these commands don't exist try: DEFAULT_MUSIC_DIR = subprocess.check_output(["xdg-user-dir", "MUSIC"], text=True).strip() or os.path.expanduser("~/Music") except Exception: DEFAULT_MUSIC_DIR = os.path.expanduser("~/Music") 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') 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" ] 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 } SIDEBAR_MENU = [ { # Section 'items': [ { # Item 'title': _("Home"), 'icon-name': "user-home-symbolic", 'page-tag': 'home' }, { # Item 'title': _("Artists"), 'icon-name': "music-artist-symbolic", 'page-tag': 'artists' } ] }, { # Section 'title': _("Albums"), 'items': [ { # Item 'title': _("All"), 'icon-name': "music-queue-symbolic", 'page-tag': 'albums-all' }, { # Item 'title': _("Random"), 'icon-name': "playlist-shuffle-symbolic", 'page-tag': 'albums-random' }, { # Item 'title': _("Favorites"), 'icon-name': "heart-filled-symbolic", 'page-tag': 'albums-starred' }, { # Item 'title': _("Recently Added"), 'icon-name': "list-add-symbolic", 'page-tag': 'albums-newest' }, { # Item 'title': _("Recently Played"), 'icon-name': "media-playback-start-symbolic", 'page-tag': 'albums-recent' }, { # Item 'title': _("Most Played"), 'icon-name': "media-playlist-repeat-symbolic", 'page-tag': 'albums-frequent' } ] }, { # Section 'title': _("Songs"), 'items': [ { # Item 'title': _("All"), 'icon-name': "music-note-symbolic", 'page-tag': 'songs-all' }, { # Item 'title': _("Favorites"), 'icon-name': "heart-filled-symbolic", 'page-tag': 'songs-starred' }, { # Item 'title': _("Radios"), 'icon-name': "sound-wave-symbolic", 'page-tag': 'radios' } ] }, { # Section 'title': _("Playlists"), 'items': [ { # Item 'title': _("All"), 'icon-name': "playlist-symbolic", 'page-tag': "playlists" } ] } ] 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-469023d/src/icons/000077500000000000000000000000001522207411400170465ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/icons/check-plain-symbolic.svg000066400000000000000000000013361522207411400235670ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/cross-large-circle-filled-symbolic.svg000066400000000000000000000016201522207411400263220ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/cross-large-symbolic.svg000066400000000000000000000015121522207411400236260ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/dialog-warning-symbolic.svg000066400000000000000000000012111522207411400243030ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/document-edit-symbolic.svg000066400000000000000000000010761522207411400241530ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/external-link-symbolic.svg000066400000000000000000000024241522207411400241650ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/folder-download-symbolic.svg000066400000000000000000000011751522207411400244720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/folder-open-symbolic.svg000066400000000000000000000031221522207411400236160ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/globe-symbolic.svg000066400000000000000000000052701522207411400225020ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/go-next-symbolic.svg000066400000000000000000000011761522207411400227740ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/go-previous-symbolic.svg000066400000000000000000000011761522207411400236720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/grid-filled-symbolic.svg000066400000000000000000000013421522207411400235700ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/heart-filled-symbolic.svg000066400000000000000000000011251522207411400237450ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/heart-outline-thick-symbolic.svg000066400000000000000000000023271522207411400252720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/info-outline-symbolic.svg000066400000000000000000000014551522207411400240230ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/list-add-symbolic.svg000066400000000000000000000003441522207411400231100ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/list-drag-handle-symbolic.svg000066400000000000000000000017061522207411400245310ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/list-high-priority-symbolic.svg000066400000000000000000000013561522207411400251620ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/list-low-priority-symbolic.svg000066400000000000000000000013671522207411400250460ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playback-pause-symbolic.svg000066400000000000000000000010071522207411400253620ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playback-start-symbolic.svg000066400000000000000000000010521522207411400254020ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playlist-consecutive-symbolic.svg000066400000000000000000000012241522207411400266500ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playlist-repeat-song-symbolic.svg000066400000000000000000000032621522207411400265510ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playlist-repeat-symbolic.svg000066400000000000000000000024061522207411400256040ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-playlist-shuffle-symbolic.svg000066400000000000000000000032671522207411400257660ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-skip-backward-symbolic.svg000066400000000000000000000013331522207411400252050ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/media-skip-forward-symbolic.svg000066400000000000000000000013321522207411400250720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/music-artist-symbolic.svg000066400000000000000000000012571522207411400240370ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/music-note-symbolic.svg000066400000000000000000000014061522207411400234720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/music-queue-symbolic.svg000066400000000000000000000015061522207411400236520ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/network-server-symbolic.svg000066400000000000000000000030701522207411400244030ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/non-starred-symbolic.svg000066400000000000000000000026661522207411400236540ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/object-select-symbolic.svg000066400000000000000000000013471522207411400241360ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/open-menu-symbolic.svg000066400000000000000000000004721522207411400233140ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/paintbrush-symbolic.svg000066400000000000000000000010671522207411400235710ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/pan-end-symbolic.svg000066400000000000000000000011071522207411400227270ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/pan-start-symbolic.svg000066400000000000000000000010611522207411400233150ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/paper-symbolic.svg000066400000000000000000000007651522207411400225250ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/person-symbolic.svg000066400000000000000000000006021522207411400227120ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/pip-in-symbolic.svg000066400000000000000000000020001522207411400225720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/pip-out-symbolic.svg000066400000000000000000000016421522207411400230060ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/playback-options-symbolic.svg000066400000000000000000000042721522207411400246720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/playlist-shuffle-symbolic.svg000066400000000000000000000032621522207411400247040ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/playlist-symbolic.svg000066400000000000000000000007211522207411400232470ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/playlist2-symbolic.svg000066400000000000000000000006741522207411400233400ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/preferences-system-symbolic.svg000066400000000000000000000037451522207411400252420ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/sad-computer-symbolic.svg000066400000000000000000000043301522207411400240110ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/sidebar-show-right-symbolic.svg000066400000000000000000000012001522207411400251010ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/snapshots-alt-symbolic.svg000066400000000000000000000017551522207411400242160ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/sound-symbolic.svg000066400000000000000000000021071522207411400225360ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/sound-wave-symbolic.svg000066400000000000000000000014471522207411400235040ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/speaker-0-symbolic.svg000066400000000000000000000020701522207411400231740ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/speaker-1-symbolic.svg000066400000000000000000000016221522207411400231770ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/speaker-2-symbolic.svg000066400000000000000000000027121522207411400232010ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/speaker-3-symbolic.svg000066400000000000000000000026311522207411400232020ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/starred-symbolic.svg000066400000000000000000000013171522207411400230540ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/step-back-symbolic.svg000066400000000000000000000015611522207411400232620ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/stopwatch-symbolic.svg000066400000000000000000000020131522207411400234160ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/system-search-symbolic.svg000066400000000000000000000011411522207411400241720ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/text-justify-center-symbolic.svg000066400000000000000000000010551522207411400253440ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/update-symbolic.svg000066400000000000000000000025011522207411400226660ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/user-home-symbolic.svg000066400000000000000000000016421522207411400233150ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/user-trash-symbolic.svg000066400000000000000000000017611522207411400235100ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/video-clip-symbolic.svg000066400000000000000000000016451522207411400234470ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/view-fullscreen-symbolic.svg000066400000000000000000000016011522207411400245160ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/view-list-bullet-symbolic.svg000066400000000000000000000022211522207411400246130ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/view-more-symbolic.svg000066400000000000000000000007631522207411400233260ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/view-unfullscreen-symbolic.svg000066400000000000000000000013071522207411400250640ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/icons/window-close-symbolic.svg000066400000000000000000000013771522207411400240300ustar00rootroot00000000000000 Jeffser-Nocturne-469023d/src/integrations/000077500000000000000000000000001522207411400204415ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/integrations/__init__.py000066400000000000000000000024401522207411400225520ustar00rootroot00000000000000# __init__.py from .jellyfin import Jellyfin from .navidrome import Navidrome, NavidromeIntegrated 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')) settings.set_string("integration-library-dir", integration.get_property('libraryDir')) settings.set_boolean("integration-trust-server", integration.get_property('trustServer')) Jeffser-Nocturne-469023d/src/integrations/base.py000066400000000000000000000423511522207411400217320ustar00rootroot00000000000000# base.py from gi.repository import GLib, GObject, Gdk from . import models, secret, sql_instance from ..constants import get_nocturne_version, DEFAULT_MUSIC_DIR, INTEGRATIONS_DIR import requests, urllib3, time, os, json from datetime import datetime from requests.adapters import HTTPAdapter, Retry # Just so that the logs don't get cluttered with warnings if trust-server = True urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 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, default=DEFAULT_MUSIC_DIR) # 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 retry rules retries = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504] ) # Use session instead of calling requests session_adapter = HTTPAdapter( pool_connections=5, pool_maxsize=100, max_retries=retries ) session = requests.Session() session.mount("http://", session_adapter) session.mount("https://", session_adapter) 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 Exception: 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)' }, **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_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 *_, parameter=parameter, model_id=model_id: GLib.idle_add(callback, self.loaded_models.get(model_id).get_property(parameter)) ) GLib.idle_add(callback, self.loaded_models.get(model_id).get_property(parameter)) return connection_id 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 getCoverArt(self, model_id:str='', big:bool=False) -> Gdk.Paintable: # should set gdkPaintable to Model # should return Gdk.Paintable (texture) # Important: if big is set to True then DO NOT save the picture to the model, just return it # Resolutios: 240 normally, 720 if big is set to True print('WARNING', 'getCoverArt', 'not implemented') return None def getCoverArtUrl(self, model_id:str='', big:bool=False) -> str: # Returns URL that can be used to get coverArt directly by external services # Returns empty string when a url is not available 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) 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 getStarredSongs(self) -> list: # returns a list of IDs of songs print('WARNING', 'getStarredSongs', '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 getCoverArt 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 getCoverArt 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 getCoverArt 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 getCoverArt 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 getLyrics(self, songId:str) -> dict: # returns same dicts as lyrics -> helpers -> get_lyrics return {'type': '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-469023d/src/integrations/discord_rpc.py000066400000000000000000000232721522207411400233140ustar00rootroot00000000000000# 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(" dict: headers = { "Authorization": self.AUTH_HEADER } 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: params = { **params } headers = { **self.get_base_header(), "Accept": "application/json" } try: if mode == 'GET': response = self.session.get( self.get_url(action, **action_keys), params=params, json=json, headers=headers, verify=not self.get_property('trustServer') ) elif mode == 'POST': response = self.session.post( self.get_url(action, **action_keys), params=params, json=json, headers=headers, verify=not self.get_property('trustServer') ) elif mode == 'DELETE': response = self.session.delete( self.get_url(action, **action_keys), params=params, json=json, headers=headers, verify=not self.get_property('trustServer') ) if response.status_code in (200, 201): return response.json() elif response.status_code == 204: return {'state': 'ok'} except Exception as e: logger.error(f"action error {action}: {e}") return {} # ----------- # 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: model = self.loaded_models.get(song_id) if radioStreamUrl := model.get_property('radioStreamUrl'): 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: response = self.session.get( self.get_url('Items/{id}/Images/Primary', id=model_id), headers=self.get_base_header(), params={ 'maxWidth': size, 'quality': 90 }, verify=not self.get_property('trustServer'), timeout=10 ) 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 getCoverArt(self, model_id:str='', big:bool=False) -> Gdk.Paintable: if not model_id: return None if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): return local.Local.getCoverArt(self, model_id, big=big) if not big and model.get_property('gdkPaintable') is not None: return model.get_property('gdkPaintable') if response_bytes := self.getCoverArtBytes(model_id, 720 if big else 240): try: gbytes = GLib.Bytes.new(response_bytes) texture = Gdk.Texture.new_from_bytes(gbytes) if big: return texture model.set_property('gdkPaintable', texture) return model.get_property('gdkPaintable') except Exception as e: logger.error(f"can't convert image from {model_id}: {e}") return None def getCoverArtUrl(self, model_id:str='', big:bool=False) -> str: if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): return "" params = { 'maxWidth': 720 if big else 240, 'quality': 90 } if token := self.get_property('accessToken'): params['api_key'] = token return '{}?{}'.format(self.get_url('Items/{id}/Images/Primary', id=model_id), 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'): 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", } 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', []) id_list = [] for album in albums: artists = album.get("ArtistItems", []) songs = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "ParentId": album.get("Id"), "IncludeItemTypes": "Audio", "Fields": "RunTimeTicks" } ).get("Items", []) duration = int(sum(song.get("RunTimeTicks", 0) for song in songs) / 10000000) album_model = models.Album( id=album.get("Id"), name=album.get("Name"), artist=artists[0].get("Name") if artists else "Unknown", artistId=artists[0].get("Id") if artists else "", songCount=len(songs), duration=duration, artists=[{"id": art.get("Id"), "name": art.get("Name")} for art in artists], 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")) ) self.loaded_models[album.get("Id")] = album_model id_list.append(album.get("Id")) return id_list def getArtists(self, size:int=10) -> list: params = { "Limit": size, "Recursive": "true", "Fields": "Overview,SimilarItems,UserData", "SortBy": "Random", "SortOrder": "Ascending" } response = self.make_request( action='Artists', mode='GET', params=params ) id_list = [] for artist in response.get('Items', []): albums = self.make_request( action="Users/{userId}/Items", mode="GET", params={ "ArtistIds": artist.get("Id"), "IncludeItemTypes": "MusicAlbum", "Recursive": "true" } ).get("Items", []) artist_model = models.Artist( id=artist.get('Id'), name=artist.get('Name'), albumCount=len(albums), album=[{'id': alb.get("Id"), 'name': alb.get("Name")} for alb in albums], starred=artist.get("UserData", {}).get("IsFavorite", False), biography=artist.get("Overview", ""), similarArtist=[{'id': art.get("Id"), 'name': art.get("Name")} for art in artist.get("SimilarItems", [])], userRating=self.get_rating(artist.get("Id")) ) self.loaded_models[artist.get("Id")] = artist_model id_list.append(artist.get("Id")) return id_list def getPlaylists(self) -> list: params = { "IncludeItemTypes": "Playlist", "Recursive": "true", "Fields": "None" } response = self.make_request( action='Users/{userId}/Items', mode='GET', params=params ) id_list = [] for playlist in response.get('Items', []): songs = self.make_request( action='Playlists/{id}/Items', action_keys={"id": playlist.get("Id")}, mode="GET", params={ "Fields": "RunTimeTicks", "UserId": self.get_property("userId") } ).get("Items", []) duration = int(sum(song.get("RunTimeTicks", 0) for song in songs) / 10000000) playlist_model = models.Playlist( id=playlist.get("Id"), name=playlist.get("Name"), songCount=len(songs), duration=duration, entry=[{"id": song.get("Id"), "name": song.get("Name")} for song in songs] ) self.loaded_models[playlist.get("Id")] = playlist_model id_list.append(playlist.get("Id")) return id_list def getStarredSongs(self) -> list: song_list = [] songs = self.make_request( action="Users/{userId}/Items", mode="GET", params={ "IncludeItemTypes": "Audio", "Recursive": "true", "Fields": "Id", "Filters": "IsFavorite" } ).get("Items", []) return [song.get("Id") for song in songs] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): def run(): artist = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if artist.get("Id"): albums = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "ParentId": model_id, "IncludeItemTypes": "MusicAlbum", "Recursive": "true", "Fields": "ItemCounts", "SortBy": "PremiereDate" } ).get("Items", []) self.loaded_models.get(model_id).update_data( id=artist.get("Id"), name=artist.get("Name"), albumCount=len(albums), album=[{"id": alb.get("Id"), "name": alb.get("Name")} for alb in albums], starred=artist.get("UserData", {}).get("IsFavorite", False), biography=artist.get("Overview", ""), similarArtists=[{"id": art.get("Id"), "name": art.get("Name")} for art in artist.get("SimilarItems", [])], userRating=self.get_rating(artist.get("Id")) ) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models or force_update: if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Artist(id=model_id) if use_threading: threading.Thread(target=run, daemon=True).start() else: run() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): def run(): album = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if album.get("Id"): 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", []) 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, 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) ) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models or force_update: if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Album(id=model_id) if use_threading: threading.Thread(target=run, daemon=True).start() else: run() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True): def run(): playlist = self.make_request( action='Users/{userId}/Items/{id}', action_keys={"id": model_id}, mode="GET" ) if playlist.get("Id"): songs = self.make_request( action='Users/{userId}/Items', mode="GET", params={ "ParentId": model_id, "IncludeItemTypes": "Audio", "Recursive": "true", "Fields": "RunTimeTicks" } ).get("Items", []) duration = int(sum(song.get("RunTimeTicks", 0) for song in songs) / 10000000) self.loaded_models.get(model_id).update_data( id=playlist.get("Id"), name=playlist.get("Name"), songCount=len(songs), duration=duration, entry=[{"id": song.get("Id"), "name": song.get("Name")} for song in songs] ) elif model_id in self.loaded_models: del self.loaded_models[model_id] if model_id not in self.loaded_models or force_update: if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Playlist(id=model_id) if use_threading: threading.Thread(target=run, daemon=True).start() else: run() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def verifySong(self, model_id:str, force_update:bool=False, use_threading:bool=True): def run(): 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"): 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"), 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) ) 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 or force_update: if model_id not in self.loaded_models: self.loaded_models[model_id] = models.Song(id=model_id) if use_threading: threading.Thread(target=run, daemon=True).start() else: run() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() 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", []) id_list = [] for song in songs: if song.get("Id"): duration = int(song.get("RunTimeTicks", 0) / 10000000) properties = { "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"), "duration": duration, "artists": [{"id": art.get("Id"), "name": art.get("Name")} for art in song.get("ArtistItems", [])], "starred": song.get("UserData", {}).get("IsFavorite", False), "userRating": self.get_rating(song.get("Id")) } if song.get("Id") in self.loaded_models: self.loaded_models.get(song.get("Id")).update_data(**properties) else: self.loaded_models[song.get("Id")] = models.Song(**properties) id_list.append(song.get("Id")) return id_list 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" } ).get('Items', []) id_list = [] for song in songs: if song.get("Id"): duration = int(song.get("RunTimeTicks", 0) / 10000000) properties = { "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"), "duration": duration, "artists": [{"id": art.get("Id"), "name": art.get("Name")} for art in song.get("ArtistItems", [])], "starred": song.get("UserData", {}).get("IsFavorite", False), "userRating": self.get_rating(song.get("Id")) } if song.get("Id") in self.loaded_models: self.loaded_models.get(song.get("Id")).update_data(**properties) else: self.loaded_models[song.get("Id")] = models.Song(**properties) id_list.append(song.get("Id")) return id_list def getLyrics(self, songId:str) -> dict: result = self.make_request( action='Audio/{id}/Lyrics', action_keys={'id': songId}, mode='GET' ) isSynced = bool(result.get('Lyrics', [{}])[0].get('Start')) if isSynced: lines = [] for line in result.get('Lyrics', []): lines.append({ 'content': line.get('Text'), 'ms': line.get('Start') / 10000 }) return { 'type': 'lrc', 'content': lines } else: text = '\n'.join([line.get('Text') for line in result.get('Lyrics', [])]) if text: return { 'type': 'plain', 'content': text } return {'type': 'not-found'} def __fetch_type(self, item_type:str, query:str, limit:int=5, offset:int=0, fields:str=""): # Method exclusive to Jellyfin, helper for searches return self.make_request( action='Users/{userId}/Items', mode="GET", params={ "SearchTerm": query, "IncludeItemTypes": item_type, "Recursive": "true", "Limit": limit, "StartIndex": offset, "Fields": fields } ).get('Items', []) 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)], 'album': [item.get("Id") for item in self.__fetch_type("MusicAlbum", query, albumCount, albumOffset)], 'song': [item.get("Id") for item in self.__fetch_type("Audio", query, songCount, songOffset)], 'playlist': [item.get("Id") for item in self.__fetch_type("Playlist", query, playlistCount, playlistOffset)] } 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'): radio_model = models.Song( id=radio.get("Id"), title=radio.get("Name"), duration=-1 ) 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")) 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: return self.updatePlaylist( playlistId=playlistId, songIdToAdd=songId ) 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: 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' } ).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): headers = { **self.get_base_header(), "Accept": "application/json" } try: with self.session.get(self.get_url('Items/{id}/Download', id=model_id), headers=headers, 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: params = { "maxWidth": 240, "quality": 90 } response = self.session.get( self.get_url('Users/{userId}/Images/Primary'), params=params, verify=not self.get_property('trustServer') ) response_bytes = response.content if response.status_code == 200 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-469023d/src/integrations/local.py000066400000000000000000000711261522207411400221140ustar00rootroot00000000000000# local.py from gi.repository import GLib, GObject, Gdk 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 get_stream_url(self, song_id:str) -> str: model = self.loaded_models.get(song_id) if radioStreamUrl := model.get_property('radioStreamUrl'): return radioStreamUrl return 'file://{}'.format(model.get_property('path')) 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 getCoverArt(self, model_id:str='', big:bool=False) -> Gdk.Paintable: if not model_id: return None if model := self.loaded_models.get(model_id): if not big and not isinstance(model, models.Playlist) and model.get_property('gdkPaintable'): return model.get_property('gdkPaintable') try: if raw_data := self.getCoverArtBytes(model_id, 720 if big else 240): gbytes = GLib.Bytes.new(raw_data) texture = Gdk.Texture.new_from_bytes(gbytes) model.set_property('gdkPaintable', texture) return model.get_property('gdkPaintable') except Exception as e: logger.error(f"can't get image from {model_id}: {e}") return None def getCoverArtUrl(self, model_id:str="", big:bool=False) -> 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) if paintable := self.getCoverArt(model_id, big): 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 getStarredSongs(self) -> 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() return [song_id for song_id in star_list if song_id.startswith("SONG:") and song_id in self.loaded_models] def verifyArtist(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def verifyAlbum(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def verifyPlaylist(self, model_id:str, force_update:bool=False, use_threading:bool=True): threading.Thread(target=self.getCoverArt, 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.getCoverArt(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) -> dict: 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 {'type': 'lrc-unprepared', 'content': lyrics_str} else: return {'type': 'plain', 'content': lyrics_str} return {'type': '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: 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") } try: gecos_temp = pwd.getpwnam(getpass.getuser()).pw_gecos.split(',') if len(gecos_temp) > 0: server_information["username"] = pwd.getpwnam(getpass.getuser()).pw_gecos.split(',')[0].title() except Exception as e: logger.error(f"can't get server information: {e}") return server_information class Offline(Local): __gtype_name__ = 'NocturneIntegrationOffline' login_page_metadata = {} 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") } try: gecos_temp = pwd.getpwnam(getpass.getuser()).pw_gecos.split(',') if len(gecos_temp) > 0: server_information["username"] = pwd.getpwnam(getpass.getuser()).pw_gecos.split(',')[0].title() except Exception as e: logger.error(f"can't get server information: {e}") return server_information Jeffser-Nocturne-469023d/src/integrations/meson.build000066400000000000000000000005021522207411400226000ustar00rootroot00000000000000moduledir = 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-469023d/src/integrations/models.py000066400000000000000000000216651522207411400223100ustar00rootroot00000000000000# 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) 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) #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) 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) 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) Jeffser-Nocturne-469023d/src/integrations/navidrome.py000066400000000000000000000717471522207411400230170ustar00rootroot00000000000000# navidrome.py from gi.repository import GLib, GObject, Gdk, Gio from . import secret, models, local 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 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': "network-server-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 / Navidrome 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) def send_request(self, action: str, params:dict={}): return self.session.get( self.get_url(action), params={**self.get_base_params(), **params}, verify=not self.get_property('trustServer') ) def make_request(self, action:str, params:dict={}) -> dict: try: response = self.send_request(action, params) if response.status_code == 200: 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 == 200: 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 song_id not in self.loaded_models: self.verifySong(song_id, use_threading=False) model = self.loaded_models.get(song_id) if radioStreamUrl := model.get_property('radioStreamUrl'): 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 = "&".join([f"{k}={v}" for k, v in params.items()]) 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 response = self.session.get( self.get_url('getCoverArt'), params={ **self.get_base_params(), 'id': cover_id, 'size': size }, verify=not self.get_property('trustServer') ) 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 getCoverArt(self, model_id:str='', big:bool=False) -> Gdk.Paintable: if not model_id: return None if model := self.loaded_models.get(model_id): if isinstance(model, models.Song) and model.get_property('isExternalFile'): return local.Local.getCoverArt(self, model_id, big=big) if not big and model.get_property('gdkPaintable'): return model.get_property('gdkPaintable') if response_bytes := self.getCoverArtBytes(model_id, 720 if big else 240): try: gbytes = GLib.Bytes.new(response_bytes) texture = Gdk.Texture.new_from_bytes(gbytes) if big: return texture model.set_property('gdkPaintable', texture) return model.get_property('gdkPaintable') except Exception as e: logger.error(f"can't convert image from {model_id}: {e}") return None def getCoverArtUrl(self, model_id:str='', big:bool=False) -> 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': 720 if big else 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 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 getStarredSongs(self) -> list: songs = self.make_request('getStarred2').get('starred2', {}).get('song', []) return [song.get('id') for song in songs] 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: threading.Thread(target=update, daemon=True).start() else: update() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() 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: threading.Thread(target=update, daemon=True).start() else: update() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() 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: threading.Thread(target=update, daemon=True).start() else: update() threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() 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)) threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() 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: threading.Thread(target=update, daemon=True).start() else: update() else: threading.Thread(target=self.getCoverArt, args=(model_id,), daemon=True).start() def star(self, model_id:str) -> bool: response = self.make_request('star', {'id': model_id}) return response.get('status') == 'ok' def unstar(self, model_id:str) -> bool: 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) -> dict: lyrics_data = self.make_request('getLyricsBySongId', {'id': songId}).get('lyricsList') or {} lyrics = (lyrics_data.get('structuredLyrics') or [{}])[0] if lyrics.get('synced', False): lrc_lines = [] for line in lyrics.get('line', []): lrc_lines.append({ 'ms': line.get('start'), 'content': line.get('value') }) return { 'type': 'lrc', 'content': lrc_lines } return {'type': '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: 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', []): 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', []): 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', []): 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', [])], 'album': [m.get('id') for m in search_results.get('album', [])], 'song': [m.get('id') for m in search_results.get('song', [])], 'playlist': playlist_ids } 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 response = self.make_request('createPlaylist', { 'playlistId': playlistId, 'name': name, 'songId': songId }) return response.get('playlist', {}).get('id') 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.session.get( self.get_url('ping'), params=self.get_base_params(), verify=not self.get_property('trustServer') ) if response.status_code == 200: 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: params = { **self.get_base_params(), 'username': self.get_property('user') } response = self.session.get( self.get_url('getAvatar'), params=params, verify=not self.get_property('trustServer') ) response_bytes = response.content if response.status_code == 200 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 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) Jeffser-Nocturne-469023d/src/integrations/secret.py000066400000000000000000000032161522207411400223020ustar00rootroot00000000000000# 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-469023d/src/integrations/sql_instance.py000066400000000000000000000116071522207411400235030ustar00rootroot00000000000000# sql_instance.py import sqlite3, os 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-469023d/src/main.py000066400000000000000000000231361522207411400172360ustar00rootroot00000000000000# 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 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') from gi.repository import Gtk, Gdk, Gio, Adw, GLib from .window import NocturneWindow from .preferences import NocturnePreferences from .constants import get_song_info_from_file, TRANSLATORS, DEFAULT_MUSIC_DIR, 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") 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 not selected_local_folder: settings.set_string("integration-library-dir", DEFAULT_MUSIC_DIR) 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: 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="ยฉ 2026 Jeffry Samuel", 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)] ) print("Nocturne version:", version) set_version(version) return NocturneApplication(version).run(sys.argv) Jeffser-Nocturne-469023d/src/meson.build000066400000000000000000000054051522207411400201010ustar00rootroot00000000000000pkgdatadir = 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/songs_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: 'r-xr-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-469023d/src/nocturne.gresource.xml000066400000000000000000000242411522207411400223120ustar00rootroot00000000000000 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 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/songs_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-469023d/src/nocturne.in000077500000000000000000000025541522207411400201310ustar00rootroot00000000000000#!@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-469023d/src/nocturne_search_provider.py.in000066400000000000000000000105131522207411400240060ustar00rootroot00000000000000#!/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(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-469023d/src/preferences.py000066400000000000000000000402031522207411400206050ustar00rootroot00000000000000# preferences.py from gi.repository import Gtk, Adw, GLib, Gio, Gdk, Pango from .integrations import get_current_integration, secret from .constants import SIDEBAR_MENU, BITRATE_OPTIONS, IN_FLATPAK import os @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() 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() carousel_pan_buttons_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 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( "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: for item in section.get('items', []): 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") if integration: data = integration.getServerInformation() self.instance_el.set_title(data.get('username', "")) self.instance_el.set_subtitle(data.get('title', "")) self.instance_el.set_tooltip_text(data.get('link')) self.instance_el.set_action_target_value(GLib.Variant('s', data.get('link', ''))) self.instance_icon_el.set_visible(data.get('link')) self.instance_el.set_activatable(data.get('link')) self.instance_avatar_el.set_custom_image(data.get('picture')) self.instance_avatar_el.set_text(data.get('username', '')) self.instance_el.set_visible(len(data) > 0) self.session_group_el.set_visible(True) else: self.session_group_el.set_visible(False) # 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( "show-carousel-pan-buttons", self.carousel_pan_buttons_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( "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_pages = settings.get_value('sidebar-disabled-pages').unpack() for section in SIDEBAR_MENU: section_expander = None if section.get("title"): section_expander = Adw.ExpanderRow( title=section.get("title") ) self.sidebar_group.add(section_expander) for item in section.get('items', []): if item.get('page-tag') != 'home': row = Adw.SwitchRow( title=item.get("title"), active=item.get("page-tag") not in disabled_pages, name=item.get("page-tag") ) row.connect('notify::active', self.sidebar_item_toggled) if item.get("icon-name"): row.add_prefix( Gtk.Image(icon_name=item.get("icon-name")) ) if section_expander: section_expander.add_row(row) else: self.sidebar_group.add(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 )) @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") ) def sidebar_item_toggled(self, row, gp): settings = Gio.Settings(schema_id="com.jeffser.Nocturne") enabled_pages = settings.get_value('sidebar-disabled-pages').unpack() name = row.get_name() if not row.get_active(): if name not in enabled_pages: enabled_pages.append(name) else: if name in enabled_pages: enabled_pages.remove(name) settings.set_value('sidebar-disabled-pages', GLib.Variant('as', enabled_pages)) if main_window := self.get_root().get_application().main_window: GLib.idle_add(main_window.setup_sidebar) 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-469023d/src/style.css000066400000000000000000000110641522207411400176070ustar00rootroot00000000000000.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; } sidebar scrolledwindow > viewport > *, navigation-view-page > toolbarview > scrolledwindow > viewport > *, navigation-view-page > toolbarview > stack > scrolledwindow > viewport > * { padding-bottom: 100px; } 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); } toast { margin-bottom: 84px; } @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-469023d/src/ui/000077500000000000000000000000001522207411400163505ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/album/000077500000000000000000000000001522207411400174505ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/album/button.blp000066400000000000000000000047671522207411400215000ustar00rootroot00000000000000using 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; }; action-name: "app.show_artist"; } }Jeffser-Nocturne-469023d/src/ui/album/page.blp000066400000000000000000000105051522207411400210640ustar00rootroot00000000000000using 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 {} }; }; }; }; } Jeffser-Nocturne-469023d/src/ui/album/row.blp000066400000000000000000000016541522207411400207640ustar00rootroot00000000000000using 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-469023d/src/ui/artist/000077500000000000000000000000001522207411400176565ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/artist/button.blp000066400000000000000000000013421522207411400216700ustar00rootroot00000000000000using 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-469023d/src/ui/artist/page.blp000066400000000000000000000106471522207411400213010ustar00rootroot00000000000000using 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; } $NocturneWrapbox album_wrapbox {} $NocturneCarousel artist_carousel {} }; }; }; }; }Jeffser-Nocturne-469023d/src/ui/artist/row.blp000066400000000000000000000014241522207411400211650ustar00rootroot00000000000000using 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-469023d/src/ui/containers/000077500000000000000000000000001522207411400205155ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/containers/carousel.blp000066400000000000000000000034511522207411400230340ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneCarousel: 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: _("List"); }; tooltip-text: _("List"); visible: false; } Gtk.Overlay { [overlay] Adw.Bin { halign: start; vexpand: true; width-request: 10; styles [ "carousel-overlay-left" ] } [overlay] Gtk.Button pan_start_el { tooltip-text: _("Pan to the Left"); icon-name: "pan-start-symbolic"; valign: end; halign: start; margin-start: 10; margin-bottom: 10; visible: false; clicked => $pan_start(); styles [ "circular", "accent" ] } [overlay] Adw.Bin { halign: end; vexpand: true; width-request: 10; styles [ "carousel-overlay-right" ] } [overlay] Gtk.Button pan_end_el { tooltip-text: _("Pan to the Right"); icon-name: "pan-end-symbolic"; valign: end; halign: end; margin-end: 10; margin-bottom: 10; visible: false; clicked => $pan_end(); styles [ "circular", "accent" ] } 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(); Gtk.EventControllerScroll { scroll => $on_scroll(); flags: both_axes; } Adw.Spinner { height-request: 240; width-request: 240; } }; } }Jeffser-Nocturne-469023d/src/ui/containers/download_row.blp000066400000000000000000000021461522207411400237150ustar00rootroot00000000000000using 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-469023d/src/ui/containers/downloads_queue_button.blp000066400000000000000000000015671522207411400260160ustar00rootroot00000000000000using 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-469023d/src/ui/containers/page_dialog.blp000066400000000000000000000002471522207411400234520ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePageDialog: Adw.Dialog { content-width: 1000; content-height: 700; child: Adw.NavigationView navigation_view {}; }Jeffser-Nocturne-469023d/src/ui/containers/wrapbox.blp000066400000000000000000000011411522207411400226730ustar00rootroot00000000000000using 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: _("List"); }; tooltip-text: _("List"); visible: false; } Adw.WrapBox list_el { hexpand: true; halign: center; child-spacing: 10; line-spacing: 10; Adw.Spinner { height-request: 240; width-request: 240; } } }Jeffser-Nocturne-469023d/src/ui/lyrics/000077500000000000000000000000001522207411400176555ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/lyrics/dialog.blp000066400000000000000000000075101522207411400216160ustar00rootroot00000000000000using 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-469023d/src/ui/lyrics/edit_row.blp000066400000000000000000000025471522207411400222000ustar00rootroot00000000000000using 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-469023d/src/ui/pages/000077500000000000000000000000001522207411400174475ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/pages/albums.blp000066400000000000000000000035341522207411400214360ustar00rootroot00000000000000using 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-469023d/src/ui/pages/albums_all.blp000066400000000000000000000063241522207411400222660ustar00rootroot00000000000000using 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-469023d/src/ui/pages/artists.blp000066400000000000000000000061761522207411400216510ustar00rootroot00000000000000using 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-469023d/src/ui/pages/home.blp000066400000000000000000000034141522207411400211000ustar00rootroot00000000000000using 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: 800; child: Gtk.Box { orientation: vertical; spacing: 10; $NocturneWrapbox song_wrapbox {} $NocturneCarousel album_carousel {} $NocturneCarousel artist_carousel {} $NocturneCarousel playlist_carousel {} }; }; }; } Gtk.StackPage { name: "no-content"; child: Adw.StatusPage { icon-name: "sad-computer-symbolic"; title: _("No Elements Found"); }; } }; }; }Jeffser-Nocturne-469023d/src/ui/pages/login.blp000066400000000000000000000063251522207411400212640ustar00rootroot00000000000000using 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 server_status_el { title: _("Server Status"); subtitle: _("Not Running"); tooltip-text: _("Restart Instance"); activatable: true; activated => $server_restart_requested(); [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"); [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(); activated => $open_local_directory(); activatable: true; [prefix] Gtk.Image { icon-name: "folder-open-symbolic"; valign: center; } } Adw.EntryRow user_el { title: _("Username"); } Adw.PasswordEntryRow password_el { title: _("Password"); entry-activated => $login_button_clicked(); } } 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-469023d/src/ui/pages/playback.blp000066400000000000000000000065641522207411400217470ustar00rootroot00000000000000using 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-469023d/src/ui/pages/playlists.blp000066400000000000000000000066551522207411400222060ustar00rootroot00000000000000using 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-469023d/src/ui/pages/radios.blp000066400000000000000000000027351522207411400214360ustar00rootroot00000000000000using 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-469023d/src/ui/pages/setup.blp000066400000000000000000000041751522207411400213150ustar00rootroot00000000000000using 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-469023d/src/ui/pages/songs_all.blp000066400000000000000000000062431522207411400221340ustar00rootroot00000000000000using 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-469023d/src/ui/pages/songs_starred.blp000066400000000000000000000051631522207411400230300ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneSongsStarredPage: Adw.NavigationPage { title: _("Favorite Songs"); tag: "songs-starred"; 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: _("Favorite Songs"); 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 { icon-name: "sad-computer-symbolic"; title: _("No Songs Found"); }; } }; }; }Jeffser-Nocturne-469023d/src/ui/pages/welcome.blp000066400000000000000000000011031522207411400215740ustar00rootroot00000000000000using 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-469023d/src/ui/playing/000077500000000000000000000000001522207411400200135ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/playing/control_page.blp000066400000000000000000000201641522207411400231710ustar00rootroot00000000000000using 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-469023d/src/ui/playing/cover_art.blp000066400000000000000000000023251522207411400225000ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePlayingCoverArt: Gtk.Box{ orientation: vertical; spacing: 5; 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"); } }; } }; } Adw.ViewSwitcher view_switcher_el { stack: view_stack_el; halign: center; policy: wide; } }Jeffser-Nocturne-469023d/src/ui/playing/equalizer_page.blp000066400000000000000000000033071522207411400235120ustar00rootroot00000000000000using 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-469023d/src/ui/playing/footer.blp000066400000000000000000000053261522207411400220160ustar00rootroot00000000000000using 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: 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-469023d/src/ui/playing/lyrics_page.blp000066400000000000000000000106001522207411400230100ustar00rootroot00000000000000using 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-469023d/src/ui/playing/playback_mode_button.blp000066400000000000000000000003351522207411400247000ustar00rootroot00000000000000using 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-469023d/src/ui/playing/popout_window.blp000066400000000000000000000065641522207411400234420ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturnePopoutWindow: Adw.ApplicationWindow { title: _("Pop-out Window"); width-request: 430; 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: 520sp") setters { split_view.collapsed: true; header.title-widget: null; header.show-title: false; switch_bar.reveal: true; } } Adw.Breakpoint { condition ("max-height: 120sp") setters { main_stack.visible-child-name: "footer"; } } content: Adw.ToastOverlay toast_overlay{ Gtk.Stack main_stack { transition-type: crossfade; 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; }; [overlay] $NocturnePlayingFooter footer {} }; }; } } }; } Jeffser-Nocturne-469023d/src/ui/playing/queue_page.blp000066400000000000000000000015001522207411400226260ustar00rootroot00000000000000using 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 { visible: false; 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-469023d/src/ui/playing/volume_button.blp000066400000000000000000000013651522207411400234210ustar00rootroot00000000000000using 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-469023d/src/ui/playlist/000077500000000000000000000000001522207411400202115ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/playlist/button.blp000066400000000000000000000031751522207411400222310ustar00rootroot00000000000000using 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-469023d/src/ui/playlist/dialog.blp000066400000000000000000000020561522207411400221520ustar00rootroot00000000000000using 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-469023d/src/ui/playlist/page.blp000066400000000000000000000050371522207411400216310ustar00rootroot00000000000000using 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-469023d/src/ui/playlist/row.blp000066400000000000000000000016651522207411400215270ustar00rootroot00000000000000using 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-469023d/src/ui/playlist/selector_row.blp000066400000000000000000000010161522207411400234150ustar00rootroot00000000000000using 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-469023d/src/ui/preferences.blp000066400000000000000000000246111522207411400213540ustar00rootroot00000000000000using 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 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: _("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"); } 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 carousel_pan_buttons_el { title: _("Show Pan Buttons on Carousels"); subtitle: _("Only visible when a carousel has 5 or more items"); } 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.SpinRow hp_songs_el { title: _("Songs"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; value: 1; }; } Adw.SpinRow hp_albums_el { title: _("Albums"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; value: 1; }; } Adw.SpinRow hp_artists_el { title: _("Artists"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; value: 1; }; } Adw.SpinRow hp_playlists_el { title: _("Playlists"); adjustment: Gtk.Adjustment { lower: 0; upper: 25; value: 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-469023d/src/ui/shortcuts-dialog.blp000066400000000000000000000025151522207411400223450ustar00rootroot00000000000000using 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: _("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-469023d/src/ui/song/000077500000000000000000000000001522207411400173165ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/ui/song/button.blp000066400000000000000000000034451522207411400213360ustar00rootroot00000000000000using 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-469023d/src/ui/song/details_dialog.blp000066400000000000000000000023161522207411400227630ustar00rootroot00000000000000using 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-469023d/src/ui/song/queue.blp000066400000000000000000000057361522207411400211540ustar00rootroot00000000000000using 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: _("List"); }; tooltip-text: _("List"); visible: false; 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 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; 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-469023d/src/ui/song/row.blp000066400000000000000000000055731522207411400206360ustar00rootroot00000000000000using 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-469023d/src/ui/song/small_row.blp000066400000000000000000000021411522207411400220120ustar00rootroot00000000000000using 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-469023d/src/ui/window.blp000066400000000000000000000216751522207411400203710ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $NocturneWindow: Adw.ApplicationWindow { title: "Nocturne"; width-request: 360; height-request: 560; close-request => $close_request(); Adw.Breakpoint big_breakpoint_el { condition ("max-width: 840sp") apply => $big_breakpoint_toggled(); unapply => $big_breakpoint_toggled(); } Adw.Breakpoint { condition ("max-width: 480sp") setters { main_split_view.collapsed: true; main_split_view.show-content: true; main_sidebar.mode: page; } } Adw.Breakpoint breakpoint_el { condition ("max-width: 400sp") 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 { Gtk.StackPage { name: "loading"; child: Adw.ToolbarView { [top] Adw.HeaderBar {} 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 {} $NocturnePlaylistsPage {} $NocturneSongsAllPage {} $NocturneSongsStarredPage {} $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-469023d/src/widgets/000077500000000000000000000000001522207411400174015ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/__init__.py000066400000000000000000000012521522207411400215120ustar00rootroot00000000000000# __init__.py from .playing import PlayingFooter, PlayingControlPage, PopoutWindow, PlayingCoverArt from .pages import HomePage, LoginDialog, ArtistsPage, PlaylistsPage, SongsStarredPage, 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, get_lyrics Jeffser-Nocturne-469023d/src/widgets/album/000077500000000000000000000000001522207411400205015ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/album/__init__.py000066400000000000000000000001451522207411400226120ustar00rootroot00000000000000# __init__.py from .button import AlbumButton from .page import AlbumPage from .row import AlbumRow Jeffser-Nocturne-469023d/src/widgets/album/button.py000066400000000000000000000133341522207411400223720ustar00rootroot00000000000000# 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): 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')) @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-469023d/src/widgets/album/meson.build000066400000000000000000000003511522207411400226420ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/album/page.py000066400000000000000000000200351522207411400217670ustar00rootroot00000000000000# 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.song_list_el.set_header(_("Songs"), "music-note-symbolic") 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)) != 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) Jeffser-Nocturne-469023d/src/widgets/album/row.py000066400000000000000000000047441522207411400216730ustar00rootroot00000000000000# 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) @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-469023d/src/widgets/artist/000077500000000000000000000000001522207411400207075ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/artist/__init__.py000066400000000000000000000001501522207411400230140ustar00rootroot00000000000000# __init__.py from .button import ArtistButton from .page import ArtistPage from .row import ArtistRow Jeffser-Nocturne-469023d/src/widgets/artist/button.py000066400000000000000000000051161522207411400225770ustar00rootroot00000000000000# 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 *_: GLib.idle_add(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-469023d/src/widgets/artist/meson.build000066400000000000000000000003521522207411400230510ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/artist/page.py000066400000000000000000000152301522207411400221760ustar00rootroot00000000000000# 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.set_header( label=_("Top Songs"), icon_name="music-note-symbolic" ) self.album_wrapbox.set_header( label=_("Albums"), icon_name="music-queue-symbolic" ) self.artist_carousel.set_header( label=_("Related Artists"), icon_name="music-artist-symbolic" ) 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] GLib.idle_add(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) Jeffser-Nocturne-469023d/src/widgets/artist/row.py000066400000000000000000000043501522207411400220720ustar00rootroot00000000000000# 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-469023d/src/widgets/containers/000077500000000000000000000000001522207411400215465ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/containers/__init__.py000066400000000000000000000004161522207411400236600ustar00rootroot00000000000000# __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-469023d/src/widgets/containers/carousel.py000066400000000000000000000071731522207411400237450ustar00rootroot00000000000000# carousel.py from gi.repository import 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_start_el = Gtk.Template.Child() pan_end_el = Gtk.Template.Child() def __init__(self): super().__init__() self.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::show-carousel-pan-buttons", self.update_pan_button_visibility) def set_header(self, label:str, icon_name:str, page_tag:str=None): self.header_button.set_tooltip_text(label) self.header_button.get_child().set_label(label) self.header_button.get_child().set_icon_name(icon_name) self.header_button.set_visible(True) if page_tag: self.header_button.set_action_target_value(GLib.Variant.new_string(page_tag)) self.header_button.set_action_name('app.replace_root_page') 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.update_pan_button_visibility, self.settings, "show-carousel-pan-buttons") def update_pan_button_visibility(self, settings, key): visible = self.list_el.get_n_pages() >= 5 and settings.get_value(key).unpack() self.pan_start_el.set_visible(visible) self.pan_end_el.set_visible(visible) @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) Jeffser-Nocturne-469023d/src/widgets/containers/context.py000066400000000000000000000101521522207411400236030ustar00rootroot00000000000000# 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-469023d/src/widgets/containers/download_row.py000066400000000000000000000033771522207411400246300ustar00rootroot00000000000000# 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-469023d/src/widgets/containers/downloads_queue_button.py000066400000000000000000000025711522207411400267160ustar00rootroot00000000000000# 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-469023d/src/widgets/containers/meson.build000066400000000000000000000004771522207411400237200ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/containers/page_dialog.py000066400000000000000000000005741522207411400243610ustar00rootroot00000000000000# 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-469023d/src/widgets/containers/wrapbox.py000066400000000000000000000021211522207411400235760ustar00rootroot00000000000000# wrapbox.py from gi.repository import 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() def set_header(self, label:str, icon_name:str, page_tag:str=None): self.header_button.set_tooltip_text(label) self.header_button.get_child().set_label(label) self.header_button.get_child().set_icon_name(icon_name) self.header_button.set_visible(True) if page_tag: self.header_button.set_action_target_value(GLib.Variant.new_string(page_tag)) self.header_button.set_action_name('app.replace_root_page') 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) Jeffser-Nocturne-469023d/src/widgets/lyrics/000077500000000000000000000000001522207411400207065ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/lyrics/__init__.py000066400000000000000000000001351522207411400230160ustar00rootroot00000000000000# __init__.py from .dialog import LyricsDialog from .helpers import prepare_lrc, get_lyrics Jeffser-Nocturne-469023d/src/widgets/lyrics/dialog.py000066400000000000000000000177431522207411400225330ustar00rootroot00000000000000# 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 get_lyrics 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) lyrics = get_lyrics(self.id, True) if lyrics.get('type') == 'lrc': for line in lyrics.get('content'): row = LyricEditRow( ms=line.get('ms'), content=line.get('content', '') ) GLib.idle_add(self.lrc_list_el.append, row) elif lyrics.get('type') == 'plain': for index, line in enumerate(lyrics.get('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())) file_text = '\n'.join(['[{}] {}'.format(ms, content) for ms, content in lines]) target_value = GLib.Variant('a{sv}', { 'id': GLib.Variant('s', self.id), 'content': GLib.Variant('s', file_text) }) self.get_root().activate_action("app.save_lyrics", target_value) 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-469023d/src/widgets/lyrics/helpers.py000066400000000000000000000104741522207411400227300ustar00rootroot00000000000000# helpers.py from ...constants import DATA_DIR from ...integrations import get_current_integration import syncedlyrics, os def online_get(track_name:str, artist_name:str, lrc_path): return syncedlyrics.search( "[{}] [{}]".format(track_name, artist_name), enhanced=True, synced_only=True, save_path=lrc_path ) 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 def list_to_lrc_str(lrc_list:list) -> str: lrc_lines = [] for item in lrc_list: ms = int(item.get('ms', 0)) minutes = ms // 60000 seconds = (ms % 60000) // 1000 centiseconds = (ms % 1000) // 10 timestamp = f"[{minutes:02d}:{seconds:02d}.{centiseconds:02d}]" lrc_lines.append(f"{timestamp} {item.get('content').strip()}") return '\n'.join(lrc_lines) def get_lyrics(song_id:str, online_download:bool) -> dict: # returns these keys: # type (instrumental, lrc, plain, not-found, not-found-locally, radio) # content (none (instrumental/not-found/not-found-locally/radio), list (lrc), str (plain)) integration = get_current_integration() if not integration: return {'type': 'not-found', 'content': None} model = integration.loaded_models.get(song_id) if not model: return {'type': 'not-found', 'content': None} if model.get_property('radioStreamUrl'): return {'type': 'radio', 'content': None} lyrics_dir = os.path.join(DATA_DIR, 'lyrics') os.makedirs(lyrics_dir, exist_ok=True) 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') ) file_name_without_ext = file_name_without_ext.replace('/', '-') lrc_path = os.path.join(lyrics_dir, file_name_without_ext+'.lrc') plain_lyrics_path = os.path.join(lyrics_dir, file_name_without_ext+'.txt') if os.path.isfile(lrc_path): with open(lrc_path, 'r') as f: return {'type': 'lrc', 'content': prepare_lrc(f.read())} if os.path.isfile(plain_lyrics_path): with open(plain_lyrics_path, 'r') as f: content = f.read() if content == '[instrumental]': return {'type': 'instrumental', 'content': None} else: return {'type': 'plain', 'content': content} if not online_download: result = integration.getLyrics(song_id) if result.get('type') != 'not-found': if result.get('type') == 'lrc': with open(lrc_path, 'w+') as f: f.write(list_to_lrc_str(result.get('content'))) elif result.get('type') == 'lrc-unprepared': with open(lrc_path, 'w+') as f: f.write(result.get('content')) result = { 'type': 'lrc', 'content': prepare_lrc(result.get('content')) } elif result.get('type') == 'plain': with open(plain_lyrics_path, 'w+') as f: f.write(result.get('content')) return result if not online_download: return {'type': 'not-found-locally', 'content': None} lyrics = online_get( track_name=model.get_property('title'), artist_name=model.get_property('artist'), lrc_path=lrc_path ) if not lyrics: return {'type': 'not-found', 'content': None} return {'type': 'lrc', 'content': prepare_lrc(lyrics)} Jeffser-Nocturne-469023d/src/widgets/lyrics/meson.build000066400000000000000000000003411522207411400230460ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/meson.build000066400000000000000000000005031522207411400215410ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/pages/000077500000000000000000000000001522207411400205005ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/pages/__init__.py000066400000000000000000000006441522207411400226150ustar00rootroot00000000000000# __init__.py from .home import HomePage from .login import LoginDialog from .artists import ArtistsPage from .playlists import PlaylistsPage from .songs_starred import SongsStarredPage 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-469023d/src/widgets/pages/albums.py000066400000000000000000000051531522207411400223410ustar00rootroot00000000000000# albums.py from gi.repository import Gtk, Adw, GLib 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 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() albums = integration.getAlbumList( list_type=self.get_tag().split('-')[1], size=20, offset=self.offset ) for album_id in albums: 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, True) else: 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) < 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-469023d/src/widgets/pages/albums_all.py000066400000000000000000000067551522207411400232020ustar00rootroot00000000000000# 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 def __init__(self): super().__init__() Gio.Settings(schema_id="com.jeffser.Nocturne").bind( "default-view-mode", 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 query = self.search_entry.get_text() integration = get_current_integration() search_results = integration.search( query=query, albumCount=30, albumOffset=self.offset ) for album_id in search_results.get('album'): 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, True) else: 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, True) else: 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')) < 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-469023d/src/widgets/pages/artists.py000066400000000000000000000067661522207411400225620ustar00rootroot00000000000000# 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", 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 query = self.search_entry.get_text() integration = get_current_integration() 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-469023d/src/widgets/pages/home.py000066400000000000000000000136131522207411400220060ustar00rootroot00000000000000# home.py from gi.repository import Gtk, Adw, GLib, Gio from ...integrations import get_current_integration from ..album import AlbumButton from ..artist import ArtistButton from ..playlist import PlaylistButton from ..song import SongSmallRow import threading @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() 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.settings = Gio.Settings(schema_id="com.jeffser.Nocturne") 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.set_header( label=_("Songs"), icon_name="music-note-symbolic", page_tag="songs-all" ) 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) self.album_carousel.set_header( label=_("Albums"), icon_name="music-queue-symbolic", page_tag="albums-all" ) self.artist_carousel.set_header( label=_("Artists"), icon_name="music-artist-symbolic", page_tag="artists" ) self.playlist_carousel.set_header( label=_("Playlists"), icon_name="playlist-symbolic", page_tag="playlists" ) def get_default_results(self) -> dict: if integration := get_current_integration(): 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 { 'song': songs, 'album': albums, 'artist': artists, 'playlist': playlists } return {} def reload(self): 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).start() GLib.idle_add(self.search_mode_toggled, self.search_toggle) def reset(self): 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 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 ) else: search_results = self.get_default_results() threading.Thread( target=self.song_wrapbox.set_widgets, args=([SongSmallRow(id) for id in search_results.get('song', [])],), daemon=True ).start() threading.Thread( target=self.album_carousel.set_widgets, args=([AlbumButton(id) for id in search_results.get('album', [])],), daemon=True ).start() threading.Thread( target=self.artist_carousel.set_widgets, args=([ArtistButton(id) for id in search_results.get('artist', [])],), daemon=True ).start() threading.Thread( target=self.playlist_carousel.set_widgets, args=([PlaylistButton(id) for id in search_results.get('playlist', [])],), daemon=True ).start() has_results = any([len(search_results.get(key)) > 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_clamp.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).start() Jeffser-Nocturne-469023d/src/widgets/pages/login.py000066400000000000000000000153211522207411400221640ustar00rootroot00000000000000# login.py from gi.repository import Gtk, Adw, Gio, GLib from ...integrations import secret from ...constants import DEFAULT_MUSIC_DIR 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() 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 # 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 '') # 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")) self.login_button_el.set_sensitive(True) # 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', {}), '')) @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()) initial_folder = Gio.File.new_for_path(row.get_subtitle() or DEFAULT_MUSIC_DIR) dialog = Gtk.FileDialog( title=_("Local Music Library"), initial_folder=initial_folder ) 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) self.integration.set_property('url', self.url_el.get_text()) self.integration.set_property('trustServer', self.trust_server_el.get_active()) self.integration.set_property('user', self.user_el.get_text()) secret.store_password(self.password_el.get_text()) 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-469023d/src/widgets/pages/meson.build000066400000000000000000000006061522207411400226440ustar00rootroot00000000000000moduledir = get_option('prefix') / get_option('datadir') / meson.project_name() / 'nocturne' / 'widgets' / 'pages' widgets = [ '__init__.py', 'home.py', 'login.py', 'artists.py', 'playlists.py', 'songs_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-469023d/src/widgets/pages/playback.py000066400000000000000000000052651522207411400226500ustar00rootroot00000000000000# 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) @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-469023d/src/widgets/pages/playlists.py000066400000000000000000000070251522207411400231020ustar00rootroot00000000000000# 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", 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 query = self.search_entry.get_text() integration = get_current_integration() 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-469023d/src/widgets/pages/radios.py000066400000000000000000000025661522207411400223440ustar00rootroot00000000000000# 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-469023d/src/widgets/pages/setup.py000066400000000000000000000070361522207411400222200ustar00rootroot00000000000000# 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 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 self.integration.start_instance(): GLib.idle_add(self.main_stack.set_visible_child_name, 'success') if os.path.isfile(download_path): os.remove(download_path) else: self.main_stack.set_visible_child_name('error') except Exception: 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-469023d/src/widgets/pages/songs_all.py000066400000000000000000000070441522207411400230400ustar00rootroot00000000000000# 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", 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') 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-469023d/src/widgets/pages/songs_starred.py000066400000000000000000000062241522207411400237330ustar00rootroot00000000000000# songs_starred.py from gi.repository import Gtk, Adw, GLib from ...integrations import get_current_integration from ..song import SongRow, SongButton import re, threading @Gtk.Template(resource_path='/com/jeffser/Nocturne/pages/songs_starred.ui') class SongsStarredPage(Adw.NavigationPage): __gtype_name__ = 'NocturneSongsStarredPage' 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() song_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)) 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 song_id in self.song_ids: if model := integration.loaded_models.get(song_id): if re.search(query, model.get_property('title') + model.get_property('artist'), re.IGNORECASE): ids_to_show.append(song_id) else: ids_to_show = self.song_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 song_id in missing_ids: GLib.idle_add(self.list_el.list_el.append, SongRow(song_id)) GLib.idle_add(self.wrapbox_el.append, SongButton(song_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 self.list_el.list_el.remove_all() for el in list(self.wrapbox_el): self.wrapbox_el.remove(el) integration = get_current_integration() self.song_ids = integration.getStarredSongs() def reload(self): GLib.idle_add(self.main_stack.set_visible_child_name, 'loading') GLib.idle_add(self.reset) GLib.idle_add(self.search) @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.song_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') return self.main_stack.set_visible_child_name('no-content') Jeffser-Nocturne-469023d/src/widgets/pages/welcome.py000066400000000000000000000030241522207411400225040ustar00rootroot00000000000000# welcome.py from gi.repository import Gtk, Adw from . import LoginDialog from ...integrations import Local, Navidrome, NavidromeIntegrated, 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, 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 ) 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-469023d/src/widgets/playing/000077500000000000000000000000001522207411400210445ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/playing/__init__.py000066400000000000000000000007021522207411400231540ustar00rootroot00000000000000# __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-469023d/src/widgets/playing/control_page.py000066400000000000000000000255311522207411400241000ustar00rootroot00000000000000# 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 song_connections = { 'songId': '', 'connections': [] } 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', 'songId', self.song_changed) 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): GLib.idle_add(stack.get_parent().set_overflow, Gtk.Overflow.HIDDEN) if stack2 := stack.get_parent().get_parent(): GLib.idle_add(stack2.get_parent().set_overflow, Gtk.Overflow.HIDDEN) if extra_widget := self.get_property('extra-widget'): self.main_container.prepend(extra_widget) 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): self.get_root().breakpoint_el.connect('apply', lambda *_: self.breakpoint_toggled(True)) self.get_root().breakpoint_el.connect('unapply', lambda *_: self.breakpoint_toggled(False)) is_small = self.get_root().get_width() < 480 self.breakpoint_toggled(is_small and self.get_root().get_width() > 0) @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_container.set_visible(self.rating_container.get_visible() and not radioStreamUrl) self.star_el.set_visible(self.star_el.get_visible() and 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)) 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.radio_homepage_el.get_child().set_label(display_artist) self.artist_el.get_child().set_label(display_artist) self.artist_el.set_tooltip_text(display_artist) def song_changed(self, song_id:str): def run(): integration = get_current_integration() integration.verifySong(song_id, use_threading=False) if song_id and song_id in integration.loaded_models: # Set Defaults GLib.idle_add(self.rating_container.set_visible, True) GLib.idle_add(self.star_el.set_visible, True) GLib.idle_add(self.star_el.set_action_target_value, GLib.Variant.new_string(song_id)) # Disconnect From Previous Song if previousSong := integration.loaded_models.get(self.song_connections.get('songId', '')): for connection_id in self.song_connections.get('connections', []).copy(): try: GLib.idle_add(previousSong.disconnect, connection_id) except: pass # Connect UI connections = { '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 } self.song_connections['connections'] = [] self.song_connections['songId'] = song_id for property_name, cb in connections.items(): if connection_id := integration.connect_to_model(song_id, property_name, cb): self.song_connections['connections'].append(connection_id) threading.Thread(target=run, daemon=True).start() Jeffser-Nocturne-469023d/src/widgets/playing/cover_art.py000066400000000000000000000046601522207411400234100ustar00rootroot00000000000000# coverArt.py from gi.repository import 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): __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() def setup(self): integration = get_current_integration() integration.connect_to_model('currentSong', 'songId', self.song_changed) 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 song_changed(self, songId:str): def run(): integration = get_current_integration() if songId in integration.loaded_models: paintable = integration.getCoverArt(songId, big=True) if not paintable: paintable = integration.getCoverArt(songId) if paintable: GLib.idle_add(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 ) GLib.idle_add(self.cover_el.add_css_class, 'p50') GLib.idle_add(self.cover_el.set_paintable, paintable) threading.Thread(target=run, daemon=True).start() 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-469023d/src/widgets/playing/equalizer_page.py000066400000000000000000000026411522207411400244160ustar00rootroot00000000000000# 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-469023d/src/widgets/playing/footer.py000066400000000000000000000122121522207411400227120ustar00rootroot00000000000000# 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 integration = get_current_integration() integration.connect_to_model('currentSong', 'songId', self.song_changed) 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 = Gio.Settings(schema_id="com.jeffser.Nocturne") self.settings.connect("changed::use-big-footer", self.big_mode_toggled) self.big_mode_toggled(self.settings, 'use-big-footer') 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 song_changed(self, song_id:str): self.update_progress_el_visibility() integration = get_current_integration() if song := integration.loaded_models.get(song_id): self.progress_el.get_adjustment().set_upper(song.get_property('duration')) threading.Thread(target=self.update_cover_art, daemon=True).start() 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_cover_art(self): integration = get_current_integration() song_id = integration.loaded_models.get('currentSong').get_property('songId') if model := integration.loaded_models.get(song_id): if paintable := integration.getCoverArt(song_id): GLib.idle_add(self.cover_el.set_from_paintable, paintable) GLib.idle_add(self.cover_el.set_pixel_size, self.cover_el.get_size_request()[0]) else: GLib.idle_add(self.cover_el.set_from_icon_name, 'music-note-symbolic') GLib.idle_add(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-469023d/src/widgets/playing/lyrics_page.py000066400000000000000000000242021522207411400237170ustar00rootroot00000000000000# lyrics_page.py from gi.repository import Gtk, GObject, GLib, Gio, Pango, Gst from ...integrations import get_current_integration from ..lyrics.helpers import get_lyrics 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 song_changed(self, song_id:str, lrclib_download:bool=False): self.set_visible_child_name('loading') def update_lyrics(): lyrics = get_lyrics(song_id, lrclib_download) GLib.idle_add(self.set_visible_child_name, lyrics.get('type')) if lyrics.get('type') == 'plain': GLib.idle_add(self.plain_label_el.set_label, lyrics.get('content')) elif lyrics.get('type') == 'lrc': GLib.idle_add(self.lrc_list_el.remove_all) if lyrics.get('content')[0].get('content'): lyrics['content'].insert(0, {'content': '', 'ms': 0}) settings = Gio.Settings(schema_id="com.jeffser.Nocturne") simulate_wbwl = settings.get_value('simulate-word-by-word-lyrics').unpack() content = lyrics.get('content') 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) threading.Thread(target=update_lyrics, 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) ) def do_scroll(): 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) GLib.idle_add(do_scroll) @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 get_lrc_path(self) -> str: integration = get_current_integration() song_id = integration.loaded_models.get('currentSong').get_property('songId') model = integration.loaded_models.get(song_id) lyrics_dir = os.path.join(DATA_DIR, 'lyrics') os.makedirs(lyrics_dir, exist_ok=True) 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') ) return os.path.join(lyrics_dir, file_name_without_ext+'.lrc') def copy_lyrics_lrc(self, dialog, task): if source_file := dialog.open_finish(task): lrc_path = self.get_lrc_path() destination_file = Gio.File.new_for_path(lrc_path) integration = get_current_integration() song_id = integration.loaded_models.get('currentSong').get_property('songId') source_file.copy_async( destination_file, Gio.FileCopyFlags.OVERWRITE, GLib.PRIORITY_DEFAULT, None, None, lambda *_: self.song_changed(song_id, False) ) @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') if existing_path := self.get_lrc_path(): if os.path.isfile(existing_path): os.remove(existing_path) Jeffser-Nocturne-469023d/src/widgets/playing/meson.build000066400000000000000000000006401522207411400232060ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/playing/playback_mode_button.py000066400000000000000000000026061522207411400256070ustar00rootroot00000000000000# 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-469023d/src/widgets/playing/player.py000066400000000000000000000655321522207411400227250ustar00rootroot00000000000000# 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 from PIL import Image, ImageFilter from colorthief import ColorThief 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 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() return MetadataObj( album=song.get_property('album'), art_url=integration.getCoverArtUrl(song.get_property('id')), artists=[urlparse(song.get_property('radioStreamUrl')).netloc.capitalize()] if song.get_property('radioStreamUrl') else ([a.get('name') for a in song.get_property('artists')] or [song.get_property('artist')]), as_text=[song.get_property('title')], length=song.get_property('duration')*1000000, title=song.get_property('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: print("ERROR", msg.parse_error()[0].message)) self.bus.connect("message::state-changed", lambda *_: threading.Thread(target=self.handle_message_state_changed, args=(_), daemon=True).start()) 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.song_connections = { 'songId': '', 'connections': [] } 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() 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(): threading.Thread(target=self.auto_play, daemon=True).start() 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_message_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_message_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): if message.src == self.gst: 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 = tag_list.get_string(Gst.TAG_TITLE) if success and title and title != 'null': current_title = model.get_property('displaySongTitle') if current_title != title: model.set_property('displaySongTitle', title) success, artist = tag_list.get_string(Gst.TAG_ARTIST) if success and artist and artist != 'null': current_artist = model.get_property('displaySongArtist') if current_artist != artist: model.set_property('displaySongArtist', artist) 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, raw_bytes:bytes): # Load Image 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) ); }} """ GLib.idle_add(self.application.css_provider.load_from_string, css) def song_changed(self, song_id:str): integration = get_current_integration() def update_default_metadata(songId): if model := integration.loaded_models.get(songId): # Disconnect From Previous Song if previousSong := integration.loaded_models.get(self.song_connections.get('songId', '')): for connection_id in self.song_connections.get('connections', []).copy(): try: GLib.idle_add(previousSong.disconnect, connection_id) except: pass connections = { 'title': lambda title: integration.loaded_models.get('currentSong').set_property('displaySongTitle', title) } if model.get_property('radioStreamUrl'): # is radio connections['radioStreamUrl'] = lambda streamUrl: integration.loaded_models.get('currentSong').set_property('displaySongArtist', urlparse(streamUrl).netloc.capitalize()) else: connections['artists'] = lambda artists: integration.loaded_models.get('currentSong').set_property('displaySongArtist', artists[0].get('name') if len(artists) > 0 else '') self.song_connections['connections'] = [] self.song_connections['songId'] = songId for property_name, cb in connections.items(): if connection_id := integration.connect_to_model(song_id, property_name, cb): self.song_connections['connections'].append(connection_id) new_gain = 0.0 album_mode = False if self.settings.get_value('use-gain').unpack(): new_gain = model.get_property('trackGain') if last_model := integration.loaded_models.get(self.song_connections.get('songId')): if last_model.get_property('albumId') == model.get_property('albumId'): new_gain = model.get_property('albumGain') album_mode = True self.song_connections['songId'] = songId GLib.idle_add(self.rg_volume.set_property, "fallback-gain", new_gain) GLib.idle_add(self.rg_volume.set_property, "album-mode", album_mode) if paintable := integration.getCoverArt(songId): if raw_bytes := paintable.save_to_png_bytes().get_data(): threading.Thread(target=self.update_palette, args=(raw_bytes,), daemon=True).start() if model := integration.loaded_models.get(songId): 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) def update_uri(songId): if stream_url := integration.get_stream_url(songId): 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) else: self.gst.set_state(Gst.State.NULL) if song_id: if song_id != self.song_connections.get('songId'): 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") )) threading.Thread(target=integration.scrobble, args=(song_id,), kwargs={'submission': False}, daemon=True).start() threading.Thread(target=update_default_metadata, args=(song_id,), daemon=True).start() threading.Thread(target=update_uri, args=(song_id,), daemon=True).start() else: self.gst.set_state(Gst.State.NULL) Jeffser-Nocturne-469023d/src/widgets/playing/popout_window.py000066400000000000000000000104431522207411400243350ustar00rootroot00000000000000# 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 song_connections = { 'songId': '', 'connections': [] } 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-469023d/src/widgets/playing/queue_page.py000066400000000000000000000035661522207411400235500ustar00rootroot00000000000000# 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 ) def setup(self): integration = get_current_integration() integration.connect_to_model('currentSong', 'generatingQueue', self.autoplay_spinner_el.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) def run(): for i in range(added): if item := global_queue.get_item(position + i): row = SongRow( item.get_string(), draggable=True, removable=True ) GLib.idle_add(self.song_list_el.list_el.insert, row, position + i) threading.Thread(target=run, daemon=True).start() Jeffser-Nocturne-469023d/src/widgets/playing/spectrum.py000066400000000000000000000142221522207411400232610ustar00rootroot00000000000000# 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', 'songId', self.song_changed) 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 song_changed(self, songId:str): integration = get_current_integration() def set_color(): if paintable := integration.getCoverArt(songId): img_io = io.BytesIO(paintable.save_to_png_bytes().get_data()) self.accent_color = [min(c/255, 1) for c in ColorThief(img_io).get_color(quality=10)] if integration.loaded_models.get(songId): threading.Thread(target=set_color, daemon=True).start() else: self.stopped = True def playback_changed(self, playbackState:str): if playbackState == "play": self.stopped = True else: self.stopped = False Jeffser-Nocturne-469023d/src/widgets/playing/volume_button.py000066400000000000000000000016261522207411400243250ustar00rootroot00000000000000# 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-469023d/src/widgets/playlist/000077500000000000000000000000001522207411400212425ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/playlist/__init__.py000066400000000000000000000002771522207411400233610ustar00rootroot00000000000000# __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-469023d/src/widgets/playlist/button.py000066400000000000000000000063271522207411400231370ustar00rootroot00000000000000# 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 *_: GLib.idle_add(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-469023d/src/widgets/playlist/dialog.py000066400000000000000000000042721522207411400230600ustar00rootroot00000000000000# 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-469023d/src/widgets/playlist/meson.build000066400000000000000000000004201522207411400234000ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/playlist/page.py000066400000000000000000000121021522207411400225240ustar00rootroot00000000000000# 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__() self.song_list_el.set_header(_("Songs"), "music-note-symbolic") 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) # needed to avoid error def reload(self): ... Jeffser-Nocturne-469023d/src/widgets/playlist/row.py000066400000000000000000000050731522207411400224300ustar00rootroot00000000000000# 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-469023d/src/widgets/playlist/selector_row.py000066400000000000000000000025301522207411400243230ustar00rootroot00000000000000# 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): 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-469023d/src/widgets/song/000077500000000000000000000000001522207411400203475ustar00rootroot00000000000000Jeffser-Nocturne-469023d/src/widgets/song/__init__.py000066400000000000000000000002661522207411400224640ustar00rootroot00000000000000# __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-469023d/src/widgets/song/button.py000066400000000000000000000125011522207411400222330ustar00rootroot00000000000000# 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 *_: GLib.idle_add(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-469023d/src/widgets/song/details_dialog.py000066400000000000000000000101411522207411400236620ustar00rootroot00000000000000# 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')) self.cover_el.set_paintable(integration.getCoverArt(self.id)) self.cover_el.set_visible(self.cover_el.get_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-469023d/src/widgets/song/meson.build000066400000000000000000000004221522207411400225070ustar00rootroot00000000000000moduledir = 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-469023d/src/widgets/song/queue.py000066400000000000000000000150241522207411400220470ustar00rootroot00000000000000# queue.py from gi.repository import 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 = "" def set_header(self, label:str, icon_name:str, page_tag:str=None): self.header_button.set_tooltip_text(label) self.header_button.get_child().set_label(label) self.header_button.get_child().set_icon_name(icon_name) self.header_button.set_visible(True) if page_tag: self.header_button.set_action_target_value(GLib.Variant.new_string(page_tag)) self.header_button.set_action_name('app.replace_root_page') 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: GLib.idle_add(self.list_el.remove, list(self.list_el)[index]) def verify_visibility(): self.main_stack.set_visible_child_name('content' if len(list(self.list_el)) > 0 else 'no-content') GLib.idle_add(verify_visibility) 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 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() Jeffser-Nocturne-469023d/src/widgets/song/row.py000066400000000000000000000307211522207411400215330ustar00rootroot00000000000000# 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.capitalize(), 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-469023d/src/widgets/song/small_row.py000066400000000000000000000071771522207411400227340ustar00rootroot00000000000000# 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-469023d/src/window.py000066400000000000000000000425311522207411400176210ustar00rootroot00000000000000# 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"): self.get_application().create_action( name=callback.__name__, callback=lambda at, va, cb=callback, win=self: cb(win, va.unpack()) if va is not None else cb(win), shortcuts=shortcuts, parameter_type=GLib.VariantType.new(parameter_type) if parameter_type else None ) def setup_sidebar(self): disabled_pages = self.settings.get_value('sidebar-disabled-pages').unpack() self.main_sidebar.remove_all() for section in SIDEBAR_MENU: section_el = Adw.SidebarSection( title=section.get('title') ) append_section = False for item in section.get('items'): if item.get('page-tag') not in disabled_pages: row = SidebarItem( title=item.get('title'), icon_name=item.get('icon-name'), page_tag=item.get('page-tag') ) section_el.append(row) append_section = True if append_section: self.main_sidebar.append(section_el) threading.Thread(target=self.update_playlist_section_of_sidebar, daemon=True).start() 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): if 'playlists' in self.settings.get_value('sidebar-disabled-pages').unpack(): return 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) playlist_section = self.main_sidebar.get_sections()[-1] GLib.idle_add(playlist_section.remove_all) item = SidebarItem( title=_("All"), icon_name="playlist-symbolic", page_tag="playlists" ) GLib.idle_add(playlist_section.append, item) for playlistId in integration.getPlaylists()[:4]: if model := integration.loaded_models.get(playlistId): item = SidebarItem( page_tag="playlists", playlist_id=playlistId ) GLib.idle_add(playlist_section.append, item) integration.connect_to_model(playlistId, "name", lambda name, row=item: row.set_title(name)) integration.connect_to_model(playlistId, "songCount", lambda n, row=item: row.set_subtitle(('{} Songs' if n > 1 else '{} Song').format(n))) 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.notify_playback() self.setup_sidebar() self.sidebar_playing_page.show_sidebar_el.set_visible(False) 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") 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.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.save_lyrics, parameter_type="a{sv}") 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: GLib.idle_add(self.player_sidebar_splitview.set_show_sidebar, False) GLib.idle_add(self.main_bottom_sheet.set_reveal_bottom_bar, song_playing) GLib.idle_add(self.main_bottom_sheet.set_can_open, song_playing) else: show_sidebar = self.settings.get_value('use-sidebar-player').unpack() GLib.idle_add(self.player_sidebar_splitview.set_show_sidebar, show_sidebar and song_playing) GLib.idle_add(self.main_bottom_sheet.set_reveal_bottom_bar, not show_sidebar and song_playing) GLib.idle_add(self.main_bottom_sheet.set_can_open, not show_sidebar and song_playing) if show_sidebar: GLib.idle_add(self.main_bottom_sheet.set_open, False) if not song_playing: GLib.idle_add(self.main_bottom_sheet.set_open, False) Jeffser-Nocturne-469023d/stubs/000077500000000000000000000000001522207411400163045ustar00rootroot00000000000000Jeffser-Nocturne-469023d/stubs/gi/000077500000000000000000000000001522207411400167035ustar00rootroot00000000000000Jeffser-Nocturne-469023d/stubs/gi/__init__.pyi000066400000000000000000000004601522207411400211650ustar00rootroot00000000000000from 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-469023d/stubs/gi/repository.pyi000066400000000000000000000004321522207411400216440ustar00rootroot00000000000000from 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-469023d/translation_tool.sh000077500000000000000000000023121522207411400210740ustar00rootroot00000000000000#!/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