From b69e9e161f3175c69f811c63ec1089986b7596e5 Mon Sep 17 00:00:00 2001 From: Games Date: Tue, 20 Aug 2024 17:38:01 -0400 Subject: [PATCH 1/6] modified the extract SRT subtitles to file so that it will extract ass subtitles. --- source/extract_ass_subtitles_to_files/LICENSE | 674 ++++++++++++++++++ .../extract_ass_subtitles_to_files/README.md | 9 + .../changelog.md | 34 + .../description.md | 15 + .../extract_ass_subtitles_to_files/icon.png | Bin 0 -> 20360 bytes .../extract_ass_subtitles_to_files/info.json | 19 + .../lib/__init__.py | 23 + .../lib/ffmpeg/LICENSE | 674 ++++++++++++++++++ .../lib/ffmpeg/README.md | 429 +++++++++++ .../lib/ffmpeg/__init__.py | 38 + .../lib/ffmpeg/mimetype_overrides.py | 66 ++ .../lib/ffmpeg/parser.py | 180 +++++ .../lib/ffmpeg/probe.py | 235 ++++++ .../lib/ffmpeg/stream_mapper.py | 503 +++++++++++++ .../lib/ffmpeg/tools.py | 130 ++++ .../extract_ass_subtitles_to_files/plugin.py | 358 ++++++++++ .../requirements.txt | 0 17 files changed, 3387 insertions(+) create mode 100644 source/extract_ass_subtitles_to_files/LICENSE create mode 100644 source/extract_ass_subtitles_to_files/README.md create mode 100644 source/extract_ass_subtitles_to_files/changelog.md create mode 100644 source/extract_ass_subtitles_to_files/description.md create mode 100644 source/extract_ass_subtitles_to_files/icon.png create mode 100644 source/extract_ass_subtitles_to_files/info.json create mode 100644 source/extract_ass_subtitles_to_files/lib/__init__.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/LICENSE create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/README.md create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/__init__.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/mimetype_overrides.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/parser.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/probe.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/stream_mapper.py create mode 100644 source/extract_ass_subtitles_to_files/lib/ffmpeg/tools.py create mode 100644 source/extract_ass_subtitles_to_files/plugin.py create mode 100644 source/extract_ass_subtitles_to_files/requirements.txt diff --git a/source/extract_ass_subtitles_to_files/LICENSE b/source/extract_ass_subtitles_to_files/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/source/extract_ass_subtitles_to_files/README.md b/source/extract_ass_subtitles_to_files/README.md new file mode 100644 index 000000000..028c3ceef --- /dev/null +++ b/source/extract_ass_subtitles_to_files/README.md @@ -0,0 +1,9 @@ +# Extract text subtitle streams to SRT files +Plugin for [Unmanic](https://github.com/Unmanic) + +--- + +### Information: + +- [Description](description.md) +- [Changelog](changelog.md) diff --git a/source/extract_ass_subtitles_to_files/changelog.md b/source/extract_ass_subtitles_to_files/changelog.md new file mode 100644 index 000000000..6315c636b --- /dev/null +++ b/source/extract_ass_subtitles_to_files/changelog.md @@ -0,0 +1,34 @@ +**0.0.9** +- Add settings to specify: + - Which languages to extract + - Whether to include "title" in output file name + +**0.0.8** +- Add library tester to search for files with SRT streams +- Update FFmpeg helper + +**0.0.7** +- Update FFmpeg helper +- Add platform declaration + +**0.0.6** +- Update Plugin for Unmanic v2 PluginHandler compatibility + +**0.0.5** +- Fix bug in stream mapping causing subtitles from the previous file being added to the command of the current file + +**0.0.4** +- Limit plugin to only process files with a "video" mimetype +- Remove support for older versions of Unmanic (requires >= 0.1.0) +- Fix issue when creating SRT file naming (TypeError: expected string or bytes-like object) +- Add better debug logging + +**0.0.3** +- Fix import issue in plugin file + +**0.0.2** +- Update Plugin for Unmanic v1 PluginHandler compatibility +- Update icon + +**0.0.1** +- Initial version diff --git a/source/extract_ass_subtitles_to_files/description.md b/source/extract_ass_subtitles_to_files/description.md new file mode 100644 index 000000000..167048aea --- /dev/null +++ b/source/extract_ass_subtitles_to_files/description.md @@ -0,0 +1,15 @@ + +Any SRT subtitle streams found in the file will be exported as *.srt files in the same directory as the original file. + +:::warning +This plugin is not compatible with linking as the remote link will not have access to the original source file's directory. +::: + +To include other formats, such as ASS, consider first converting the subtitle streams to SRT using the Plugin(s) + +Install the **"Convert any ASS subtitle streams in videos to SRT"** plugin and configure the plugin flow to set it before this one + +:::note +This Plugin does not contain a file tester to detect files that contain SRT subtitle streams. +Ensure it is pared with another Plugin. +::: diff --git a/source/extract_ass_subtitles_to_files/icon.png b/source/extract_ass_subtitles_to_files/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dbc845ecb1a9985ed11d2ddba3596d4cc0ac7309 GIT binary patch literal 20360 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�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 IXn*7X0tl1U1^@s6 literal 0 HcmV?d00001 diff --git a/source/extract_ass_subtitles_to_files/info.json b/source/extract_ass_subtitles_to_files/info.json new file mode 100644 index 000000000..b1d7654f8 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/info.json @@ -0,0 +1,19 @@ +{ + "author": "Josh.5, Bambanah", + "compatibility": [ + 1, + 2 + ], + "description": "Extract text based subtitle streams to *.ass files.", + "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.extract_srt_subtitles_to_files/master/icon.png", + "id": "extract_ass_subtitles_to_files", + "name": "Extract text subtitle streams to ASS files", + "platform": [ + "all" + ], + "priorities": { + "on_worker_process": 2 + }, + "tags": "subtitle,ffmpeg", + "version": "0.0.10" +} \ No newline at end of file diff --git a/source/extract_ass_subtitles_to_files/lib/__init__.py b/source/extract_ass_subtitles_to_files/lib/__init__.py new file mode 100644 index 000000000..42ee0b3ec --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/__init__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + plugins.__init__.py + + Written by: Josh.5 + Date: 23 Aug 2021, (00:03 PM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/LICENSE b/source/extract_ass_subtitles_to_files/lib/ffmpeg/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/README.md b/source/extract_ass_subtitles_to_files/lib/ffmpeg/README.md new file mode 100644 index 000000000..894d8afa8 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/README.md @@ -0,0 +1,429 @@ +# unmanic.plugin.helpers.ffmpeg + +This python module is a helper library for any Unmanic plugin that needs to build FFmpeg commands to be executed. + + +# Using the module + +## Adding it to your project + +```bash +└── my_plugin_id/ + ├── changelog.md + ├── description.md + ├── .gitignore + ├── icon.png + ├── info.json + ├── lib/ + | └── ffmpeg/ + | ├── __init__.py + | ├── LICENSE + | ├── mimetype_overrides.py + | ├── parser.py + | ├── probe.py + | ├── README.md + | └── stream_mapper.py + ├── LICENSE + ├── plugin.py + └── requirements.txt +``` + +### Git Submodule +It can be included in your plugin project as a submodule. +``` +git submodule add https://github.com/Josh5/unmanic.plugin.helpers.ffmpeg.git ./lib/ffmpeg +``` +If you use it sure to include all files in the lib directory when publishing your project to the Unmanic plugin repository. + +### Project source download +Download the git repository as zip file and extract it to `lib` directory. +``` +mkdir -p ./lib +curl -L "https://github.com/Josh5/unmanic.plugin.helpers.ffmpeg/archive/refs/heads/master.zip" --output /tmp/unmanic.plugin.helpers.ffmpeg.zip +unzip /tmp/unmanic.plugin.helpers.ffmpeg.zip -d ./lib/ +mv -v ./lib/unmanic.plugin.helpers.ffmpeg-master ./lib/ffmpeg +``` + +--- + +## Importing it in your project + +This module comes with x3 classes to assist in generating FFmpeg commands for your Unmanic plugin. + +You can import all 3 classes into your plugin like this: + +```python +from my_plugin_id.lib.ffmpeg import Parser, Probe, StreamMapper +``` +> **Note** +> Be sure to rename 'my_plugin_id' in the example above. + +--- + +## Using the `Probe` class + +The Probe class is a wrapper around the `ffprobe` cli. This can be used to generate a file probe object containing file format and stream info. + +Add this to your plugin runner function: +```python + # Get file probe + probe = Probe.init_probe(data, logger, allowed_mimetypes=['video', 'audio']) + if not probe: + # File not able to be probed by ffprobe. The file is probably not a audio/video file. + return +``` + +You can then use this newly created Probe object in your plugin. To read the FFprobe data, add this: +```python + ffprobe_data = probe.get_probe() +``` + +### FFprobe Example +
+ Show + + ```json +{ + "streams": [ + { + "index": 0, + "codec_name": "hevc", + "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", + "profile": "Main", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1080, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 2, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 120, + "color_range": "tv", + "color_space": "bt709", + "color_transfer": "bt709", + "color_primaries": "bt709", + "chroma_location": "left", + "refs": 1, + "r_frame_rate": "24000/1001", + "avg_frame_rate": "24000/1001", + "time_base": "1/1000", + "start_pts": 21, + "start_time": "0.021000", + "extradata_size": 2471, + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "DURATION": "00:00:10.239000000" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "extradata_size": 5, + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng", + "title": "Surround", + "DURATION": "00:00:10.005000000" + } + }, + { + "index": 2, + "codec_name": "ass", + "codec_long_name": "ASS (Advanced SSA) subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 10614, + "duration": "10.614000", + "extradata_size": 487, + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "bul", + "DURATION": "00:00:10.614000000" + } + } + ], + "chapters": [ + { + "id": 1, + "time_base": "1/1000000000", + "start": 0, + "start_time": "0.000000", + "end": 10000000000, + "end_time": "10.000000", + "tags": { + "title": "Chapter 1" + } + } + ], + "format": { + "filename": "TEST_FILE.mkv", + "nb_streams": 3, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "10.614000", + "size": "1280059", + "bit_rate": "964807", + "probe_score": 100, + "tags": { + "ENCODER": "Lavf59.27.100" + } + } +} + ``` +
+ +--- + +## Using the `StreamMapper` class + +The StreamMapper class is used to simplify building a ffmpeg command. It uses a previously initialised probe object as an input and uses it to define stream mapping from the input file to the output. + +This class should be extended with a child class to configure it and implement the custom functions required to manage streams that will need to be processed. + +```python +class PluginStreamMapper(StreamMapper): + def __init__(self): + super(PluginStreamMapper, self).__init__(logger, ['video']) + self.settings = None + + def set_settings(self, settings): + self.settings = settings + + def test_stream_needs_processing(self, stream_info: dict): + """ + Run through a set of test against the given stream_info. + + Return 'True' if it needs to be process. + Return 'False' if it should just be copied over to the new file. + + :param stream_info: + :return: bool + """ + if stream_info.get('codec_name').lower() in ['h264']: + return False + return True + + def custom_stream_mapping(self, stream_info: dict, stream_id: int): + """ + Will be provided with stream_info and the stream_id of a stream that has been + determined to need processing by the `test_stream_needs_processing` function. + + Use this function to `-map` (select) an input stream to be included in the output file + and apply a `-c` (codec) selection and encoder arguments to the command. + + This function must return a dictionary containing 2 key values: + { + 'stream_mapping': [], + 'stream_encoding': [], + } + + Where: + - 'stream_mapping' is a list of arguments for input streams to map. Eg. ['-map', '0:v:1'] + - 'stream_encoding' is a list of encoder arguments. Eg. ['-c:v:1', 'libx264', '-preset', 'slow'] + + + :param stream_info: + :param stream_id: + :return: dict + """ + if self.settings.get_setting('advanced'): + stream_encoding = ['-c:v:{}'.format(stream_id), 'libx264'] + stream_encoding += self.settings.get_setting('custom_options').split() + else: + stream_encoding = [ + '-c:v:{}'.format(stream_id), 'libx264', + '-preset', str(self.settings.get_setting('preset')), + '-crf', str(self.settings.get_setting('crf')), + ] + + return { + 'stream_mapping': ['-map', '0:v:{}'.format(stream_id)], + 'stream_encoding': stream_encoding, + } +``` + +Once you have created your stream mapper class, you can use it to determine if a file needs a FFmpeg command executed against it using its `streams_need_processing` function. + +```python +def on_library_management_file_test(data): + + ... + + # Get plugin settings + settings = Settings(library_id=data.get('library_id')) + + # Get file probe + probe = Probe.init_probe(data, logger, allowed_mimetypes=['audio']) + if not probe: + # File not able to be probed by FFprobe. The file is probably not a audio/video file. + return + + # Get stream mapper + mapper = PluginStreamMapper() + mapper.set_settings(settings) + mapper.set_probe(probe) + + # Check if file needs a FFmpeg command run against it + if mapper.streams_need_processing(): + # Mark this file to be added to the pending tasks + data['add_file_to_pending_tasks'] = True + + +def on_worker_process(data): + + ... + + # Get plugin settings + settings = Settings(library_id=data.get('library_id')) + + # Get file probe + probe = Probe.init_probe(data, logger, allowed_mimetypes=['audio']) + if not probe: + # File not able to be probed by FFprobe. The file is probably not a audio/video file. + return + + # Get stream mapper + mapper = PluginStreamMapper() + mapper.set_settings(settings) + mapper.set_probe(probe) + + # Check if file needs a FFmpeg command run against it + if mapper.streams_need_processing(): + + """ + HERE: Configure FFmpeg command args as required for this plugin + """ + + # Set the input and output file + mapper.set_input_file(data.get('file_in')) + mapper.set_output_file(data.get('file_out')) + + # Get final generated FFmpeg args + ffmpeg_args = mapper.get_ffmpeg_args() + + # Apply FFmpeg args to command for Unmanic to execute + data['exec_command'] = ['ffmpeg'] + data['exec_command'] += ffmpeg_args + +``` + +--- + +## Using the `Parser` class + +Unmanic has the ability to execute a command provided by a plugin and display a output of that command's progress. As Unmanic is able to execute any command a plugin provides it, we need a way + +This progress is only possible if the provided command is accompanied with a progress parser function. If such a function is not provided to Unmanic, then the command will still be executed, but the Unmanic worker will only report an indeterminate progress status with the logs. + +This python module provides a function for parsing the output of a FFmpeg command to determine progress of that command's execution. + +This should be returned with the built command in the `on_worker_process` plugin function: + + +```python +def on_worker_process(data): + + ... + + # Get file probe + probe = Probe.init_probe(data, logger, allowed_mimetypes=['audio']) + if not probe: + # File not able to be probed by FFprobe. The file is probably not a audio/video file. + return + + # Set the parser + parser = Parser(logger) + parser.set_probe(probe) + data['command_progress_parser'] = parser.parse_progress +``` + +--- + +## Examples +For examples of how to use this module, see these plugin sources: +- [Limit Library Search by FFprobe Data](https://github.com/Unmanic/plugin.limit_library_search_by_ffprobe_data/blob/master/plugin.py) +- [Re-order audio streams by language](https://github.com/Unmanic/plugin.reorder_audio_streams_by_language/blob/master/plugin.py) +- [Transcode Video Files](https://github.com/Unmanic/plugin.video_transcoder/blob/master/plugin.py) diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/__init__.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/__init__.py new file mode 100644 index 000000000..658bd6744 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/__init__.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + unmanic.__init__.py + + Written by: Josh.5 + Date: 30 Jul 2021, (12:12 PM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" + +from __future__ import absolute_import +import warnings + +from .parser import Parser +from .probe import Probe +from .stream_mapper import StreamMapper + +__author__ = 'Josh.5 (jsunnex@gmail.com)' + +__all__ = ( + 'Parser', + 'Probe', + 'StreamMapper', +) diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/mimetype_overrides.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/mimetype_overrides.py new file mode 100644 index 000000000..1b7892a83 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/mimetype_overrides.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + plugins.probe.py + + Written by: Josh.5 + Date: 17 Mar 2022, (9:29 AM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" + + +class MimetypeOverrides(object): + audio = { + '.flac': 'audio/flac', + } + video = { + '.m4v': 'video/x-m4v', + '.3gp': 'video/3gpp', + '.axv': 'video/annodex', + '.dl': 'video/dl', + '.dif': 'video/dv', + '.dv': 'video/dv', + '.fli': 'video/fli', + '.gl': 'video/gl', + '.mpeg': 'video/mpeg', + '.mpg': 'video/mpeg', + '.mpe': 'video/mpeg', + '.ts': 'video/MP2T', + '.mp4': 'video/mp4', + '.qt': 'video/quicktime', + '.mov': 'video/quicktime', + '.ogv': 'video/ogg', + '.webm': 'video/webm', + '.mxu': 'video/vnd.mpegurl', + '.flv': 'video/x-flv', + '.lsf': 'video/x-la-asf', + '.lsx': 'video/x-la-asf', + '.mng': 'video/x-mng', + '.asf': 'video/x-ms-asf', + '.asx': 'video/x-ms-asf', + '.wm': 'video/x-ms-wm', + '.wmv': 'video/x-ms-wmv', + '.wmx': 'video/x-ms-wmx', + '.wvx': 'video/x-ms-wvx', + '.avi': 'video/x-msvideo', + '.movie': 'video/x-sgi-movie', + '.mpv': 'video/x-matroska', + '.mkv': 'video/x-matroska', + } + + def get_all(self): + return {**self.audio, **self.video} diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/parser.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/parser.py new file mode 100644 index 000000000..d7394070e --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/parser.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + plugins.parser.py + + Written by: Josh.5 + Date: 12 Aug 2021, (9:00 AM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" +import datetime +import math +import re +from logging import Logger + +from .probe import Probe + + +class Parser(object): + """ + Parser + + Class to manage parsing the FFmpeg commandline output and return progress data for Unmanic. + """ + + percent = '0' + time = '0' + frame = '0' + speed = '0' + bitrate = '0' + + src_fps = None + duration = None + total_frames = None + + def __init__(self, logger: Logger, duration=None, total_frames=None): + self.logger = logger + + def set_probe(self, probe: Probe): + """ + Set the information require to calculate progress from the file probe + + :param probe: + :return: + """ + # Get FPS from file probe info + self.src_fps = None + try: + file_probe_streams = probe.get('streams', []) + self.src_fps = eval(file_probe_streams[0]['avg_frame_rate']) + except ZeroDivisionError: + # Warning, Cannot use input FPS + self.logger.warning('Cannot use input FPS for FFmpeg conversion progress') + except KeyError: + # Warning, Cannot use input FPS + self.logger.warning('Cannot use input FPS for FFmpeg conversion progress - key not found in probe') + if self.src_fps == 0: + raise ValueError('Unexpected zero FPS') + + # Get Duration from file probe info + self.duration = None + try: + file_probe_format = probe.get('format', {}) + self.duration = float(file_probe_format['duration']) + except ZeroDivisionError: + # Warning, Cannot use input Duration + self.logger.warning('Cannot use input Duration for FFmpeg conversion progress') + except KeyError: + # Warning, Cannot use input Duration + self.logger.warning('Cannot use input Duration for FFmpeg conversion progress - key not found in probe') + + if self.src_fps is None and self.duration is None: + raise ValueError('Unable to match against FPS or Duration.') + + # If we have probed both the source FPS and total duration, then we can calculate the total frames + if self.duration and self.src_fps and self.duration > 0 and self.src_fps > 0: + self.total_frames = int(self.duration * self.src_fps) + + def parse_progress(self, line_text): + """ + Given a single line of STDOUT text, parse it using regex and extract progress as a percent value. + + :param line_text: + :return: + """ + # Fetch data from line text + if line_text and ('frame=' in line_text or 'time=' in line_text): + # Update time + _time = self.get_progress_from_regex_of_string(line_text, r"time=(\s+|)(\d+:\d+:\d+\.\d+)", self.time) + if _time: + self.time = str(self.time_string_to_seconds(_time)) + + # Update frames + _frame = self.get_progress_from_regex_of_string(line_text, r"frame=(\s+|)(\d+)", self.frame) + if _frame and int(_frame) > int(self.frame): + self.frame = _frame + + # Update speed + _speed = self.get_progress_from_regex_of_string(line_text, r"speed=(\s+|)(\d+\.\d+)", self.speed) + if _speed: + self.speed = str(_speed) + + # Update bitrate + _bitrate = self.get_progress_from_regex_of_string(line_text, r"bitrate=(\s+|)(\d+\.\d+\w+|\d+w)", + self.bitrate) + if _bitrate: + self.bitrate = "{}/s".format(_bitrate) + + # Update file size + _size = self.get_progress_from_regex_of_string(line_text, r"size=(\s+|)(\d+\w+|\d+.\d+\w+)", self.frame) + if _size: + self.file_size = _size + + # Update percent + _percent = None + if _frame and self.total_frames and int(_frame) > 0 and int(self.total_frames) > 0: + # If we have both the current frame and the total number of frames, then we can easily calculate the % + # _percent = float(int(_frame) / int(self.total_frames)) + _percent = float(int(_frame) / int(self.total_frames)) * 100 + _percent = math.trunc(_percent) + elif self.time and self.duration and int(self.time) > 0 and int(self.duration) > 0: + # If that was not successful, we need to resort to assuming the percent by the duration and the time + # passed so far + _percent = float(int(self.time) / int(self.duration)) * 100 + _percent = math.trunc(_percent) + if _percent and int(_percent) > int(self.percent): + self.percent = str(_percent) + + # Return the values. + # Currently Unmanic only cares about the percent. So for now we will ignore everything else. + return { + 'percent': self.percent + } + + @staticmethod + def time_string_to_seconds(time_string): + """ + Converts a time string from the FFmpeg output into an epoch timestamp + + :param time_string: + :return: + """ + pt = datetime.datetime.strptime(time_string, '%H:%M:%S.%f') + return pt.second + pt.minute * 60 + pt.hour * 3600 + + @staticmethod + def get_progress_from_regex_of_string(line, regex_string, default=None): + """ + Parse value from line text using the given regular expression. + If no match is found, return the given default value. + + :param line: + :param regex_string: + :param default: + :return: + """ + if default is None: + default = 0 + + return_value = default + regex = re.compile(regex_string) + findall = re.findall(regex, line) + if findall: + split_list = findall[-1] + if len(split_list) == 2: + return_value = split_list[1].strip() + return return_value diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/probe.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/probe.py new file mode 100644 index 000000000..9f3ff51a0 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/probe.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + plugins.probe.py + + Written by: Josh.5 + Date: 12 Aug 2021, (9:20 AM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" +import json +import mimetypes +import os +import shutil +import subprocess +from logging import Logger + +from .mimetype_overrides import MimetypeOverrides + + +class FFProbeError(Exception): + """ + FFProbeError + Custom exception for errors encountered while executing the ffprobe command. + """ + + def __init___(self, path, info): + Exception.__init__(self, "Unable to fetch data from file {}. {}".format(path, info)) + self.path = path + self.info = info + + +def ffprobe_cmd(params): + """ + Execute a ffprobe command subprocess and read the output + + :param params: + :return: + """ + command = ["ffprobe"] + params + + pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, err = pipe.communicate() + + # Check for results + try: + raw_output = out.decode("utf-8") + except Exception as e: + raise FFProbeError(command, str(e)) + + if 'error' in raw_output: + try: + info = json.loads(raw_output) + except Exception as e: + raise FFProbeError(command, raw_output) + if pipe.returncode == 1: + raise FFProbeError(command, raw_output) + if not raw_output: + raise FFProbeError(command, 'No info found') + + return raw_output + + +def ffprobe_file(vid_file_path): + """ + Returns a dictionary result from ffprobe command line prove of a file + + :param vid_file_path: The absolute (full) path of the video file, string. + :return: + """ + if type(vid_file_path) != str: + raise Exception('Give ffprobe a full file path of the video') + + params = [ + "-loglevel", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", + "-show_error", + "-show_chapters", + vid_file_path + ] + + # Check result + results = ffprobe_cmd(params) + try: + info = json.loads(results) + except Exception as e: + raise FFProbeError(vid_file_path, str(e)) + + return info + + +class Probe(object): + """ + Probe + """ + + probe_info = {} + + def __init__(self, logger: Logger, allowed_mimetypes=None): + # Ensure ffprobe is installed + if shutil.which('ffprobe') is None: + raise Exception("Unable to find executable 'ffprobe'. Please ensure that FFmpeg is installed correctly.") + + self.logger = logger + if allowed_mimetypes is None: + allowed_mimetypes = ['audio', 'video', 'image'] + self.allowed_mimetypes = allowed_mimetypes + + # Init (reset) our mimetype list + mimetypes.init() + + # Add mimetype overrides to mimetype dictionary (replaces any existing entries) + mimetype_overrides = MimetypeOverrides() + all_mimetype_overrides = mimetype_overrides.get_all() + for extension in all_mimetype_overrides: + mimetypes.add_type(all_mimetype_overrides.get(extension), extension) + + def __test_valid_mimetype(self, file_path): + """ + Test the given file path for its mimetype. + If the mimetype cannot be detected, it will fail this test. + If the detected mimetype is not in the configured 'allowed_mimetypes' + class variable, it will fail this test. + + :param file_path: + :return: + """ + # Only run this check against video/audio/image MIME types + file_type = mimetypes.guess_type(file_path)[0] + + # If the file has no MIME type then it cannot be tested + if file_type is None: + self.logger.debug("Unable to fetch file MIME type - '{}'".format(file_path)) + return False + + # Make sure the MIME type is either audio, video or image + file_type_category = file_type.split('/')[0] + if file_type_category not in self.allowed_mimetypes: + self.logger.debug("File MIME type not in [{}] - '{}'".format(', '.join(self.allowed_mimetypes), file_path)) + return False + + return True + + @staticmethod + def init_probe(data, logger, allowed_mimetypes=None): + """ + Fetch the Probe object given a plugin's data object + + :param data: + :param logger: + :param allowed_mimetypes: + :return: + """ + probe = Probe(logger, allowed_mimetypes=allowed_mimetypes) + # Start by fetching probe data from 'shared_info'. + ffprobe_data = data.get('shared_info', {}).get('ffprobe') + if ffprobe_data: + if not probe.set_probe(ffprobe_data): + # Failed to set ffprobe from 'shared_info'. + # Probably due to it being for an incompatible mimetype declared above. + return + return probe + # No 'shared_info' ffprobe exists. Attempt to probe file. + if not probe.file(data.get('path')): + # File probe failed, skip the rest of this test. + # Again, probably due to it being for an incompatible mimetype. + return + # Successfully probed file. + # Set file probe to 'shared_info' for subsequent file test runners. + if 'shared_info' not in data: + data['shared_info'] = {} + data['shared_info']['ffprobe'] = probe.get_probe() + return probe + + def file(self, file_path): + """ + Sets the 'probe' dict by probing the given file path. + Files that are not able to be probed will not set the 'probe' dict. + + :param file_path: + :return: + """ + self.probe_info = {} + + # Ensure file exists + if not os.path.exists(file_path): + self.logger.debug("File does not exist - '{}'".format(file_path)) + return + + if not self.__test_valid_mimetype(file_path): + return + + try: + # Get the file probe info + self.probe_info = ffprobe_file(file_path) + return True + except FFProbeError: + # This will only happen if it was not a file that could be probed. + self.logger.debug("File unable to be probed by FFProbe - '{}'".format(file_path)) + return + + def set_probe(self, probe_info): + """Sets the probe dictionary""" + file_path = probe_info.get('format', {}).get('filename') + if not file_path: + self.logger.error("Provided file probe information does not contain the expected 'filename' key.") + return + if not self.__test_valid_mimetype(file_path): + return + + self.probe_info = probe_info + return self.probe_info + + def get_probe(self): + """Return the probe dictionary""" + return self.probe_info + + def get(self, key, default=None): + """Return the value of the given key from the probe dictionary""" + return self.probe_info.get(key, default) diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/stream_mapper.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/stream_mapper.py new file mode 100644 index 000000000..a4182caac --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/stream_mapper.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + unmanic.stream_mapper.py + + Written by: Josh.5 + Date: 30 Jul 2021, (9:41 AM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" +import os +import shutil +from logging import Logger + +from .probe import Probe + + +class StreamMapper(object): + """ + StreamMapper + + Manage FFmpeg stream mapping and generating FFmpeg command-line args. + """ + + probe: Probe = None + + stream_type_idents = { + 'video': 'v', + 'audio': 'a', + 'subtitle': 's', + 'data': 'd', + 'attachment': 't' + } + + processing_stream_type = '' + found_streams_to_encode = False + stream_mapping = [] + stream_encoding = [] + video_stream_count = 0 + audio_stream_count = 0 + subtitle_stream_count = 0 + data_stream_count = 0 + attachment_stream_count = 0 + + input_file = '' + output_file = '' + generic_options = [] + main_options = [] + advanced_options = [] + format_options = [] + + def __init__(self, logger: Logger, processing_stream_type: list): + # Ensure ffmpeg is installed + if shutil.which('ffmpeg') is None: + raise Exception("Unable to find executable 'ffmpeg'. Please ensure that FFmpeg is installed correctly.") + + self.logger = logger + if processing_stream_type is not None: + if any(pst for pst in processing_stream_type if + pst not in ['video', 'audio', 'subtitle', 'data', 'attachment']): + raise Exception( + "processing_stream_type must be one of ['video','audio','subtitle','data','attachment']") + self.processing_stream_type = processing_stream_type + + # Set default Generic options + self.generic_options = [ + '-hide_banner', + '-loglevel', 'info', + ] + + # Set default Main options + self.main_options = [] + + # Set default Advanced options + self.advanced_options = [ + '-strict', '-2', + '-max_muxing_queue_size', '4096', + ] + + def __copy_stream_mapping(self, codec_type, stream_id): + """ + Create stream mapping to simply copy the stream without encoding. + Apply this to the 'stream_mapping' and 'stream_encoding' attributes. + + :param codec_type: + :param stream_id: + :return: + """ + # Map this stream for copy to the destination file + self.stream_mapping += ['-map', '0:{}:{}'.format(codec_type, stream_id)] + # Add a encoding flag copying this stream + self.stream_encoding += ['-c:{}:{}'.format(codec_type, stream_id), 'copy'] + + def __apply_custom_stream_mapping(self, mapping_dict): + """ + Apply a custom stream mapping. + This method tests that the provided mapping dictionary is valid. + If it is valid, apply it to the 'stream_mapping' and 'stream_encoding' attributes. + + :param mapping_dict: + :return: + """ + # Ensure the mapping dictionary provided is correct + if not isinstance(mapping_dict, dict): + raise Exception("processing_stream_type must return a dictionary") + if 'stream_mapping' not in mapping_dict: + raise Exception("processing_stream_type return dictionary must contain 'stream_mapping' key") + if not isinstance(mapping_dict.get('stream_mapping'), list): + raise Exception("processing_stream_type 'stream_mapping' value must be of type 'list'") + if 'stream_encoding' not in mapping_dict: + raise Exception("processing_stream_type return dictionary must contain 'stream_encoding' key") + if not isinstance(mapping_dict.get('stream_encoding'), list): + raise Exception("processing_stream_type 'stream_mapping' value must be of type 'list'") + # Append this custom stream mapping + self.stream_mapping += mapping_dict.get('stream_mapping') + # Append these custom encoding flags + self.stream_encoding += mapping_dict.get('stream_encoding') + + def set_probe(self, probe: Probe): + """Set the ffprobe Probe object""" + self.probe = probe + + def test_stream_needs_processing(self, stream_info: dict): + """ + Overwrite this function to test a stream. + Return 'True' if it needs to be process. + Return 'False' if it should just be copied over to the new file. + + :param stream_info: + :return: bool + """ + raise NotImplementedError + + def custom_stream_mapping(self, stream_info: dict, stream_id: int): + """ + Configure custom mapping for a single stream + This function must return a dictionary containing 2 key values: + { + 'stream_mapping': [], + 'stream_encoding': [], + } + + :param stream_info: + :param stream_id: + :return: dict + """ + raise NotImplementedError + + def __set_stream_mapping(self): + """ + Sets a list of stream maps and encoding variables + + :return: + """ + + # Require a list of probe streams to continue + file_probe_streams = self.probe.get('streams') + if not file_probe_streams: + return False + + # What type of streams are we looking for ('video', 'audio', 'subtitle', 'data' or 'attachment') + processing_stream_type = self.processing_stream_type + + # Map the streams into two arrays that will be placed together in the correct order. + self.stream_mapping = [] + self.stream_encoding = [] + + # Count streams by type + self.video_stream_count = 0 + self.audio_stream_count = 0 + self.subtitle_stream_count = 0 + self.data_stream_count = 0 + self.attachment_stream_count = 0 + + # Set flag for finding a stream that needs to be processed as False by default. + found_streams_to_process = False + + # Loop over all streams found in the file probe + for stream_info in file_probe_streams: + codec_type = stream_info.get('codec_type', '').lower() + # Fore each of these streams: + + # If this is a video/image stream? + if codec_type == "video": + # Map the video stream + if "video" in processing_stream_type: + if not self.test_stream_needs_processing(stream_info): + self.__copy_stream_mapping('v', self.video_stream_count) + self.video_stream_count += 1 + continue + else: + mapping = self.custom_stream_mapping(stream_info, self.video_stream_count) + if mapping: + found_streams_to_process = True + self.__apply_custom_stream_mapping(mapping) + else: + self.__copy_stream_mapping('v', self.video_stream_count) + self.video_stream_count += 1 + continue + else: + self.__copy_stream_mapping('v', self.video_stream_count) + self.video_stream_count += 1 + continue + + # If this is a audio stream? + elif codec_type == "audio": + # Map the audio stream + if "audio" in processing_stream_type: + if not self.test_stream_needs_processing(stream_info): + self.__copy_stream_mapping('a', self.audio_stream_count) + self.audio_stream_count += 1 + continue + else: + mapping = self.custom_stream_mapping(stream_info, self.audio_stream_count) + if mapping: + found_streams_to_process = True + self.__apply_custom_stream_mapping(mapping) + else: + self.__copy_stream_mapping('a', self.audio_stream_count) + self.audio_stream_count += 1 + continue + else: + if self.settings.get_setting('mode') == 'advanced': + amaps = self.settings.get_setting('custom_options').split() + self.logger.debug("Advanced Mode Video Settings with custom audio encoding: '%s'", amaps) + if '-c:a' not in amaps: + self.logger.debug("-c:a not detected in custom mappings: '%s'", amaps) + self.__copy_stream_mapping('a', self.audio_stream_count) + else: + self.logger.debug("-c:a detected in custom mappings: '%s'", amaps) + self.stream_mapping += ['-map', '0:{}:{}'.format('a', self.audio_stream_count)] + self.audio_stream_count += 1 + else: + self.__copy_stream_mapping('a', self.audio_stream_count) + self.audio_stream_count += 1 + continue + + # If this is a subtitle stream? + elif codec_type == "subtitle": + # Map the subtitle stream + if "subtitle" in processing_stream_type: + if not self.test_stream_needs_processing(stream_info): + self.__copy_stream_mapping('s', self.subtitle_stream_count) + self.subtitle_stream_count += 1 + continue + else: + mapping = self.custom_stream_mapping(stream_info, self.subtitle_stream_count) + if mapping: + found_streams_to_process = True + self.__apply_custom_stream_mapping(mapping) + else: + self.__copy_stream_mapping('s', self.subtitle_stream_count) + self.subtitle_stream_count += 1 + continue + else: + if self.settings.get_setting('mode') == 'advanced': + submaps = self.settings.get_setting('custom_options').split() + self.logger.debug("Advanced Mode Video Settings with custom subtitle encoding: '%s'", submaps) + if '-c:s' not in submaps: + self.logger.debug("-c:s not detected in custom mappings: '%s'", submaps) + self.__copy_stream_mapping('s', self.subtitle_stream_count) + else: + self.logger.debug("-c:s detected in custom mappings: '%s'", submaps) + self.stream_mapping += ['-map', '0:{}:{}'.format('s', self.subtitle_stream_count)] + self.subtitle_stream_count += 1 + else: + self.__copy_stream_mapping('s', self.subtitle_stream_count) + self.subtitle_stream_count += 1 + continue + + # If this is a data stream? + elif codec_type == "data": + # Map the data stream + if "data" in processing_stream_type: + if not self.test_stream_needs_processing(stream_info): + self.__copy_stream_mapping('d', self.data_stream_count) + self.data_stream_count += 1 + continue + else: + mapping = self.custom_stream_mapping(stream_info, self.data_stream_count) + if mapping: + found_streams_to_process = True + self.__apply_custom_stream_mapping(mapping) + else: + self.__copy_stream_mapping('d', self.data_stream_count) + self.data_stream_count += 1 + continue + else: + self.__copy_stream_mapping('d', self.data_stream_count) + self.data_stream_count += 1 + continue + + # If this is a attachment stream? + elif codec_type == "attachment": + # Map the attachment stream + if "attachment" in processing_stream_type: + if not self.test_stream_needs_processing(stream_info): + self.__copy_stream_mapping('t', self.attachment_stream_count) + self.attachment_stream_count += 1 + continue + else: + mapping = self.custom_stream_mapping(stream_info, self.attachment_stream_count) + if mapping: + found_streams_to_process = True + self.__apply_custom_stream_mapping(mapping) + else: + self.__copy_stream_mapping('t', self.attachment_stream_count) + self.attachment_stream_count += 1 + continue + else: + self.__copy_stream_mapping('t', self.attachment_stream_count) + self.attachment_stream_count += 1 + continue + + return found_streams_to_process + + def __build_args(self, options: list, *args, **kwargs): + """ + Build a list of FFmpeg options based on the given default options, args and kwargs + + :param options: + :param args: + :param kwargs: + :return: + """ + for arg in args: + if arg in options: + options = [value for value in options if value != arg] + options += [arg] + else: + options += [arg] + for kwarg in kwargs: + key = kwarg + value = kwargs.get(kwarg) + if key in options: + key_pos = options.index(key) + val_pos = int(key_pos) + 1 + options[key_pos] = key + options[val_pos] = value + else: + options += [key, value] + return + + def streams_need_processing(self): + """ + Returns True/False if the streams need to be processed. + If at least one stream needs custom stream mapping (processing), then this will return True. + If the stream mapping will copy all streams to output file untouched, then this will return False. + + :return: + """ + return self.__set_stream_mapping() + + def container_needs_remuxing(self, container_extension): + """ + Returns True/False if the file container needs to be processed. + + :return: + """ + if not self.input_file: + raise Exception("Input file not yet set") + + split_file_in = os.path.splitext(self.input_file) + if split_file_in[1].lstrip('.') != container_extension.lstrip('.'): + return True + return False + + def set_input_file(self, path): + """Set the input file for the FFmpeg args""" + self.input_file = os.path.abspath(path) + + def set_output_file(self, path): + """Set the output file for the FFmpeg args""" + self.output_file = os.path.abspath(path) + + def set_output_null(self): + """Set the output container to NULL for the FFmpeg args""" + self.output_file = '-' + if os.name == "nt": + # Windows uses NUL instead + self.output_file = 'NUL' + main_options = { + "-f": 'null', + } + self.__build_args(self.main_options, **main_options) + + def set_ffmpeg_generic_options(self, *args, **kwargs): + """ + Set FFmpeg Generic options. + These are the initial options that follow the 'ffmpeg' command. + + Ref: + http://ffmpeg.org/ffmpeg-all.html#Generic-options + + + :param args: + :param kwargs: + :return: + """ + self.__build_args(self.generic_options, *args, **kwargs) + + def set_ffmpeg_main_options(self, *args, **kwargs): + """ + Set FFmpeg Main options. + These options follow the generic options. + They include things like the input file(s), metadata mapping, etc. + + Ref: + http://ffmpeg.org/ffmpeg-all.html#Main-options + + :return: + """ + self.__build_args(self.main_options, *args, **kwargs) + + def set_ffmpeg_advanced_options(self, *args, **kwargs): + """ + Set FFmpeg Advanced options. + These options follow the generic options. + They include things like the custom stream mapping, custom metadata mapping, + filters, etc. + + Note: + The custom stream mapping is carried out with another method. + This method should not be used for creating custom stream mapping if the + 'stream_mapping' and 'stream_encoding' attributes are set. + + Ref: + http://ffmpeg.org/ffmpeg-all.html#Advanced-options + + :return: + """ + self.__build_args(self.advanced_options, *args, **kwargs) + + def get_stream_mapping(self): + """ + Fetch the custom stream mapping generated by this class. + If the mapping args are not yet generated, generate them at this point. + + :return: + """ + if not self.stream_mapping: + self.__set_stream_mapping() + return self.stream_mapping + + def get_stream_encoding(self): + """ + Fetch the custom stream encoding args generated by this class. + If the encoding args are not yet generated, generate them at this point. + + :return: + """ + if not self.stream_encoding: + self.__set_stream_mapping() + return self.stream_encoding + + def get_ffmpeg_args(self): + """ + Build the FFmpeg command args and return them as a list. + + :return: + """ + args = [] + + # Add generic options first + args += self.generic_options + + # Add other main options + args += self.main_options + + # Add the input file + # This class requires at least one input file specified with the input_file attribute + if not self.input_file: + raise Exception("Input file has not been set") + args += ['-i', self.input_file] + + # Add advanced options. This includes the stream mapping and the encoding args + args += self.advanced_options + args += self.stream_mapping + args += self.stream_encoding + + # Add the output file + # This class requires at least one output file specified with the output_file attribute + if not self.output_file: + raise Exception("Output file has not been set") + elif self.output_file == '-': + args += [self.output_file] + else: + args += ['-y', self.output_file] + + return args diff --git a/source/extract_ass_subtitles_to_files/lib/ffmpeg/tools.py b/source/extract_ass_subtitles_to_files/lib/ffmpeg/tools.py new file mode 100644 index 000000000..c4d9c5475 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/lib/ffmpeg/tools.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + plugins.tools.py + + Written by: Josh.5 + Date: 17 Feb 2023, (12:07 PM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" + +image_video_codecs = [ + 'alias_pix', + 'apng', + 'brender_pix', + 'dds', + 'dpx', + 'exr', + 'fits', + 'gif', + 'mjpeg', + 'mjpegb', + 'pam', + 'pbm', + 'pcx', + 'pfm', + 'pgm', + 'pgmyuv', + 'pgx', + 'photocd', + 'pictor', + 'pixlet', + 'png', + 'ppm', + 'ptx', + 'sgi', + 'sunrast', + 'tiff', + 'vc1image', + 'wmv3image', + 'xbm', + 'xface', + 'xpm', + 'xwd', +] + +resolution_map = { + '480p_sdtv': { + 'width': 854, + 'height': 480, + 'label': "480p (SDTV)", + }, + '576p_sdtv': { + 'width': 1024, + 'height': 576, + 'label': "576p (SDTV)", + }, + '720p_hdtv': { + 'width': 1280, + 'height': 720, + 'label': "720p (HDTV)", + }, + '1080p_hdtv': { + 'width': 1920, + 'height': 1080, + 'label': "1080p (HDTV)", + }, + 'dci_2k_hdtv': { + 'width': 2048, + 'height': 1080, + 'label': "DCI 2K (HDTV)", + }, + '1440p': { + 'width': 2560, + 'height': 1440, + 'label': "1440p (WQHD)", + }, + '4k_uhd': { + 'width': 3840, + 'height': 2160, + 'label': "4K (UHD)", + }, + 'dci_4k': { + 'width': 4096, + 'height': 2160, + 'label': "DCI 4K", + }, + '8k_uhd': { + 'width': 8192, + 'height': 4608, + 'label': "8k (UHD)", + }, +} + + +def get_video_stream_resolution(streams: list) -> object: + """ + Given a list of streams from a video file, returns the first video + stream's resolution and index. + + :param streams: The list of streams for the video file. + :type streams: list + :return: A tuple of the (width, height, stream_index,) + :rtype: object + """ + width = 0 + height = 0 + video_stream_index = 0 + + for stream in streams: + if stream.get('codec_type', '') == 'video': + width = stream.get('width', stream.get('coded_width', 0)) + height = stream.get('height', stream.get('coded_height', 0)) + video_stream_index = stream.get('index') + break + + return width, height, video_stream_index diff --git a/source/extract_ass_subtitles_to_files/plugin.py b/source/extract_ass_subtitles_to_files/plugin.py new file mode 100644 index 000000000..000184d2f --- /dev/null +++ b/source/extract_ass_subtitles_to_files/plugin.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Written by: Josh.5 + Date: 18 April 2021, (1:41 AM) + + Copyright: + Copyright (C) 2021 Josh Sunnex + + 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, version 3. + + 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 . + +""" +import logging +import os +import re + +from unmanic.libs.unplugins.settings import PluginSettings +from unmanic.libs.directoryinfo import UnmanicDirectoryInfo + +from extract_ass_subtitles_to_files.lib.ffmpeg import StreamMapper, Probe, Parser + +# Configure plugin logger +logger = logging.getLogger("Unmanic.Plugin.extract_ass_subtitles_to_files") + + +class Settings(PluginSettings): + settings = { + "languages_to_extract": "", + "include_title_in_output_file_name": True + } + + def __init__(self, *args, **kwargs): + super(Settings, self).__init__(*args, **kwargs) + + self.form_settings = { + "languages_to_extract": { + "label": "Subtitle languages to extract (leave empty for all)", + }, + "include_title_in_output_file_name": { + "label": "Include title in output file name", + }, + } + + +class PluginStreamMapper(StreamMapper): + def __init__(self): + super(PluginStreamMapper, self).__init__(logger, ['subtitle']) + self.sub_streams = [] + self.settings = None + + def set_settings(self, settings): + self.settings = settings + + def _get_language_list(self): + language_list = self.settings.get_setting('languages_to_extract') + language_list = re.sub('\s', '-', language_list) + languages = list(filter(None, language_list.lower().split(','))) + + return [language.strip() for language in languages] + + def test_stream_needs_processing(self, stream_info: dict): + """Any text based will need to be processed""" + + if stream_info.get('codec_name', '').lower() not in ['ass', 'subrip', 'mov_text']: + return False + + languages = self._get_language_list() + + # If no languages specified, extract all + if len(languages) == 0: + return True + + language_tag = stream_info.get('tags').get('language', '').lower() + + return language_tag in languages + + def custom_stream_mapping(self, stream_info: dict, stream_id: int): + stream_tags = stream_info.get('tags', {}) + + # e.g. 'eng', 'fra' + language_tag = stream_tags.get('language', '').lower() + # e.g. 'English', 'French' + title_tag = stream_tags.get('title', '') + + languages = self._get_language_list() + + # Skip stream + if len(languages) > 0 and language_tag not in languages: + return { + 'stream_mapping': [], + 'stream_encoding': [], + } + + # Find a tag for this subtitle + subtitle_tag = '' + + if language_tag: + subtitle_tag = "{}.{}".format(subtitle_tag, language_tag) + + if title_tag and self.settings.get_setting('include_title_in_output_file_name'): + subtitle_tag = "{}.{}".format(subtitle_tag, title_tag) + + # If there were no tags, just number the file + if not subtitle_tag: + subtitle_tag = "{}.{}".format(subtitle_tag, stream_info.get('index')) + + # Ensure subtitle tag does not contain whitespace or slashes + subtitle_tag = re.sub('\s|/|\\\\', '-', subtitle_tag) + + self.sub_streams.append( + { + 'stream_id': stream_id, + 'subtitle_tag': subtitle_tag, + 'stream_mapping': ['-map', '0:s:{}'.format(stream_id)], + } + ) + + # Copy the streams to the destination. This will actually do nothing... + return { + 'stream_mapping': ['-map', '0:s:{}'.format(stream_id)], + 'stream_encoding': ['-c:s:{}'.format(stream_id), 'copy'], + } + + def get_ffmpeg_args(self): + """ + Overwrite default function. We only need the first lot of args. + + :return: + """ + args = [] + + # Add generic options first + args += self.generic_options + + # Add the input file + # This class requires at least one input file specified with the input_file attribute + if not self.input_file: + raise Exception("Input file has not been set") + args += ['-i', self.input_file] + + # Add other main options + args += self.main_options + + # Add advanced options. This includes the stream mapping and the encoding args + args += self.advanced_options + + return args + +def ass_already_extracted(settings, path): + directory_info = UnmanicDirectoryInfo(os.path.dirname(path)) + + try: + already_extracted = directory_info.get('extract_ass_subtitles_to_files', os.path.basename(path)) + except NoSectionError as e: + already_extracted = '' + except NoOptionError as e: + already_extracted = '' + except Exception as e: + logger.debug("Unknown exception {}.".format(e)) + already_extracted = '' + + if already_extracted: + logger.debug("File's ass subtitle streams were previously extracted with {}.".format(already_extracted)) + return True + + # Default to... + return False + +def on_library_management_file_test(data): + """ + Runner function - enables additional actions during the library management file tests. + + The 'data' object argument includes: + library_id - The library that the current task is associated with + path - String containing the full path to the file being tested. + issues - List of currently found issues for not processing the file. + add_file_to_pending_tasks - Boolean, is the file currently marked to be added to the queue for processing. + priority_score - Integer, an additional score that can be added to set the position of the new task in the task queue. + shared_info - Dictionary, information provided by previous plugin runners. This can be appended to for subsequent runners. + + :param data: + :return: + + """ + # Configure settings object (maintain compatibility with v1 plugins) + if data.get('library_id'): + settings = Settings(library_id=data.get('library_id')) + else: + settings = Settings() + + # Get the path to the file + abspath = data.get('path') + + # Get file probe + probe = Probe(logger, allowed_mimetypes=['video']) + if 'ffprobe' in data.get('shared_info', {}): + if not probe.set_probe(data.get('shared_info', {}).get('ffprobe')): + # Failed to set ffprobe from shared info. + # Probably due to it being for an incompatible mimetype declared above + return + elif not probe.file(abspath): + # File probe failed, skip the rest of this test + return + # Set file probe to shared infor for subsequent file test runners + if 'shared_info' in data: + data['shared_info'] = {} + data['shared_info']['ffprobe'] = probe.get_probe() + + # Get stream mapper + mapper = PluginStreamMapper() + mapper.set_settings(settings) + mapper.set_probe(probe) + + if not ass_already_extracted(settings, abspath): + # Mark this file to be added to the pending tasks + data['add_file_to_pending_tasks'] = True + logger.debug("File '{}' should be added to task list. File has not been previously had ass extracted.".format(abspath)) + else: + logger.debug("File '{}' has been previously had ass extracted.".format(abspath)) + + return data + +def on_worker_process(data): + """ + Runner function - enables additional configured processing jobs during the worker stages of a task. + + The 'data' object argument includes: + exec_command - A command that Unmanic should execute. Can be empty. + command_progress_parser - A function that Unmanic can use to parse the STDOUT of the command to collect progress stats. Can be empty. + file_in - The source file to be processed by the command. + file_out - The destination that the command should output (may be the same as the file_in if necessary). + original_file_path - The absolute path to the original file. + repeat - Boolean, should this runner be executed again once completed with the same variables. + + DEPRECIATED 'data' object args passed for legacy Unmanic versions: + exec_ffmpeg - Boolean, should Unmanic run FFMPEG with the data returned from this plugin. + ffmpeg_args - A list of Unmanic's default FFMPEG args. + + :param data: + :return: + + """ + # Default to no FFMPEG command required. This prevents the FFMPEG command from running if it is not required + data['exec_command'] = [] + data['repeat'] = False + + # Get the path to the file + abspath = data.get('file_in') + + # Get file probe + probe = Probe(logger, allowed_mimetypes=['video']) + if not probe.file(abspath): + # File probe failed, skip the rest of this test + return + + if data.get('library_id'): + settings = Settings(library_id=data.get('library_id')) + else: + settings = Settings() + + if not ass_already_extracted(settings, data.get('file_in')): + # Get stream mapper + mapper = PluginStreamMapper() + + mapper.set_settings(settings) + mapper.set_probe(probe) + + split_original_file_path = os.path.splitext(data.get('original_file_path')) + original_file_directory = os.path.dirname(data.get('original_file_path')) + + if mapper.streams_need_processing(): + # Set the input file + mapper.set_input_file(abspath) + + # Get generated ffmpeg args + ffmpeg_args = mapper.get_ffmpeg_args() + + # Append STR extract args + for sub_stream in mapper.sub_streams: + stream_mapping = sub_stream.get('stream_mapping', []) + subtitle_tag = sub_stream.get('subtitle_tag') + + ffmpeg_args += stream_mapping + ffmpeg_args += [ + "-y", + os.path.join(original_file_directory, "{}{}.ass".format(split_original_file_path[0], subtitle_tag)), + ] + + # Apply ffmpeg args to command + data['exec_command'] = ['ffmpeg'] + data['exec_command'] += ffmpeg_args + + # Set the parser + parser = Parser(logger) + parser.set_probe(probe) + data['command_progress_parser'] = parser.parse_progress + return data + +def on_postprocessor_task_results(data): + """ + Runner function - provides a means for additional postprocessor functions based on the task success. + + The 'data' object argument includes: + task_processing_success - Boolean, did all task processes complete successfully. + file_move_processes_success - Boolean, did all postprocessor movement tasks complete successfully. + destination_files - List containing all file paths created by postprocessor file movements. + source_data - Dictionary containing data pertaining to the original source file. + + :param data: + :return: + + """ + # We only care that the task completed successfully. + # If a worker processing task was unsuccessful, dont mark the file streams as kept + # TODO: Figure out a way to know if a file's streams were kept but another plugin was the + # cause of the task processing failure flag + if not data.get('task_processing_success'): + return data + + # Configure settings object (maintain compatibility with v1 plugins) + if data.get('library_id'): + settings = Settings(library_id=data.get('library_id')) + else: + settings = Settings() + + abspath = data.get('source_data').get('abspath') + probe_data=Probe(logger, allowed_mimetypes=['video']) + if probe_data.file(abspath): + probe_streams=probe_data.get_probe()["streams"] + else: + probe_streams=[] + + # Loop over the destination_files list and update the directory info file for each one + for destination_file in data.get('destination_files'): + langs = "" + langs = settings.get_setting('languages_to_extract') + if probe_streams: + subs = [probe_streams[i]["tags"]["language"] for i in range(len(probe_streams)) if probe_streams[i]["codec_type"] == "subtitle" and "tags" in probe_streams[i] and "language" in probe_streams[i]["tags"]] + if langs: + subs = [i for i in subs if i in langs] + subs = ' '.join(subs) + else: + subs="" + directory_info = UnmanicDirectoryInfo(os.path.dirname(destination_file)) + directory_info.set('extract_ass_subtitles_to_files', os.path.basename(destination_file), subs) + directory_info.save() + logger.info("ass subtitles processed for '{}' and recorded in .unmanic file.".format(destination_file)) + + return data \ No newline at end of file diff --git a/source/extract_ass_subtitles_to_files/requirements.txt b/source/extract_ass_subtitles_to_files/requirements.txt new file mode 100644 index 000000000..e69de29bb From 97198e969c5501fe68a46f09690d92b2e8bf504a Mon Sep 17 00:00:00 2001 From: Games Date: Tue, 20 Aug 2024 17:43:19 -0400 Subject: [PATCH 2/6] updated info.json to add name, updated icon.png to show ASS/ssa, updated description file to remove mention of SRT and the note of converting ASS to SRT first. --- .../description.md | 8 ++------ .../extract_ass_subtitles_to_files/icon.png | Bin 20360 -> 24304 bytes .../extract_ass_subtitles_to_files/info.json | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/source/extract_ass_subtitles_to_files/description.md b/source/extract_ass_subtitles_to_files/description.md index 167048aea..02f44f314 100644 --- a/source/extract_ass_subtitles_to_files/description.md +++ b/source/extract_ass_subtitles_to_files/description.md @@ -1,15 +1,11 @@ -Any SRT subtitle streams found in the file will be exported as *.srt files in the same directory as the original file. +Any ASS/SSA subtitle streams found in the file will be exported as *.ass files in the same directory as the original file. :::warning This plugin is not compatible with linking as the remote link will not have access to the original source file's directory. ::: -To include other formats, such as ASS, consider first converting the subtitle streams to SRT using the Plugin(s) - -Install the **"Convert any ASS subtitle streams in videos to SRT"** plugin and configure the plugin flow to set it before this one - :::note -This Plugin does not contain a file tester to detect files that contain SRT subtitle streams. +This Plugin does not contain a file tester to detect files that contain ASS/SSA subtitle streams. Ensure it is pared with another Plugin. ::: diff --git a/source/extract_ass_subtitles_to_files/icon.png b/source/extract_ass_subtitles_to_files/icon.png index dbc845ecb1a9985ed11d2ddba3596d4cc0ac7309..ac5f1851d6c263466fb5aa6767a6738b1a38977f 100644 GIT binary patch literal 24304 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 O9?{m*N;z=;_x}Scg1xi= literal 20360 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�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 IXn*7X0tl1U1^@s6 diff --git a/source/extract_ass_subtitles_to_files/info.json b/source/extract_ass_subtitles_to_files/info.json index b1d7654f8..66e1d65ee 100644 --- a/source/extract_ass_subtitles_to_files/info.json +++ b/source/extract_ass_subtitles_to_files/info.json @@ -1,5 +1,5 @@ { - "author": "Josh.5, Bambanah", + "author": "Josh.5, Bambanah, soultaco83", "compatibility": [ 1, 2 @@ -15,5 +15,5 @@ "on_worker_process": 2 }, "tags": "subtitle,ffmpeg", - "version": "0.0.10" + "version": "0.0.1" } \ No newline at end of file From 9aec4144c6bebb575077191a925b855d0507282a Mon Sep 17 00:00:00 2001 From: Games Date: Tue, 20 Aug 2024 17:45:00 -0400 Subject: [PATCH 3/6] added check for .unmanic file, updated version to 0.0.10 --- .../extract_srt_subtitles_to_files/plugin.py | 143 +++++++++++++----- 1 file changed, 109 insertions(+), 34 deletions(-) diff --git a/source/extract_srt_subtitles_to_files/plugin.py b/source/extract_srt_subtitles_to_files/plugin.py index 9896d888c..b681048ce 100644 --- a/source/extract_srt_subtitles_to_files/plugin.py +++ b/source/extract_srt_subtitles_to_files/plugin.py @@ -24,6 +24,7 @@ import re from unmanic.libs.unplugins.settings import PluginSettings +from unmanic.libs.directoryinfo import UnmanicDirectoryInfo from extract_srt_subtitles_to_files.lib.ffmpeg import StreamMapper, Probe, Parser @@ -154,6 +155,25 @@ def get_ffmpeg_args(self): return args +def srt_already_extracted(settings, path): + directory_info = UnmanicDirectoryInfo(os.path.dirname(path)) + + try: + already_extracted = directory_info.get('extract_srt_subtitles_to_files', os.path.basename(path)) + except NoSectionError as e: + already_extracted = '' + except NoOptionError as e: + already_extracted = '' + except Exception as e: + logger.debug("Unknown exception {}.".format(e)) + already_extracted = '' + + if already_extracted: + logger.debug("File's srt subtitle streams were previously extracted with {}.".format(already_extracted)) + return True + + # Default to... + return False def on_library_management_file_test(data): """ @@ -200,13 +220,14 @@ def on_library_management_file_test(data): mapper.set_settings(settings) mapper.set_probe(probe) - if mapper.streams_need_processing(): + if not srt_already_extracted(settings, abspath): # Mark this file to be added to the pending tasks data['add_file_to_pending_tasks'] = True - logger.debug("File '{}' should be added to task list. Probe found streams require processing.".format(abspath)) + logger.debug("File '{}' should be added to task list. File has not been previously had SRT extracted.".format(abspath)) else: - logger.debug("File '{}' does not contain streams require processing.".format(abspath)) + logger.debug("File '{}' has been previously had SRT extracted.".format(abspath)) + return data def on_worker_process(data): """ @@ -245,39 +266,93 @@ def on_worker_process(data): settings = Settings(library_id=data.get('library_id')) else: settings = Settings() + + if not srt_already_extracted(settings, data.get('file_in')): + # Get stream mapper + mapper = PluginStreamMapper() + + mapper.set_settings(settings) + mapper.set_probe(probe) + + split_original_file_path = os.path.splitext(data.get('original_file_path')) + original_file_directory = os.path.dirname(data.get('original_file_path')) + + if mapper.streams_need_processing(): + # Set the input file + mapper.set_input_file(abspath) + + # Get generated ffmpeg args + ffmpeg_args = mapper.get_ffmpeg_args() + + # Append STR extract args + for sub_stream in mapper.sub_streams: + stream_mapping = sub_stream.get('stream_mapping', []) + subtitle_tag = sub_stream.get('subtitle_tag') + + ffmpeg_args += stream_mapping + ffmpeg_args += [ + "-y", + os.path.join(original_file_directory, "{}{}.srt".format(split_original_file_path[0], subtitle_tag)), + ] + + # Apply ffmpeg args to command + data['exec_command'] = ['ffmpeg'] + data['exec_command'] += ffmpeg_args + + # Set the parser + parser = Parser(logger) + parser.set_probe(probe) + data['command_progress_parser'] = parser.parse_progress + return data + +def on_postprocessor_task_results(data): + """ + Runner function - provides a means for additional postprocessor functions based on the task success. - # Get stream mapper - mapper = PluginStreamMapper() - - mapper.set_settings(settings) - mapper.set_probe(probe) - - split_original_file_path = os.path.splitext(data.get('original_file_path')) - original_file_directory = os.path.dirname(data.get('original_file_path')) - - if mapper.streams_need_processing(): - # Set the input file - mapper.set_input_file(abspath) - - # Get generated ffmpeg args - ffmpeg_args = mapper.get_ffmpeg_args() + The 'data' object argument includes: + task_processing_success - Boolean, did all task processes complete successfully. + file_move_processes_success - Boolean, did all postprocessor movement tasks complete successfully. + destination_files - List containing all file paths created by postprocessor file movements. + source_data - Dictionary containing data pertaining to the original source file. - # Append STR extract args - for sub_stream in mapper.sub_streams: - stream_mapping = sub_stream.get('stream_mapping', []) - subtitle_tag = sub_stream.get('subtitle_tag') + :param data: + :return: - ffmpeg_args += stream_mapping - ffmpeg_args += [ - "-y", - os.path.join(original_file_directory, "{}{}.srt".format(split_original_file_path[0], subtitle_tag)), - ] + """ + # We only care that the task completed successfully. + # If a worker processing task was unsuccessful, dont mark the file streams as kept + # TODO: Figure out a way to know if a file's streams were kept but another plugin was the + # cause of the task processing failure flag + if not data.get('task_processing_success'): + return data - # Apply ffmpeg args to command - data['exec_command'] = ['ffmpeg'] - data['exec_command'] += ffmpeg_args + # Configure settings object (maintain compatibility with v1 plugins) + if data.get('library_id'): + settings = Settings(library_id=data.get('library_id')) + else: + settings = Settings() - # Set the parser - parser = Parser(logger) - parser.set_probe(probe) - data['command_progress_parser'] = parser.parse_progress + abspath = data.get('source_data').get('abspath') + probe_data=Probe(logger, allowed_mimetypes=['video']) + if probe_data.file(abspath): + probe_streams=probe_data.get_probe()["streams"] + else: + probe_streams=[] + + # Loop over the destination_files list and update the directory info file for each one + for destination_file in data.get('destination_files'): + langs = "" + langs = settings.get_setting('languages_to_extract') + if probe_streams: + subs = [probe_streams[i]["tags"]["language"] for i in range(len(probe_streams)) if probe_streams[i]["codec_type"] == "subtitle" and "tags" in probe_streams[i] and "language" in probe_streams[i]["tags"]] + if langs: + subs = [i for i in subs if i in langs] + subs = ' '.join(subs) + else: + subs="" + directory_info = UnmanicDirectoryInfo(os.path.dirname(destination_file)) + directory_info.set('extract_srt_subtitles_to_files', os.path.basename(destination_file), subs) + directory_info.save() + logger.info("SRT subtitles processed for '{}' and recorded in .unmanic file.".format(destination_file)) + + return data \ No newline at end of file From 89acb3f0de48bf7c41cf467150e60583cff8d3d5 Mon Sep 17 00:00:00 2001 From: Games Date: Tue, 20 Aug 2024 17:48:12 -0400 Subject: [PATCH 4/6] added .gitignore --- source/extract_ass_subtitles_to_files/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 source/extract_ass_subtitles_to_files/.gitignore diff --git a/source/extract_ass_subtitles_to_files/.gitignore b/source/extract_ass_subtitles_to_files/.gitignore new file mode 100644 index 000000000..499ca09c3 --- /dev/null +++ b/source/extract_ass_subtitles_to_files/.gitignore @@ -0,0 +1,4 @@ +**/__pycache__ +*.py[cod] +**/site-packages +settings.json From 2ff53153ec172e44880479174b373a760a61037b Mon Sep 17 00:00:00 2001 From: Games Date: Tue, 20 Aug 2024 18:08:45 -0400 Subject: [PATCH 5/6] fixed version on info.json for srt extract. As it appears to already be on 10, bumped to 11. Copied info.json and changelog to ass extract --- source/extract_ass_subtitles_to_files/changelog.md | 3 +++ source/extract_ass_subtitles_to_files/info.json | 4 ++-- source/extract_srt_subtitles_to_files/changelog.md | 3 +++ source/extract_srt_subtitles_to_files/info.json | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/source/extract_ass_subtitles_to_files/changelog.md b/source/extract_ass_subtitles_to_files/changelog.md index 6315c636b..2d2a925be 100644 --- a/source/extract_ass_subtitles_to_files/changelog.md +++ b/source/extract_ass_subtitles_to_files/changelog.md @@ -1,3 +1,6 @@ +**0.0.11** +- Added .unmanic file + **0.0.9** - Add settings to specify: - Which languages to extract diff --git a/source/extract_ass_subtitles_to_files/info.json b/source/extract_ass_subtitles_to_files/info.json index 66e1d65ee..5cca7c148 100644 --- a/source/extract_ass_subtitles_to_files/info.json +++ b/source/extract_ass_subtitles_to_files/info.json @@ -7,7 +7,7 @@ "description": "Extract text based subtitle streams to *.ass files.", "icon": "https://raw.githubusercontent.com/Josh5/unmanic.plugin.extract_srt_subtitles_to_files/master/icon.png", "id": "extract_ass_subtitles_to_files", - "name": "Extract text subtitle streams to ASS files", + "name": "Extract text subtitle streams to ASS/SSA files", "platform": [ "all" ], @@ -15,5 +15,5 @@ "on_worker_process": 2 }, "tags": "subtitle,ffmpeg", - "version": "0.0.1" + "version": "0.0.11" } \ No newline at end of file diff --git a/source/extract_srt_subtitles_to_files/changelog.md b/source/extract_srt_subtitles_to_files/changelog.md index 6315c636b..2d2a925be 100644 --- a/source/extract_srt_subtitles_to_files/changelog.md +++ b/source/extract_srt_subtitles_to_files/changelog.md @@ -1,3 +1,6 @@ +**0.0.11** +- Added .unmanic file + **0.0.9** - Add settings to specify: - Which languages to extract diff --git a/source/extract_srt_subtitles_to_files/info.json b/source/extract_srt_subtitles_to_files/info.json index 9330ae7e3..bcb2d1c6e 100644 --- a/source/extract_srt_subtitles_to_files/info.json +++ b/source/extract_srt_subtitles_to_files/info.json @@ -15,5 +15,5 @@ "on_worker_process": 2 }, "tags": "subtitle,ffmpeg", - "version": "0.0.10" + "version": "0.0.11" } \ No newline at end of file From af2848234d72e0d6a1c09594c9f1be39aafc4c41 Mon Sep 17 00:00:00 2001 From: soultaco83 <76927195+soultaco83@users.noreply.github.com> Date: Tue, 20 Aug 2024 18:33:12 -0400 Subject: [PATCH 6/6] Update README.md Changed srt to ass/ssa --- source/extract_ass_subtitles_to_files/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/extract_ass_subtitles_to_files/README.md b/source/extract_ass_subtitles_to_files/README.md index 028c3ceef..b8fe942a7 100644 --- a/source/extract_ass_subtitles_to_files/README.md +++ b/source/extract_ass_subtitles_to_files/README.md @@ -1,4 +1,4 @@ -# Extract text subtitle streams to SRT files +# Extract text subtitle streams to ASS/SSA files Plugin for [Unmanic](https://github.com/Unmanic) ---