diff options
| author | navewindre <boneyaard@gmail.com> | 2025-04-05 03:00:29 +0200 |
|---|---|---|
| committer | navewindre <boneyaard@gmail.com> | 2025-04-05 03:00:29 +0200 |
| commit | d6c4365b8de32b621ac46074a9b69908b95686c0 (patch) | |
| tree | 495cb5b1aa7e68ab6ec07fa5fb09904a8c7e47e7 /config/mpv/scripts/subs2srsa | |
| parent | b24463f3d045783b8f4e72926054d53b908e150f (diff) | |
a
Diffstat (limited to 'config/mpv/scripts/subs2srsa')
35 files changed, 0 insertions, 5076 deletions
diff --git a/config/mpv/scripts/subs2srsa/.gitignore b/config/mpv/scripts/subs2srsa/.gitignore deleted file mode 100644 index 850b67c..0000000 --- a/config/mpv/scripts/subs2srsa/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea -.vscode -/.github/RELEASE/*.zip -/.github/RELEASE/*.html -*TODO* diff --git a/config/mpv/scripts/subs2srsa/LICENSE b/config/mpv/scripts/subs2srsa/LICENSE deleted file mode 100644 index f288702..0000000 --- a/config/mpv/scripts/subs2srsa/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> - 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. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - 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 <https://www.gnu.org/licenses/>. - -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: - - <program> Copyright (C) <year> <name of author> - 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 -<https://www.gnu.org/licenses/>. - - 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 -<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/config/mpv/scripts/subs2srsa/Makefile b/config/mpv/scripts/subs2srsa/Makefile deleted file mode 100644 index fc346ce..0000000 --- a/config/mpv/scripts/subs2srsa/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -PROJECT := mpvacious -PREFIX ?= /etc/mpv/ -BRANCH ?= remotes/origin/master -VERSION := $(shell git describe --tags $(BRANCH)) -RELEASE_DIR := .github/RELEASE -ZIP := $(RELEASE_DIR)/$(PROJECT)_$(VERSION).zip -DOCS := $(RELEASE_DIR)/README_$(VERSION).html -MD2HTML = md2html --github --full-html - -.PHONY: all clean docs install uninstall - -all: $(ZIP) -docs: $(DOCS) - -$(ZIP): - git archive \ - --prefix=$(PROJECT)_$(VERSION)/ \ - --format=zip \ - -o $@ \ - $(BRANCH) \ - -$(DOCS): - git show "$(BRANCH):README.md" | $(MD2HTML) -o $@ - -install: - find . -type f -iname '*.lua' | while read -r file; do \ - install -Dm644 "$$file" "$(PREFIX)/scripts/$(PROJECT)/$$file"; \ - done - install -Dm644 $(RELEASE_DIR)/subs2srs.conf "$(PREFIX)/script-opts/subs2srs.conf" - -uninstall: - rm -rf -- "$(PREFIX)/scripts/$(PROJECT)" - rm -- "$(PREFIX)/script-opts/subs2srs.conf" - -clean: - rm -- $(ZIP) $(DOCS) diff --git a/config/mpv/scripts/subs2srsa/README.md b/config/mpv/scripts/subs2srsa/README.md deleted file mode 100644 index deefe3c..0000000 --- a/config/mpv/scripts/subs2srsa/README.md +++ /dev/null @@ -1,478 +0,0 @@ -<p align="center"> -<img src="https://user-images.githubusercontent.com/69171671/117440218-4ae26800-af23-11eb-87b4-1d9026fc953f.png"/> -</p> - -# mpvacious - -[](https://aur.archlinux.org/packages/mpv-mpvacious/) -[](https://tatsumoto-ren.github.io/blog/join-our-community.html) - -[](https://tatsumoto.neocities.org/blog/donating-to-tatsumoto.html) - -mpvacious is your semi-automatic subs2srs for mpv. -It supports multiple workflows and allows you to quickly create Anki cards -while watching your favorite TV show. -**[Video demonstration](https://redirect.invidious.io/watch?v=vU85ramvyo4)**. - -## Requirements - -<table> -<tr> - <th><a href="https://www.gnu.org/gnu/about-gnu.html">GNU/Linux</a></th> - <th><a href="https://www.gnu.org/proprietary/malware-microsoft.en.html">Windows 10+</a></th> - <th><a href="https://www.gnu.org/proprietary/malware-apple.en.html">macOS</a></th> - <th>Comments</th> -</tr> -<tr> - <td><a href="https://wiki.archlinux.org/index.php/Mpv">mpv</a></td> - <td><a href="https://sourceforge.net/projects/mpv-player-windows/files">mpv</a></td> - <td><a href="https://mpv.io/installation/">mpv</a></td> - <td>v0.32.0 or newer.</td> -</tr> -<tr> - <td><a href="https://wiki.archlinux.org/index.php/Anki">Anki</a></td> - <td colspan="2" align="center"><a href="https://apps.ankiweb.net/">Anki</a></td> - <td></td> -</tr> -<tr> - <td colspan="3" align="center"><a href="https://ankiweb.net/shared/info/2055492159">AnkiConnect</a></td> - <td>Install from AnkiWeb.</td> -</tr> -<tr> - <td><a href="https://www.archlinux.org/packages/core/x86_64/curl/">curl</a></td> - <td colspan="2" align="center"><a href="https://curl.haxx.se/">curl</a></td> - <td>Installed by default on all platforms except Windows 7.</td> -</tr> -<tr> - <td><a href="https://www.archlinux.org/packages/extra/x86_64/xclip/">xclip</a> or <a href="https://archlinux.org/packages/extra/x86_64/wl-clipboard">wl-copy</a></td> - <td></td> - <td>pbcopy</td> - <td>To copy subtitle text to clipboard.</td> -</tr> -</table> - -Install all dependencies at once (on [Arch-based](https://www.parabola.nu/) -[distros](https://www.gnu.org/distros/free-distros.en.html)): - -``` -$ sudo pacman -Syu mpv anki curl xclip --needed -``` - -## Prerequisites - -* A guide on how to set up Anki can be found [on our site](https://tatsumoto.neocities.org/blog/setting-up-anki.html). -* If you're on a [Windows](https://www.fsf.org/windows) or a [Windows-like](https://reactos.org/) machine, - a mpv build by `shinchiro` is recommended. -* **macOS** users are advised to use [homebrew](https://brew.sh/) or manually add `mpv` to `PATH`. -* Note that it is not recommended to use FlatPak or similar containers. - If you still want to, [read this](howto/flatpak.md). -* Make sure that your build of mpv supports encoding of audio and images. - This shell command can be used to test it. - - ``` - $ mpv 'test_video.mkv' --loop-file=no --frames=1 -o='test_image.jpg' - ``` - - If the command fails, find a compatible build on the [mpv website](https://mpv.io/installation/) - or instead install FFmpeg and [enable FFmpeg support](#configuration) in `mpvacious`'s config file. -* Most problems with adding audio or images to Anki cards can be fixed - by installing FFmpeg and enabling it settings. - -## Installation - -There are multiple ways you can install `mpvacious`. -I recommend installing with `git` so that you can easily update on demand. - -`mpvacious` is a user-script for mpv, -so it has to be installed in the directory `mpv` reads its user-scripts from. - -| OS | Location | -|-----------|--------------------------------------------------| -| GNU/Linux | `~/.config/mpv/scripts/` | -| Windows | `C:/Users/Username/AppData/Roaming/mpv/scripts/` | - -Windows is not recommended, -but we acknowledge that some people haven't switched to GNU/Linux yet. - -### Using git - -Clone the repo to the `scripts` directory. - -``` -mkdir -p ~/.config/mpv/scripts/ -git clone 'https://github.com/Ajatt-Tools/mpvacious.git' ~/.config/mpv/scripts/subs2srs -``` - -To update, run the following command. - -``` -cd ~/.config/mpv/scripts/subs2srs && git pull -``` - -### From the AUR - -`mpvacious` can be installed with the [mpv-mpvacious](https://aur.archlinux.org/packages/mpv-mpvacious/) package. - -### Manually - -This way is not recommended because it's easy to make a mistake during the process -and end up with a broken install. - -Download -[the repository](https://github.com/Ajatt-Tools/mpvacious/archive/refs/heads/master.zip) -or -[the latest release](https://github.com/Ajatt-Tools/mpvacious/releases) -and extract the folder containing -[subs2srs.lua](https://raw.githubusercontent.com/Ajatt-Tools/mpvacious/master/subs2srs.lua) -to your [mpv scripts](https://github.com/mpv-player/mpv/wiki/User-Scripts) directory. - -<details> - -<summary>Expected directory tree</summary> - -``` -~/.config/mpv/scripts -|-- other script 1 -|-- other script 2 -|-- subs2srs -| |-- main.lua -| |-- subs2srs.lua -| `-- other files -`-- other script 3 -``` - -</details> - -<details> - -<summary>A note for mpv v0.32 and older</summary> - -Older versions of `mpv` don't know how to handle user-scripts in subdirectories. -You need to tell mpv where to look for `mpvacious`. - -Open or create `~/.config/mpv/scripts/modules.lua` and add these lines: -``` -local mpv_scripts_dir_path = os.getenv("HOME") .. "/.config/mpv/scripts/" -package.path = package.path .. ';' .. os.getenv("HOME") .. '/.config/mpv/scripts/subs2srs/?.lua' -function load(relative_path) dofile(mpv_scripts_dir_path .. relative_path) end -load("subs2srs/subs2srs.lua") -``` - -</details> - -**Note:** in [Celluloid](https://www.archlinux.org/packages/community/x86_64/celluloid/) -user scripts are installed in `/.config/celluloid/scripts/`. -When following the instructions above, replace `.config/mpv` with `.config/celluloid` -and optionally `subs2srs` with the name of the folder mpvacious is cloned into. - -## Configuration - -The config file should be created by the user, if needed. - -| OS | Config location | -|--------------------|-------------------------------------------------------------------| -| GNU/Linux | `~/.config/mpv/script-opts/subs2srs.conf` | -| Windows | `C:/Users/Username/AppData/Roaming/mpv/script-opts/subs2srs.conf` | -| Windows (portable) | `mpv.exeフォルダ/portable_config/script-opts/subs2srs.conf` | - -If a parameter is not specified -in the config file, the default value will be used. -mpv doesn't tolerate spaces before and after `=`. - -<p align="center"> - <a href="https://github.com/Ajatt-Tools/mpvacious/blob/master/.github/RELEASE/subs2srs.conf">Example configuration file</a> -</p> - -If the first field is empty, it will be set contain the string `[empty]`. -Otherwise, Anki won't allow mpvacious to add new notes. -This won't happen if the sentence field is first in the note type settings. - -**Tip**: Try [our official note type](https://ankiweb.net/shared/info/1557722832) -if you don't want to configure note fields yourself. -Alternatively, we have a collection of user-created note types, which you can browse -[here](https://github.com/Ajatt-Tools/AnkiNoteTypes). - -If you are having problems playing media files on older mobile devices, -set `audio_format` to `mp3` and/or `snapshot_format` to `jpg`. -Otherwise, I recommend sticking with `opus` for audio, -and `avif` or `webp` for images, -as they greatly reduce the size of the generated files. - -If you still use AnkiMobile (the [proprietary](https://www.gnu.org/proprietary/) Anki app), -set `opus_container` to `m4a` or `webm`. I'll allow iOS to play Opus files, while still maintaining -compatibility with non-Apple devices. For really old iOS devices, set `opus_container` to -[`caf`](https://en.wikipedia.org/wiki/Core_Audio_Format). CAF plays only on Anki Desktop, -AnkiWeb in Safari and AnkiMobile, and is really not recommended. (Please note that -[Lockdown Mode](https://support.apple.com/en-us/105120) completely disables Opus and AVIF support, -though you may try to add an exception for AnkiMobile.) - -If no matter what mpvacious fails to create audio clips and/or snapshots, -change `use_ffmpeg` to `yes`. -By using ffmpeg instead of the encoder built in mpv you can work around most encoder issues. -You need to have ffmpeg installed for this to work. - -### Key bindings - -The user may change some global key bindings, though this step is not necessary. -See [Usage](#usage) for the explanation of what they do. - -| OS | Config location | -|-----------|----------------------------------------------------| -| GNU/Linux | `~/.config/mpv/input.conf` | -| Windows | `C:/Users/Username/AppData/Roaming/mpv/input.conf` | - -Default bindings: - -``` -a script-binding mpvacious-menu-open - -Ctrl+g script-binding mpvacious-animated-snapshot-toggle - -Ctrl+n script-binding mpvacious-export-note - -Ctrl+m script-binding mpvacious-update-last-note -Ctrl+M script-binding mpvacious-overwrite-last-note - -g script-binding mpvacious-quick-card-menu-open -Alt+g script-binding mpvacious-quick-card-sel-menu-open - -Ctrl+c script-binding mpvacious-copy-primary-sub-to-clipboard -Ctrl+C script-binding mpvacious-copy-secondary-sub-to-clipboard -Ctrl+t script-binding mpvacious-autocopy-toggle - -H script-binding mpvacious-sub-seek-back -L script-binding mpvacious-sub-seek-forward - -Alt+h script-binding mpvacious-sub-seek-back-pause -Alt+l script-binding mpvacious-sub-seek-forward-pause - -Ctrl+h script-binding mpvacious-sub-rewind -Ctrl+H script-binding mpvacious-sub-replay -Ctrl+L script-binding mpvacious-sub-play-up-to-next - -Ctrl+v script-binding mpvacious-secondary-sid-toggle -Ctrl+k script-binding mpvacious-secondary-sid-prev -Ctrl+j script-binding mpvacious-secondary-sid-next -``` - -**Note:** A capital letter means that you need to press Shift in order to activate the corresponding binding. -For example, <kbd>Ctrl+M</kbd> actually means <kbd>Ctrl+Shift+m</kbd>. -mpv accepts both variants in `input.conf`. - -## Usage - -* [Create a card](howto/create_card.md) -* [Quick card creation](howto/create_quick_card.md) -* [Open the "Add" dialog](howto/add_dialog.md) -* [Usage with Rikaitan](howto/yomichan.md) -* [Usage with GoldenDict](howto/goldendict.md) - -### Global bindings - -**Menu:** - -* <kbd>a</kbd> - Open `advanced menu`. - -**Enable\Disable animation:** - -* <kbd>Ctrl+g</kbd> - If animation is enabled, animated snapshots will be generated instead of static images. - Animated snapshot are like GIFs (just in a different format) - and will capture the video from the start to the end times selected. - -**Make a card:** - -* <kbd>Ctrl+n</kbd> - Export a card with the currently visible subtitle line on the front. -Use this when your subs are well-timed, -and the target sentence doesn't span multiple subs. - -**Quick card creation:** - -* <kbd>g</kbd> - Quick card creation menu. -* <kbd>Alt+g</kbd> - Quick card creation, card selection menu. - -**Update the last card:** - -* <kbd>Ctrl+m</kbd> - Append to the media fields of the newly added Anki card. -* <kbd>Ctrl+Shift+m</kbd> - Overwrite media fields of the newly added Anki card. - -**Clipboard:** - -* <kbd>Ctrl+c</kbd> - Copy current subtitle string to the system clipboard. -* <kbd>Ctrl+t</kbd> - Toggle automatic copying of subtitles to the clipboard. - -**Seeking:** - -* <kbd>Shift+h</kbd> and <kbd>Shift+l</kbd> - Seek to the previous or the next subtitle. -* <kbd>Alt+h</kbd> and <kbd>Alt+l</kbd> - Seek to the previous, or the next subtitle, and pause. -* <kbd>Ctrl+h</kbd> - Seek to the start of the currently visible subtitle. Use it if you missed something. -* <kbd>Ctrl+Shift+h</kbd> - Replay current subtitle line, and pause. -* <kbd>Ctrl+Shift+l</kbd> - Play until the end of the next subtitle, and pause. Useful for beginners who need - to look up words in each and every dialogue line. - -**Secondary subtitles:** - -* <kbd>Ctrl+v</kbd> - Toggle visibility. -* <kbd>Ctrl+k</kbd> - Switch to the previous subtitle if it's not already selected. -* <kbd>Ctrl+j</kbd> - Switch to the next subtitle if it's not already selected. - -### Menu options - -Advanced menu has the following options: - -* <kbd>f</kbd> - Increment number of cards to update. Only affects note updating, including quick card creation. The number of cards to update is reset to 1 upon updating a note. -* <kbd>shift+f</kbd> - Decrement number of cards to update. - -* <kbd>c</kbd> - Interactive subtitle selection. - The range of the currently displayed subtitle line is selected. The selection then grows both ways based on the following displayed lines. - It does nothing if there are no subs on screen. - -* <kbd>shift+s</kbd> - Set the start time to the current sub. The selection then grows forward based on the following displayed lines. - The default selection spans across the range of the currently displayed subtitle line. -* <kbd>shift+e</kbd> - Set the end time to the current sub. The selection then grows backward based on the following displayed lines. - The default selection spans across the range of the currently displayed subtitle line. - -Then seek with <kbd>Shift+h</kbd> and <kbd>Shift+l</kbd> to the previous/next line that you want to add. -Press <kbd>n</kbd> to make the card. - -* <kbd>r</kbd> - Forget all previously saved timings and associated dialogs. - -* <kbd>z</kbd> and <kbd>Shift+z</kbd> - Adjust subtitle delay. - -If above fails, you have to manually set timings. -* <kbd>s</kbd> - Set the start time. The selection then grows forward based on the following displayed lines. -The default selection spans across the selected start point and the end of the subtitle line. -* <kbd>e</kbd> - Set the end time. The selection then grows backward based on the following displayed lines. -The default selection spans across the selected end point and the start of the subtitle line. - -Then, as earlier, press <kbd>n</kbd> to make the card. - -**Tip**: change playback speed by pressing <kbd>[</kbd> and <kbd>]</kbd> -to precisely mark start and end of the phrase. - -### My subtitles are not in sync - -If subs are badly timed, first, you could try to re-time them. -Read [Retiming subtitles](https://tatsumoto.neocities.org/blog/retiming-subtitles). -Or shift timings using key bindings provided by mpv (usually <kbd>z</kbd> and <kbd>Shift+z</kbd>). - -### Example sentence card - -With the addon you can make cards like this in just a few seconds. - - - -### Audio cards - -It is possible to make a card with just audio, and a picture -when subtitles for the show you are watching aren't available, for example. -mpv by default allows you to do a `1` second exact seek by pressing <kbd>Shift+LEFT</kbd> and <kbd>Shift+RIGHT</kbd>. -Open the mpvacious menu by pressing <kbd>a</kbd>, seek to the position you need, and set the timings. -Then press <kbd>g</kbd> to invoke the `Add Cards` dialog. -Here's a [video demonstration](https://redirect.invidious.io/watch?v=BXhyckdHPGE). - -If the show is hard-subbed, you can use -[transformers-ocr](https://tatsumoto.neocities.org/blog/mining-from-manga.html) -to recognize and add text to the card. - -### Secondary subtitles - -If you want to add a translation to your cards, and you have the subtitles in that language, -you can add them as secondary subtitles if you run `mpv` with `--secondary-sid=<sid>` parameter, -`sid` being the track identifier for the subtitle. - -You also need to specify `secondary_field` in the [config file](#Configuration) -if it is different from the default. - -If you want to load secondary subtitles **automatically**, don't modify the run parameters -and instead set the desired languages in the config file (`secondary_sub_lang` option). - -Secondary subtitles will be visible when hovering over the top part of the `mpv` window. - -https://user-images.githubusercontent.com/69171671/188492261-909ba3e8-b82c-493f-88cf-0ec953dfcfe1.mp4 - -By pressing <kbd>Ctrl</kbd>+<kbd>v</kbd> you can control secondary sid visibility without using the mouse. - -### Other tools - -If you don't like the default Rikaitan Search tool, try: - -* Clipboard Inserter browser add-on -([chrome](https://chrome.google.com/webstore/detail/clipboard-inserter/deahejllghicakhplliloeheabddjajm)) -([firefox](https://addons.mozilla.org/ja/firefox/addon/clipboard-inserter/)) -* A html page ([1](https://pastebin.com/zDY6s3NK)) ([2](https://pastebin.com/hZ4sawL4)) -to paste the contents of your clipboard to - -You can use any html page as long as it has \<body\>\</body\> in it. - -### Additional mpv key bindings - -I recommend adding these lines to your [input.conf](#key-bindings) for smoother experience. -``` -# vim-like seeking -l seek 5 -h seek -5 -j seek -60 -k seek 60 - -# Cycle between subtitle files -K cycle sub -J cycle sub down - -# Add/subtract 50 ms delay from subs -Z add sub-delay +0.05 -z add sub-delay -0.05 - -# Adjust timing to previous/next subtitle -X sub-step 1 -x sub-step -1 -``` - -## Profiles - -Mpvacious supports config profiles. -To make use of them, create a new config file called `subs2srs_profiles.conf` -in the same folder as your [subs2srs.conf](#Configuration). -Inside the file, define available profile names (without `.conf`) and the name of the active profile: - -``` -profiles=subs2srs,english,german -active=subs2srs -``` - -In the example above, I have three profiles. -The first one is the default, -the second one is for learning English, -the third one is for learning German. - -Then in the same folder create config files for each of the defined profiles. -For example, below is the contents of my `english.conf` file: - -``` -deck_name=English sentence mining -model_name=General -sentence_field=Question -audio_field=Audio -image_field=Extra -``` - -You don't have to redefine all settings in the new profile. -Specify only the ones you want to be different from the default. - -To cycle profiles, open the advanced menu by pressing <kbd>a</kbd> and then press <kbd>p</kbd>. -At any time you can see what profile is active in the menu's status bar. - -## Hacking - -If you want to modify this script -or make an entirely new one from scratch, -these links may help. - -* https://mpv.io/manual/master/#lua-scripting -* https://github.com/mpv-player/mpv/blob/master/player/lua/defaults.lua -* https://github.com/SenneH/mpv2anki -* https://github.com/kelciour/mpv-scripts/blob/master/subs2srs.lua -* https://pastebin.com/M2gBksHT -* https://pastebin.com/NBudhMUk -* https://pastebin.com/W5YV1A9q -* https://github.com/ayuryshev/subs2srs -* https://github.com/erjiang/subs2srs diff --git a/config/mpv/scripts/subs2srsa/ankiconnect.lua b/config/mpv/scripts/subs2srsa/ankiconnect.lua deleted file mode 100644 index f9c87d8..0000000 --- a/config/mpv/scripts/subs2srsa/ankiconnect.lua +++ /dev/null @@ -1,241 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -AnkiConnect requests -]] - -local utils = require('mp.utils') -local msg = require('mp.msg') -local h = require('helpers') -local self = {} - -self.execute = function(request, completion_fn) - -- utils.format_json returns a string - -- On error, request_json will contain "null", not nil. - local request_json, error = utils.format_json(request) - - if error ~= nil or request_json == "null" then - return completion_fn and completion_fn() - else - return self.platform.curl_request(self.config.ankiconnect_url, request_json, completion_fn) - end -end - -self.parse_result = function(curl_output) - -- there are two values that we actually care about: result and error - -- but we need to crawl inside to get them. - - if curl_output == nil then - return nil, "Failed to format json or no args passed" - end - - if curl_output.status ~= 0 then - return nil, "Ankiconnect isn't running" - end - - local stdout_json = utils.parse_json(curl_output.stdout) - - if stdout_json == nil then - return nil, "Fatal error from Ankiconnect" - end - - if stdout_json.error ~= nil then - return nil, tostring(stdout_json.error) - end - - return stdout_json.result, nil -end - -self.get_media_dir_path = function() - -- Ask AnkiConnect where to store media files. - -- If AnkiConnect isn't running, returns nil. - - local ret = self.execute({ - action = "getMediaDirPath", - version = 6, - }) - local dir_path, error = self.parse_result(ret) - if not error then - return dir_path - else - msg.error(string.format("Couldn't retrieve path to collection.media folder: %s", error)) - return nil - end -end - -self.create_deck = function(deck_name) - local args = { - action = "changeDeck", - version = 6, - params = { - cards = {}, - deck = deck_name - } - } - local result_notify = function(_, result, _) - local _, error = self.parse_result(result) - if not error then - msg.info(string.format("Deck %s: check completed.", deck_name)) - else - msg.warn(string.format("Deck %s: check failed. Reason: %s.", deck_name, error)) - end - end - self.execute(args, result_notify) -end - -self.add_note = function(note_fields, tag, gui) - local action = gui and 'guiAddCards' or 'addNote' - local args = { - action = action, - version = 6, - params = { - note = { - deckName = self.config.deck_name, - modelName = self.config.model_name, - fields = note_fields, - options = { - allowDuplicate = self.config.allow_duplicates, - duplicateScope = "deck", - }, - tags = h.is_empty(tag) and {} or { tag, }, - } - } - } - local result_notify = function(_, result, _) - local note_id, error = self.parse_result(result) - if not error then - h.notify(string.format("Note added. ID = %s.", note_id)) - else - h.notify(string.format("Error: %s.", error), "error", 2) - end - end - self.execute(args, result_notify) -end - -self.get_last_note_ids = function(n_cards) - local ret = self.execute { - action = "findNotes", - version = 6, - params = { - query = "added:1" -- find all notes added today - } - } - - local note_ids, _ = self.parse_result(ret) - - if not h.is_empty(note_ids) then - return h.get_last_n_added_notes(note_ids, n_cards) - else - return {} - end -end - -self.get_note_fields = function(note_id) - local ret = self.execute { - action = "notesInfo", - version = 6, - params = { - notes = { note_id } - } - } - - local result, error = self.parse_result(ret) - - if error == nil then - result = result[1].fields - for key, value in pairs(result) do - result[key] = value.value - end - return result - else - return nil - end -end - -self.get_first_field = function(model_name) - local ret = self.execute { - action = "findModelsByName", - version = 6, - params = { - modelNames = { model_name } - } - } - - local result, error = self.parse_result(ret) - - if error == nil then - for _, field in pairs(result[1].flds) do - if field.ord == 0 then - return field.name - end - end - else - msg.error(string.format("Couldn't retrieve the first field's name of note type %s: %s", model_name, error)) - return nil - end -end - -self.gui_browse = function(query) - if not self.config.disable_gui_browse then - self.execute { - action = 'guiBrowse', - version = 6, - params = { - query = query - } - } - end -end - -self.add_tag = function(note_id, tag) - if not h.is_empty(tag) then - self.execute { - action = 'addTags', - version = 6, - params = { - notes = { note_id }, - tags = tag - } - } - end -end - -self.append_media = function(note_id, fields, create_media_fn, tag) - -- AnkiConnect will fail to update the note if it's selected in the Anki Browser. - -- https://github.com/FooSoft/anki-connect/issues/82 - -- Switch focus from the current note to avoid it. - self.gui_browse("nid:1") -- impossible nid - - local args = { - action = "updateNoteFields", - version = 6, - params = { - note = { - id = note_id, - fields = fields, - } - } - } - - local on_finish = function(_, result, _) - local _, error = self.parse_result(result) - if not error then - create_media_fn() - self.add_tag(note_id, tag) - self.gui_browse(string.format("nid:%s", note_id)) -- select the updated note in the card browser - h.notify(string.format("Note #%s updated.", note_id)) - else - h.notify(string.format("Error: %s.", error), "error", 2) - end - end - - self.execute(args, on_finish) -end - -self.init = function(config, platform) - self.config = config - self.platform = platform -end - -return self diff --git a/config/mpv/scripts/subs2srsa/cfg_mgr.lua b/config/mpv/scripts/subs2srsa/cfg_mgr.lua deleted file mode 100644 index 076c0d9..0000000 --- a/config/mpv/scripts/subs2srsa/cfg_mgr.lua +++ /dev/null @@ -1,240 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Config management, validation, loading. -]] - -local mp = require('mp') -local mpopt = require('mp.options') -local msg = require('mp.msg') -local h = require('helpers') -local utils = require('mp.utils') - -local min_side_px = 42 -local max_side_px = 640 -local default_height_px = 200 - --- This constant should be used in place of width and/or height in the config file. --- It tells the encoder to preserve aspect ratio when downscaling snapshots. --- The user almost always wants to set either width or height to this value. --- Note: If set to -1, encoding will fail with the "height/width not divisible by 2" error. -local preserve_aspect_ratio = -2 - -local self = { - config = nil, - profiles = nil, - initial_config = {} -} - -local default_profile_filename = 'subs2srs' -local profiles_filename = 'subs2srs_profiles' - -local function set_file_extension_for_opus() - -- Default to OGG, then change if an extension is supported. - -- https://en.wikipedia.org/wiki/Core_Audio_Format - self.config.audio_extension = '.ogg' - for _, extension in ipairs({ 'opus', 'm4a', 'webm', 'caf' }) do - if extension == self.config.opus_container then - self.config.audio_extension = '.' .. self.config.opus_container - break - end - end -end - -local function set_audio_format() - if self.config.audio_format == 'opus' then - -- https://opus-codec.org/ - self.config.audio_codec = 'libopus' - set_file_extension_for_opus() - else - self.config.audio_codec = 'libmp3lame' - self.config.audio_extension = '.mp3' - end -end - -local function set_video_format() - if self.config.snapshot_format == 'avif' then - self.config.snapshot_extension = '.avif' - self.config.snapshot_codec = 'libaom-av1' - elseif self.config.snapshot_format == 'webp' then - self.config.snapshot_extension = '.webp' - self.config.snapshot_codec = 'libwebp' - else - self.config.snapshot_extension = '.jpg' - self.config.snapshot_codec = 'mjpeg' - end - - -- Animated webp images can only have .webp extension. - -- The user has no choice on this. Same logic for avif. - if self.config.animated_snapshot_format == 'avif' then - self.config.animated_snapshot_extension = '.avif' - self.config.animated_snapshot_codec = 'libaom-av1' - else - self.config.animated_snapshot_extension = '.webp' - self.config.animated_snapshot_codec = 'libwebp' - end -end - -local function ensure_in_range(dimension) - self.config[dimension] = self.config[dimension] < min_side_px and preserve_aspect_ratio or self.config[dimension] - self.config[dimension] = self.config[dimension] > max_side_px and max_side_px or self.config[dimension] -end - -local function conditionally_set_defaults(width, height, quality) - if self.config[width] < 1 and self.config[height] < 1 then - self.config[width] = preserve_aspect_ratio - self.config[height] = default_height_px - end - if self.config[quality] < 0 or self.config[quality] > 100 then - self.config[quality] = 15 - end -end - -local function check_image_settings() - ensure_in_range('snapshot_width') - ensure_in_range('snapshot_height') - conditionally_set_defaults('snapshot_width', 'snapshot_height', 'snapshot_quality') -end - -local function ensure_correct_fps() - if self.config.animated_snapshot_fps == nil or self.config.animated_snapshot_fps <= 0 or self.config.animated_snapshot_fps > 30 then - self.config.animated_snapshot_fps = 10 - end -end - -local function check_animated_snapshot_settings() - ensure_in_range('animated_snapshot_width') - ensure_in_range('animated_snapshot_height') - conditionally_set_defaults('animated_snapshot_width', 'animated_snapshot_height', 'animated_snapshot_quality') - ensure_correct_fps() -end - -local function validate_config() - set_audio_format() - set_video_format() - check_image_settings() - check_animated_snapshot_settings() -end - -local function remember_initial_config() - if h.is_empty(self.initial_config) then - for key, value in pairs(self.config) do - self.initial_config[key] = value - end - else - msg.fatal("Ignoring. Initial config has been read already.") - end -end - -local function restore_initial_config() - for key, value in pairs(self.initial_config) do - self.config[key] = value - end -end - -local function read_profile_list() - mpopt.read_options(self.profiles, profiles_filename) - msg.info("Read profile list. Defined profiles: " .. self.profiles.profiles) -end - -local function read_profile(profile_name) - mpopt.read_options(self.config, profile_name) - msg.info("Read config file: " .. profile_name) -end - -local function read_default_config() - read_profile(default_profile_filename) -end - -local function reload_from_disk() - --- Loads default config file (subs2srs.conf), then overwrites it with current profile. - if not h.is_empty(self.config) and not h.is_empty(self.profiles) then - restore_initial_config() - read_default_config() - if self.profiles.active ~= default_profile_filename then - read_profile(self.profiles.active) - end - validate_config() - else - msg.fatal("Attempt to load config when init hasn't been done.") - end -end - -local function next_profile() - local first, next, new - for profile in string.gmatch(self.profiles.profiles, '[^,]+') do - if not first then - first = profile - end - if profile == self.profiles.active then - next = true - elseif next then - next = false - new = profile - end - end - if next == true or not new then - new = first - end - self.profiles.active = new - reload_from_disk() -end - -local function create_config_file() - local name = default_profile_filename - -- ~/.config/mpv/scripts/ and the mpvacious dir - local parent, child = utils.split_path(mp.get_script_directory()) - -- ~/.config/mpv/ and "scripts" - parent, child = utils.split_path(parent:gsub("/$", "")) - -- ~/.config/mpv/script-opts/subs2srs.conf - local config_filepath = utils.join_path(utils.join_path(parent, "script-opts"), string.format('%s.conf', name)) - local example_config_filepath = utils.join_path(mp.get_script_directory(), ".github/RELEASE/subs2srs.conf") - - local file_info = utils.file_info(config_filepath) - if file_info and file_info.is_file then - print("config already exists") - return - end - - local handle = io.open(example_config_filepath, 'r') - if handle == nil then - return - end - - local content = handle:read("*a") - handle:close() - - handle = io.open(config_filepath, 'w') - if handle == nil then - h.notify(string.format("Couldn't open %s.", config_filepath), "error", 4) - return - end - - handle:write(string.format("# Written by %s on %s.\n", name, os.date())) - handle:write(content) - handle:close() - h.notify("Settings saved.", "info", 2) -end - -local function init(config_table, profiles_table) - create_config_file() - self.config, self.profiles = config_table, profiles_table - -- 'subs2srs' is the main profile, it is always loaded. 'active profile' overrides it afterwards. - -- initial state is saved to another table to maintain consistency when cycling through incomplete profiles. - read_profile_list() - read_default_config() - remember_initial_config() - if self.profiles.active ~= default_profile_filename then - read_profile(self.profiles.active) - end - validate_config() -end - -return { - reload_from_disk = reload_from_disk, - init = init, - next_profile = next_profile, - default_height_px = default_height_px, - preserve_aspect_ratio = preserve_aspect_ratio, -} diff --git a/config/mpv/scripts/subs2srsa/encoder/codec_support.lua b/config/mpv/scripts/subs2srsa/encoder/codec_support.lua deleted file mode 100644 index 26cd91a..0000000 --- a/config/mpv/scripts/subs2srsa/encoder/codec_support.lua +++ /dev/null @@ -1,40 +0,0 @@ ---[[ -Copyright: Ajatt-Tools and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Check what codecs are supported by mpv. -If a desired codec is not supported, set the "use_ffmpeg" config option to "yes". -]] - -local mp = require('mp') -local h = require('helpers') - -local ovc_help = h.subprocess { 'mpv', '--ovc=help' } -local oac_help = h.subprocess { 'mpv', '--oac=help' } - -local function is_audio_supported(codec) - return oac_help.status == 0 and oac_help.stdout:find('--oac=' .. codec, 1, true) ~= nil -end - -local function is_image_supported(codec) - return ovc_help.status == 0 and ovc_help.stdout:find('--ovc=' .. codec, 1, true) ~= nil -end - -local inspection_result = { - snapshot = { - ['libaom-av1'] = is_image_supported('libaom-av1'), - libwebp = is_image_supported('libwebp'), - mjpeg = is_image_supported('mjpeg'), - }, - audio = { - libmp3lame = is_audio_supported('libmp3lame'), - libopus = is_audio_supported('libopus'), - }, -} -for type, codecs in pairs(inspection_result) do - for codec, supported in pairs(codecs) do - mp.msg.info(string.format("mpv supports %s codec %s: %s", type, codec, tostring(supported))) - end -end - -return inspection_result diff --git a/config/mpv/scripts/subs2srsa/encoder/encoder.lua b/config/mpv/scripts/subs2srsa/encoder/encoder.lua deleted file mode 100644 index b05c3db..0000000 --- a/config/mpv/scripts/subs2srsa/encoder/encoder.lua +++ /dev/null @@ -1,729 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Encoder creates audio clips and snapshots, both animated and static. -]] - -local mp = require('mp') -local utils = require('mp.utils') -local h = require('helpers') -local filename_factory = require('utils.filename_factory') -local msg = require('mp.msg') - ---Contains the state of the module -local self = { - snapshot = {}, - audio = {}, - config = nil, - store_fn = nil, - platform = nil, - encoder = nil, - output_dir_path = nil, -} - ------------------------------------------------------------- --- utility functions - -local function pad_timings(padding, start_time, end_time) - local video_duration = mp.get_property_number('duration') - start_time = start_time - padding - end_time = end_time + padding - - if start_time < 0 then - start_time = 0 - end - - if end_time > video_duration then - end_time = video_duration - end - - return start_time, end_time -end - -local function alt_path_dirs() - return { - '/opt/homebrew/bin', - '/usr/local/bin', - utils.join_path(os.getenv("HOME") or "~", '.local/bin'), - } -end - -local function find_exec(name) - local path, info - for _, alt_dir in pairs(alt_path_dirs()) do - path = utils.join_path(alt_dir, name) - info = utils.file_info(path) - if info and info.is_file then - return path - end - end - return name -end - -local function toms(timestamp) - --- Trim timestamp down to milliseconds. - return string.format("%.3f", timestamp) -end - -local function fit_quality_percentage_to_range(quality, worst_val, best_val) - local scaled = worst_val + (best_val - worst_val) * quality / 100 - -- Round to the nearest integer that's better in quality. - if worst_val > best_val then - return math.floor(scaled) - end - return math.ceil(scaled) -end - -local function quality_to_crf_avif(quality_value) - -- Quality is from 0 to 100. For avif images CRF is from 0 to 63 and reversed. - local worst_avif_crf = 63 - local best_avif_crf = 0 - return fit_quality_percentage_to_range(quality_value, worst_avif_crf, best_avif_crf) -end - -local function quality_to_jpeg_qscale(quality_value) - local worst_jpeg_quality = 31 - local best_jpeg_quality = 2 - return fit_quality_percentage_to_range(quality_value, worst_jpeg_quality, best_jpeg_quality) -end - ------------------------------------------------------------- --- ffmpeg encoder - -local ffmpeg = {} - -ffmpeg.exec = find_exec("ffmpeg") - -ffmpeg.prepend = function(...) - return { - ffmpeg.exec, "-hide_banner", "-nostdin", "-y", "-loglevel", "quiet", "-sn", - ..., - } -end - -local function make_scale_filter(algorithm, width, height) - -- algorithm is either "sinc" or "lanczos" - -- Static image scaling uses "sinc", which is the best downscaling algorithm: https://stackoverflow.com/a/6171860 - -- Animated images use Lanczos, which is faster. - return string.format( - "scale='min(%d,iw)':'min(%d,ih)':flags=%s+accurate_rnd", - width, height, algorithm - ) -end - -local function static_scale_filter() - return make_scale_filter('sinc', self.config.snapshot_width, self.config.snapshot_height) -end - -local function animated_scale_filter() - return make_scale_filter( - 'lanczos', - self.config.animated_snapshot_width, - self.config.animated_snapshot_height - ) -end - -ffmpeg.make_static_snapshot_args = function(source_path, output_path, timestamp) - local encoder_args - if self.config.snapshot_format == 'avif' then - encoder_args = { - '-c:v', 'libaom-av1', - -- cpu-used < 6 can take a lot of time to encode. - '-cpu-used', '6', - -- Avif quality can be controlled with crf. - '-crf', tostring(quality_to_crf_avif(self.config.snapshot_quality)), - '-still-picture', '1', - } - elseif self.config.snapshot_format == 'webp' then - encoder_args = { - '-c:v', 'libwebp', - '-compression_level', '6', - '-quality', tostring(self.config.snapshot_quality), - } - else - encoder_args = { - '-c:v', 'mjpeg', - '-q:v', tostring(quality_to_jpeg_qscale(self.config.snapshot_quality)), - } - end - - local args = ffmpeg.prepend( - '-an', - '-ss', toms(timestamp), - '-i', source_path, - '-map_metadata', '-1', - '-vf', static_scale_filter(), - '-frames:v', '1', - h.unpack(encoder_args) - ) - table.insert(args, output_path) - return args -end - -ffmpeg.make_animated_snapshot_args = function(source_path, output_path, start_timestamp, end_timestamp) - local encoder_args - if self.config.animated_snapshot_format == 'avif' then - encoder_args = { - '-c:v', 'libaom-av1', - -- cpu-used < 6 can take a lot of time to encode. - '-cpu-used', '6', - -- Avif quality can be controlled with crf. - '-crf', tostring(quality_to_crf_avif(self.config.animated_snapshot_quality)), - } - else - -- Documentation: https://www.ffmpeg.org/ffmpeg-all.html#libwebp - encoder_args = { - '-c:v', 'libwebp', - '-compression_level', '6', - '-quality', tostring(self.config.animated_snapshot_quality), - } - end - - local args = ffmpeg.prepend( - '-an', - '-ss', toms(start_timestamp), - '-to', toms(end_timestamp), - '-i', source_path, - '-map_metadata', '-1', - '-loop', '0', - '-vf', string.format( - 'fps=%d,%s', - self.config.animated_snapshot_fps, - animated_scale_filter() - ), - h.unpack(encoder_args) - ) - table.insert(args, output_path) - return args -end - -local function make_loudnorm_targets() - return string.format( - 'loudnorm=I=%s:LRA=%s:TP=%s:dual_mono=true', - self.config.loudnorm_target, - self.config.loudnorm_range, - self.config.loudnorm_peak - ) -end - -local function parse_loudnorm(loudnorm_targets, json_extractor, loudnorm_consumer) - local function warn() - msg.warn('Failed to measure loudnorm stats, falling back on dynamic loudnorm.') - end - - return function(success, result) - local json - if success and result.status == 0 then - json = json_extractor(result.stdout, result.stderr) - end - - if json == nil then - warn() - loudnorm_consumer(loudnorm_targets) - return - end - - local loudnorm_args = { loudnorm_targets } - local function add_arg(name, val) - -- loudnorm sometimes fails to gather stats for extremely short inputs. - -- Simply omit the stat to fall back on dynamic loudnorm. - if val ~= '-inf' and val ~= 'inf' then - table.insert(loudnorm_args, string.format('%s=%s', name, val)) - else - warn() - end - end - - local stats = utils.parse_json(json) - add_arg('measured_I', stats.input_i) - add_arg('measured_LRA', stats.input_lra) - add_arg('measured_TP', stats.input_tp) - add_arg('measured_thresh', stats.input_thresh) - add_arg('offset', stats.target_offset) - - loudnorm_consumer(table.concat(loudnorm_args, ':')) - end -end - -local function add_filter(filters, filter) - if #filters == 0 then - filters = filter - else - filters = string.format('%s,%s', filters, filter) - end -end - -local function separate_filters(filters, new_args, args) - -- Would've strongly preferred - -- if args[i] == '-af' or args[i] == '-filter:a' then - -- i = i + 1 - -- add_filter(args[i]) - -- but https://lua.org/manual/5.4/manual.html#3.3.5 says that - -- "You should not change the value of the control variable during the loop." - local expect_filter = false - for i = 1, #args do - if args[i] == '-af' or args[i] == '-filter:a' then - expect_filter = true - else - if expect_filter then - add_filter(filters, args[i]) - else - table.insert(new_args, args[i]) - end - expect_filter = false - end - end -end - -ffmpeg.append_user_audio_args = function(args) - local new_args = {} - local filters = '' - - separate_filters(filters, new_args, args) - if self.config.tie_volumes then - add_filter(filters, string.format("volume=%.1f", mp.get_property_native('volume') / 100.0)) - end - - local user_args = {} - for arg in string.gmatch(self.config.ffmpeg_audio_args, "%S+") do - table.insert(user_args, arg) - end - separate_filters(filters, new_args, user_args) - - if #filters > 0 then - table.insert(new_args, '-af') - table.insert(new_args, filters) - end - return new_args -end - -ffmpeg.make_audio_args = function( - source_path, output_path, start_timestamp, end_timestamp, args_consumer -) - local audio_track = h.get_active_track('audio') - local audio_track_id = audio_track['ff-index'] - - if audio_track and audio_track.external == true then - source_path = audio_track['external-filename'] - audio_track_id = 'a' - end - - local function make_ffargs(...) - return ffmpeg.append_user_audio_args( - ffmpeg.prepend( - '-vn', - '-ss', toms(start_timestamp), - '-to', toms(end_timestamp), - '-i', source_path, - '-map_metadata', '-1', - '-map_chapters', '-1', - '-map', string.format("0:%s", tostring(audio_track_id)), - '-ac', '1', - ... - ) - ) - end - - local function make_encoding_args(loudnorm_args) - local encoder_args - if self.config.audio_format == 'opus' then - encoder_args = { - '-c:a', 'libopus', - '-application', 'voip', - '-apply_phase_inv', '0', -- Improves mono audio. - } - if self.config.opus_container == 'm4a' then - table.insert(encoder_args, '-f') - table.insert(encoder_args, 'mp4') - end - else - -- https://wiki.hydrogenaud.io/index.php?title=LAME#Recommended_encoder_settings: - -- "For very low bitrates, up to 100kbps, ABR is most often the best solution." - encoder_args = { - '-c:a', 'libmp3lame', - '-compression_level', '0', - '-abr', '1', - } - end - - encoder_args = { '-b:a', tostring(self.config.audio_bitrate), h.unpack(encoder_args) } - if loudnorm_args then - table.insert(encoder_args, '-af') - table.insert(encoder_args, loudnorm_args) - end - local args = make_ffargs(h.unpack(encoder_args)) - table.insert(args, output_path) - args_consumer(args) - end - - if not self.config.loudnorm then - make_encoding_args(nil) - return - end - - local loudnorm_targets = make_loudnorm_targets() - local args = make_ffargs( - '-loglevel', 'info', - '-af', loudnorm_targets .. ':print_format=json' - ) - table.insert(args, '-f') - table.insert(args, 'null') - table.insert(args, '-') - h.subprocess( - args, - parse_loudnorm( - loudnorm_targets, - function(stdout, stderr) - local start, stop, json = string.find(stderr, '%[Parsed_loudnorm_0.-({.-})') - return json - end, - make_encoding_args - ) - ) -end - ------------------------------------------------------------- --- mpv encoder - -local mpv = { } - -mpv.exec = find_exec("mpv") - -mpv.prepend_common_args = function(source_path, ...) - return { - mpv.exec, - source_path, - '--no-config', - '--loop-file=no', - '--keep-open=no', - '--no-sub', - '--no-ocopy-metadata', - ..., - } -end - -mpv.make_static_snapshot_args = function(source_path, output_path, timestamp) - local encoder_args - if self.config.snapshot_format == 'avif' then - encoder_args = { - '--ovc=libaom-av1', - -- cpu-used < 6 can take a lot of time to encode. - '--ovcopts-add=cpu-used=6', - string.format('--ovcopts-add=crf=%d', quality_to_crf_avif(self.config.snapshot_quality)), - '--ovcopts-add=still-picture=1', - } - elseif self.config.snapshot_format == 'webp' then - encoder_args = { - '--ovc=libwebp', - '--ovcopts-add=compression_level=6', - string.format('--ovcopts-add=quality=%d', self.config.snapshot_quality), - } - else - encoder_args = { - '--ovc=mjpeg', - '--vf-add=scale=out_range=jpeg', - string.format( - '--ovcopts=global_quality=%d*QP2LAMBDA,flags=+qscale', - quality_to_jpeg_qscale(self.config.snapshot_quality) - ), - } - end - - return mpv.prepend_common_args( - source_path, - '--audio=no', - '--frames=1', - '--start=' .. toms(timestamp), - string.format('--vf-add=lavfi=[%s]', static_scale_filter()), - '-o=' .. output_path, - h.unpack(encoder_args) - ) -end - -mpv.make_animated_snapshot_args = function(source_path, output_path, start_timestamp, end_timestamp) - local encoder_args - if self.config.animated_snapshot_format == 'avif' then - encoder_args = { - '--ovc=libaom-av1', - -- cpu-used < 6 can take a lot of time to encode. - '--ovcopts-add=cpu-used=6', - string.format('--ovcopts-add=crf=%d', quality_to_crf_avif(self.config.animated_snapshot_quality)), - } - else - encoder_args = { - '--ovc=libwebp', - '--ovcopts-add=compression_level=6', - string.format('--ovcopts-add=quality=%d', self.config.animated_snapshot_quality), - } - end - - return mpv.prepend_common_args( - source_path, - '--audio=no', - '--start=' .. toms(start_timestamp), - '--end=' .. toms(end_timestamp), - '--ofopts-add=loop=0', - string.format('--vf-add=fps=%d', self.config.animated_snapshot_fps), - string.format('--vf-add=lavfi=[%s]', animated_scale_filter()), - '-o=' .. output_path, - h.unpack(encoder_args) - ) -end - -mpv.make_audio_args = function(source_path, output_path, - start_timestamp, end_timestamp, args_consumer) - local audio_track = h.get_active_track('audio') - local audio_track_id = mp.get_property("aid") - - if audio_track and audio_track.external == true then - source_path = audio_track['external-filename'] - audio_track_id = 'auto' - end - - local function make_mpvargs(...) - local args = mpv.prepend_common_args( - source_path, - '--video=no', - '--aid=' .. audio_track_id, - '--audio-channels=mono', - '--start=' .. toms(start_timestamp), - '--end=' .. toms(end_timestamp), - string.format( - '--volume=%d', - self.config.tie_volumes and mp.get_property('volume') or 100 - ), - ... - ) - for arg in string.gmatch(self.config.mpv_audio_args, "%S+") do - table.insert(args, arg) - end - return args - end - - local function make_encoding_args(loudnorm_args) - local encoder_args - if self.config.audio_format == 'opus' then - encoder_args = { - '--oac=libopus', - '--oacopts-add=application=voip', - '--oacopts-add=apply_phase_inv=0', -- Improves mono audio. - } - if self.config.opus_container == 'm4a' then - table.insert(encoder_args, '--of=mp4') - end - else - -- https://wiki.hydrogenaud.io/index.php?title=LAME#Recommended_encoder_settings: - -- "For very low bitrates, up to 100kbps, ABR is most often the best solution." - encoder_args = { - '--oac=libmp3lame', - '--oacopts-add=compression_level=0', - '--oacopts-add=abr=1', - } - end - - local args = make_mpvargs( - '--oacopts-add=b=' .. self.config.audio_bitrate, - '-o=' .. output_path, - h.unpack(encoder_args) - ) - if loudnorm_args then - table.insert(args, '--af-append=' .. loudnorm_args) - end - args_consumer(args) - end - - if not self.config.loudnorm then - make_encoding_args(nil) - return - end - - local loudnorm_targets = make_loudnorm_targets() - h.subprocess( - make_mpvargs( - '-v', - '--af-append=' .. loudnorm_targets .. ':print_format=json', - '--ao=null', - '--of=null' - ), - parse_loudnorm( - loudnorm_targets, - function(stdout, stderr) - local start, stop, json = string.find(stdout, '%[ffmpeg%] ({.-})') - if json then - json = string.gsub(json, '%[ffmpeg%]', '') - end - return json - end, - make_encoding_args - ) - ) -end - ------------------------------------------------------------- --- main interface - -local create_animated_snapshot = function(start_timestamp, end_timestamp, source_path, output_path, on_finish_fn) - -- Creates the animated snapshot and then calls on_finish_fn - local args = self.encoder.make_animated_snapshot_args(source_path, output_path, start_timestamp, end_timestamp) - h.subprocess(args, on_finish_fn) -end - -local create_static_snapshot = function(timestamp, source_path, output_path, on_finish_fn) - -- Creates a static snapshot, in other words an image, and then calls on_finish_fn - if not self.config.screenshot then - local args = self.encoder.make_static_snapshot_args(source_path, output_path, timestamp) - h.subprocess(args, on_finish_fn) - else - local args = { 'screenshot-to-file', output_path, 'video', } - mp.command_native_async(args, on_finish_fn) - end - -end - -local report_creation_result = function(file_path) - return function(success, result) - -- result is nil on success for screenshot-to-file. - if success and (result == nil or result.status == 0) and h.file_exists(file_path) then - msg.info(string.format("Created file: %s", file_path)) - return true - else - msg.error(string.format("Couldn't create file: %s", file_path)) - return false - end - end -end - -local create_snapshot = function(start_timestamp, end_timestamp, current_timestamp, filename) - if h.is_empty(self.output_dir_path) then - return msg.error("Output directory wasn't provided. Image file will not be created.") - end - - -- Calls the proper function depending on whether or not the snapshot should be animated - if not h.is_empty(self.config.image_field) then - local source_path = mp.get_property("path") - local output_path = utils.join_path(self.output_dir_path, filename) - - local on_finish = report_creation_result(output_path) - if self.config.animated_snapshot_enabled then - create_animated_snapshot(start_timestamp, end_timestamp, source_path, output_path, on_finish) - else - create_static_snapshot(current_timestamp, source_path, output_path, on_finish) - end - else - print("Snapshot will not be created.") - end -end - -local background_play = function(file_path, on_finish) - return h.subprocess( - { mpv.exec, '--audio-display=no', '--force-window=no', '--keep-open=no', '--really-quiet', file_path }, - on_finish - ) -end - -local create_audio = function(start_timestamp, end_timestamp, filename, padding) - if h.is_empty(self.output_dir_path) then - return msg.error("Output directory wasn't provided. Audio file will not be created.") - end - - if not h.is_empty(self.config.audio_field) then - local source_path = mp.get_property("path") - local output_path = utils.join_path(self.output_dir_path, filename) - - if padding > 0 then - start_timestamp, end_timestamp = pad_timings(padding, start_timestamp, end_timestamp) - end - - local function start_encoding(args) - local on_finish = function(success, result) - local conversion_check = report_creation_result(output_path) - if conversion_check(success, result) and self.config.preview_audio then - background_play(output_path, function() - print("Played file: " .. output_path) - end) - end - end - - h.subprocess(args, on_finish) - end - - self.encoder.make_audio_args( - source_path, output_path, start_timestamp, end_timestamp, start_encoding - ) - else - print("Audio will not be created.") - end -end - -local make_snapshot_filename = function(start_time, end_time, timestamp) - -- Generate a filename for the snapshot, taking care of its extension and whether it's animated or static - if self.config.animated_snapshot_enabled then - return filename_factory.make_filename(start_time, end_time, self.config.animated_snapshot_extension) - else - return filename_factory.make_filename(timestamp, self.config.snapshot_extension) - end -end - -local make_audio_filename = function(start_time, end_time) - -- Generates a filename for the audio - return filename_factory.make_filename(start_time, end_time, self.config.audio_extension) -end - -local toggle_animation = function() - -- Toggles on and off animated snapshot generation at runtime. It is called whenever ctrl+g is pressed - self.config.animated_snapshot_enabled = not self.config.animated_snapshot_enabled - h.notify("Animation " .. (self.config.animated_snapshot_enabled and "enabled" or "disabled"), "info", 2) -end - -local init = function(config) - -- Sets the module to its preconfigured status - self.config = config - self.encoder = config.use_ffmpeg and ffmpeg or mpv -end - -local set_output_dir = function(dir_path) - -- Set directory where media files should be saved. - -- This function is called every time a card is created or updated. - self.output_dir_path = dir_path -end - -local create_job = function(type, sub, audio_padding) - local filename, run_async, current_timestamp - if type == 'snapshot' and h.has_video_track() then - current_timestamp = mp.get_property_number("time-pos", 0) - filename = make_snapshot_filename(sub['start'], sub['end'], current_timestamp) - run_async = function() - create_snapshot(sub['start'], sub['end'], current_timestamp, filename) - end - elseif type == 'audioclip' and h.has_audio_track() then - filename = make_audio_filename(sub['start'], sub['end']) - run_async = function() - create_audio(sub['start'], sub['end'], filename, audio_padding) - end - else - run_async = function() - print(type .. " will not be created.") - end - end - return { - filename = filename, - run_async = run_async, - } -end - -return { - init = init, - set_output_dir = set_output_dir, - snapshot = { - create_job = function(sub) - return create_job('snapshot', sub) - end, - toggle_animation = toggle_animation, - }, - audio = { - create_job = function(sub, padding) - return create_job('audioclip', sub, padding) - end, - }, -} diff --git a/config/mpv/scripts/subs2srsa/find_anki_col.sh b/config/mpv/scripts/subs2srsa/find_anki_col.sh deleted file mode 100755 index a98199b..0000000 --- a/config/mpv/scripts/subs2srsa/find_anki_col.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# Find full path to an opened Anki collection. - -readlink -f -- /proc/$(pgrep '^anki$')/fd/* | grep 'collection.anki2$' diff --git a/config/mpv/scripts/subs2srsa/helpers.lua b/config/mpv/scripts/subs2srsa/helpers.lua deleted file mode 100644 index e8f911e..0000000 --- a/config/mpv/scripts/subs2srsa/helpers.lua +++ /dev/null @@ -1,280 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Various helper functions. -]] - -local mp = require('mp') -local msg = require('mp.msg') -local utils = require('mp.utils') -local this = {} - -this.unpack = unpack and unpack or table.unpack - -this.remove_all_spaces = function(str) - return str:gsub('%s*', '') -end - -this.table_get = function(table, key, default) - if table[key] == nil then - return default or 'nil' - else - return table[key] - end -end - -this.max_num = function(table) - local max = table[1] - for _, value in ipairs(table) do - if value > max then - max = value - end - end - return max -end - -this.get_last_n_added_notes = function(note_ids, n) - table.sort(note_ids) - return { this.unpack(note_ids, math.max(#note_ids - n + 1, 1), #note_ids) } -end - -this.contains = function(table, element) - for _, value in pairs(table) do - if value == element then - return true - end - end - return false -end - -this.minutes_ago = function(m) - return (os.time() - 60 * m) * 1000 -end - -this.is_wayland = function() - return os.getenv('WAYLAND_DISPLAY') ~= nil -end - -this.is_win = function() - return mp.get_property('options/vo-mmcss-profile') ~= nil -end - -this.is_mac = function() - return mp.get_property('options/macos-force-dedicated-gpu') ~= nil -end - -local function map(tab, func) - local t = {} - for k, v in pairs(tab) do - t[k] = func(v) - end - return t -end - -local function args_as_str(args) - return table.concat(map(args, function(str) return string.format("'%s'", str) end), " ") -end - -this.subprocess = function(args, completion_fn, override_settings) - -- if `completion_fn` is passed, the command is ran asynchronously, - -- and upon completion, `completion_fn` is called to process the results. - msg.info("Executing: " .. args_as_str(args)) - local command_native = type(completion_fn) == 'function' and mp.command_native_async or mp.command_native - local command_table = { - name = "subprocess", - playback_only = false, - capture_stdout = true, - capture_stderr = true, - args = args - } - if not this.is_empty(override_settings) then - for k,v in pairs(override_settings) do - command_table[k] = v - end - end - return command_native(command_table, completion_fn) -end - -this.subprocess_detached = function(args, completion_fn) - local overwrite_settings = { - detach=true, - capture_stdout = false, - capture_stderr = false, - } - return this.subprocess(args, completion_fn, overwrite_settings) -end - -this.is_empty = function(var) - return var == nil or var == '' or (type(var) == 'table' and next(var) == nil) -end - -this.contains_non_latin_letters = function(str) - return str:match("[^%c%p%s%w—]") -end - -this.capitalize_first_letter = function(string) - return string:gsub("^%l", string.upper) -end - -this.remove_leading_trailing_spaces = function(str) - return str:gsub('^%s*(.-)%s*$', '%1') -end - -this.remove_leading_trailing_dashes = function(str) - return str:gsub('^[%-_]*(.-)[%-_]*$', '%1') -end - -this.remove_text_in_parentheses = function(str) - -- Remove text like (泣き声) or (ドアの開く音) - -- No deletion is performed if there's no text after the parentheses. - -- Note: the modifier `-´ matches zero or more occurrences. - -- However, instead of matching the longest sequence, it matches the shortest one. - return str:gsub('(%b())(.)', '%2'):gsub('((.-))(.)', '%2') -end - -this.remove_newlines = function(str) - return str:gsub('[\n\r]+', ' ') -end - -this.trim = function(str) - str = this.remove_leading_trailing_spaces(str) - str = this.remove_text_in_parentheses(str) - str = this.remove_newlines(str) - return str -end - -this.escape_special_characters = (function() - local entities = { - ['&'] = '&', - ['"'] = '"', - ["'"] = ''', - ['<'] = '<', - ['>'] = '>', - } - return function(s) - return s:gsub('[&"\'<>]', entities) - end -end)() - -this.remove_extension = function(filename) - return filename:gsub('%.%w+$', '') -end - -this.remove_special_characters = function(str) - return str:gsub('[%c%p%s]', ''):gsub(' ', '') -end - -this.remove_text_in_brackets = function(str) - return str:gsub('%b[]', ''):gsub('【.-】', '') -end - -this.remove_filename_text_in_parentheses = function(str) - return str:gsub('%b()', ''):gsub('(.-)', '') -end - -this.remove_common_resolutions = function(str) - -- Also removes empty leftover parentheses and brackets. - return str:gsub("2160p", ""):gsub("1080p", ""):gsub("720p", ""):gsub("576p", ""):gsub("480p", ""):gsub("%(%)", ""):gsub("%[%]", "") -end - -this.human_readable_time = function(seconds) - if type(seconds) ~= 'number' or seconds < 0 then - return 'empty' - end - - local parts = { - h = math.floor(seconds / 3600), - m = math.floor(seconds / 60) % 60, - s = math.floor(seconds % 60), - ms = math.floor((seconds * 1000) % 1000), - } - - local ret = string.format("%02dm%02ds%03dms", parts.m, parts.s, parts.ms) - - if parts.h > 0 then - ret = string.format('%dh%s', parts.h, ret) - end - - return ret -end - -this.get_episode_number = function(filename) - -- Reverses the filename to start the search from the end as the media title might contain similar numbers. - local filename_reversed = filename:reverse() - - local ep_num_patterns = { - "[%s_](%d?%d?%d)[pP]?[eE]", -- Starting with E or EP (case-insensitive). "Example Series S01E01 [94Z295D1]" - "^(%d?%d?%d)[pP]?[eE]", -- Starting with E or EP (case-insensitive) at the end of filename. "Example Series S01E01" - "%)(%d?%d?%d)%(", -- Surrounded by parentheses. "Example Series (12)" - "%](%d?%d?%d)%[", -- Surrounded by brackets. "Example Series [01]" - "%s(%d?%d?%d)%s", -- Surrounded by whitespace. "Example Series 124 [1080p 10-bit]" - "_(%d?%d?%d)_", -- Surrounded by underscores. "Example_Series_04_1080p" - "^(%d?%d?%d)[%s_]", -- Ending to the episode number. "Example Series 124" - "(%d?%d?%d)%-edosipE", -- Prepended by "Episode-". "Example Episode-165" - } - - local s, e, episode_num - for _, pattern in pairs(ep_num_patterns) do - s, e, episode_num = string.find(filename_reversed, pattern) - if not this.is_empty(episode_num) then - return #filename - e, #filename - s, episode_num:reverse() - end - end -end - -this.notify = function(message, level, duration) - level = level or 'info' - duration = duration or 1 - msg[level](message) - mp.osd_message(message, duration) -end - -this.get_active_track = function(track_type) - -- track_type == audio|sub - for _, track in pairs(mp.get_property_native('track-list')) do - if track.type == track_type and track.selected == true then - return track - end - end - return nil -end - -this.has_video_track = function() - return mp.get_property_native('vid') ~= false -end - -this.has_audio_track = function() - return mp.get_property_native('aid') ~= false -end - -this.str_contains = function(s, pattern) - return not this.is_empty(s) and string.find(string.lower(s), string.lower(pattern)) ~= nil -end - -this.filter = function(arr, func) - local filtered = {} - for _, elem in ipairs(arr) do - if func(elem) == true then - table.insert(filtered, elem) - end - end - return filtered -end - -this.file_exists = function(filepath) - if not this.is_empty(filepath) then - local info = utils.file_info(filepath) - if info and info.is_file and info.size > 0 then - return true - end - end - return false -end - -this.get_loaded_tracks = function(track_type) - --- Return all sub tracks, audio tracks, etc. - return this.filter(mp.get_property_native('track-list'), function(track) return track.type == track_type end) -end - -return this diff --git a/config/mpv/scripts/subs2srsa/howto/add_dialog.md b/config/mpv/scripts/subs2srsa/howto/add_dialog.md deleted file mode 100644 index 22093c6..0000000 --- a/config/mpv/scripts/subs2srsa/howto/add_dialog.md +++ /dev/null @@ -1,9 +0,0 @@ -# Open the "Add" dialog - -1) Open a video in `mpv`. -1) Press <kbd>a</kbd> to open advanced menu. -1) Optionally, press <kbd>c</kbd> and select the desired subtitle lines with the interactive selection. -1) Press <kbd>g</kbd> to open the Add dialog in Anki. -1) Add dictionary definitions using software like GoldenDict, Qolibri, etc. - -After the card is created, you can find it by typing `added:1` in the Anki Browser. diff --git a/config/mpv/scripts/subs2srsa/howto/create_card.md b/config/mpv/scripts/subs2srsa/howto/create_card.md deleted file mode 100644 index e92f77d..0000000 --- a/config/mpv/scripts/subs2srsa/howto/create_card.md +++ /dev/null @@ -1,14 +0,0 @@ -# Make a simple sentence card - -To make a card from the currently visible subtitle line, press <kbd>Ctrl+n</kbd>. - -To make a card from two or more subtitle lines: - -1) Press <kbd>a</kbd> to open advanced menu. -2) Press <kbd>c</kbd> to start interactive selection. -3) Seek to the previous/next subtitle with <kbd>Shift+h</kbd> and <kbd>Shift+l</kbd>. -4) Press <kbd>n</kbd> to create a new card. - -After the card is created, you can find it by typing `added:1` in the Anki Browser. -The card doesn't contain dictionary definitions. -You need to add them yourself afterward, using software like GoldenDict, Qolibri, etc. diff --git a/config/mpv/scripts/subs2srsa/howto/create_quick_card.md b/config/mpv/scripts/subs2srsa/howto/create_quick_card.md deleted file mode 100644 index 619794a..0000000 --- a/config/mpv/scripts/subs2srsa/howto/create_quick_card.md +++ /dev/null @@ -1,31 +0,0 @@ -# Quick Card Creation - -The goal of the quick card creation menu is to streamline a specific flow for media with **well-timed subtitles**: - -1) Add a new note to Anki via - [Rikaitan](https://tatsumoto.neocities.org/blog/setting-up-yomichan.html) - or any other tool that works with AnkiConnect. -2) Update the note via mpvacious. - -To update the most recently added card from the currently visible subtitle line, press <kbd>gg</kbd>. - -To make a card from two or more subtitle lines: - -1) Press <kbd>g</kbd> to open the quick card creation menu. -2) Press any number <kbd>[2-9]</kbd>. This number corresponds to the number of lines to create the card from. - -Note: <kbd>g1</kbd> is also valid. -However, <kbd>gg</kbd> is an additional bind to further streamline the most common scenario. - -For example, -<kbd>g2</kbd> creates a card using 2 subtitle lines. - -Like the advanced menu, you can also update multiple cards: - -1) Press <kbd>Alt+g</kbd> to select the number of cards for quick card creation. -2) Press any number <kbd>[2-9]</kbd>. This number corresponds to the number of cards to update. -3) Press any number <kbd>[1-9]</kbd> again. This is the number of lines. - -Note: upon completing the note update, the selected number of cards resets back to the default of one. - -For example, <kbd>(Alt+g)22</kbd> would update the last 2 notes using 2 subtitle lines. diff --git a/config/mpv/scripts/subs2srsa/howto/flatpak.md b/config/mpv/scripts/subs2srsa/howto/flatpak.md deleted file mode 100644 index 46a5787..0000000 --- a/config/mpv/scripts/subs2srsa/howto/flatpak.md +++ /dev/null @@ -1,27 +0,0 @@ -# Flatpak notes - -We think it's best to never use Flatpak. -Specifically, try not to use Flatpak to install `mpv` and `anki`. -Install packages from the official repositories of your distro or from the AUR. - -Read the following notes if you still decide to use Flatpak. - -Make these changes in Flatseal: - -* Enable "Filesystem > All system files" - so it could see `wl-copy`. - Unfortunately, there's no option to provide only a specific system file. -* Add `~/.var/app/net.ankiweb.Anki` to "Filesystem > Other Files" - so mpvacious could add encoded snapshots and audio to Anki. -* Add `PATH=/home/USERNAME/.local/bin:/home/USERNAME/bin:/app/bin:/usr/bin:/run/host/usr/bin` to "Environment > Variables". - There's no option to add a path to `PATH` in Flatseal, - so I opened container, - saved it's PATH and added `/run/host/usr/bin` - so mpvacuous could access `wl-copy`. -* Enable "Shared > Network". - It's enabled by default, but anyway. - -The mpv config root is `~/.var/app/io.mpv.Mpv/config/mpv` - -* `~/.var/app/io.mpv.Mpv/config/mpv/scripts` -* `~/.var/app/io.mpv.Mpv/config/mpv/script-opts` diff --git a/config/mpv/scripts/subs2srsa/howto/goldendict.md b/config/mpv/scripts/subs2srsa/howto/goldendict.md deleted file mode 100644 index 7f355c6..0000000 --- a/config/mpv/scripts/subs2srsa/howto/goldendict.md +++ /dev/null @@ -1,45 +0,0 @@ -# Modifying cards added with GoldenDict - -You can add a card first using GoldenDict, -and then append an audio clip and a picture to it. - -**Note:** the only version of GoldenDict that can create Anki cards with configurable fields is -[xiaoyifang's goldendict](https://github.com/xiaoyifang/goldendict-ng). -Read [Setting up GoldenDict](https://tatsumoto-ren.github.io/blog/setting-up-goldendict.html) and -[How to connect with Anki](https://github.com/xiaoyifang/goldendict-ng/blob/staged/website/docs/topic_anki.md) -if you are new to GoldenDict. - -To send subtitles from `mpv` directly to GoldenDict, -append the following line to `subs2srs.conf`: - -``` -autoclip_method=goldendict -``` - -**Note:** If `goldendict` is not in the PATH, -you have to [add it to the PATH](https://wiki.archlinux.org/title/Environment_variables#Per_user). - -1) Press <kbd>a</kbd> to open `advanced menu`. -2) Press <kbd>t</kbd> to toggle the `autoclip` option. - -Now as subtitles appear on the screen, -they will be immediately sent to GoldenDict instead of the system clipboard. - -1) Open GoldenDict. -2) Play a video in `mpv`. -3) When you find an unknown word, - select the definition text, - right-click and select "send word to anki" to make a card, - or press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd>. -4) Go back to mpv and add an image and an audio clip - to the card you've just made by pressing <kbd>m</kbd> while the `advanced menu` is open. - Pressing <kbd>Shift+m</kbd> will overwrite any existing data in media fields. - -https://github.com/Ajatt-Tools/mpvacious/assets/69171671/0fc02d24-d320-4d2c-b7a9-cb478e9f0067 - -Don't forget to set the right timings and join lines together -if the sentence is split between multiple subs. -To do it, enter interactive selection by pressing <kbd>c</kbd> -and seek to the next or previous subtitle. - -To pair Mecab and GoldenDict, install [gd-tools](https://github.com/Ajatt-Tools/gd-tools). diff --git a/config/mpv/scripts/subs2srsa/howto/yomichan.md b/config/mpv/scripts/subs2srsa/howto/yomichan.md deleted file mode 100644 index 9ea16d6..0000000 --- a/config/mpv/scripts/subs2srsa/howto/yomichan.md +++ /dev/null @@ -1,24 +0,0 @@ -# Modifying cards added with Rikaitan - -You can add a card first using -[Rikaitan](https://tatsumoto.neocities.org/blog/setting-up-yomichan.html), -and then append an audio clip and a picture to it. - -1) Press <kbd>a</kbd> to open `advanced menu`. -1) Press <kbd>t</kbd> to toggle the `autoclip` option. - -Now as subtitles appear on the screen, they will be immediately copied to the clipboard. -You can use it in combination with clipboard monitor. - -1) Open [Rikaitan Search](https://tatsumoto.neocities.org/blog/what-is-yomichan-search) - by pressing <kbd>Alt+Insert</kbd> in your web browser. -1) Play a video in `mpv`. -1) When you find an unknown word, click the <kbd>+</kbd> button to make a card for it. -4) Go back to mpv and add an image and an audio clip - to the card you've just made by pressing <kbd>m</kbd> while the `advanced menu` is open. - Pressing <kbd>Shift+m</kbd> will overwrite any existing data in media fields. - -Don't forget to set the right timings and join lines together -if the sentence is split between multiple subs. -To do it, enter interactive selection by pressing <kbd>c</kbd> -and seek to the next or previous subtitle. diff --git a/config/mpv/scripts/subs2srsa/main.lua b/config/mpv/scripts/subs2srsa/main.lua deleted file mode 100644 index 6817586..0000000 --- a/config/mpv/scripts/subs2srsa/main.lua +++ /dev/null @@ -1 +0,0 @@ -require('subs2srs') diff --git a/config/mpv/scripts/subs2srsa/menu.lua b/config/mpv/scripts/subs2srsa/menu.lua deleted file mode 100644 index 4d34937..0000000 --- a/config/mpv/scripts/subs2srsa/menu.lua +++ /dev/null @@ -1,77 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Menu for mpvacious -]] - -local mp = require('mp') -local msg = require('mp.msg') -local h = require('helpers') - -local Menu = { - active = false, - keybindings = {}, - overlay = mp.create_osd_overlay and mp.create_osd_overlay('ass-events'), -} - -function Menu:new(o) - o = o or {} - setmetatable(o, self) - self.__index = self - return o -end - -function Menu:with_update(params) - return function() - local status, error = pcall(h.unpack(params)) - if not status then - msg['error'](error) - end - self:update() - end -end - -function Menu:make_osd() - return nil -end - -function Menu:update() - if self.active == false then return end - self.overlay.data = self:make_osd():get_text() - self.overlay:update() -end - -function Menu:open() - if self.overlay == nil then - h.notify("OSD overlay is not supported in " .. mp.get_property("mpv-version"), "error", 5) - return - end - - if self.active == true then - self:close() - return - end - - for _, val in pairs(self.keybindings) do - mp.add_forced_key_binding(val.key, val.key, val.fn) - end - - self.active = true - self:update() -end - -function Menu:close() - if self.active == false then - return - end - - for _, val in pairs(self.keybindings) do - mp.remove_key_binding(val.key) - end - - self.overlay:remove() - self.active = false -end - -return Menu diff --git a/config/mpv/scripts/subs2srsa/osd_styler.lua b/config/mpv/scripts/subs2srsa/osd_styler.lua deleted file mode 100644 index 3a865dc..0000000 --- a/config/mpv/scripts/subs2srsa/osd_styler.lua +++ /dev/null @@ -1,97 +0,0 @@ ---[[ -A helper class for styling OSD messages -http://docs.aegisub.org/3.2/ASS_Tags/ - -Copyright (C) 2021 Ren Tatsumoto - -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 <https://www.gnu.org/licenses/>. -]] - -local OSD = {} -OSD.__index = OSD - -function OSD:new() - return setmetatable({ messages = {} }, self) -end - -function OSD:append(s) - table.insert(self.messages, tostring(s)) - return self -end - -function OSD:newline() - return self:append([[\N]]) -end - -function OSD:tab() - return self:append([[\h\h\h\h]]) -end - -function OSD:size(size) - return self:append('{\\fs'):append(size):append('}') -end - -function OSD:font(name) - return self:append('{\\fn'):append(name):append('}') -end - -function OSD:align(number) - return self:append('{\\an'):append(number):append('}') -end - -function OSD:get_text() - return table.concat(self.messages) -end - -function OSD:color(code) - return self:append('{\\1c&H') - :append(code:sub(5, 6)) - :append(code:sub(3, 4)) - :append(code:sub(1, 2)) - :append('&}') -end - -function OSD:text(text) - return self:append(text) -end - -function OSD:new_layer() - return self:append('\n') -end - -function OSD:bold(s) - return self:append('{\\b1}'):append(s):append('{\\b0}') -end - -function OSD:italics(s) - return self:append('{\\i1}'):append(s):append('{\\i0}') -end - -function OSD:submenu(text) - return self:color('ffe1d0'):bold(text):color('ffffff') -end - -function OSD:item(text) - return self:color('fef6dd'):bold(text):color('ffffff') -end - -function OSD:selected(text) - return self:color('48a868'):bold(text):color('ffffff') -end - -function OSD:red(text) - return self:color('ff0000'):bold(text):color('ffffff') -end - -return OSD diff --git a/config/mpv/scripts/subs2srsa/platform/init.lua b/config/mpv/scripts/subs2srsa/platform/init.lua deleted file mode 100644 index 825d8d4..0000000 --- a/config/mpv/scripts/subs2srsa/platform/init.lua +++ /dev/null @@ -1,14 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Platform-specific functions. -]] - -local h = require('helpers') - -if h.is_win() then - return require('platform.win') -else - return require('platform.nix') -end diff --git a/config/mpv/scripts/subs2srsa/platform/nix.lua b/config/mpv/scripts/subs2srsa/platform/nix.lua deleted file mode 100644 index cbf6c85..0000000 --- a/config/mpv/scripts/subs2srsa/platform/nix.lua +++ /dev/null @@ -1,49 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Platform-specific functions for *nix systems. -]] - -local h = require('helpers') -local self = { healthy = true, clip_util = "", clip_cmd = "", } - -if h.is_mac() then - self.clip_util = "pbcopy" - self.clip_cmd = "LANG=en_US.UTF-8 " .. self.clip_util -elseif h.is_wayland() then - local function is_wl_copy_installed() - local handle = h.subprocess { 'wl-copy', '--version' } - return handle.status == 0 and handle.stdout:match("wl%-clipboard") ~= nil - end - - self.clip_util = "wl-copy" - self.clip_cmd = self.clip_util - self.healthy = is_wl_copy_installed() -else - local function is_xclip_installed() - local handle = h.subprocess { 'xclip', '-version' } - return handle.status == 0 and handle.stderr:match("xclip version") ~= nil - end - - self.clip_util = "xclip" - self.clip_cmd = self.clip_util .. " -i -selection clipboard" - self.healthy = is_xclip_installed() -end - -self.tmp_dir = function() - return os.getenv("TMPDIR") or '/tmp' -end - -self.copy_to_clipboard = function(text) - local handle = io.popen(self.clip_cmd, 'w') - handle:write(text) - handle:close() -end - -self.curl_request = function(url, request_json, completion_fn) - local args = { 'curl', '-s', url, '-X', 'POST', '-d', request_json } - return h.subprocess(args, completion_fn) -end - -return self diff --git a/config/mpv/scripts/subs2srsa/platform/win.lua b/config/mpv/scripts/subs2srsa/platform/win.lua deleted file mode 100644 index e40dd7a..0000000 --- a/config/mpv/scripts/subs2srsa/platform/win.lua +++ /dev/null @@ -1,72 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Platform-specific functions for Windows. -]] - -local mp = require('mp') -local h = require('helpers') -local utils = require('mp.utils') -local base64 = require('utils.base64') -local self = { windows = true, healthy = true, clip_util = "cmd", } -local tmp_files = {} - -mp.register_event('shutdown', function() - for _, file in ipairs(tmp_files) do - os.remove(file) - end -end) - -self.tmp_dir = function() - return os.getenv('TEMP') -end - -self.copy_to_clipboard = function(text) - local args = { - "powershell", "-NoLogo", "-NoProfile", "-WindowStyle", "Hidden", "-Command", - string.format( - "Set-Clipboard ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('%s')))", - base64.enc(text) - ) - } - return h.subprocess_detached( - args, - function() - end - ) -end - -self.gen_random_tmp_file_path = function() - return utils.join_path(self.tmp_dir(), string.format('curl_tmp_%d.txt', math.random(10 ^ 9))) -end - -self.gen_unique_tmp_file_path = function() - local curl_tmpfile_path = self.gen_random_tmp_file_path() - while h.file_exists(curl_tmpfile_path) do - curl_tmpfile_path = self.gen_random_tmp_file_path() - end - return curl_tmpfile_path -end - -self.curl_request = function(url, request_json, completion_fn) - local curl_tmpfile_path = self.gen_unique_tmp_file_path() - local handle = io.open(curl_tmpfile_path, "w") - handle:write(request_json) - handle:close() - table.insert(tmp_files, curl_tmpfile_path) - local args = { - 'curl', - '-s', - url, - '-H', - 'Content-Type: application/json; charset=UTF-8', - '-X', - 'POST', - '--data-binary', - table.concat { '@', curl_tmpfile_path } - } - return h.subprocess(args, completion_fn) -end - -return self diff --git a/config/mpv/scripts/subs2srsa/subs2srs.lua b/config/mpv/scripts/subs2srsa/subs2srs.lua deleted file mode 100644 index 34c79a9..0000000 --- a/config/mpv/scripts/subs2srsa/subs2srs.lua +++ /dev/null @@ -1,746 +0,0 @@ ---[[ -Copyright (C) 2020-2022 Ren Tatsumoto and contributors - -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 <https://www.gnu.org/licenses/>. - -Requirements: -* mpv >= 0.32.0 -* AnkiConnect -* curl -* xclip (when running X11) -* wl-copy (when running Wayland) - -Usage: -1. Change `config` according to your needs -* Config path: ~/.config/mpv/script-opts/subs2srs.conf -* Config file isn't created automatically. - -2. Open a video - -3. Use key bindings to manipulate the script -* Open mpvacious menu - `a` -* Create a note from the current subtitle line - `Ctrl + n` - -For complete usage guide, see <https://github.com/Ajatt-Tools/mpvacious/blob/master/README.md> -]] - -local mp = require('mp') -local OSD = require('osd_styler') -local cfg_mgr = require('cfg_mgr') -local encoder = require('encoder.encoder') -local h = require('helpers') -local Menu = require('menu') -local ankiconnect = require('ankiconnect') -local switch = require('utils.switch') -local play_control = require('utils.play_control') -local secondary_sid = require('subtitles.secondary_sid') -local platform = require('platform.init') -local forvo = require('utils.forvo') -local subs_observer = require('subtitles.observer') -local codec_support = require('encoder.codec_support') - -local menu, quick_menu, quick_menu_card -local quick_creation_opts = { - _n_lines = nil, - _n_cards = 1, - set_cards = function(self, n) - self._n_cards = math.max(0, n) - end, - set_lines = function(self, n) - self._n_lines = math.max(0, n) - end, - get_cards = function(self) - return self._n_cards - end, - get_lines = function(self) - return self._n_lines - end, - increment_cards = function(self) - self:set_cards(self._n_cards + 1) - end, - decrement_cards = function(self) - self:set_cards(self._n_cards - 1) - end, - clear_options = function(self) - self._n_lines = nil - self._n_cards = 1 - end -} ------------------------------------------------------------- --- default config - -local config = { - -- The user should not modify anything below. - - -- Common - nuke_spaces = false, -- remove all spaces from the primary subtitles on exported anki cards and when copying text to clipboard. - clipboard_trim_enabled = true, -- remove unnecessary characters from strings before copying to the clipboard - use_ffmpeg = false, -- if set to true, use ffmpeg to create audio clips and snapshots. by default use mpv. - reload_config_before_card_creation = true, -- for convenience, read config file from disk before a card is made. - - -- Clipboard and external communication - autoclip = false, -- enable copying subs to the clipboard when mpv starts - autoclip_method = "clipboard", -- one of the methods - autoclip_custom_args = "", -- command to run when autoclip is triggered and autoclip_method and set to "custom_command". - - -- Secondary subtitle - secondary_sub_auto_load = true, -- Automatically load secondary subtitle track when a video file is opened. - secondary_sub_lang = 'eng,en,rus,ru,jp,jpn,ja', -- Language of secondary subs that should be automatically loaded. - secondary_sub_area = 0.15, -- Hover area. Fraction of the window from the top. - secondary_sub_visibility = 'auto', -- One of: 'auto', 'never', 'always'. Controls secondary_sid visibility. Ctrl+V to cycle. - - -- Snapshots - snapshot_format = "avif", -- avif, webp or jpg - snapshot_quality = 15, -- from 0=lowest to 100=highest - snapshot_width = cfg_mgr.preserve_aspect_ratio, -- a positive integer or -2 for auto - snapshot_height = cfg_mgr.default_height_px, -- same - screenshot = false, -- create a screenshot instead of a snapshot; see example config. - - -- Animations - animated_snapshot_enabled = false, -- if enabled captures the selected segment of the video, instead of just a frame - animated_snapshot_format = "avif", -- avif or webp - animated_snapshot_fps = 10, -- positive integer between 0 and 30 (30 included) - animated_snapshot_width = cfg_mgr.preserve_aspect_ratio, -- positive integer or -2 to scale it maintaining ratio (height must not be -2 in that case) - animated_snapshot_height = cfg_mgr.default_height_px, -- positive integer or -2 to scale it maintaining ratio (width must not be -2 in that case) - animated_snapshot_quality = 5, -- positive integer between 0 and 100 (100 included) - - -- Audio clips - audio_format = "opus", -- opus or mp3 - opus_container = "ogg", -- ogg, opus, m4a, webm or caf - audio_bitrate = "18k", -- from 16k to 32k - audio_padding = 0.12, -- Set a pad to the dialog timings. 0.5 = audio is padded by .5 seconds. 0 = disable. - tie_volumes = false, -- if set to true, the volume of the outputted audio file depends on the volume of the player at the time of export - preview_audio = false, -- play created audio clips in background. - - -- Menu - menu_font_name = "Noto Sans CJK JP", - menu_font_size = 25, - show_selected_text = true, - - -- Make sure to remove loudnorm from ffmpeg_audio_args and mpv_audio_args before enabling. - loudnorm = false, - loudnorm_target = -16, - loudnorm_range = 11, - loudnorm_peak = -1.5, - - -- Custom encoding args - -- Defaults are for backward compatibility, in case someone - -- updates mpvacious without updating their config. - -- Better to remove loudnorm from custom args and enable two-pass loudnorm. - -- Enabling loudnorm both through the separate switch and through custom args - -- can lead to unpredictable results. - ffmpeg_audio_args = '-af loudnorm=I=-16:TP=-1.5:LRA=11:dual_mono=true', - mpv_audio_args = '--af-append=loudnorm=I=-16:TP=-1.5:LRA=11:dual_mono=true', - - -- Anki - create_deck = false, -- automatically create a deck for new cards - allow_duplicates = false, -- allow making notes with the same sentence field - deck_name = "Learning", -- name of the deck for new cards - model_name = "Japanese sentences", -- Tools -> Manage note types - sentence_field = "SentKanji", - secondary_field = "SentEng", - audio_field = "SentAudio", - audio_template = '[sound:%s]', - image_field = "Image", - image_template = '<img alt="snapshot" src="%s">', - append_media = true, -- True to append video media after existing data, false to insert media before - disable_gui_browse = false, -- Lets you disable anki browser manipulation by mpvacious. - ankiconnect_url = '127.0.0.1:8765', - - -- Note tagging - -- The tag(s) added to new notes. Spaces separate multiple tags. - -- Change to "" to disable tagging completely. - -- The following substitutions are supported: - -- %n - the name of the video - -- %t - timestamp - -- %d - episode number (if none found, returns nothing) - -- %e - SUBS2SRS_TAGS environment variable - note_tag = "subs2srs %n", - tag_nuke_brackets = true, -- delete all text inside brackets before substituting filename into tag - tag_nuke_parentheses = false, -- delete all text inside parentheses before substituting filename into tag - tag_del_episode_num = true, -- delete the episode number if found - tag_del_after_episode_num = true, -- delete everything after the found episode number (does nothing if tag_del_episode_num is disabled) - tag_filename_lowercase = false, -- convert filename to lowercase for tagging. - - -- Misc info - miscinfo_enable = true, - miscinfo_field = "Notes", -- misc notes and source information field - miscinfo_format = "%n EP%d (%t)", -- format string to use for the miscinfo_field, accepts note_tag-style format strings - - -- Forvo support - use_forvo = "yes", -- 'yes', 'no', 'always' - vocab_field = "VocabKanji", -- target word field - vocab_audio_field = "VocabAudio", -- target word audio -} - --- Defines config profiles --- Each name references a file in ~/.config/mpv/script-opts/*.conf --- Profiles themselves are defined in ~/.config/mpv/script-opts/subs2srs_profiles.conf -local profiles = { - profiles = "subs2srs,subs2srs_english", - active = "subs2srs", -} - ------------------------------------------------------------- --- utility functions -local function _(params) - return function() - return pcall(h.unpack(params)) - end -end - -local function escape_for_osd(str) - str = h.trim(str) - str = str:gsub('[%[%]{}]', '') - return str -end - -local function ensure_deck() - if config.create_deck == true then - ankiconnect.create_deck(config.deck_name) - end -end - -local function load_next_profile() - cfg_mgr.next_profile() - ensure_deck() - h.notify("Loaded profile " .. profiles.active) -end - -local function tag_format(filename) - filename = h.remove_extension(filename) - filename = h.remove_common_resolutions(filename) - - local s, e, episode_num = h.get_episode_number(filename) - - if config.tag_del_episode_num == true and not h.is_empty(s) then - if config.tag_del_after_episode_num == true then - -- Removing everything (e.g. episode name) after the episode number including itself. - filename = filename:sub(1, s) - else - -- Removing the first found instance of the episode number. - filename = filename:sub(1, s) .. filename:sub(e + 1, -1) - end - end - - if config.tag_nuke_brackets == true then - filename = h.remove_text_in_brackets(filename) - end - if config.tag_nuke_parentheses == true then - filename = h.remove_filename_text_in_parentheses(filename) - end - - if config.tag_filename_lowercase == true then - filename = filename:lower() - end - - filename = h.remove_leading_trailing_spaces(filename) - filename = filename:gsub(" ", "_") - filename = filename:gsub("_%-_", "_") -- Replaces garbage _-_ substrings with a underscore - filename = h.remove_leading_trailing_dashes(filename) - return filename, episode_num or '' -end - -local substitute_fmt = (function() - local function substitute_filename(tag, filename) - return tag:gsub("%%n", filename) - end - - local function substitute_episode_number(tag, episode) - return tag:gsub("%%d", episode) - end - - local function substitute_time_pos(tag) - local time_pos = h.human_readable_time(mp.get_property_number('time-pos')) - return tag:gsub("%%t", time_pos) - end - - local function substitute_envvar(tag) - local env_tags = os.getenv('SUBS2SRS_TAGS') or '' - return tag:gsub("%%e", env_tags) - end - - return function(tag) - if not h.is_empty(tag) then - local filename, episode = tag_format(mp.get_property("filename")) - tag = substitute_filename(tag, filename) - tag = substitute_episode_number(tag, episode) - tag = substitute_time_pos(tag) - tag = substitute_envvar(tag) - tag = h.remove_leading_trailing_spaces(tag) - end - return tag - end -end)() - -local function prepare_for_exporting(sub_text) - if not h.is_empty(sub_text) then - sub_text = h.trim(sub_text) - sub_text = h.escape_special_characters(sub_text) - end - return sub_text -end - -local function construct_note_fields(sub_text, secondary_text, snapshot_filename, audio_filename) - local ret = { - [config.sentence_field] = subs_observer.maybe_remove_all_spaces(prepare_for_exporting(sub_text)), - } - if not h.is_empty(config.secondary_field) then - ret[config.secondary_field] = prepare_for_exporting(secondary_text) - end - if not h.is_empty(config.image_field) and not h.is_empty(snapshot_filename) then - ret[config.image_field] = string.format(config.image_template, snapshot_filename) - end - if not h.is_empty(config.audio_field) and not h.is_empty(audio_filename) then - ret[config.audio_field] = string.format(config.audio_template, audio_filename) - end - if config.miscinfo_enable == true then - ret[config.miscinfo_field] = substitute_fmt(config.miscinfo_format) - end - return ret -end - -local function join_media_fields(new_data, stored_data) - for _, field in pairs { config.audio_field, config.image_field, config.miscinfo_field } do - if not h.is_empty(field) then - new_data[field] = h.table_get(stored_data, field, "") .. h.table_get(new_data, field, "") - end - end - return new_data -end - -local function update_sentence(new_data, stored_data) - -- adds support for TSCs - -- https://tatsumoto-ren.github.io/blog/discussing-various-card-templates.html#targeted-sentence-cards-or-mpvacious-cards - -- if the target word was marked by yomichan, this function makes sure that the highlighting doesn't get erased. - - if h.is_empty(stored_data[config.sentence_field]) then - -- sentence field is empty. can't continue. - return new_data - elseif h.is_empty(new_data[config.sentence_field]) then - -- *new* sentence field is empty, but old one contains data. don't delete the existing sentence. - new_data[config.sentence_field] = stored_data[config.sentence_field] - return new_data - end - - local _, opentag, target, closetag, _ = stored_data[config.sentence_field]:match('^(.-)(<[^>]+>)(.-)(</[^>]+>)(.-)$') - if target then - local prefix, _, suffix = new_data[config.sentence_field]:match(table.concat { '^(.-)(', target, ')(.-)$' }) - if prefix and suffix then - new_data[config.sentence_field] = table.concat { prefix, opentag, target, closetag, suffix } - end - end - return new_data -end - -local function audio_padding() - local video_duration = mp.get_property_number('duration') - if config.audio_padding == 0.0 or not video_duration then - return 0.0 - end - if subs_observer.user_altered() then - return 0.0 - end - return config.audio_padding -end - ------------------------------------------------------------- --- front for adding and updating notes - -local function maybe_reload_config() - if config.reload_config_before_card_creation then - cfg_mgr.reload_from_disk() - end -end - -local function get_anki_media_dir_path() - return ankiconnect.get_media_dir_path() -end - -local function export_to_anki(gui) - maybe_reload_config() - local sub = subs_observer.collect_from_current() - - if not sub:is_valid() then - return h.notify("Nothing to export.", "warn", 1) - end - - if not gui and h.is_empty(sub['text']) then - sub['text'] = string.format("mpvacious wasn't able to grab subtitles (%s)", os.time()) - end - - encoder.set_output_dir(get_anki_media_dir_path()) - local snapshot = encoder.snapshot.create_job(sub) - local audio = encoder.audio.create_job(sub, audio_padding()) - - snapshot.run_async() - audio.run_async() - - local first_field = ankiconnect.get_first_field(config.model_name) - local note_fields = construct_note_fields(sub['text'], sub['secondary'], snapshot.filename, audio.filename) - - if not h.is_empty(first_field) and h.is_empty(note_fields[first_field]) then - note_fields[first_field] = "[empty]" - end - - ankiconnect.add_note(note_fields, substitute_fmt(config.note_tag), gui) - subs_observer.clear() -end - -local function update_last_note(overwrite) - maybe_reload_config() - local sub - local n_lines = quick_creation_opts:get_lines() - local n_cards = quick_creation_opts:get_cards() - if n_lines then - sub = subs_observer.collect_from_all_dialogues(n_lines) - else - sub = subs_observer.collect_from_current() - end - -- this now returns a table - local last_note_ids = ankiconnect.get_last_note_ids(n_cards) - n_cards = #last_note_ids - - if not sub:is_valid() then - return h.notify("Nothing to export. Have you set the timings?", "warn", 2) - end - - if h.is_empty(sub['text']) then - -- In this case, don't modify whatever existing text there is and just - -- modify the other fields we can. The user might be trying to add - -- audio to a card which they've manually transcribed (either the video - -- has no subtitles or it has image subtitles). - sub['text'] = nil - end - - --first element is the earliest - - if h.is_empty(last_note_ids) or last_note_ids[1] < h.minutes_ago(10) then - return h.notify("Couldn't find the target note.", "warn", 2) - end - - local anki_media_dir = get_anki_media_dir_path() - encoder.set_output_dir(anki_media_dir) - local snapshot = encoder.snapshot.create_job(sub) - local audio = encoder.audio.create_job(sub, audio_padding()) - - local create_media = function() - snapshot.run_async() - audio.run_async() - end - for i = 1, n_cards do - local new_data = construct_note_fields(sub['text'], sub['secondary'], snapshot.filename, audio.filename) - local stored_data = ankiconnect.get_note_fields(last_note_ids[i]) - if stored_data then - forvo.set_output_dir(anki_media_dir) - new_data = forvo.append(new_data, stored_data) - new_data = update_sentence(new_data, stored_data) - if not overwrite then - if config.append_media then - new_data = join_media_fields(new_data, stored_data) - else - new_data = join_media_fields(stored_data, new_data) - end - end - end - - -- If the text is still empty, put some dummy text to let the user know why - -- there's no text in the sentence field. - if h.is_empty(new_data[config.sentence_field]) then - new_data[config.sentence_field] = string.format("mpvacious wasn't able to grab subtitles (%s)", os.time()) - end - - ankiconnect.append_media(last_note_ids[i], new_data, create_media, substitute_fmt(config.note_tag)) - end - subs_observer.clear() - quick_creation_opts:clear_options() -end - ------------------------------------------------------------- --- main menu - -menu = Menu:new { - hints_state = switch.new { 'basic', 'menu', 'global', 'hidden', }, -} - -menu.keybindings = { - { key = 'S', fn = menu:with_update { subs_observer.set_manual_timing_to_sub, 'start' } }, - { key = 'E', fn = menu:with_update { subs_observer.set_manual_timing_to_sub, 'end' } }, - { key = 's', fn = menu:with_update { subs_observer.set_manual_timing, 'start' } }, - { key = 'e', fn = menu:with_update { subs_observer.set_manual_timing, 'end' } }, - { key = 'c', fn = menu:with_update { subs_observer.set_to_current_sub } }, - { key = 'r', fn = menu:with_update { subs_observer.clear_and_notify } }, - { key = 'g', fn = menu:with_update { export_to_anki, true } }, - { key = 'n', fn = menu:with_update { export_to_anki, false } }, - { key = 'm', fn = menu:with_update { update_last_note, false } }, - { key = 'M', fn = menu:with_update { update_last_note, true } }, - { key = 'f', fn = menu:with_update { function() - quick_creation_opts:increment_cards() - end } }, - { key = 'F', fn = menu:with_update { function() - quick_creation_opts:decrement_cards() - end } }, - { key = 't', fn = menu:with_update { subs_observer.toggle_autocopy } }, - { key = 'T', fn = menu:with_update { subs_observer.next_autoclip_method } }, - { key = 'i', fn = menu:with_update { menu.hints_state.bump } }, - { key = 'p', fn = menu:with_update { load_next_profile } }, - { key = 'ESC', fn = function() - menu:close() - end }, - { key = 'q', fn = function() - menu:close() - end }, -} - -function menu:print_header(osd) - if self.hints_state.get() == 'hidden' then - return - end - osd:submenu('mpvacious options'):newline() - osd:item('Timings: '):text(h.human_readable_time(subs_observer.get_timing('start'))) - osd:item(' to '):text(h.human_readable_time(subs_observer.get_timing('end'))):newline() - osd:item('Clipboard autocopy: '):text(subs_observer.autocopy_status_str()):newline() - osd:item('Active profile: '):text(profiles.active):newline() - osd:item('Deck: '):text(config.deck_name):newline() - osd:item('# cards: '):text(quick_creation_opts:get_cards()):newline() -end - -function menu:print_bindings(osd) - if self.hints_state.get() == 'global' then - osd:submenu('Global bindings'):newline() - osd:tab():item('ctrl+c: '):text('Copy current subtitle to clipboard'):newline() - osd:tab():item('ctrl+h: '):text('Seek to the start of the line'):newline() - osd:tab():item('ctrl+g: '):text('Toggle animated snapshots'):newline() - osd:tab():item('ctrl+shift+h: '):text('Replay current subtitle'):newline() - osd:tab():item('shift+h/l: '):text('Seek to the previous/next subtitle'):newline() - osd:tab():item('alt+h/l: '):text('Seek to the previous/next subtitle and pause'):newline() - osd:italics("Press "):item('i'):italics(" to hide mpvacious options."):newline() - elseif self.hints_state.get() == 'menu' then - osd:submenu('Menu bindings'):newline() - osd:tab():item('c: '):text('Set timings to the current sub'):newline() - osd:tab():item('s: '):text('Set start time to current position'):newline() - osd:tab():item('e: '):text('Set end time to current position'):newline() - osd:tab():item('shift+s: '):text('Set start time to current subtitle'):newline() - osd:tab():item('shift+e: '):text('Set end time to current subtitle'):newline() - osd:tab():item('f: '):text('Increment # cards to update '):italics('(+shift to decrement)'):newline() - osd:tab():item('r: '):text('Reset timings'):newline() - osd:tab():item('n: '):text('Export note'):newline() - osd:tab():item('g: '):text('GUI export'):newline() - osd:tab():item('m: '):text('Update the last added note '):italics('(+shift to overwrite)'):newline() - osd:tab():item('t: '):text('Toggle clipboard autocopy'):newline() - osd:tab():item('T: '):text('Switch to the next clipboard method'):newline() - osd:tab():item('p: '):text('Switch to next profile'):newline() - osd:tab():item('ESC: '):text('Close'):newline() - osd:italics("Press "):item('i'):italics(" to show global bindings."):newline() - elseif self.hints_state.get() == 'hidden' then - -- Menu bindings are active but hidden - else - osd:italics("Press "):item('i'):italics(" to show menu bindings."):newline() - end -end - -function menu:warn_formats(osd) - if config.use_ffmpeg then - return - end - for type, codecs in pairs(codec_support) do - for codec, supported in pairs(codecs) do - if not supported and config[type .. '_codec'] == codec then - osd:red('warning: '):newline() - osd:tab():text(string.format("your version of mpv does not support %s.", codec)):newline() - osd:tab():text(string.format("mpvacious won't be able to create %s files.", type)):newline() - end - end - end -end - -function menu:warn_clipboard(osd) - if subs_observer.autocopy_current_method() == "clipboard" and platform.healthy == false then - osd:red('warning: '):text(string.format("%s is not installed.", platform.clip_util)):newline() - end -end - -function menu:print_legend(osd) - osd:new_layer():size(config.menu_font_size):font(config.menu_font_name):align(4) - self:print_header(osd) - self:print_bindings(osd) - self:warn_formats(osd) - self:warn_clipboard(osd) -end - -function menu:print_selection(osd) - if subs_observer.is_appending() and config.show_selected_text then - osd:new_layer():size(config.menu_font_size):font(config.menu_font_name):align(6) - osd:submenu("Primary text"):newline() - for _, s in ipairs(subs_observer.recorded_subs()) do - osd:text(escape_for_osd(s['text'])):newline() - end - if not h.is_empty(config.secondary_field) then - -- If the user wants to add secondary subs to Anki, - -- it's okay to print them on the screen. - osd:submenu("Secondary text"):newline() - for _, s in ipairs(subs_observer.recorded_secondary_subs()) do - osd:text(escape_for_osd(s['text'])):newline() - end - end - end -end - -function menu:make_osd() - local osd = OSD:new() - self:print_legend(osd) - self:print_selection(osd) - return osd -end - ------------------------------------------------------------- ---quick_menu line selection -local choose_cards = function(i) - quick_creation_opts:set_cards(i) - quick_menu_card:close() - quick_menu:open() -end -local choose_lines = function(i) - quick_creation_opts:set_lines(i) - update_last_note(true) - quick_menu:close() -end - -quick_menu = Menu:new() -quick_menu.keybindings = {} -for i = 1, 9 do - table.insert(quick_menu.keybindings, { key = tostring(i), fn = function() - choose_lines(i) - end }) -end -table.insert(quick_menu.keybindings, { key = 'g', fn = function() - choose_lines(1) -end }) -table.insert(quick_menu.keybindings, { key = 'ESC', fn = function() - quick_menu:close() -end }) -table.insert(quick_menu.keybindings, { key = 'q', fn = function() - quick_menu:close() -end }) -function quick_menu:print_header(osd) - osd:submenu('quick card creation: line selection'):newline() - osd:item('# lines: '):text('Enter 1-9'):newline() -end -function quick_menu:print_legend(osd) - osd:new_layer():size(config.menu_font_size):font(config.menu_font_name):align(4) - self:print_header(osd) - menu:warn_formats(osd) -end -function quick_menu:make_osd() - local osd = OSD:new() - self:print_legend(osd) - return osd -end - --- quick_menu card selection -quick_menu_card = Menu:new() -quick_menu_card.keybindings = {} -for i = 1, 9 do - table.insert(quick_menu_card.keybindings, { key = tostring(i), fn = function() - choose_cards(i) - end }) -end -table.insert(quick_menu_card.keybindings, { key = 'ESC', fn = function() - quick_menu_card:close() -end }) -table.insert(quick_menu_card.keybindings, { key = 'q', fn = function() - quick_menu_card:close() -end }) -function quick_menu_card:print_header(osd) - osd:submenu('quick card creation: card selection'):newline() - osd:item('# cards: '):text('Enter 1-9'):newline() -end -function quick_menu_card:print_legend(osd) - osd:new_layer():size(config.menu_font_size):font(config.menu_font_name):align(4) - self:print_header(osd) - menu:warn_formats(osd) -end -function quick_menu_card:make_osd() - local osd = OSD:new() - self:print_legend(osd) - return osd -end - ------------------------------------------------------------- --- main - -local main = (function() - local main_executed = false - return function() - if main_executed then - subs_observer.clear_all_dialogs() - return - else - main_executed = true - end - - cfg_mgr.init(config, profiles) - ankiconnect.init(config, platform) - forvo.init(config, platform) - encoder.init(config) - secondary_sid.init(config) - ensure_deck() - subs_observer.init(menu, config) - - -- Key bindings - mp.add_forced_key_binding("Ctrl+c", "mpvacious-copy-sub-to-clipboard", subs_observer.copy_current_primary_to_clipboard) - mp.add_key_binding("Ctrl+C", "mpvacious-copy-secondary-sub-to-clipboard", subs_observer.copy_current_secondary_to_clipboard) - mp.add_key_binding("Ctrl+t", "mpvacious-autocopy-toggle", subs_observer.toggle_autocopy) - mp.add_key_binding("Ctrl+g", "mpvacious-animated-snapshot-toggle", encoder.snapshot.toggle_animation) - - -- Secondary subtitles - mp.add_key_binding("Ctrl+v", "mpvacious-secondary-sid-toggle", secondary_sid.change_visibility) - mp.add_key_binding("Ctrl+k", "mpvacious-secondary-sid-prev", secondary_sid.select_previous) - mp.add_key_binding("Ctrl+j", "mpvacious-secondary-sid-next", secondary_sid.select_next) - - -- Open advanced menu - mp.add_key_binding("a", "mpvacious-menu-open", function() - menu:open() - end) - - -- Add note - mp.add_forced_key_binding("Ctrl+n", "mpvacious-export-note", menu:with_update { export_to_anki, false }) - - -- Note updating - mp.add_key_binding("Ctrl+m", "mpvacious-update-last-note", menu:with_update { update_last_note, false }) - mp.add_key_binding("Ctrl+M", "mpvacious-overwrite-last-note", menu:with_update { update_last_note, true }) - - mp.add_key_binding("g", "mpvacious-quick-card-menu-open", function() - quick_menu:open() - end) - mp.add_key_binding("Alt+g", "mpvacious-quick-card-sel-menu-open", function() - quick_menu_card:open() - end) - - -- Vim-like seeking between subtitle lines - mp.add_key_binding("H", "mpvacious-sub-seek-back", _ { play_control.sub_seek, 'backward' }) - mp.add_key_binding("L", "mpvacious-sub-seek-forward", _ { play_control.sub_seek, 'forward' }) - - mp.add_key_binding("Alt+h", "mpvacious-sub-seek-back-pause", _ { play_control.sub_seek, 'backward', true }) - mp.add_key_binding("Alt+l", "mpvacious-sub-seek-forward-pause", _ { play_control.sub_seek, 'forward', true }) - - mp.add_key_binding("Ctrl+h", "mpvacious-sub-rewind", _ { play_control.sub_rewind }) - mp.add_key_binding("Ctrl+H", "mpvacious-sub-replay", _ { play_control.play_till_sub_end }) - mp.add_key_binding("Ctrl+L", "mpvacious-sub-play-up-to-next", _ { play_control.play_till_next_sub_end }) - - mp.msg.warn("Press 'a' to open the mpvacious menu.") - end -end)() - -mp.register_event("file-loaded", main) diff --git a/config/mpv/scripts/subs2srsa/subtitles/observer.lua b/config/mpv/scripts/subs2srsa/subtitles/observer.lua deleted file mode 100644 index d9284c5..0000000 --- a/config/mpv/scripts/subs2srsa/subtitles/observer.lua +++ /dev/null @@ -1,344 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Observer waits for subtitles to appear on the screen and adds them to a list. -]] - -local h = require('helpers') -local timings = require('utils.timings') -local sub_list = require('subtitles.sub_list') -local Subtitle = require('subtitles.subtitle') -local mp = require('mp') -local platform = require('platform.init') -local switch = require('utils.switch') - -local self = {} - -local dialogs = sub_list.new() -local secondary_dialogs = sub_list.new() -local all_dialogs = sub_list.new() -local all_secondary_dialogs = sub_list.new() -local user_timings = timings.new() - -local append_dialogue = false -local autoclip_enabled = false -local autoclip_method = {} - - ------------------------------------------------------------- --- private - -local function copy_primary_sub() - if autoclip_enabled then - autoclip_method.call() - end -end - -local function append_primary_sub() - local current_sub = Subtitle:now() - all_dialogs.insert(current_sub) - if append_dialogue and dialogs.insert(current_sub) then - self.menu:update() - end -end - -local function append_secondary_sub() - local current_secondary = Subtitle:now('secondary') - all_secondary_dialogs.insert(current_secondary) - if append_dialogue and secondary_dialogs.insert(Subtitle:now('secondary')) then - self.menu:update() - end -end - -local function start_appending() - append_dialogue = true - append_primary_sub() - append_secondary_sub() -end - -local function handle_primary_sub() - append_primary_sub() - copy_primary_sub() -end - -local function handle_secondary_sub() - append_secondary_sub() -end - -local function on_external_finish(success, result, error) - if success ~= true or error ~= nil then - h.notify("Command failed: " .. table.concat(result)) - end -end - -local function external_command_args(cur_lines) - local args = {} - for arg in string.gmatch(self.config.autoclip_custom_args, "%S+") do - if arg == '%MPV_PRIMARY%' then - arg = cur_lines.primary - elseif arg == '%MPV_SECONDARY%' then - arg = cur_lines.secondary - end - table.insert(args, arg) - end - return args -end - -local function call_external_command(cur_lines) - if not h.is_empty(self.config.autoclip_custom_args) then - h.subprocess(external_command_args(cur_lines), on_external_finish) - end -end - -local function current_subtitle_lines() - local primary = dialogs.get_text() - - if h.is_empty(primary) then - primary = mp.get_property("sub-text") - end - - if h.is_empty(primary) then - return nil - end - - local secondary = secondary_dialogs.get_text() - - if h.is_empty(secondary) then - secondary = mp.get_property("secondary-sub-text") or "" - end - - return { primary = self.clipboard_prepare(primary), secondary = secondary } -end - -local function ensure_goldendict_running() - --- Ensure that goldendict is running and is disowned by mpv. - --- Avoid goldendict getting killed when mpv exits. - if autoclip_enabled and self.autocopy_current_method() == "goldendict" then - os.execute("setsid -f goldendict") - end -end - ------------------------------------------------------------- --- autoclip methods - -autoclip_method = (function() - local methods = { 'clipboard', 'goldendict', 'custom_command', } - local current_method = switch.new(methods) - - local function call() - local cur_lines = current_subtitle_lines() - if h.is_empty(cur_lines) then - return - end - - if current_method.get() == 'clipboard' then - self.copy_to_clipboard("autocopy action", cur_lines.primary) - elseif current_method.get() == 'goldendict' then - h.subprocess_detached({ 'goldendict', cur_lines.primary }, on_external_finish) - elseif current_method.get() == 'custom_command' then - call_external_command(cur_lines) - end - end - - return { - call = call, - get = current_method.get, - bump = current_method.bump, - set = current_method.set, - } -end)() - -local function copy_subtitle(subtitle_id) - self.copy_to_clipboard("copy-on-demand", mp.get_property(subtitle_id)) -end - ------------------------------------------------------------- --- public - -self.copy_to_clipboard = function(_, text) - if platform.healthy == false then - h.notify(platform.clip_util .. " is not installed.", "error", 5) - end - if not h.is_empty(text) then - platform.copy_to_clipboard(self.clipboard_prepare(text)) - end -end - -self.clipboard_prepare = function(text) - text = self.config.clipboard_trim_enabled and h.trim(text) or h.remove_newlines(text) - text = self.maybe_remove_all_spaces(text) - return text -end - -self.maybe_remove_all_spaces = function(str) - if self.config.nuke_spaces == true and h.contains_non_latin_letters(str) then - return h.remove_all_spaces(str) - else - return str - end -end - -self.copy_current_primary_to_clipboard = function() - copy_subtitle("sub-text") -end - -self.copy_current_secondary_to_clipboard = function() - copy_subtitle("secondary-sub-text") -end - -self.user_altered = function() - --- Return true if the user manually set at least start or end. - return user_timings.is_set('start') or user_timings.is_set('end') -end - -self.get_timing = function(position) - if user_timings.is_set(position) then - return user_timings.get(position) - elseif not dialogs.is_empty() then - return dialogs.get_time(position) - end - return -1 -end - -self.collect_from_all_dialogues = function(n_lines) - local current_sub = Subtitle:now() - local current_secondary_sub = Subtitle:now('secondary') - all_dialogs.insert(current_sub) - all_secondary_dialogs.insert(current_secondary_sub) - if current_sub == nil then - return Subtitle:new() -- return a default empty new Subtitle to let consumer handle - end - local text, end_sub = all_dialogs.get_n_text(current_sub, n_lines) - local secondary_text, _ - if current_secondary_sub == nil then - secondary_text = '' - else - secondary_text, _ = all_secondary_dialogs.get_n_text(current_secondary_sub, n_lines) -- we'll use main sub's timing - end - return Subtitle:new { - ['text'] = text, - ['secondary'] = secondary_text, - ['start'] = current_sub['start'], - ['end'] = end_sub['end'], - } -end - -self.collect_from_current = function() - --- Return all recorded subtitle lines as one subtitle object. - --- The caller has to call subs_observer.clear() afterwards. - if dialogs.is_empty() then - dialogs.insert(Subtitle:now()) - end - if secondary_dialogs.is_empty() then - secondary_dialogs.insert(Subtitle:now('secondary')) - end - return Subtitle:new { - ['text'] = dialogs.get_text(), - ['secondary'] = secondary_dialogs.get_text(), - ['start'] = self.get_timing('start'), - ['end'] = self.get_timing('end'), - } -end - -self.set_manual_timing = function(position) - user_timings.set(position, mp.get_property_number('time-pos') - mp.get_property("audio-delay")) - h.notify(h.capitalize_first_letter(position) .. " time has been set.") - start_appending() -end - -self.set_manual_timing_to_sub = function(position) - local sub = Subtitle:now() - if sub then - user_timings.set(position, sub[position] - mp.get_property("audio-delay")) - h.notify(h.capitalize_first_letter(position) .. " time has been set.") - start_appending() - else - h.notify("There's no visible subtitle.", "info", 2) - end -end - -self.set_to_current_sub = function() - self.clear() - if Subtitle:now() then - start_appending() - h.notify("Timings have been set to the current sub.", "info", 2) - else - h.notify("There's no visible subtitle.", "info", 2) - end -end - -self.clear = function() - append_dialogue = false - dialogs = sub_list.new() - secondary_dialogs = sub_list.new() - user_timings = timings.new() -end - -self.clear_all_dialogs = function() - all_dialogs = sub_list.new() - all_secondary_dialogs = sub_list.new() -end - -self.clear_and_notify = function() - --- Clear then notify the user. - --- Called by the OSD menu when the user presses a button to drop recorded subtitles. - self.clear() - h.notify("Timings have been reset.", "info", 2) -end - -self.is_appending = function() - return append_dialogue -end - -self.recorded_subs = function() - return dialogs.get_subs_list() -end - -self.recorded_secondary_subs = function() - return secondary_dialogs.get_subs_list() -end - -self.autocopy_status_str = function() - return string.format( - "%s (%s)", - (autoclip_enabled and 'enabled' or 'disabled'), - autoclip_method.get():gsub('_', ' ') - ) -end - -self.autocopy_current_method = function() - return autoclip_method.get() -end - -local function notify_autocopy() - if autoclip_enabled then - copy_primary_sub() - end - h.notify(string.format("Clipboard autocopy has been %s.", self.autocopy_status_str())) -end - -self.toggle_autocopy = function() - autoclip_enabled = not autoclip_enabled - notify_autocopy() -end - -self.next_autoclip_method = function() - autoclip_method.bump() - notify_autocopy() -end - -self.init = function(menu, config) - self.menu = menu - self.config = config - - -- The autoclip state is copied as a local value - -- to prevent it from being reset when the user reloads the config file. - autoclip_enabled = self.config.autoclip - autoclip_method.set(self.config.autoclip_method) - - mp.observe_property("sub-text", "string", handle_primary_sub) - mp.observe_property("secondary-sub-text", "string", handle_secondary_sub) -end - -return self diff --git a/config/mpv/scripts/subs2srsa/subtitles/secondary_sid.lua b/config/mpv/scripts/subs2srsa/subtitles/secondary_sid.lua deleted file mode 100644 index 21576e8..0000000 --- a/config/mpv/scripts/subs2srsa/subtitles/secondary_sid.lua +++ /dev/null @@ -1,196 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -This module automatically finds and sets secondary sid if it's not already set. -Secondary sid will be shown when mouse is moved to the top part of the mpv window. -]] - -local mp = require('mp') -local h = require('helpers') - -local self = { - visibility = 'auto', - visibility_states = { auto = true, never = true, always = true, }, -} - -local function is_accepted_language(sub_lang) - -- for missing keys compares nil to true - return self.accepted_languages[sub_lang] == true -end - -local function is_selected_language(track, active_track) - return track.id == mp.get_property_native('sid') or (active_track and active_track.lang == track.lang) -end - -local function is_full(track) - return h.str_contains(track.title, 'full') -end - -local function is_garbage(track) - for _, keyword in pairs({ 'song', 'sign', 'caption', 'commentary' }) do - if h.str_contains(track.title, keyword) then - return true - end - end - return false -end - -local function prioritize_full_subs(tracks_list) - return table.sort(tracks_list, function(first, second) - return (is_full(first) and not is_full(second)) or (is_garbage(second) and not is_garbage(first)) - end) -end - -local function find_best_secondary_sid() - local active_track = h.get_active_track('sub') - local sub_tracks = h.get_loaded_tracks('sub') - prioritize_full_subs(sub_tracks) - for _, track in ipairs(sub_tracks) do - if is_accepted_language(track.lang) and not is_selected_language(track, active_track) then - return track.id - end - end - return nil -end - -local function window_height() - return mp.get_property_native('osd-dimensions/h') -end - -local function get_accepted_sub_langs() - local languages = {} - for lang in self.config.secondary_sub_lang:gmatch('[a-zA-Z-]+') do - languages[lang] = true - end - return languages -end - -local function on_mouse_move(_, state) - -- state = {x=int,y=int, hover=true|false, } - if mp.get_property_native('secondary-sid') and self.visibility == 'auto' and state ~= nil then - mp.set_property_bool( - 'secondary-sub-visibility', - state.hover and (state.y / window_height()) < self.config.secondary_sub_area - ) - end -end - -local function on_file_loaded() - -- If secondary sid is not already set, try to find and set it. - local secondary_sid = mp.get_property_native('secondary-sid') - if secondary_sid == false and self.config.secondary_sub_auto_load == true then - secondary_sid = find_best_secondary_sid() - if secondary_sid ~= nil then - mp.set_property_native('secondary-sid', secondary_sid) - end - end -end - -local function update_visibility() - mp.set_property_bool('secondary-sub-visibility', self.visibility == 'always') -end - -local function init(config) - self.config = config - self.visibility = config.secondary_sub_visibility - self.accepted_languages = get_accepted_sub_langs() - mp.register_event('file-loaded', on_file_loaded) - if config.secondary_sub_area > 0 then - mp.observe_property('mouse-pos', 'native', on_mouse_move) - end - update_visibility() -end - -local function change_visibility() - while true do - self.visibility = next(self.visibility_states, self.visibility) - if self.visibility ~= nil then - break - end - end - update_visibility() - h.notify("Secondary sid visibility: " .. self.visibility) -end - -local function compare_by_preference_then_id(track1, track2) - if is_accepted_language(track1.lang) and not is_accepted_language(track2.lang) then - return true - elseif not is_accepted_language(track1.lang) and is_accepted_language(track2.lang) then - return false - else - return (track1.id < track2.id) - end -end - -local function split_before_after(previous_tracks, next_tracks, all_tracks, current_track_id) - -- works like take_while() and drop_while() combined - local prev = true - for _, track in ipairs(all_tracks) do - if prev == true and track.id == current_track_id then - prev = false - end - if track.id ~= current_track_id then - if prev then - table.insert(previous_tracks, track) - else - table.insert(next_tracks, track) - end - end - end -end - -local function not_primary_sid(track) - return mp.get_property_native('sid') ~= track.id -end - -local function find_new_secondary_sub(direction) - local subtitle_tracks = h.filter(h.get_loaded_tracks('sub'), not_primary_sid) - table.sort(subtitle_tracks, compare_by_preference_then_id) - - local secondary_sid = mp.get_property_native('secondary-sid') - local new_secondary_sub = { id = false, title = "removed" } - - if #subtitle_tracks > 0 then - if not secondary_sid then - new_secondary_sub = (direction == 'prev') and subtitle_tracks[#subtitle_tracks] or subtitle_tracks[1] - else - local previous_tracks = {} - local next_tracks = {} - split_before_after(previous_tracks, next_tracks, subtitle_tracks, secondary_sid) - if direction == 'prev' and #previous_tracks > 0 then - new_secondary_sub = previous_tracks[#previous_tracks] - elseif direction == 'next' and #next_tracks > 0 then - new_secondary_sub = next_tracks[1] - end - end - end - return new_secondary_sub -end - -local function switch_secondary_sid(direction) - local new_secondary_sub = find_new_secondary_sub(direction) - - mp.set_property_native('secondary-sid', new_secondary_sub.id) - if new_secondary_sub.id == false then - h.notify("Removed secondary sid.") - else - h.notify(string.format( - "Secondary #%d: %s (%s)", - new_secondary_sub.id, - new_secondary_sub.title or "No title", - new_secondary_sub.lang or "Unknown" - )) - end -end - -return { - init = init, - change_visibility = change_visibility, - select_previous = function() - switch_secondary_sid('prev') - end, - select_next = function() - switch_secondary_sid('next') - end, -} diff --git a/config/mpv/scripts/subs2srsa/subtitles/sub_list.lua b/config/mpv/scripts/subs2srsa/subtitles/sub_list.lua deleted file mode 100644 index de7e1c2..0000000 --- a/config/mpv/scripts/subs2srsa/subtitles/sub_list.lua +++ /dev/null @@ -1,74 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Subtitle list remembers selected subtitle lines. -]] - -local h = require('helpers') - -local new_sub_list = function() - local subs_list = {} - - local find_i = function(sub) - for i, v in ipairs(subs_list) do - if sub < v then - return i - end - end - return #subs_list + 1 - end - local get_time = function(position) - local i = position == 'start' and 1 or #subs_list - return subs_list[i][position] - end - local get_text = function() - local speech = {} - for _, sub in ipairs(subs_list) do - table.insert(speech, sub['text']) - end - return table.concat(speech, ' ') - end - local get_n_text = function(sub, n_lines) - local speech = {} - local end_sub = sub - for _, v in ipairs(subs_list) do - if v['start'] - end_sub['end'] >= 20 then - break - end - if v >= sub and #speech < n_lines then - table.insert(speech, v['text']) - end_sub = v - end - end - return table.concat(speech, ' '), end_sub - end - local insert = function(sub) - if sub ~= nil and not h.contains(subs_list, sub) then - table.insert(subs_list, find_i(sub), sub) - return true - end - return false - end - local get_subs_list = function() - local copy = {} - for key, value in pairs(subs_list) do - copy[key] = value - end - return copy - end - return { - get_subs_list = get_subs_list, - get_time = get_time, - get_text = get_text, - get_n_text = get_n_text, - insert = insert, - is_empty = function() - return h.is_empty(subs_list) - end, - } -end - -return { - new = new_sub_list, -} diff --git a/config/mpv/scripts/subs2srsa/subtitles/subtitle.lua b/config/mpv/scripts/subs2srsa/subtitles/subtitle.lua deleted file mode 100644 index 4e8df41..0000000 --- a/config/mpv/scripts/subs2srsa/subtitles/subtitle.lua +++ /dev/null @@ -1,56 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Subtitle class provides methods for storing and comparing subtitle lines. -]] - -local mp = require('mp') - -local Subtitle = { - ['text'] = '', - ['secondary'] = '', - ['start'] = -1, - ['end'] = -1, -} - -function Subtitle:new(o) - o = o or {} - setmetatable(o, self) - self.__index = self - return o -end - -function Subtitle:now(secondary) - local prefix = secondary and "secondary-" or "" - local this = self:new { - ['text'] = mp.get_property(prefix .. "sub-text"), - ['start'] = mp.get_property_number(prefix .. "sub-start"), - ['end'] = mp.get_property_number(prefix .. "sub-end"), - } - if this:is_valid() then - return this:delay(mp.get_property_native("sub-delay") - mp.get_property_native("audio-delay")) - else - return nil - end -end - -function Subtitle:delay(delay) - self['start'] = self['start'] + delay - self['end'] = self['end'] + delay - return self -end - -function Subtitle:is_valid() - return self['start'] and self['end'] and self['start'] >= 0 and self['end'] > self['start'] -end - -Subtitle.__eq = function(lhs, rhs) - return lhs['text'] == rhs['text'] -end - -Subtitle.__lt = function(lhs, rhs) - return lhs['start'] < rhs['start'] -end - -return Subtitle diff --git a/config/mpv/scripts/subs2srsa/test.lua b/config/mpv/scripts/subs2srsa/test.lua deleted file mode 100644 index d1291d2..0000000 --- a/config/mpv/scripts/subs2srsa/test.lua +++ /dev/null @@ -1,32 +0,0 @@ -local helpers = require('helpers') - -local function assert_equals(expected, actual) - if expected ~= actual then - error(string.format("TEST FAILED: Expected '%s', got '%s'", expected, actual)) - end -end - -local function test_get_episode_number() - local test_cases = { - { nil, "A Whisker Away.mkv" }, - { nil, "[Placeholder] Gekijouban SHIROBAKO [Ma10p_1080p][x265_flac]" }, - { "06", "[Placeholder] Sono Bisque Doll wa Koi wo Suru - 06 [54E495D0]" }, - { "02", "(Hi10)_Kobayashi-san_Chi_no_Maid_Dragon_-_02_(BD_1080p)_(Placeholder)_(12C5D2B4)" }, - { "01", "[Placeholder] Koi to Yobu ni wa Kimochi Warui - 01 (1080p) [D517C9F0]" }, - { "01", "[Placeholder] Tsukimonogatari 01 [BD 1080p x264 10-bit FLAC] [5CD88145]" }, - { "01", "[Placeholder] 86 - Eighty Six - 01 (1080p) [1B13598F]" }, - { "00", "[Placeholder] Fate Stay Night - Unlimited Blade Works - 00 (BD 1080p Hi10 FLAC) [95590B7F]" }, - { "01", "House, M.D. S01E01 Pilot - Everybody Lies (1080p x265 Placeholder)" }, - { "165", "A Generic Episode-165" } - } - - for _, case in pairs(test_cases) do - local _, _, episode_num = helpers.get_episode_number(case[2]) - assert_equals(case[1], episode_num) - end -end - --- Runs tests -test_get_episode_number() - -os.exit(print("Tests passed")) diff --git a/config/mpv/scripts/subs2srsa/utils/base64.lua b/config/mpv/scripts/subs2srsa/utils/base64.lua deleted file mode 100644 index 0fe2d06..0000000 --- a/config/mpv/scripts/subs2srsa/utils/base64.lua +++ /dev/null @@ -1,46 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Encoding and decoding in base64 -]] - --- http://lua-users.org/wiki/BaseSixtyFour - --- character table string -local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' - --- encoding -local function enc(data) - return ((data:gsub('.', function(x) - local r,b='',x:byte() - for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end - return r; - end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) - if (#x < 6) then return '' end - local c=0 - for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end - return b:sub(c+1,c+1) - end)..({ '', '==', '=' })[#data%3+1]) -end - --- decoding -local function dec(data) - data = string.gsub(data, '[^'..b..'=]', '') - return (data:gsub('.', function(x) - if (x == '=') then return '' end - local r,f='',(b:find(x)-1) - for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end - return r; - end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) - if (#x ~= 8) then return '' end - local c=0 - for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end - return string.char(c) - end)) -end - -return { - enc = enc, - dec = dec, -} diff --git a/config/mpv/scripts/subs2srsa/utils/filename_factory.lua b/config/mpv/scripts/subs2srsa/utils/filename_factory.lua deleted file mode 100644 index a794fc7..0000000 --- a/config/mpv/scripts/subs2srsa/utils/filename_factory.lua +++ /dev/null @@ -1,89 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Creates image and audio filenames compatible with Anki. -]] - -local mp = require('mp') -local h = require('helpers') - -local filename - -local anki_compatible_length = (function() - -- Anki forcibly mutilates all filenames longer than 119 bytes when you run `Tools->Check Media...`. - local allowed_bytes = 119 - local timestamp_bytes = #'_99h99m99s999ms-99h99m99s999ms.webp' - - return function(str, timestamp) - -- if timestamp provided, recalculate limit_bytes - local limit_bytes = allowed_bytes - (timestamp and #timestamp or timestamp_bytes) - - if #str <= limit_bytes then - return str - end - - local bytes_per_char = h.contains_non_latin_letters(str) and #'車' or #'z' - local limit_chars = math.floor(limit_bytes / bytes_per_char) - - if limit_chars == limit_bytes then - return str:sub(1, limit_bytes) - end - - local ret = h.subprocess { - 'awk', - '-v', string.format('str=%s', str), - '-v', string.format('limit=%d', limit_chars), - 'BEGIN{print substr(str, 1, limit); exit}' - } - - if ret.status == 0 then - ret.stdout = h.remove_newlines(ret.stdout) - ret.stdout = h.remove_leading_trailing_spaces(ret.stdout) - return ret.stdout - else - return 'subs2srs_' .. os.time() - end - end -end)() - -local make_media_filename = function() - filename = mp.get_property("filename") -- filename without path - filename = h.remove_extension(filename) - filename = h.remove_filename_text_in_parentheses(filename) - filename = h.remove_text_in_brackets(filename) - filename = h.remove_special_characters(filename) -end - -local function timestamp_range(start_timestamp, end_timestamp, extension) - -- Generates a filename suffix of the form: _00h00m00s000ms-99h99m99s999ms.extension - -- Extension must already contain the dot. - return string.format( - '_%s_%s%s', - h.human_readable_time(start_timestamp), - h.human_readable_time(end_timestamp), - extension - ) -end - -local function timestamp_static(timestamp, extension) - -- Generates a filename suffix of the form: _00h00m00s000ms.extension - -- Extension must already contain the dot. - return string.format( - '_%s%s', - h.human_readable_time(timestamp), - extension - ) -end - -local make_filename = function(...) - local args = {...} - local timestamp = #args < 3 and timestamp_static(...) or timestamp_range(...) - return string.lower(anki_compatible_length(filename, timestamp) .. timestamp) -end - -mp.register_event("file-loaded", make_media_filename) - -return { - make_filename = make_filename, -} diff --git a/config/mpv/scripts/subs2srsa/utils/forvo.lua b/config/mpv/scripts/subs2srsa/utils/forvo.lua deleted file mode 100644 index 09bc596..0000000 --- a/config/mpv/scripts/subs2srsa/utils/forvo.lua +++ /dev/null @@ -1,145 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Utils for downloading pronunciations from Forvo -]] - -local utils = require('mp.utils') -local msg = require('mp.msg') -local h = require('helpers') -local base64 = require('utils.base64') -local self = { - output_dir_path = nil, -} - -local function url_encode(url) - -- https://gist.github.com/liukun/f9ce7d6d14fa45fe9b924a3eed5c3d99 - local char_to_hex = function(c) - return string.format("%%%02X", string.byte(c)) - end - if url == nil then - return - end - url = url:gsub("\n", "\r\n") - url = url:gsub("([^%w _%%%-%.~])", char_to_hex) - url = url:gsub(" ", "+") - return url -end - -local function reencode(source_path, dest_path) - local args = { - 'mpv', - source_path, - '--loop-file=no', - '--keep-open=no', - '--video=no', - '--no-ocopy-metadata', - '--no-sub', - '--audio-channels=mono', - '--oacopts-add=vbr=on', - '--oacopts-add=application=voip', - '--oacopts-add=compression_level=10', - '--af-append=silenceremove=1:0:-50dB', - table.concat { '--oac=', self.config.audio_codec }, - table.concat { '--of=', self.config.audio_format }, - table.concat { '--oacopts-add=b=', self.config.audio_bitrate }, - table.concat { '-o=', dest_path } - } - return h.subprocess(args) -end - -local function reencode_and_store(source_path, filename) - local reencoded_path = utils.join_path(self.output_dir_path, filename) - local result = reencode(source_path, reencoded_path) - return result.status == 0 -end - -local function curl_save(source_url, save_location) - local curl_args = { 'curl', source_url, '-s', '-L', '-o', save_location } - return h.subprocess(curl_args).status == 0 -end - -local function get_pronunciation_url(word) - local file_format = self.config.audio_extension:sub(2) - local forvo_page = h.subprocess { 'curl', '-s', string.format('https://forvo.com/search/%s/ja', url_encode(word)) }.stdout - local play_params = string.match(forvo_page, "Play%((.-)%);") - - if play_params then - local iter = string.gmatch(play_params, "'(.-)'") - local formats = { mp3 = iter(), ogg = iter() } - return string.format('https://audio00.forvo.com/%s/%s', file_format, base64.dec(formats[file_format])) - end -end - -local function make_forvo_filename(word) - return string.format('forvo_%s%s', self.platform.windows and os.time() or word, self.config.audio_extension) -end - -local function get_forvo_pronunciation(word) - local audio_url = get_pronunciation_url(word) - - if h.is_empty(audio_url) then - msg.warn(string.format("Seems like Forvo doesn't have audio for word %s.", word)) - return - end - - local filename = make_forvo_filename(word) - local tmp_filepath = utils.join_path(self.platform.tmp_dir(), filename) - - local result - if curl_save(audio_url, tmp_filepath) and reencode_and_store(tmp_filepath, filename) then - result = string.format(self.config.audio_template, filename) - else - msg.warn(string.format("Couldn't download audio for word %s from Forvo.", word)) - end - - os.remove(tmp_filepath) - return result -end - -local append = function(new_data, stored_data) - if self.config.use_forvo == 'no' then - -- forvo functionality was disabled in the config file - return new_data - end - - if type(stored_data[self.config.vocab_audio_field]) ~= 'string' then - -- there is no field configured to store forvo pronunciation - return new_data - end - - if h.is_empty(stored_data[self.config.vocab_field]) then - -- target word field is empty. can't continue. - return new_data - end - - if self.config.use_forvo == 'always' or h.is_empty(stored_data[self.config.vocab_audio_field]) then - local forvo_pronunciation = get_forvo_pronunciation(stored_data[self.config.vocab_field]) - if not h.is_empty(forvo_pronunciation) then - if self.config.vocab_audio_field == self.config.audio_field then - -- improperly configured fields. don't lose sentence audio - new_data[self.config.audio_field] = forvo_pronunciation .. new_data[self.config.audio_field] - else - new_data[self.config.vocab_audio_field] = forvo_pronunciation - end - end - end - - return new_data -end - -local set_output_dir = function(dir_path) - self.output_dir_path = dir_path -end - -local function init(config, platform) - self.config = config - self.platform = platform -end - -return { - append = append, - init = init, - set_output_dir = set_output_dir, -} diff --git a/config/mpv/scripts/subs2srsa/utils/pause_timer.lua b/config/mpv/scripts/subs2srsa/utils/pause_timer.lua deleted file mode 100644 index e37b0ea..0000000 --- a/config/mpv/scripts/subs2srsa/utils/pause_timer.lua +++ /dev/null @@ -1,33 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Pause timer stops playback when reaching a set timing. -]] - -local mp = require('mp') -local stop_time = -1 -local check_stop - -local set_stop_time = function(time) - stop_time = time - mp.observe_property("time-pos", "number", check_stop) -end - -local stop = function() - mp.unobserve_property(check_stop) - stop_time = -1 -end - -check_stop = function(_, time) - if time > stop_time then - stop() - mp.set_property("pause", "yes") - end -end - -return { - set_stop_time = set_stop_time, - check_stop = check_stop, - stop = stop, -} diff --git a/config/mpv/scripts/subs2srsa/utils/play_control.lua b/config/mpv/scripts/subs2srsa/utils/play_control.lua deleted file mode 100644 index 901377d..0000000 --- a/config/mpv/scripts/subs2srsa/utils/play_control.lua +++ /dev/null @@ -1,61 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Provides additional methods for controlling playback. -]] - -local mp = require('mp') -local h = require('helpers') -local pause_timer = require('utils.pause_timer') -local Subtitle = require('subtitles.subtitle') - -local current_sub - -local function stop_at_the_end(sub) - pause_timer.set_stop_time(sub['end'] - 0.050) - h.notify("Playing till the end of the sub...", "info", 3) -end - -local function play_till_sub_end() - local sub = Subtitle:now() - mp.commandv('seek', sub['start'], 'absolute') - mp.set_property("pause", "no") - stop_at_the_end(sub) -end - -local function sub_seek(direction, pause) - mp.commandv("sub_seek", direction == 'backward' and '-1' or '1') - mp.commandv("seek", "0.015", "relative+exact") - if pause then - mp.set_property("pause", "yes") - end - pause_timer.stop() -end - -local function sub_rewind() - mp.commandv('seek', Subtitle:now()['start'] + 0.015, 'absolute') - pause_timer.stop() -end - -local function check_sub() - local sub = Subtitle:now() - if sub and sub ~= current_sub then - mp.unobserve_property(check_sub) - stop_at_the_end(sub) - end -end - -local function play_till_next_sub_end() - current_sub = Subtitle:now() - mp.observe_property("sub-text", "string", check_sub) - mp.set_property("pause", "no") - h.notify("Waiting till next sub...", "info", 10) -end - -return { - play_till_sub_end = play_till_sub_end, - play_till_next_sub_end = play_till_next_sub_end, - sub_seek = sub_seek, - sub_rewind = sub_rewind, -} diff --git a/config/mpv/scripts/subs2srsa/utils/switch.lua b/config/mpv/scripts/subs2srsa/utils/switch.lua deleted file mode 100644 index 5dac1c6..0000000 --- a/config/mpv/scripts/subs2srsa/utils/switch.lua +++ /dev/null @@ -1,38 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Switch cycles between values in a table. -]] - -local make_switch = function(states) - local self = { - states = states, - current_state = 1 - } - local bump = function() - self.current_state = self.current_state + 1 - if self.current_state > #self.states then - self.current_state = 1 - end - end - local get = function() - return self.states[self.current_state] - end - local set = function(new_state) - for idx, value in ipairs(self.states) do - if value == new_state then - self.current_state = idx - end - end - end - return { - bump = bump, - get = get, - set = set, - } -end - -return { - new = make_switch -} diff --git a/config/mpv/scripts/subs2srsa/utils/timings.lua b/config/mpv/scripts/subs2srsa/utils/timings.lua deleted file mode 100644 index d2408d6..0000000 --- a/config/mpv/scripts/subs2srsa/utils/timings.lua +++ /dev/null @@ -1,28 +0,0 @@ ---[[ -Copyright: Ren Tatsumoto and contributors -License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html - -Object that remembers manually set timings. -]] - -local new_timings = function() - local self = { ['start'] = -1, ['end'] = -1, } - local is_set = function(position) - return self[position] >= 0 - end - local set = function(position, time) - self[position] = time - end - local get = function(position) - return self[position] - end - return { - is_set = is_set, - set = set, - get = get, - } -end - -return { - new = new_timings, -} |
