diff --git a/dot_config/private_mpv/mpv.conf b/dot_config/private_mpv/mpv.conf new file mode 100644 index 0000000..ff66aed --- /dev/null +++ b/dot_config/private_mpv/mpv.conf @@ -0,0 +1 @@ +save-position-on-quit diff --git a/dot_config/private_mpv/script-opts/videoclip.conf b/dot_config/private_mpv/script-opts/videoclip.conf new file mode 100644 index 0000000..2878ba3 --- /dev/null +++ b/dot_config/private_mpv/script-opts/videoclip.conf @@ -0,0 +1,47 @@ +# Absolute paths to the folders where generated clips will be placed. +# `~` is supported, but environment variables (e.g. `$HOME`) are not supported due to mpv limitations. +video_folder_path=~/Desktop +audio_folder_path=~/Desktop + + +# Clean filenames (remove special characters) (yes or no) +clean_filename=yes + +# Video settings +video_width=-2 +video_height=720 +video_bitrate=1M +# Available video formats: mp4, vp9, vp8 +video_format=mp4 +# The range of the scale is 0–51, where 0 is lossless, +# 23 is the default, and 51 is worst quality possible. +# Insane values like 9999 still work but produce the worst quality. +video_quality=23 +# Use the slowest preset that you have patience for. +# https://trac.ffmpeg.org/wiki/Encode/H.264 +preset=fastest +# FPS / framerate. Set to "auto" or a number. +video_fps=auto +#video_fps=60 + +# Audio settings +# Available formats: opus or aac +audio_format=opus +# Opus sounds good at low bitrates 32-64k, but aac requires 128-256k. +audio_bitrate=32k + +# Catbox.moe upload settings +# Whether uploads should go to litterbox instead of catbox. +# catbox files are stored permanently, while litterbox is temporary +litterbox=yes +# If using litterbox, time until video expires +# Available values: 1h, 12h, 24h, 72h +litterbox_expire=72h + +# Filename format +# Available tags: %n = filename, %t = title, %s = start, %e = end, %d = duration, +# %Y = year, %M = months, %D = day, %H = hours (24), %I = hours (12), +# %P = am/pm %N = minutes, %S = seconds +# Title will fallback to filename if it's not present +#filename_template=%n_%s-%e(%d) +filename_template=%n_%s-%e diff --git a/dot_config/private_mpv/script-opts/webm.conf b/dot_config/private_mpv/script-opts/webm.conf new file mode 100644 index 0000000..8360ef1 --- /dev/null +++ b/dot_config/private_mpv/script-opts/webm.conf @@ -0,0 +1,73 @@ +# Defaults to shift+w +keybind=W +# If empty, saves on the same directory of the playing video. +# A starting "~" will be replaced by the home dir. +# This field is delimited by double-square-brackets - [[ and ]] - instead of +# quotes, because Windows users might run into a issue when using +# backslashes as a path separator. Examples of valid inputs for this field +# would be: [[]] (the default, empty value), [[C:\Users\John]] (on Windows), +# and [[/home/john]] (on Unix-like systems eg. Linux). +# The [[]] delimiter is not needed when using from a configuration file +# in the script-opts folder. +output_directory=~/Desktop +run_detached=no +# Template string for the output file +# %f - Filename, with extension +# %F - Filename, without extension +# %T - Media title, if it exists, or filename, with extension (useful for some streams, such as YouTube). +# %s, %e - Start and end time, with milliseconds +# %S, %E - Start and end time, without milliseconds +# %M - "-audio", if audio is enabled, empty otherwise +# %R - "-(height)p", where height is the video's height, or scale_height, if it's enabled. +# More specifiers are supported, see https://mpv.io/manual/master/#options-screenshot-template +# Property expansion is supported (with %{} at top level, ${} when nested), see https://mpv.io/manual/master/#property-expansion +output_template=%F-[%s-%e]%M +# Scale video to a certain height, keeping the aspect ratio. -1 disables it. +scale_height=-1 +# Change the FPS of the output video, dropping or duplicating frames as needed. +# -1 means the FPS will be unchanged from the source. +fps=-1 +# Target filesize, in kB. This will be used to calculate the bitrate +# used on the encode. If this is set to <= 0, the video bitrate will be set +# to 0, which might enable constant quality modes, depending on the +# video codec that's used (VP8 and VP9, for example). +target_filesize=2500 +# If true, will use stricter flags to ensure the resulting file doesn't +# overshoot the target filesize. Not recommended, as constrained quality +# mode should work well, unless you're really having trouble hitting +# the target size. +strict_filesize_constraint=no +strict_bitrate_multiplier=0.95 +# In kilobits. +strict_audio_bitrate=64 +# Sets the output format, from a few predefined ones. +# Currently we have: +# webm-vp8 (libvpx/libvorbis) +# webm-vp9 (libvpx-vp9/libopus) +# mp4 (h264/AAC) +# mp4-nvenc (h264-NVENC/AAC) +# raw (rawvideo/pcm_s16le). +# mp3 (libmp3lame) +# and gif +output_format=mp4 +twopass=no +# If set, applies the video filters currently used on the playback to the encode. +apply_current_filters=yes +# If set, writes the video's filename to the "Title" field on the metadata. +write_filename_on_metadata=no +# Set the number of encoding threads, for codecs libvpx and libvpx-vp9 +libvpx_threads=4 +additional_flags= +# Constant Rate Factor (CRF). The value meaning and limits may change, +# from codec to codec. Set to -1 to disable. +crf=28 +# Useful for flags that may impact output filesize, such as qmin, qmax etc +# Won't be applied when strict_filesize_constraint is on. +non_strict_additional_flags= +# Display the encode progress, in %. Requires run_detached to be disabled. +# On Windows, it shows a cmd popup. "auto" will display progress on non-Windows platforms. +display_progress=auto +# The font size used in the menu. Isn't used for the notifications (started encode, finished encode etc) +font_size=28 +margin=10 +message_duration=5 diff --git a/dot_config/private_mpv/scripts/symlink_sponsorblock-minimal.lua b/dot_config/private_mpv/scripts/symlink_sponsorblock-minimal.lua new file mode 100644 index 0000000..b02411e --- /dev/null +++ b/dot_config/private_mpv/scripts/symlink_sponsorblock-minimal.lua @@ -0,0 +1 @@ +/usr/lib/mpv/sponsorblock-minimal.lua diff --git a/dot_config/private_mpv/scripts/videoclip/LICENSE b/dot_config/private_mpv/scripts/videoclip/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/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/dot_config/private_mpv/scripts/videoclip/README.md b/dot_config/private_mpv/scripts/videoclip/README.md new file mode 100644 index 0000000..3b78284 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/README.md @@ -0,0 +1,172 @@ +![screenshot](https://github.com/Lemmmy/videoclip/assets/858456/855bff15-b0cd-4c12-a9ac-40a5e01d3b83) + +# videoclip + +[![Chat](https://img.shields.io/badge/chat-join-green)](https://tatsumoto-ren.github.io/blog/join-our-community.html) +![GitHub](https://img.shields.io/github/license/Ajatt-Tools/videoclip) +![GitHub top language](https://img.shields.io/github/languages/top/Ajatt-Tools/videoclip) +[![Patreon](https://img.shields.io/badge/support-patreon-orange)](https://tatsumoto.neocities.org/blog/donating-to-tatsumoto.html) + +Easily create video and audio clips with mpv in a few keypresses. +Videoclips are saved as `.mp4` or `.webm`. +Subtitles can be embedded into the clips. + +## Prerequisites + +1) [Install mpv](https://mpv.io/installation/). +2) Add the directory where `mpv` is installed + to the [PATH](https://www.mojeek.com/search?q=path+variable). + + If you're using GNU/Linux, this step is likely unnecessary + because package managers (`apt`, `pacman`, etc.) + place executable files to `/usr/bin` which is already added to the `PATH`. + If you have installed `mpv` to a non-standard location, + or if you're not using the GNU operating system, + you need to make sure that `mpv` is added to the `PATH`. + +## Installation + +### Using git + +Clone the repository to the `mpv/scripts` directory. +The command below works on the GNU operating system with `git` installed. + +``` bash +git clone 'https://github.com/Ajatt-Tools/videoclip.git' ~/.config/mpv/scripts/videoclip +``` + +To update the user-script on demand later, you can execute: + +``` bash +cd ~/.config/mpv/scripts/videoclip && git pull +``` + +### Manually + +Download +[the repository](https://github.com/Ajatt-Tools/videoclip/archive/refs/heads/master.zip) +and extract the folder containing +`videoclip.lua` +to your [mpv scripts](https://github.com/mpv-player/mpv/wiki/User-Scripts) directory: + +| OS | Location | +| --- | --- | +| GNU/Linux | `~/.config/mpv/scripts/` | +| Windows | `C:/Users/Username/AppData/Roaming/mpv/scripts/` | + +Note: in [Celluloid](https://www.archlinux.org/packages/community/x86_64/celluloid/) +user scripts are installed by switching to the "Plugins" tab +in the preferences dialog and dropping the files there. + +
+ +Expected directory tree + +``` +~/.config/mpv/scripts +|-- other_addon_1 +|-- other_addon_2 +`-- videoclip + |-- main.lua + |-- ... + `-- videoclip.lua +``` + +
+ +## Configuration + +The config file should be created by the user, if needed. + +| OS | Config location | +| --- | --- | +| GNU/Linux | `~/.config/mpv/script-opts/videoclip.conf` | +| Windows | `C:/Users/Username/AppData/Roaming/mpv/script-opts/videoclip.conf` | + +If a parameter is not specified in the config file, the default value will be used. +mpv doesn't tolerate spaces before and after `=`. + +Example configuration file: + +``` +# Absolute paths to the folders where generated clips will be placed. +# `~` is supported, but environment variables (e.g. `$HOME`) are not supported due to mpv limitations. +video_folder_path=~/Videos +audio_folder_path=~/Music + +# Menu size +font_size=24 + +# OSD settings. Line alignment: https://aegisub.org/docs/3.2/ASS_Tags/#\an +osd_align=7 +osd_outline=1.5 + +# Clean filenames (remove special characters) (yes or no) +clean_filename=yes + +# Video settings +video_width=-2 +video_height=480 +video_bitrate=1M +# Available video formats: mp4, vp9, vp8 +video_format=mp4 +# The range of the scale is 0–51, where 0 is lossless, +# 23 is the default, and 51 is worst quality possible. +# Insane values like 9999 still work but produce the worst quality. +video_quality=23 +# Use the slowest preset that you have patience for. +# https://trac.ffmpeg.org/wiki/Encode/H.264 +preset=faster +# FPS / framerate. Set to "auto" or a number. +video_fps=auto +#video_fps=60 + +# Audio settings +# Available formats: opus or aac +audio_format=opus +# Opus sounds good at low bitrates 32-64k, but aac requires 128-256k. +audio_bitrate=32k + +# Catbox.moe upload settings +# Whether uploads should go to litterbox instead of catbox. +# catbox files are stored permanently, while litterbox is temporary +litterbox=yes +# If using litterbox, time until video expires +# Available values: 1h, 12h, 24h, 72h +litterbox_expire=72h + +# Filename format +# Available tags: %n = filename, %t = title, %s = start, %e = end, %d = duration, +# %Y = year, %M = months, %D = day, %H = hours (24), %I = hours (12), +# %P = am/pm %N = minutes, %S = seconds +# Title will fallback to filename if it's not present +#filename_template=%n_%s-%e(%d) +filename_template=%n_%s-%e +``` + +### Key bindings + +| OS | Config location | +| --- | --- | +| GNU/Linux | `~/.config/mpv/input.conf` | +| Windows | `C:/Users/Username/AppData/Roaming/mpv/input.conf` | + +Add this line if you want to change the key that opens the script's menu. + +``` +c script-binding videoclip-menu-open +``` + +## Usage + +- Open a file in mpv and press `c` to open the script menu. +- Follow the onscreen instructions. You need to set the `start point`, +`end point`, and then press `c` to create the clip. + +It is possible to create silent videoclips. +To do that, first mute audio in mpv. +The default key binding is `m`. + +If a video has visible subtitles, they will be embedded automatically. +Toggle them off in mpv if you don't want any subtitles to be visible. +The default key binding is `v`. diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/HEAD b/dot_config/private_mpv/scripts/videoclip/dot_git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/config b/dot_config/private_mpv/scripts/videoclip/dot_git/config new file mode 100644 index 0000000..ae756d9 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/config @@ -0,0 +1,11 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +[remote "origin"] + url = https://github.com/Ajatt-Tools/videoclip.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/description b/dot_config/private_mpv/scripts/videoclip/dot_git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_applypatch-msg.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_applypatch-msg.sample new file mode 100644 index 0000000..a5d7b84 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_commit-msg.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_commit-msg.sample new file mode 100644 index 0000000..b58d118 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_fsmonitor-watchman.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_fsmonitor-watchman.sample new file mode 100644 index 0000000..23e856f --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_fsmonitor-watchman.sample @@ -0,0 +1,174 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + my $last_update_line = ""; + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + $last_update_line = qq[\n"since": $last_update_token,]; + } + my $query = <<" END"; + ["query", "$git_work_tree", {$last_update_line + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_post-update.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_post-update.sample new file mode 100644 index 0000000..ec17ec1 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-applypatch.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-applypatch.sample new file mode 100644 index 0000000..4142082 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-commit.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-commit.sample new file mode 100644 index 0000000..29ed5ee --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff-index --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-merge-commit.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-merge-commit.sample new file mode 100644 index 0000000..399eab1 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-push.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-push.sample new file mode 100644 index 0000000..4ce688d --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-rebase.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-rebase.sample new file mode 100644 index 0000000..6cbef5c --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-receive.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-receive.sample new file mode 100644 index 0000000..a1fd29e --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_prepare-commit-msg.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_prepare-commit-msg.sample new file mode 100644 index 0000000..10fa14c --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_push-to-checkout.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_push-to-checkout.sample new file mode 100644 index 0000000..af5a0c0 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + exit 1 +} + +unset GIT_DIR GIT_WORK_TREE +cd "$worktree" && + +if grep -q "^diff --git " "$1" +then + validate_patch "$1" +else + validate_cover_letter "$1" +fi && + +if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" +then + git config --unset-all sendemail.validateWorktree && + trap 'git worktree remove -ff "$worktree"' EXIT && + validate_series +fi diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_update.sample b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_update.sample new file mode 100644 index 0000000..c4d426b --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/hooks/executable_update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/index b/dot_config/private_mpv/scripts/videoclip/dot_git/index new file mode 100644 index 0000000..f71c360 Binary files /dev/null and b/dot_config/private_mpv/scripts/videoclip/dot_git/index differ diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/info/exclude b/dot_config/private_mpv/scripts/videoclip/dot_git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/logs/HEAD b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/HEAD new file mode 100644 index 0000000..8dfb699 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 44d4087c5fd9cc15e73309778b3af5f73064b358 Rain 1759363834 -0400 clone: from https://github.com/Ajatt-Tools/videoclip.git diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/heads/master b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/heads/master new file mode 100644 index 0000000..8dfb699 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/heads/master @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 44d4087c5fd9cc15e73309778b3af5f73064b358 Rain 1759363834 -0400 clone: from https://github.com/Ajatt-Tools/videoclip.git diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/remotes/origin/HEAD b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/remotes/origin/HEAD new file mode 100644 index 0000000..8dfb699 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/logs/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 44d4087c5fd9cc15e73309778b3af5f73064b358 Rain 1759363834 -0400 clone: from https://github.com/Ajatt-Tools/videoclip.git diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/objects/info/.keep b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/info/.keep new file mode 100644 index 0000000..e69de29 diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.idx b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.idx new file mode 100644 index 0000000..29f9bc3 Binary files /dev/null and b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.idx differ diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.pack b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.pack new file mode 100644 index 0000000..11217fa Binary files /dev/null and b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.pack differ diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.rev b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.rev new file mode 100644 index 0000000..4549f32 Binary files /dev/null and b/dot_config/private_mpv/scripts/videoclip/dot_git/objects/pack/readonly_pack-98877bd46235e2bcf152cdad6227778126a1e6c9.rev differ diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/packed-refs b/dot_config/private_mpv/scripts/videoclip/dot_git/packed-refs new file mode 100644 index 0000000..aa9f642 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/packed-refs @@ -0,0 +1,6 @@ +# pack-refs with: peeled fully-peeled sorted +44d4087c5fd9cc15e73309778b3af5f73064b358 refs/remotes/origin/master +08058ab4b73639120f68b8022d46b9b8ae2f2d46 refs/tags/v0.1 +^249122d245bc5ec2a0687346af730b1cc2273b21 +b8a9c7bf38c4f1b3a069b4d2fedc99fde5464872 refs/tags/v0.2 +^785eb86bc080c445e8feb947d7caa8f3a097bf2b diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/refs/heads/master b/dot_config/private_mpv/scripts/videoclip/dot_git/refs/heads/master new file mode 100644 index 0000000..5c9c190 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/refs/heads/master @@ -0,0 +1 @@ +44d4087c5fd9cc15e73309778b3af5f73064b358 diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/refs/remotes/origin/HEAD b/dot_config/private_mpv/scripts/videoclip/dot_git/refs/remotes/origin/HEAD new file mode 100644 index 0000000..6efe28f --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_git/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +ref: refs/remotes/origin/master diff --git a/dot_config/private_mpv/scripts/videoclip/dot_git/refs/tags/.keep b/dot_config/private_mpv/scripts/videoclip/dot_git/refs/tags/.keep new file mode 100644 index 0000000..e69de29 diff --git a/dot_config/private_mpv/scripts/videoclip/dot_github/FUNDING.yml b/dot_config/private_mpv/scripts/videoclip/dot_github/FUNDING.yml new file mode 100644 index 0000000..8039a43 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +patreon: tatsumoto_ren +liberapay: Tatsumoto +custom: https://tatsumoto.neocities.org/blog/donating-to-tatsumoto.html diff --git a/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/config.yml b/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..54f5b38 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +blank_issues_enabled: false +contact_links: + - name: Bug Reports + url: https://tatsumoto.neocities.org/blog/join-our-community + about: This issue tracker is for developers. Please report any bugs you encounter over in the chats, and they will be triaged there. + + - name: Questions/Support + url: https://tatsumoto.neocities.org/blog/join-our-community + about: If you have a question or need support, please post in the community chats. + + - name: Feature Requests/Suggestions + url: https://tatsumoto.neocities.org/blog/join-our-community + about: Please post suggestions and feature requests in our chat. diff --git a/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/issue.md b/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 0000000..f4a2a01 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/dot_github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,37 @@ +--- +name: Issue +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +- The add-on crashes? +- The add-on stopped working? +- Have a feature request? + +Please ask on https://tatsumoto.neocities.org/blog/join-our-community.html instead. +It is easier for our members to provide support over in the chats. Thank you! + +Don't forget to provide the following information: + +**Environment** + +1) OS name [e.g. Parabola, Debian] +2) OS version +3) mpv version +4) Logs +5) Screenshots, if applicable + +**Describe the bug** +A description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1) Press x '...' +2) Click on '....' +3) See error + +**Expected behavior** +A description of what you expected to happen. diff --git a/dot_config/private_mpv/scripts/videoclip/encoder.lua b/dot_config/private_mpv/scripts/videoclip/encoder.lua new file mode 100644 index 0000000..ea206b0 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/encoder.lua @@ -0,0 +1,246 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +Encoder provides interface for creating audio/video clips. +]] + +local mp = require('mp') +local h = require('helpers') +local utils = require('mp.utils') +local this = {} + +local function toms(timestamp) + --- Trim timestamp down to milliseconds. + return string.format("%.3f", timestamp) +end + +local function clean_filename(filename) + filename = h.remove_extension(filename) + if this.config.clean_filename then + filename = h.remove_text_in_brackets(filename) + filename = h.remove_special_characters(filename) + -- remove_text_in_brackets might leave spaces at the start or the end, so trim those + filename = h.strip(filename) + end + return filename +end + +local function clean_forbidden_characters(title) + return title:gsub('[<>:"/\\|%?%*]+', '.') +end + +local function construct_output_filename_noext() + + local filename = mp.get_property("filename") -- filename without path + local title = mp.get_property("media-title") -- if the video doesn't have a title, it will fallback to filename + local date = os.date("*t") -- get current date and time as table + + -- Apply the same operation when the video doesn't have a title + -- thus it will be the same as filename + if title == filename then + filename = clean_filename(filename) + title = filename + else + filename = clean_filename(filename) + title = clean_forbidden_characters(title) + end + + -- Available tags: %n = filename, %t = title, %s = start, %e = end, %d = duration, + -- %Y = year, %M = months, %D = day, %H = hours (24), %I = hours (12), + -- %P = am/pm %N = minutes, %S = seconds + filename = this.config.filename_template + :gsub("%%n", filename) + :gsub("%%t", title) + :gsub("%%s", h.human_readable_time(this.timings['start'])) + :gsub("%%e", h.human_readable_time(this.timings['end'])) + :gsub("%%d", h.human_readable_time(this.timings['end'] - this.timings['start'])) + :gsub("%%Y", date.year) + :gsub("%%M", h.two_digit(date.month)) + :gsub("%%D", h.two_digit(date.day)) + :gsub("%%H", h.two_digit(date.hour)) + :gsub("%%I", h.two_digit(h.twelve_hour(date.hour)['hour'])) + :gsub("%%P", h.twelve_hour(date.hour)['sign']) + :gsub("%%N", h.two_digit(date.min)) + :gsub("%%S", h.two_digit(date.sec)) + + return filename +end + +function this.get_ext_subs_paths() + local track_list = mp.get_property_native('track-list') + local external_subs_list = {} + for _, track in pairs(track_list) do + if track.type == 'sub' and track.external == true then + external_subs_list[track.id] = track['external-filename'] + end + end + return external_subs_list +end + +function this.append_embed_subs_args(args) + local ext_subs_paths = this.get_ext_subs_paths() + for _, ext_subs_path in pairs(ext_subs_paths) do + table.insert(args, #args, table.concat { '--sub-files-append=', ext_subs_path, }) + end + return args +end + +this.mk_out_path_video = function(clip_filename_noext) + return utils.join_path(h.expand_path(this.config.video_folder_path), clip_filename_noext .. this.config.video_extension) +end + +this.mkargs_video = function(out_clip_path) + local args = { + this.player, + mp.get_property('path'), + '--loop-file=no', + '--keep-open=no', + '--no-ocopy-metadata', + '--no-sub', + '--audio-channels=2', + '--oacopts-add=vbr=on', + '--oacopts-add=application=voip', + '--oacopts-add=compression_level=10', + '--vf-add=format=yuv420p', + '--sub-font-provider=auto', + '--embeddedfonts=yes', + table.concat { '--sub-font=', this.config.sub_font }, + table.concat { '--ovc=', this.config.video_codec }, + table.concat { '--oac=', this.config.audio_codec }, + table.concat { '--start=', toms(this.timings['start']) }, + table.concat { '--end=', toms(this.timings['end']) }, + table.concat { '--aid=', mp.get_property("aid") }, -- track number + table.concat { '--mute=', mp.get_property("mute") }, + table.concat { '--volume=', mp.get_property('volume') }, + table.concat { '--ovcopts-add=b=', this.config.video_bitrate }, + table.concat { '--oacopts-add=b=', this.config.audio_bitrate }, + table.concat { '--ovcopts-add=crf=', this.config.video_quality }, + table.concat { '--ovcopts-add=preset=', this.config.preset }, + table.concat { '--vf-add=scale=', this.config.video_width, ':', this.config.video_height }, + table.concat { '--ytdl-format=', mp.get_property("ytdl-format") }, + table.concat { '--o=', out_clip_path }, + table.concat { '--sid=', mp.get_property("sid") }, + table.concat { '--secondary-sid=', mp.get_property("secondary-sid") }, + table.concat { '--sub-delay=', mp.get_property("sub-delay") }, + table.concat { '--sub-visibility=', mp.get_property("sub-visibility") }, + table.concat { '--secondary-sub-visibility=', mp.get_property("secondary-sub-visibility") }, + table.concat { '--sub-back-color=', mp.get_property("sub-back-color") }, + table.concat { '--sub-border-style=', mp.get_property("sub-border-style") }, + } + + if this.config.video_fps ~= 'auto' then + table.insert(args, #args, table.concat { '--vf-add=fps=', this.config.video_fps }) + end + + args = this.append_embed_subs_args(args) + + return args +end + +this.mk_out_path_audio = function(clip_filename_noext) + return utils.join_path(h.expand_path(this.config.audio_folder_path), clip_filename_noext .. this.config.audio_extension) +end + +this.mkargs_audio = function(out_clip_path) + return { + this.player, + mp.get_property('path'), + '--loop-file=no', + '--keep-open=no', + '--no-ocopy-metadata', + '--no-sub', + '--audio-channels=2', + '--video=no', + '--oacopts-add=vbr=on', + '--oacopts-add=application=voip', + '--oacopts-add=compression_level=10', + table.concat { '--oac=', this.config.audio_codec }, + table.concat { '--start=', toms(this.timings['start']) }, + table.concat { '--end=', toms(this.timings['end']) }, + table.concat { '--volume=', mp.get_property('volume') }, + table.concat { '--aid=', mp.get_property("aid") }, -- track number + table.concat { '--oacopts-add=b=', this.config.audio_bitrate }, + table.concat { '--ytdl-format=', mp.get_property("ytdl-format") }, + table.concat { '--o=', out_clip_path } + } +end + +this.create_clip = function(clip_type, on_complete) + if clip_type == nil then + return + end + + if not this.timings:validate() then + h.notify_error("Wrong timings. Aborting.", "warn", 2) + return + end + + h.notify("Please wait...", "info", 9999) + + local output_file_path, args = (function() + local clip_filename_noext = construct_output_filename_noext() + if clip_type == 'video' then + local output_path = this.mk_out_path_video(clip_filename_noext) + return output_path, this.mkargs_video(output_path) + else + local output_path = this.mk_out_path_audio(clip_filename_noext) + return output_path, this.mkargs_audio(output_path) + end + end)() + + print("The following args will be executed:", table.concat(h.quote_if_necessary(args), " ")) + + local output_dir_path = utils.split_path(output_file_path) + local location_info = utils.file_info(output_dir_path) + if not location_info or not location_info.is_dir then + h.notify_error(string.format("Error: location %s doesn't exist.", output_dir_path), "error", 5) + return + end + + local process_result = function(_, ret, _) + if ret.status ~= 0 or string.match(ret.stdout, "could not open") then + h.notify_error(string.format("Error: couldn't create clip %s.", output_file_path), "error", 5) + else + h.notify(string.format("Clip saved to %s.", output_file_path), "info", 2) + if on_complete then + on_complete(output_file_path) + end + end + end + + h.subprocess_async(args, process_result) + this.timings:reset() +end + +this.set_encoder_alive = function() + local args_mpvnet = { 'mpvnet', '--version' } + local process_result_mpvnet = function(_, ret, _) + -- for some reason stdout is empty + if ret.status ~= 0 then + this.alive = false + else + this.alive = true + this.player = 'mpvnet' + end + end + + local args = { 'mpv', '--version' } + local process_result = function(_, ret, _) + if ret.status ~= 0 or string.match(ret.stdout, "mpv") == nil then + h.subprocess_async(args_mpvnet, process_result_mpvnet) + else + this.alive = true + this.player = 'mpv' + end + end + h.subprocess_async(args, process_result) +end + +this.init = function(config, timings_mgr) + this.config = config + this.timings = timings_mgr + this.set_encoder_alive() +end + +return this diff --git a/dot_config/private_mpv/scripts/videoclip/helpers.lua b/dot_config/private_mpv/scripts/videoclip/helpers.lua new file mode 100644 index 0000000..96e02c8 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/helpers.lua @@ -0,0 +1,141 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +Various helper functions. +]] + +local mp = require('mp') +local this = {} +local ass_start = mp.get_property_osd("osd-ass-cc/0") + +this.is_wayland = function() + return os.getenv('WAYLAND_DISPLAY') ~= nil +end + +this.is_win = function() + return mp.get_property("platform") == "windows" +end + +this.is_mac = function() + return mp.get_property("platform") == "darwin" +end + +this.notify = function(message, level, duration) + level = level or 'info' + duration = duration or 1 + mp.msg[level](message) + mp.osd_message(ass_start .. "{\\fs12}{\\bord0.75}" .. message, duration) +end + +this.notify_error = function(message, level, duration) + this.notify("{\\c&H7171f8&}" .. message, level, duration) +end + +this.subprocess = function(args, stdin) + local command_table = { + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args, + stdin_data = (stdin or ""), + } + return mp.command_native(command_table) +end + +this.subprocess_async = function(args, on_complete) + local command_table = { + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args + } + return mp.command_native_async(command_table, on_complete) +end + +this.remove_extension = function(filename) + return filename:gsub('%.%w+$', '') +end + +this.remove_text_in_brackets = function(str) + return str:gsub('%b[]', '') +end + +this.remove_special_characters = function(str) + return str:gsub('[%-_]', ' '):gsub('[%c%p]', ''):gsub('%s+', ' ') +end + +this.strip = function(str) + return str:gsub("^%s*(.-)%s*$", "%1") +end + +this.two_digit = function(num) + return string.format("%02d", num) +end + +this.twelve_hour = function(num) + local sign = "pm" + local hour = num + + if num > 12 then + hour = hour - 12 + else + sign = "am" + end + + return { sign = sign, hour = hour } +end + +this.expand_path = function (str) + return mp.command_native({"expand-path", str}) +end + +this.human_readable_time = function(seconds) + if type(seconds) ~= 'number' or seconds < 0 then + return 'empty' + end + + local parts = {} + + parts.h = math.floor(seconds / 3600) + parts.m = math.floor(seconds / 60) % 60 + parts.s = math.floor(seconds % 60) + parts.ms = math.floor((seconds * 1000) % 1000) + + local ret = string.format("%02dm%02ds%03dms", parts.m, parts.s, parts.ms) + + if parts.h > 0 then + ret = string.format('%dh%s', parts.h, ret) + end + + return ret +end + +this.quote_if_necessary = function(args) + local ret = {} + for _, v in ipairs(args) do + if v:find(" ", 1, true) or v:find("[", 1, true) then + table.insert(ret, (v:find("'") and string.format('"%s"', v) or string.format("'%s'", v))) + else + table.insert(ret, v) + end + end + return ret +end + +this.query_xdg_user_dir = function(name) + local r = this.subprocess({ "xdg-user-dir", name }) + if r.status == 0 then + return this.strip(r.stdout) + end + return nil +end + +this.query_user_home_dir = function() + --- "USERPROFILE" is used on ReactOS and other Windows-like systems. + return os.getenv("HOME") or os.getenv("USERPROFILE") +end + +return this diff --git a/dot_config/private_mpv/scripts/videoclip/main.lua b/dot_config/private_mpv/scripts/videoclip/main.lua new file mode 100644 index 0000000..4603636 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/main.lua @@ -0,0 +1 @@ +require('videoclip') diff --git a/dot_config/private_mpv/scripts/videoclip/osd_styler.lua b/dot_config/private_mpv/scripts/videoclip/osd_styler.lua new file mode 100644 index 0000000..7da1d24 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/osd_styler.lua @@ -0,0 +1,108 @@ +--[[ +A helper class for styling OSD messages +http://docs.aegisub.org/3.2/ASS_Tags/ + +Copyright (C) 2021 Ren Tatsumoto + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +]] + +local OSD = {} +OSD.__index = OSD + +function OSD:new() + return setmetatable({ messages = {} }, self) +end + +function OSD:append(s) + table.insert(self.messages, tostring(s)) + return self +end + +function OSD:newline() + return self:append([[\N]]) +end + +function OSD:tab() + return self:append([[\h\h\h\h]]) +end + +function OSD:config(config) + return self + :size(config.font_size) + :align(config.osd_align) + :outline(config.osd_outline) +end + +function OSD:outline(size) + return self:append('{\\bord'):append(size):append('}') +end + +function OSD:size(size) + return self:append('{\\fs'):append(size):append('}') +end + +function OSD:font(name) + return self:append('{\\fn'):append(name):append('}') +end + +function OSD:align(number) + return self:append('{\\an'):append(number):append('}') +end + +function OSD:get_text() + return table.concat(self.messages) +end + +function OSD:color(code) + return self:append('{\\1c&H') + :append(code:sub(5, 6)) + :append(code:sub(3, 4)) + :append(code:sub(1, 2)) + :append('&}') +end + +function OSD:text(text) + return self:append(text) +end + +function OSD:new_layer() + return self:append('\n') +end + +function OSD:bold(s) + return self:append('{\\b1}'):append(s):append('{\\b0}') +end + +function OSD:italics(s) + return self:append('{\\i1}'):append(s):append('{\\i0}') +end + +function OSD:submenu(text) + return self:color('e56243'):bold(text):color('ffffff') +end + +function OSD:item(text) + return self:color('f5d38a'):bold(text):color('ffffff') +end + +function OSD:selected(text) + return self:color('48a868'):bold(text):color('ffffff') +end + +function OSD:red(text) + return self:color('ff0000'):bold(text):color('ffffff') +end + +return OSD diff --git a/dot_config/private_mpv/scripts/videoclip/platform.lua b/dot_config/private_mpv/scripts/videoclip/platform.lua new file mode 100644 index 0000000..f56667b --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/platform.lua @@ -0,0 +1,95 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +OS-related constants and functions. +]] + +local h = require('helpers') +local mp = require('mp') +local utils = require('mp.utils') +local this = {} + +local function get_fallback_video_dir() + return utils.join_path( + h.query_user_home_dir(), + (this.platform == this.Platform.macos and "Movies" or "Videos") + ) +end + +local function get_fallback_music_dir() + return utils.join_path(h.query_user_home_dir(), "Music") +end + +this.Platform = { + gnu_linux = "gnu_linux", + macos = "macos", + windows = "windows", +} +this.platform = ( + h.is_win() and this.Platform.windows + or h.is_mac() and this.Platform.macos + or this.Platform.gnu_linux +) +this.default_video_folder = h.query_xdg_user_dir("VIDEOS") or get_fallback_video_dir() +this.default_audio_folder = h.query_xdg_user_dir("MUSIC") or get_fallback_music_dir() +this.curl_exe = (this.platform == this.Platform.windows and 'curl.exe' or 'curl') +this.open_utility = ( + this.platform == this.Platform.windows and 'explorer.exe' + or this.platform == this.Platform.macos and 'open' + or this.platform == this.Platform.gnu_linux and 'xdg-open' +) +this.open = function(file_or_url) + return mp.commandv('run', this.open_utility, file_or_url) +end + +this.clipboard = (function() + local self = {} + if this.platform == this.Platform.windows then + self.clip_exe = "powershell.exe" + self.copy = function(text) + return h.subprocess({ self.clip_exe, '-command', 'Set-Clipboard -Value ' .. text }) + end + else + if this.platform == this.Platform.macos then + self.clip_exe = "pbcopy" + self.clip_cmd = "LANG=en_US.UTF-8 pbcopy" + elseif h.is_wayland() then + self.clip_exe = "wl-copy" + self.clip_cmd = "wl-copy" + else + self.clip_exe = "xclip" + self.clip_cmd = "xclip -i -selection clipboard" + end + self.copy = function(text) + local handle = io.popen(self.clip_cmd, 'w') + if handle then + handle:write(text) + local success, status, signal = handle:close() + if success then + status = 0 + end + return { status = status } + else + return { status = 1 } + end + end + end + return self +end)() + +this.copy_or_open_url = function(url) + local cb = this.clipboard.copy(url) + if cb.status ~= 0 then + local msg = string.format( + "Failed to copy URL to clipboard, trying to open in browser instead. Make sure %s is installed.", + this.clipboard.clip_exe + ) + h.notify_error(msg, "warn", 4) + this.open(url) + else + h.notify("Done! Copied URL to clipboard.", "info", 2) + end + return cb +end +return this diff --git a/dot_config/private_mpv/scripts/videoclip/timings_mgr.lua b/dot_config/private_mpv/scripts/videoclip/timings_mgr.lua new file mode 100644 index 0000000..1c17c5d --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/timings_mgr.lua @@ -0,0 +1,29 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +Timings class +]] + +local Timings = { + ['start'] = -1, + ['end'] = -1, +} + +function Timings:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + return o +end + +function Timings:reset() + self['start'] = -1 + self['end'] = -1 +end + +function Timings:validate() + return self['start'] >= 0 and self['start'] < self['end'] +end + +return Timings diff --git a/dot_config/private_mpv/scripts/videoclip/videoclip.lua b/dot_config/private_mpv/scripts/videoclip/videoclip.lua new file mode 100644 index 0000000..d22d004 --- /dev/null +++ b/dot_config/private_mpv/scripts/videoclip/videoclip.lua @@ -0,0 +1,476 @@ +--[[ +Videoclip - mp4/webm clips creator for mpv. + +Copyright (C) 2021 Ren Tatsumoto + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +]] + +local NAME = 'videoclip' +local mp = require('mp') +local mpopt = require('mp.options') +local utils = require('mp.utils') +local OSD = require('osd_styler') +local p = require('platform') +local h = require('helpers') +local encoder = require('encoder') +local Timings = require('timings_mgr') + +------------------------------------------------------------ +-- System-dependent variables + +-- Options can be changed here or in a separate config file. +-- Config path: ~/.config/mpv/script-opts/videoclip.conf +local config = { + -- absolute paths + -- relative paths (e.g. ~ for home dir) do NOT work. + video_folder_path = p.default_video_folder, + audio_folder_path = p.default_audio_folder, + -- The range of the CRF scale is 0–51, where 0 is lossless, + -- 23 is the default, and 51 is worst quality possible. + -- Insane values like 9999 still work but produce the worst quality. + video_quality = 23, + -- Use the slowest preset that you have patience for. + -- https://trac.ffmpeg.org/wiki/Encode/H.264 + preset = 'faster', + video_format = 'mp4', -- mp4, vp9, vp8 + video_bitrate = '1M', + video_width = -2, + video_height = 480, + video_fps = 'auto', + audio_format = 'opus', -- aac, opus + audio_bitrate = '32k', -- 32k, 64k, 128k, 256k. aac requires higher bitrates. + font_size = 24, + osd_align = 7, -- https://aegisub.org/docs/3.2/ASS_Tags/#\an + osd_outline = 1.5, + clean_filename = true, + -- Whether to upload to catbox (permanent) or litterbox (temporary) + litterbox = true, + -- Determines expire time of files uploaded to litterbox + litterbox_expire = '72h', -- 1h, 12h, 24h, 72h + sub_font = 'Noto Sans CJK JP', + -- Filename format + -- Available tags: %n = filename, %t = title, %s = start, %e = end, %d = duration, + -- %Y = year, %M = months, %D = day, %H = hours (24), %I = hours (12), + -- %P = am/pm %N = minutes, %S = seconds + filename_template='%n_%s-%e', +} + +mpopt.read_options(config, NAME) +local main_menu +local pref_menu + +local allowed_presets = { + ultrafast = true, + superfast = true, + veryfast = true, + faster = true, + fast = true, + medium = true, + slow = true, + slower = true, + veryslow = true, +} + +------------------------------------------------------------ +-- Utility functions + +local function force_resolution(width, height, clip_fn, ...) + local cached_prefs = { + video_width = config.video_width, + video_height = config.video_height, + } + config.video_width = width + config.video_height = height + clip_fn(...) + config.video_width = cached_prefs.video_width + config.video_height = cached_prefs.video_height +end + +local function set_encoding_settings() + if config.video_format == 'mp4' then + config.video_codec = 'libx264' + config.video_extension = '.mp4' + elseif config.video_format == 'vp9' then + config.video_codec = 'libvpx-vp9' + config.video_extension = '.webm' + else + config.video_codec = 'libvpx' + config.video_extension = '.webm' + end + + if config.audio_format == 'aac' then + config.audio_codec = 'aac' + config.audio_extension = '.aac' + else + config.audio_codec = 'libopus' + config.audio_extension = '.opus' + end +end + +local function validate_config() + if not config.audio_bitrate:match('^%d+[kK]$') then + config.audio_bitrate = (tonumber(config.audio_bitrate) or 32) .. 'k' + end + + if not config.video_bitrate:match('^%d+[kKmM]$') then + config.video_bitrate = '1M' + end + + if not allowed_presets[config.preset] then + config.preset = 'faster' + end + + set_encoding_settings() +end + +local function upload_to_catbox(outfile) + local endpoint = config.litterbox and 'https://litterbox.catbox.moe/resources/internals/api.php' or 'https://catbox.moe/user/api.php' + h.notify("Uploading to " .. (config.litterbox and "litterbox.catbox.moe..." or "catbox.moe..."), "info", 9999) + + -- This uses cURL to send a request to the cat-/litterbox API. + -- cURL is included on Windows 10 and up, most Linux distributions and macOS. + + local r = h.subprocess({ -- This is technically blocking, but I don't think it has any real consequences ..? + p.curl_exe, '-s', + '-F', 'reqtype=fileupload', + '-F', 'time=' .. config['litterbox_expire'], + '-F', 'fileToUpload=@"' .. outfile .. '"', + endpoint + }) + + -- Exit codes in the range [0, 99] are returned by cURL itself. + -- Any other exit code means the shell failed to execute cURL. + if r.status < 0 or r.status > 99 then + h.notify_error("Error: Failed to upload. Make sure cURL is installed and in your PATH.", "error", 3) + return + elseif r.status ~= 0 then + h.notify_error("Error: Failed to upload to " .. (config.litterbox and "litterbox.catbox.moe" or "catbox.moe"), "error", 2) + return + end + + mp.msg.info("Catbox URL: " .. r.stdout) + -- Copy to clipboard + p.copy_or_open_url(r.stdout) +end + +------------------------------------------------------------ +-- Menu interface + +local Menu = {} +Menu.__index = Menu + +function Menu:new(parent) + local o = { + parent = parent, + overlay = parent and parent.overlay or mp.create_osd_overlay('ass-events'), + keybindings = { }, + } + return setmetatable(o, self) +end + +function Menu:overlay_draw(text) + self.overlay.data = text + self.overlay:update() +end + +function Menu:open() + if self.parent then + self.parent:close() + end + for _, val in pairs(self.keybindings) do + mp.add_forced_key_binding(val.key, val.key, val.fn) + end + self:update() +end + +function Menu:close() + for _, val in pairs(self.keybindings) do + mp.remove_key_binding(val.key) + end + if self.parent then + self.parent:open() + else + self.overlay:remove() + end +end + +function Menu:update() + local osd = OSD:new():config(config) + osd:append('Dummy menu.'):newline() + self:overlay_draw(osd:get_text()) +end + +------------------------------------------------------------ +-- Main menu + +main_menu = Menu:new() +main_menu.timings = Timings:new() + +main_menu.keybindings = { + { key = 's', fn = function() main_menu:set_time('start') end }, + { key = 'e', fn = function() main_menu:set_time('end') end }, + { key = 'S', fn = function() main_menu:set_time_sub('start') end }, + { key = 'E', fn = function() main_menu:set_time_sub('end') end }, + { key = 'r', fn = function() main_menu:reset_timings() end }, + { key = 'c', fn = function() main_menu:create_clip('video') end }, + { key = 'C', fn = function() force_resolution(1920, -2, encoder.create_clip, 'video') end }, + { key = 'a', fn = function() main_menu:create_clip('audio') end }, + { key = 'x', fn = function() main_menu:create_clip('video', upload_to_catbox) end }, + { key = 'X', fn = function() force_resolution(1920, -2, main_menu.create_clip, 'video', upload_to_catbox) end }, + { key = 'p', fn = function() pref_menu:open() end }, + { key = 'o', fn = function() p.open('https://streamable.com/') end }, + { key = 'ESC', fn = function() main_menu:close() end }, +} + +function main_menu:set_time(property) + self.timings[property] = math.max(0, mp.get_property_number('time-pos')) + self:update() +end + +function main_menu:set_time_sub(property) + local sub_delay = mp.get_property_native("sub-delay") + local time_pos = mp.get_property_number(string.format("sub-%s", property)) + + if time_pos == nil then + h.notify_error("Warning: No subtitles visible.", "warn", 2) + return + end + + self.timings[property] = math.max(0, time_pos + sub_delay) + self:update() +end + +function main_menu:reset_timings() + self.timings:reset() + self:update() +end + +main_menu.open = function() + Menu.open(main_menu) +end + +function main_menu:update() + local osd = OSD:new():config(config) + if encoder.alive == false then + osd:red("Error: "):append("mpv is not found in the PATH."):newline() + end + osd:submenu('Timings '):italics('(+shift use sub timings)'):newline() + osd:tab():item('s: '):append('start time '):item(h.human_readable_time(self.timings['start'])):newline() + osd:tab():item('e: '):append('end time '):item(h.human_readable_time(self.timings['end'])):newline() + osd:tab():item('r: '):append('reset'):newline() + osd:submenu('Create clip '):italics('(+shift to force fullHD preset)'):newline() + osd:tab():item('c: '):append('video clip'):newline() + osd:tab():item('a: '):append('audio clip'):newline() + osd:tab():item('x: '):append('video clip to ' .. (config.litterbox and 'litterbox.catbox.moe (' .. config.litterbox_expire .. ')' or 'catbox.moe')):newline() + osd:submenu('Options '):newline() + osd:tab():item('p: '):append('Open preferences'):newline() + osd:tab():item('o: '):append('Open streamable.com'):newline() + osd:tab():item('ESC: '):append('Close'):newline() + + self:overlay_draw(osd:get_text()) +end + +function main_menu:create_clip(clip_type, on_complete_fn) + self:close() + encoder.create_clip(clip_type, on_complete_fn) +end + +------------------------------------------------------------ +-- Preferences + +pref_menu = Menu:new(main_menu) + +pref_menu.keybindings = { + { key = 'f', fn = function() pref_menu:cycle_video_formats() end }, + { key = 'a', fn = function() pref_menu:cycle_audio_formats() end }, + { key = 'm', fn = function() pref_menu:toggle_mute_audio() end }, + { key = 'r', fn = function() pref_menu:cycle_resolutions() end }, + { key = 'b', fn = function() pref_menu:cycle_audio_bitrates() end }, + { key = 'e', fn = function() pref_menu:toggle_embed_subtitles() end }, + { key = 'x', fn = function() pref_menu:toggle_catbox() end }, + { key = 'z', fn = function() pref_menu:cycle_litterbox_expiration() end }, + { key = 's', fn = function() pref_menu:save() end }, + { key = 'c', fn = function() end }, + { key = 'ESC', fn = function() pref_menu:close() end }, + { key = 'q', fn = function() pref_menu:close() end }, +} + +pref_menu.resolutions = { + { w = config.video_width, h = config.video_height, }, + { w = -2, h = -2, }, + { w = -2, h = 240, }, + { w = -2, h = 360, }, + { w = -2, h = 480, }, + { w = -2, h = 720, }, + { w = -2, h = 1080, }, + { w = -2, h = 1440, }, + { w = -2, h = 2160, }, + selected = 1, +} +pref_menu.audio_bitrates = { + config.audio_bitrate, + '32k', + '64k', + '128k', + '256k', + '384k', + selected = 1, +} + +pref_menu.vid_formats = { 'mp4', 'vp9', 'vp8', } +pref_menu.aud_formats = { 'aac', 'opus', } +pref_menu.litterbox_expirations = { '1h', '12h', '24h', '72h', } + +function pref_menu:get_selected_resolution() + return string.format( + '%s x %s', + config.video_width == -2 and 'auto' or config.video_width, + config.video_height == -2 and 'auto' or config.video_height + ) +end + +function pref_menu:cycle_resolutions() + self.resolutions.selected = self.resolutions.selected + 1 > #self.resolutions and 1 or self.resolutions.selected + 1 + local res = self.resolutions[self.resolutions.selected] + config.video_width = res.w + config.video_height = res.h + self:update() +end + +function pref_menu:cycle_audio_bitrates() + self.audio_bitrates.selected = self.audio_bitrates.selected + 1 > #self.audio_bitrates and 1 or self.audio_bitrates.selected + 1 + config.audio_bitrate = self.audio_bitrates[self.audio_bitrates.selected] + self:update() +end + +function pref_menu:cycle_formats(config_type) + local formats + if config_type == 'video_format' then + formats = pref_menu.vid_formats + else + formats = pref_menu.aud_formats + end + + local selected = 1 + for i, format in ipairs(formats) do + if config[config_type] == format then + selected = i + break + end + end + config[config_type] = formats[selected + 1] or formats[1] + set_encoding_settings() + self:update() +end + +function pref_menu:cycle_video_formats() + pref_menu:cycle_formats('video_format') +end + +function pref_menu:cycle_audio_formats() + pref_menu:cycle_formats('audio_format') +end + +function pref_menu:toggle_mute_audio() + mp.commandv("cycle", "mute") + self:update() +end + +function pref_menu:toggle_embed_subtitles() + mp.commandv("cycle", "sub-visibility") + self:update() +end + +function pref_menu:toggle_catbox() + config['litterbox'] = not config['litterbox'] + self:update() +end + +function pref_menu:cycle_litterbox_expiration() + if not config['litterbox'] then + return + end + local expirations = pref_menu.litterbox_expirations + + local selected = 1 + for i, expiration in ipairs(expirations) do + if config['litterbox_expire'] == expiration then + selected = i + break + end + end + config['litterbox_expire'] = expirations[selected + 1] or expirations[1] + self:update() +end + +function pref_menu:update() + local osd = OSD:new():config(config) + osd:submenu('Preferences'):newline() + osd:tab():item('r: Video resolution: '):append(self:get_selected_resolution()):newline() + osd:tab():item('f: Video format: '):append(config.video_format):newline() + osd:tab():item('a: Audio format: '):append(config.audio_format):newline() + osd:tab():item('b: Audio bitrate: '):append(config.audio_bitrate):newline() + osd:tab():item('m: Mute audio: '):append(mp.get_property("mute")):newline() + osd:tab():item('e: Embed subtitles: '):append(mp.get_property("sub-visibility")):newline() + osd:submenu('Catbox'):newline() + osd:tab():item('x: Using: '):append(config.litterbox and 'Litterbox (temporary)' or 'Catbox (permanent)'):newline() + if config.litterbox then + osd:tab():item('z: Litterbox expires after: '):append(config.litterbox_expire):newline() + else + osd:tab():color("b0b0b0"):text('x: Litterbox expires after: '):append("N/A"):newline() + end + osd:submenu('Save'):newline() + osd:tab():item('s: Save preferences'):newline() + self:overlay_draw(osd:get_text()) +end + +function pref_menu:save() + local function lua_to_mpv(config_value) + if type(config_value) == 'boolean' then + return config_value and 'yes' or 'no' + else + return config_value + end + end + local ignore_list = { + video_extension = true, + audio_extension = true, + video_codec = true, + audio_codec = true, + } + local mpv_dirpath = string.gsub(mp.get_script_directory(), "scripts[\\/][^\\/]+", "") + local config_filepath = utils.join_path(utils.join_path(mpv_dirpath, "script-opts"), string.format('%s.conf', NAME)) + local handle = io.open(config_filepath, 'w') + if handle ~= nil then + handle:write(string.format("# Written by %s on %s.\n", NAME, os.date())) + for key, value in pairs(config) do + if ignore_list[key] == nil then + handle:write(string.format('%s=%s\n', key, lua_to_mpv(value))) + end + end + handle:close() + h.notify("Settings saved.", "info", 2) + else + h.notify_error(string.format("Couldn't open %s.", config_filepath), "error", 4) + end +end + +------------------------------------------------------------ +-- Finally, set an 'entry point' in mpv + +validate_config() +encoder.init(config, main_menu.timings) +mp.add_key_binding('c', 'videoclip-menu-open', main_menu.open) +mp.msg.warn("Press 'c' to open the videoclip menu.") diff --git a/dot_config/private_mpv/scripts/webm.lua b/dot_config/private_mpv/scripts/webm.lua new file mode 100644 index 0000000..57a0942 --- /dev/null +++ b/dot_config/private_mpv/scripts/webm.lua @@ -0,0 +1,2914 @@ +local mp = require("mp") +local assdraw = require("mp.assdraw") +local msg = require("mp.msg") +local utils = require("mp.utils") +local mpopts = require("mp.options") +local options = { + -- Defaults to shift+w + keybind = "W", + -- If empty, saves on the same directory of the playing video. + -- A starting "~" will be replaced by the home dir. + -- This field is delimited by double-square-brackets - [[ and ]] - instead of + -- quotes, because Windows users might run into a issue when using + -- backslashes as a path separator. Examples of valid inputs for this field + -- would be: [[]] (the default, empty value), [[C:\Users\John]] (on Windows), + -- and [[/home/john]] (on Unix-like systems eg. Linux). + -- The [[]] delimiter is not needed when using from a configuration file + -- in the script-opts folder. + output_directory = [[]], + run_detached = false, + -- Template string for the output file + -- %f - Filename, with extension + -- %F - Filename, without extension + -- %T - Media title, if it exists, or filename, with extension (useful for some streams, such as YouTube). + -- %s, %e - Start and end time, with milliseconds + -- %S, %E - Start and end time, without milliseconds + -- %M - "-audio", if audio is enabled, empty otherwise + -- %R - "-(height)p", where height is the video's height, or scale_height, if it's enabled. + -- More specifiers are supported, see https://mpv.io/manual/master/#options-screenshot-template + -- Property expansion is supported (with %{} at top level, ${} when nested), see https://mpv.io/manual/master/#property-expansion + output_template = "%F-[%s-%e]%M", + -- Scale video to a certain height, keeping the aspect ratio. -1 disables it. + scale_height = -1, + -- Change the FPS of the output video, dropping or duplicating frames as needed. + -- -1 means the FPS will be unchanged from the source. + fps = -1, + -- Target filesize, in kB. This will be used to calculate the bitrate + -- used on the encode. If this is set to <= 0, the video bitrate will be set + -- to 0, which might enable constant quality modes, depending on the + -- video codec that's used (VP8 and VP9, for example). + target_filesize = 2500, + -- If true, will use stricter flags to ensure the resulting file doesn't + -- overshoot the target filesize. Not recommended, as constrained quality + -- mode should work well, unless you're really having trouble hitting + -- the target size. + strict_filesize_constraint = false, + strict_bitrate_multiplier = 0.95, + -- In kilobits. + strict_audio_bitrate = 64, + -- Sets the output format, from a few predefined ones. + -- Currently we have: + -- webm-vp8 (libvpx/libvorbis) + -- webm-vp9 (libvpx-vp9/libopus) + -- mp4 (h264/AAC) + -- mp4-nvenc (h264-NVENC/AAC) + -- raw (rawvideo/pcm_s16le). + -- mp3 (libmp3lame) + -- and gif + output_format = "webm-vp8", + twopass = true, + -- If set, applies the video filters currently used on the playback to the encode. + apply_current_filters = true, + -- If set, writes the video's filename to the "Title" field on the metadata. + write_filename_on_metadata = false, + -- Set the number of encoding threads, for codecs libvpx and libvpx-vp9 + libvpx_threads = 4, + additional_flags = "", + -- Constant Rate Factor (CRF). The value meaning and limits may change, + -- from codec to codec. Set to -1 to disable. + crf = 10, + -- Useful for flags that may impact output filesize, such as qmin, qmax etc + -- Won't be applied when strict_filesize_constraint is on. + non_strict_additional_flags = "", + -- Display the encode progress, in %. Requires run_detached to be disabled. + -- On Windows, it shows a cmd popup. "auto" will display progress on non-Windows platforms. + display_progress = "auto", + -- The font size used in the menu. Isn't used for the notifications (started encode, finished encode etc) + font_size = 28, + margin = 10, + message_duration = 5, + -- gif dither mode, 0-5 for bayer w/ bayer_scale 0-5, 6 for paletteuse default (sierra2_4a) + gif_dither = 2, + -- Force square pixels on output video + -- Some players like recent Firefox versions display videos with non-square pixels with wrong aspect ratio + force_square_pixels = false, +} + +mpopts.read_options(options) +local base64_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + +-- encoding +function base64_encode(data) + return ((data:gsub('.', function(x) + local r,b='',x:byte() + for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end + return r; + end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) + if (#x < 6) then return '' end + local c=0 + for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end + return base64_chars:sub(c+1,c+1) + end)..({ '', '==', '=' })[#data%3+1]) +end + +-- decoding +function base64_decode(data) + data = string.gsub(data, '[^'..base64_chars..'=]', '') + return (data:gsub('.', function(x) + if (x == '=') then return '' end + local r,f='',(base64_chars:find(x)-1) + for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end + return r; + end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) + if (#x ~= 8) then return '' end + local c=0 + for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end + return string.char(c) + end)) +end +local emit_event +emit_event = function(event_name, ...) + return mp.commandv("script-message", "webm-" .. tostring(event_name), ...) +end +local test_set_options +test_set_options = function(new_options_json) + local new_options = utils.parse_json(new_options_json) + for k, v in pairs(new_options) do + options[k] = v + end +end +mp.register_script_message("mpv-webm-set-options", test_set_options) +local bold +bold = function(text) + return "{\\b1}" .. tostring(text) .. "{\\b0}" +end +local message +message = function(text, duration) + local ass = mp.get_property_osd("osd-ass-cc/0") + ass = ass .. text + return mp.osd_message(ass, duration or options.message_duration) +end +local append +append = function(a, b) + for _, val in ipairs(b) do + a[#a + 1] = val + end + return a +end +local seconds_to_time_string +seconds_to_time_string = function(seconds, no_ms, full) + if seconds < 0 then + return "unknown" + end + local ret = "" + if not (no_ms) then + ret = string.format(".%03d", seconds * 1000 % 1000) + end + ret = string.format("%02d:%02d%s", math.floor(seconds / 60) % 60, math.floor(seconds) % 60, ret) + if full or seconds > 3600 then + ret = string.format("%d:%s", math.floor(seconds / 3600), ret) + end + return ret +end +local seconds_to_path_element +seconds_to_path_element = function(seconds, no_ms, full) + local time_string = seconds_to_time_string(seconds, no_ms, full) + local _ + time_string, _ = time_string:gsub(":", ".") + return time_string +end +local file_exists +file_exists = function(name) + local info, err = utils.file_info(name) + if info ~= nil then + return true + end + return false +end +local expand_properties +expand_properties = function(text, magic) + if magic == nil then + magic = "$" + end + for prefix, raw, prop, colon, fallback, closing in text:gmatch("%" .. magic .. "{([?!]?)(=?)([^}:]*)(:?)([^}]*)(}*)}") do + local err + local prop_value + local compare_value + local original_prop = prop + local get_property = mp.get_property_osd + if raw == "=" then + get_property = mp.get_property + end + if prefix ~= "" then + for actual_prop, compare in prop:gmatch("(.-)==(.*)") do + prop = actual_prop + compare_value = compare + end + end + if colon == ":" then + prop_value, err = get_property(prop, fallback) + else + prop_value, err = get_property(prop, "(error)") + end + prop_value = tostring(prop_value) + if prefix == "?" then + if compare_value == nil then + prop_value = err == nil and fallback .. closing or "" + else + prop_value = prop_value == compare_value and fallback .. closing or "" + end + prefix = "%" .. prefix + elseif prefix == "!" then + if compare_value == nil then + prop_value = err ~= nil and fallback .. closing or "" + else + prop_value = prop_value ~= compare_value and fallback .. closing or "" + end + else + prop_value = prop_value .. closing + end + if colon == ":" then + local _ + text, _ = text:gsub("%" .. magic .. "{" .. prefix .. raw .. original_prop:gsub("%W", "%%%1") .. ":" .. fallback:gsub("%W", "%%%1") .. closing .. "}", expand_properties(prop_value)) + else + local _ + text, _ = text:gsub("%" .. magic .. "{" .. prefix .. raw .. original_prop:gsub("%W", "%%%1") .. closing .. "}", prop_value) + end + end + return text +end +local format_filename +format_filename = function(startTime, endTime, videoFormat) + local hasAudioCodec = videoFormat.audioCodec ~= "" + local replaceFirst = { + ["%%mp"] = "%%mH.%%mM.%%mS", + ["%%mP"] = "%%mH.%%mM.%%mS.%%mT", + ["%%p"] = "%%wH.%%wM.%%wS", + ["%%P"] = "%%wH.%%wM.%%wS.%%wT" + } + local replaceTable = { + ["%%wH"] = string.format("%02d", math.floor(startTime / (60 * 60))), + ["%%wh"] = string.format("%d", math.floor(startTime / (60 * 60))), + ["%%wM"] = string.format("%02d", math.floor(startTime / 60 % 60)), + ["%%wm"] = string.format("%d", math.floor(startTime / 60)), + ["%%wS"] = string.format("%02d", math.floor(startTime % 60)), + ["%%ws"] = string.format("%d", math.floor(startTime)), + ["%%wf"] = string.format("%s", startTime), + ["%%wT"] = string.sub(string.format("%.3f", startTime % 1), 3), + ["%%mH"] = string.format("%02d", math.floor(endTime / (60 * 60))), + ["%%mh"] = string.format("%d", math.floor(endTime / (60 * 60))), + ["%%mM"] = string.format("%02d", math.floor(endTime / 60 % 60)), + ["%%mm"] = string.format("%d", math.floor(endTime / 60)), + ["%%mS"] = string.format("%02d", math.floor(endTime % 60)), + ["%%ms"] = string.format("%d", math.floor(endTime)), + ["%%mf"] = string.format("%s", endTime), + ["%%mT"] = string.sub(string.format("%.3f", endTime % 1), 3), + ["%%f"] = mp.get_property("filename"), + ["%%F"] = mp.get_property("filename/no-ext"), + ["%%s"] = seconds_to_path_element(startTime), + ["%%S"] = seconds_to_path_element(startTime, true), + ["%%e"] = seconds_to_path_element(endTime), + ["%%E"] = seconds_to_path_element(endTime, true), + ["%%T"] = mp.get_property("media-title"), + ["%%M"] = (mp.get_property_native('aid') and not mp.get_property_native('mute') and hasAudioCodec) and '-audio' or '', + ["%%R"] = (options.scale_height ~= -1) and "-" .. tostring(options.scale_height) .. "p" or "-" .. tostring(mp.get_property_native('height')) .. "p", + ["%%t%%"] = "%%" + } + local filename = options.output_template + for format, value in pairs(replaceFirst) do + local _ + filename, _ = filename:gsub(format, value) + end + for format, value in pairs(replaceTable) do + local _ + filename, _ = filename:gsub(format, value) + end + if mp.get_property_bool("demuxer-via-network", false) then + local _ + filename, _ = filename:gsub("%%X{([^}]*)}", "%1") + filename, _ = filename:gsub("%%x", "") + else + local x = string.gsub(mp.get_property("stream-open-filename", ""), string.gsub(mp.get_property("filename", ""), "%W", "%%%1") .. "$", "") + local _ + filename, _ = filename:gsub("%%X{[^}]*}", x) + filename, _ = filename:gsub("%%x", x) + end + filename = expand_properties(filename, "%") + for format in filename:gmatch("%%t([aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ])") do + local _ + filename, _ = filename:gsub("%%t" .. format, os.date("%" .. format)) + end + local _ + filename, _ = filename:gsub("[<>:\"/\\|?*]", "") + return tostring(filename) .. "." .. tostring(videoFormat.outputExtension) +end +local parse_directory +parse_directory = function(dir) + local home_dir = os.getenv("HOME") + if not home_dir then + home_dir = os.getenv("USERPROFILE") + end + if not home_dir then + local drive = os.getenv("HOMEDRIVE") + local path = os.getenv("HOMEPATH") + if drive and path then + home_dir = utils.join_path(drive, path) + else + msg.warn("Couldn't find home dir.") + home_dir = "" + end + end + local _ + dir, _ = dir:gsub("^~", home_dir) + return dir +end +local is_windows = type(package) == "table" and type(package.config) == "string" and package.config:sub(1, 1) == "\\" +local trim +trim = function(s) + return s:match("^%s*(.-)%s*$") +end +local get_null_path +get_null_path = function() + if file_exists("/dev/null") then + return "/dev/null" + end + return "NUL" +end +local run_subprocess +run_subprocess = function(params) + local res = utils.subprocess(params) + msg.verbose("Command stdout: ") + msg.verbose(res.stdout) + if res.status ~= 0 then + msg.verbose("Command failed! Reason: ", res.error, " Killed by us? ", res.killed_by_us and "yes" or "no") + return false + end + return true +end +local shell_escape +shell_escape = function(args) + local ret = { } + for i, a in ipairs(args) do + local s = tostring(a) + if string.match(s, "[^A-Za-z0-9_/:=-]") then + if is_windows then + s = '"' .. string.gsub(s, '"', '"\\""') .. '"' + else + s = "'" .. string.gsub(s, "'", "'\\''") .. "'" + end + end + table.insert(ret, s) + end + local concat = table.concat(ret, " ") + if is_windows then + concat = '"' .. concat .. '"' + end + return concat +end +local run_subprocess_popen +run_subprocess_popen = function(command_line) + local command_line_string = shell_escape(command_line) + command_line_string = command_line_string .. " 2>&1" + msg.verbose("run_subprocess_popen: running " .. tostring(command_line_string)) + return io.popen(command_line_string) +end +local calculate_scale_factor +calculate_scale_factor = function() + local baseResY = 720 + local osd_w, osd_h = mp.get_osd_size() + return osd_h / baseResY +end +local should_display_progress +should_display_progress = function() + if options.display_progress == "auto" then + return not is_windows + end + return options.display_progress +end +local reverse +reverse = function(list) + local _accum_0 = { } + local _len_0 = 1 + local _max_0 = 1 + for _index_0 = #list, _max_0 < 0 and #list + _max_0 or _max_0, -1 do + local element = list[_index_0] + _accum_0[_len_0] = element + _len_0 = _len_0 + 1 + end + return _accum_0 +end +local get_pass_logfile_path +get_pass_logfile_path = function(encode_out_path) + return tostring(encode_out_path) .. "-video-pass1.log" +end +local dimensions_changed = true +local _video_dimensions = { } +local get_video_dimensions +get_video_dimensions = function() + if not (dimensions_changed) then + return _video_dimensions + end + local video_params = mp.get_property_native("video-out-params") + if not video_params then + return nil + end + dimensions_changed = false + local keep_aspect = mp.get_property_bool("keepaspect") + local w = video_params["w"] + local h = video_params["h"] + local dw = video_params["dw"] + local dh = video_params["dh"] + if mp.get_property_number("video-rotate") % 180 == 90 then + w, h = h, w + dw, dh = dh, dw + end + _video_dimensions = { + top_left = { }, + bottom_right = { }, + ratios = { } + } + local window_w, window_h = mp.get_osd_size() + if keep_aspect then + local unscaled = mp.get_property_native("video-unscaled") + local panscan = mp.get_property_number("panscan") + local fwidth = window_w + local fheight = math.floor(window_w / dw * dh) + if fheight > window_h or fheight < h then + local tmpw = math.floor(window_h / dh * dw) + if tmpw <= window_w then + fheight = window_h + fwidth = tmpw + end + end + local vo_panscan_area = window_h - fheight + local f_w = fwidth / fheight + local f_h = 1 + if vo_panscan_area == 0 then + vo_panscan_area = window_h - fwidth + f_w = 1 + f_h = fheight / fwidth + end + if unscaled or unscaled == "downscale-big" then + vo_panscan_area = 0 + if unscaled or (dw <= window_w and dh <= window_h) then + fwidth = dw + fheight = dh + end + end + local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w) + local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h) + local split_scaling + split_scaling = function(dst_size, scaled_src_size, zoom, align, pan) + scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom) + align = (align + 1) / 2 + local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size) + if dst_start < 0 then + dst_start = dst_start + 1 + end + local dst_end = dst_start + scaled_src_size + if dst_start >= dst_end then + dst_start = 0 + dst_end = 1 + end + return dst_start, dst_end + end + local zoom = mp.get_property_number("video-zoom") + local align_x = mp.get_property_number("video-align-x") + local pan_x = mp.get_property_number("video-pan-x") + _video_dimensions.top_left.x, _video_dimensions.bottom_right.x = split_scaling(window_w, scaled_width, zoom, align_x, pan_x) + local align_y = mp.get_property_number("video-align-y") + local pan_y = mp.get_property_number("video-pan-y") + _video_dimensions.top_left.y, _video_dimensions.bottom_right.y = split_scaling(window_h, scaled_height, zoom, align_y, pan_y) + else + _video_dimensions.top_left.x = 0 + _video_dimensions.bottom_right.x = window_w + _video_dimensions.top_left.y = 0 + _video_dimensions.bottom_right.y = window_h + end + _video_dimensions.ratios.w = w / (_video_dimensions.bottom_right.x - _video_dimensions.top_left.x) + _video_dimensions.ratios.h = h / (_video_dimensions.bottom_right.y - _video_dimensions.top_left.y) + return _video_dimensions +end +local set_dimensions_changed +set_dimensions_changed = function() + dimensions_changed = true +end +local monitor_dimensions +monitor_dimensions = function() + local properties = { + "keepaspect", + "video-out-params", + "video-unscaled", + "panscan", + "video-zoom", + "video-align-x", + "video-pan-x", + "video-align-y", + "video-pan-y", + "osd-width", + "osd-height" + } + for _, p in ipairs(properties) do + mp.observe_property(p, "native", set_dimensions_changed) + end +end +local clamp +clamp = function(min, val, max) + if val <= min then + return min + end + if val >= max then + return max + end + return val +end +local clamp_point +clamp_point = function(top_left, point, bottom_right) + return { + x = clamp(top_left.x, point.x, bottom_right.x), + y = clamp(top_left.y, point.y, bottom_right.y) + } +end +local VideoPoint +do + local _class_0 + local _base_0 = { + set_from_screen = function(self, sx, sy) + local d = get_video_dimensions() + local point = clamp_point(d.top_left, { + x = sx, + y = sy + }, d.bottom_right) + self.x = math.floor(d.ratios.w * (point.x - d.top_left.x) + 0.5) + self.y = math.floor(d.ratios.h * (point.y - d.top_left.y) + 0.5) + end, + to_screen = function(self) + local d = get_video_dimensions() + return { + x = math.floor(self.x / d.ratios.w + d.top_left.x + 0.5), + y = math.floor(self.y / d.ratios.h + d.top_left.y + 0.5) + } + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self) + self.x = -1 + self.y = -1 + end, + __base = _base_0, + __name = "VideoPoint" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + VideoPoint = _class_0 +end +local Region +do + local _class_0 + local _base_0 = { + is_valid = function(self) + return self.x > -1 and self.y > -1 and self.w > -1 and self.h > -1 + end, + set_from_points = function(self, p1, p2) + self.x = math.min(p1.x, p2.x) + self.y = math.min(p1.y, p2.y) + self.w = math.abs(p1.x - p2.x) + self.h = math.abs(p1.y - p2.y) + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self) + self.x = -1 + self.y = -1 + self.w = -1 + self.h = -1 + end, + __base = _base_0, + __name = "Region" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Region = _class_0 +end +local make_fullscreen_region +make_fullscreen_region = function() + local r = Region() + local d = get_video_dimensions() + local a = VideoPoint() + local b = VideoPoint() + local xa, ya + do + local _obj_0 = d.top_left + xa, ya = _obj_0.x, _obj_0.y + end + a:set_from_screen(xa, ya) + local xb, yb + do + local _obj_0 = d.bottom_right + xb, yb = _obj_0.x, _obj_0.y + end + b:set_from_screen(xb, yb) + r:set_from_points(a, b) + return r +end +local read_double +read_double = function(bytes) + local sign = 1 + local mantissa = bytes[2] % 2 ^ 4 + for i = 3, 8 do + mantissa = mantissa * 256 + bytes[i] + end + if bytes[1] > 127 then + sign = -1 + end + local exponent = (bytes[1] % 128) * 2 ^ 4 + math.floor(bytes[2] / 2 ^ 4) + if exponent == 0 then + return 0 + end + mantissa = (math.ldexp(mantissa, -52) + 1) * sign + return math.ldexp(mantissa, exponent - 1023) +end +local write_double +write_double = function(num) + local bytes = { + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + } + if num == 0 then + return bytes + end + local anum = math.abs(num) + local mantissa, exponent = math.frexp(anum) + exponent = exponent - 1 + mantissa = mantissa * 2 - 1 + local sign = num ~= anum and 128 or 0 + exponent = exponent + 1023 + bytes[1] = sign + math.floor(exponent / 2 ^ 4) + mantissa = mantissa * 2 ^ 4 + local currentmantissa = math.floor(mantissa) + mantissa = mantissa - currentmantissa + bytes[2] = (exponent % 2 ^ 4) * 2 ^ 4 + currentmantissa + for i = 3, 8 do + mantissa = mantissa * 2 ^ 8 + currentmantissa = math.floor(mantissa) + mantissa = mantissa - currentmantissa + bytes[i] = currentmantissa + end + return bytes +end +local FirstpassStats +do + local _class_0 + local duration_multiplier, fields_before_duration, fields_after_duration + local _base_0 = { + get_duration = function(self) + local big_endian_binary_duration = reverse(self.binary_duration) + return read_double(reversed_binary_duration) / duration_multiplier + end, + set_duration = function(self, duration) + local big_endian_binary_duration = write_double(duration * duration_multiplier) + self.binary_duration = reverse(big_endian_binary_duration) + end, + _bytes_to_string = function(self, bytes) + return string.char(unpack(bytes)) + end, + as_binary_string = function(self) + local before_duration_string = self:_bytes_to_string(self.binary_data_before_duration) + local duration_string = self:_bytes_to_string(self.binary_duration) + local after_duration_string = self:_bytes_to_string(self.binary_data_after_duration) + return before_duration_string .. duration_string .. after_duration_string + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, before_duration, duration, after_duration) + self.binary_data_before_duration = before_duration + self.binary_duration = duration + self.binary_data_after_duration = after_duration + end, + __base = _base_0, + __name = "FirstpassStats" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + local self = _class_0 + duration_multiplier = 10000000.0 + fields_before_duration = 16 + fields_after_duration = 1 + self.data_before_duration_size = function(self) + return fields_before_duration * 8 + end + self.data_after_duration_size = function(self) + return fields_after_duration * 8 + end + self.size = function(self) + return (fields_before_duration + 1 + fields_after_duration) * 8 + end + self.from_bytes = function(self, bytes) + local before_duration + do + local _accum_0 = { } + local _len_0 = 1 + local _max_0 = self:data_before_duration_size() + for _index_0 = 1, _max_0 < 0 and #bytes + _max_0 or _max_0 do + local b = bytes[_index_0] + _accum_0[_len_0] = b + _len_0 = _len_0 + 1 + end + before_duration = _accum_0 + end + local duration + do + local _accum_0 = { } + local _len_0 = 1 + local _max_0 = self:data_before_duration_size() + 8 + for _index_0 = self:data_before_duration_size() + 1, _max_0 < 0 and #bytes + _max_0 or _max_0 do + local b = bytes[_index_0] + _accum_0[_len_0] = b + _len_0 = _len_0 + 1 + end + duration = _accum_0 + end + local after_duration + do + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = self:data_before_duration_size() + 8 + 1, #bytes do + local b = bytes[_index_0] + _accum_0[_len_0] = b + _len_0 = _len_0 + 1 + end + after_duration = _accum_0 + end + return self(before_duration, duration, after_duration) + end + FirstpassStats = _class_0 +end +local read_logfile_into_stats_array +read_logfile_into_stats_array = function(logfile_path) + local file = assert(io.open(logfile_path, "rb")) + local logfile_string = base64_decode(file:read()) + file:close() + local stats_size = FirstpassStats:size() + assert(logfile_string:len() % stats_size == 0) + local stats = { } + for offset = 1, #logfile_string, stats_size do + local bytes = { + logfile_string:byte(offset, offset + stats_size - 1) + } + assert(#bytes == stats_size) + stats[#stats + 1] = FirstpassStats:from_bytes(bytes) + end + return stats +end +local write_stats_array_to_logfile +write_stats_array_to_logfile = function(stats_array, logfile_path) + local file = assert(io.open(logfile_path, "wb")) + local logfile_string = "" + for _index_0 = 1, #stats_array do + local stat = stats_array[_index_0] + logfile_string = logfile_string .. stat:as_binary_string() + end + file:write(base64_encode(logfile_string)) + return file:close() +end +local vp8_patch_logfile +vp8_patch_logfile = function(logfile_path, encode_total_duration) + local stats_array = read_logfile_into_stats_array(logfile_path) + local average_duration = encode_total_duration / (#stats_array - 1) + for i = 1, #stats_array - 1 do + stats_array[i]:set_duration(average_duration) + end + stats_array[#stats_array]:set_duration(encode_total_duration) + return write_stats_array_to_logfile(stats_array, logfile_path) +end +local formats = { } +local Format +do + local _class_0 + local _base_0 = { + getPreFilters = function(self) + return { } + end, + getPostFilters = function(self) + return { } + end, + getFlags = function(self) + return { } + end, + getCodecFlags = function(self) + local codecs = { } + if self.videoCodec ~= "" then + codecs[#codecs + 1] = "--ovc=" .. tostring(self.videoCodec) + end + if self.audioCodec ~= "" then + codecs[#codecs + 1] = "--oac=" .. tostring(self.audioCodec) + end + return codecs + end, + postCommandModifier = function(self, command, region, startTime, endTime) + return command + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "Basic" + self.supportsTwopass = true + self.videoCodec = "" + self.audioCodec = "" + self.outputExtension = "" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "Format" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Format = _class_0 +end +local RawVideo +do + local _class_0 + local _parent_0 = Format + local _base_0 = { + getColorspace = function(self) + local csp = mp.get_property("colormatrix") + local _exp_0 = csp + if "bt.601" == _exp_0 then + return "bt601" + elseif "bt.709" == _exp_0 then + return "bt709" + elseif "bt.2020" == _exp_0 then + return "bt2020" + elseif "smpte-240m" == _exp_0 then + return "smpte240m" + else + msg.info("Warning, unknown colorspace " .. tostring(csp) .. " detected, using bt.601.") + return "bt601" + end + end, + getPostFilters = function(self) + return { + "format=yuv444p16", + "lavfi-scale=in_color_matrix=" .. self:getColorspace(), + "format=bgr24" + } + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "Raw" + self.supportsTwopass = false + self.videoCodec = "rawvideo" + self.audioCodec = "pcm_s16le" + self.outputExtension = "avi" + self.acceptsBitrate = false + end, + __base = _base_0, + __name = "RawVideo", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + RawVideo = _class_0 +end +formats["raw"] = RawVideo() +local WebmVP8 +do + local _class_0 + local _parent_0 = Format + local _base_0 = { + getPreFilters = function(self) + local colormatrixFilter = { + ["bt.709"] = "bt709", + ["bt.2020"] = "bt2020", + ["smpte-240m"] = "smpte240m" + } + local ret = { } + local colormatrix = mp.get_property_native("video-params/colormatrix") + if colormatrixFilter[colormatrix] then + append(ret, { + "lavfi-colormatrix=" .. tostring(colormatrixFilter[colormatrix]) .. ":bt601" + }) + end + return ret + end, + getFlags = function(self) + return { + "--ovcopts-add=threads=" .. tostring(options.libvpx_threads), + "--ovcopts-add=auto-alt-ref=1", + "--ovcopts-add=lag-in-frames=25", + "--ovcopts-add=quality=good", + "--ovcopts-add=cpu-used=0" + } + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "WebM" + self.supportsTwopass = true + self.videoCodec = "libvpx" + self.audioCodec = "libvorbis" + self.outputExtension = "webm" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "WebmVP8", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + WebmVP8 = _class_0 +end +formats["webm-vp8"] = WebmVP8() +local WebmVP9 +do + local _class_0 + local _parent_0 = Format + local _base_0 = { + getFlags = function(self) + return { + "--ovcopts-add=threads=" .. tostring(options.libvpx_threads) + } + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "WebM (VP9)" + self.supportsTwopass = false + self.videoCodec = "libvpx-vp9" + self.audioCodec = "libopus" + self.outputExtension = "webm" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "WebmVP9", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + WebmVP9 = _class_0 +end +formats["webm-vp9"] = WebmVP9() +local MP4 +do + local _class_0 + local _parent_0 = Format + local _base_0 = { } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "MP4 (h264/AAC)" + self.supportsTwopass = true + self.videoCodec = "libx264" + self.audioCodec = "aac" + self.outputExtension = "mp4" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "MP4", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MP4 = _class_0 +end +formats["mp4"] = MP4() +local MP4NVENC +do + local _class_0 + local _parent_0 = Format + local _base_0 = { } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "MP4 (h264-NVENC/AAC)" + self.supportsTwopass = true + self.videoCodec = "h264_nvenc" + self.audioCodec = "aac" + self.outputExtension = "mp4" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "MP4NVENC", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MP4NVENC = _class_0 +end +formats["mp4-nvenc"] = MP4NVENC() +local MP3 +do + local _class_0 + local _parent_0 = Format + local _base_0 = { } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "MP3 (libmp3lame)" + self.supportsTwopass = false + self.videoCodec = "" + self.audioCodec = "libmp3lame" + self.outputExtension = "mp3" + self.acceptsBitrate = true + end, + __base = _base_0, + __name = "MP3", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MP3 = _class_0 +end +formats["mp3"] = MP3() +local GIF +do + local _class_0 + local _parent_0 = Format + local _base_0 = { + postCommandModifier = function(self, command, region, startTime, endTime) + local new_command = { } + local start_ts = seconds_to_time_string(startTime, false, true) + local end_ts = seconds_to_time_string(endTime, false, true) + start_ts = start_ts:gsub(":", "\\\\:") + end_ts = end_ts:gsub(":", "\\\\:") + local cfilter = "[vid1]trim=start=" .. tostring(start_ts) .. ":end=" .. tostring(end_ts) .. "[vidtmp];" + if mp.get_property("deinterlace") == "yes" then + cfilter = cfilter .. "[vidtmp]yadif=mode=1[vidtmp];" + end + for _, v in ipairs(command) do + local _continue_0 = false + repeat + if v:match("^%-%-vf%-add=lavfi%-scale") or v:match("^%-%-vf%-add=lavfi%-crop") or v:match("^%-%-vf%-add=fps") or v:match("^%-%-vf%-add=lavfi%-eq") then + local n = v:gsub("^%-%-vf%-add=", ""):gsub("^lavfi%-", "") + cfilter = cfilter .. "[vidtmp]" .. tostring(n) .. "[vidtmp];" + else + if v:match("^%-%-video%-rotate=90") then + cfilter = cfilter .. "[vidtmp]transpose=1[vidtmp];" + else + if v:match("^%-%-video%-rotate=270") then + cfilter = cfilter .. "[vidtmp]transpose=2[vidtmp];" + else + if v:match("^%-%-video%-rotate=180") then + cfilter = cfilter .. "[vidtmp]transpose=1[vidtmp];[vidtmp]transpose=1[vidtmp];" + else + if v:match("^%-%-deinterlace=") then + _continue_0 = true + break + else + append(new_command, { + v + }) + _continue_0 = true + break + end + end + end + end + end + _continue_0 = true + until true + if not _continue_0 then + break + end + end + cfilter = cfilter .. "[vidtmp]split[topal][vidf];" + cfilter = cfilter .. "[topal]palettegen[pal];" + cfilter = cfilter .. "[vidf]fifo[vidf];" + cfilter = cfilter .. "[vidf][pal]paletteuse=diff_mode=rectangle" + if options.gif_dither ~= 6 then + cfilter = cfilter .. ":dither=bayer:bayer_scale=" .. tostring(options.gif_dither) + end + cfilter = cfilter .. "[vo]" + append(new_command, { + "--lavfi-complex=" .. tostring(cfilter) + }) + return new_command + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.displayName = "GIF" + self.supportsTwopass = false + self.videoCodec = "gif" + self.audioCodec = "" + self.outputExtension = "gif" + self.acceptsBitrate = false + end, + __base = _base_0, + __name = "GIF", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + GIF = _class_0 +end +formats["gif"] = GIF() +local Page +do + local _class_0 + local _base_0 = { + add_keybinds = function(self) + if not self.keybinds then + return + end + for key, func in pairs(self.keybinds) do + mp.add_forced_key_binding(key, key, func, { + repeatable = true + }) + end + end, + remove_keybinds = function(self) + if not self.keybinds then + return + end + for key, _ in pairs(self.keybinds) do + mp.remove_key_binding(key) + end + end, + observe_properties = function(self) + self.sizeCallback = function() + return self:draw() + end + local properties = { + "keepaspect", + "video-out-params", + "video-unscaled", + "panscan", + "video-zoom", + "video-align-x", + "video-pan-x", + "video-align-y", + "video-pan-y", + "osd-width", + "osd-height" + } + for _index_0 = 1, #properties do + local p = properties[_index_0] + mp.observe_property(p, "native", self.sizeCallback) + end + end, + unobserve_properties = function(self) + if self.sizeCallback then + mp.unobserve_property(self.sizeCallback) + self.sizeCallback = nil + end + end, + clear = function(self) + local window_w, window_h = mp.get_osd_size() + mp.set_osd_ass(window_w, window_h, "") + return mp.osd_message("", 0) + end, + prepare = function(self) + return nil + end, + dispose = function(self) + return nil + end, + show = function(self) + if self.visible then + return + end + self.visible = true + self:observe_properties() + self:add_keybinds() + self:prepare() + self:clear() + return self:draw() + end, + hide = function(self) + if not self.visible then + return + end + self.visible = false + self:unobserve_properties() + self:remove_keybinds() + self:clear() + return self:dispose() + end, + setup_text = function(self, ass) + local scale = calculate_scale_factor() + local margin = options.margin * scale + ass:append("{\\an7}") + ass:pos(margin, margin) + return ass:append("{\\fs" .. tostring(options.font_size * scale) .. "}") + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function() end, + __base = _base_0, + __name = "Page" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Page = _class_0 +end +local EncodeWithProgress +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + draw = function(self) + local progress = 100 * ((self.currentTime - self.startTime) / self.duration) + local progressText = string.format("%d%%", progress) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append("Encoding (" .. tostring(bold(progressText)) .. ")\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + parseLine = function(self, line) + local matchTime = string.match(line, "Encode time[-]pos: ([0-9.]+)") + local matchExit = string.match(line, "Exiting... [(]([%a ]+)[)]") + if matchTime == nil and matchExit == nil then + return + end + if matchTime ~= nil and tonumber(matchTime) > self.currentTime then + self.currentTime = tonumber(matchTime) + end + if matchExit ~= nil then + self.finished = true + self.finishedReason = matchExit + end + end, + startEncode = function(self, command_line) + local copy_command_line + do + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #command_line do + local arg = command_line[_index_0] + _accum_0[_len_0] = arg + _len_0 = _len_0 + 1 + end + copy_command_line = _accum_0 + end + append(copy_command_line, { + '--term-status-msg=Encode time-pos: ${=time-pos}\\n' + }) + self:show() + local processFd = run_subprocess_popen(copy_command_line) + for line in processFd:lines() do + msg.verbose(string.format('%q', line)) + self:parseLine(line) + self:draw() + end + processFd:close() + self:hide() + if self.finishedReason == "End of file" then + return true + end + return false + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, startTime, endTime) + self.startTime = startTime + self.endTime = endTime + self.duration = endTime - startTime + self.currentTime = startTime + end, + __base = _base_0, + __name = "EncodeWithProgress", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + EncodeWithProgress = _class_0 +end +local get_active_tracks +get_active_tracks = function() + local accepted = { + video = true, + audio = not mp.get_property_bool("mute"), + sub = mp.get_property_bool("sub-visibility") + } + local active = { + video = { }, + audio = { }, + sub = { } + } + for _, track in ipairs(mp.get_property_native("track-list")) do + if track["selected"] and accepted[track["type"]] then + local count = #active[track["type"]] + active[track["type"]][count + 1] = track + end + end + return active +end +local filter_tracks_supported_by_format +filter_tracks_supported_by_format = function(active_tracks, format) + local has_video_codec = format.videoCodec ~= "" + local has_audio_codec = format.audioCodec ~= "" + local supported = { + video = has_video_codec and active_tracks["video"] or { }, + audio = has_audio_codec and active_tracks["audio"] or { }, + sub = has_video_codec and active_tracks["sub"] or { } + } + return supported +end +local append_track +append_track = function(out, track) + local external_flag = { + ["audio"] = "audio-file", + ["sub"] = "sub-file" + } + local internal_flag = { + ["video"] = "vid", + ["audio"] = "aid", + ["sub"] = "sid" + } + if track['external'] and string.len(track['external-filename']) <= 2048 then + return append(out, { + "--" .. tostring(external_flag[track['type']]) .. "=" .. tostring(track['external-filename']) + }) + else + return append(out, { + "--" .. tostring(internal_flag[track['type']]) .. "=" .. tostring(track['id']) + }) + end +end +local append_audio_tracks +append_audio_tracks = function(out, tracks) + local internal_tracks = { } + for _index_0 = 1, #tracks do + local track = tracks[_index_0] + if track['external'] then + append_track(out, track) + else + append(internal_tracks, { + track + }) + end + end + if #internal_tracks > 1 then + local filter_string = "" + for _index_0 = 1, #internal_tracks do + local track = internal_tracks[_index_0] + filter_string = filter_string .. "[aid" .. tostring(track['id']) .. "]" + end + filter_string = filter_string .. "amix[ao]" + return append(out, { + "--lavfi-complex=" .. tostring(filter_string) + }) + else + if #internal_tracks == 1 then + return append_track(out, internal_tracks[1]) + end + end +end +local get_scale_filters +get_scale_filters = function() + local filters = { } + if options.force_square_pixels then + append(filters, { + "lavfi-scale=iw*sar:ih" + }) + end + if options.scale_height > 0 then + append(filters, { + "lavfi-scale=-2:" .. tostring(options.scale_height) + }) + end + return filters +end +local get_fps_filters +get_fps_filters = function() + if options.fps > 0 then + return { + "fps=" .. tostring(options.fps) + } + end + return { } +end +local get_contrast_brightness_and_saturation_filters +get_contrast_brightness_and_saturation_filters = function() + local mpv_brightness = mp.get_property("brightness") + local mpv_contrast = mp.get_property("contrast") + local mpv_saturation = mp.get_property("saturation") + if mpv_brightness == 0 and mpv_contrast == 0 and mpv_saturation == 0 then + return { } + end + local eq_saturation = (mpv_saturation + 100) / 100.0 + local eq_contrast = (mpv_contrast + 100) / 100.0 + local eq_brightness = (mpv_brightness / 50.0 + eq_contrast - 1) / 2.0 + return { + "lavfi-eq=contrast=" .. tostring(eq_contrast) .. ":saturation=" .. tostring(eq_saturation) .. ":brightness=" .. tostring(eq_brightness) + } +end +local append_property +append_property = function(out, property_name, option_name) + option_name = option_name or property_name + local prop = mp.get_property(property_name) + if prop and prop ~= "" then + return append(out, { + "--" .. tostring(option_name) .. "=" .. tostring(prop) + }) + end +end +local append_list_options +append_list_options = function(out, property_name, option_prefix) + option_prefix = option_prefix or property_name + local prop = mp.get_property_native(property_name) + if prop then + for _index_0 = 1, #prop do + local value = prop[_index_0] + append(out, { + "--" .. tostring(option_prefix) .. "-append=" .. tostring(value) + }) + end + end +end +local get_playback_options +get_playback_options = function() + local ret = { } + append_property(ret, "sub-ass-override") + append_property(ret, "sub-ass-force-style") + append_property(ret, "sub-ass-vsfilter-aspect-compat") + append_property(ret, "sub-auto") + append_property(ret, "sub-pos") + append_property(ret, "sub-delay") + append_property(ret, "video-rotate") + append_property(ret, "ytdl-format") + append_property(ret, "deinterlace") + return ret +end +local get_speed_flags +get_speed_flags = function() + local ret = { } + local speed = mp.get_property_native("speed") + if speed ~= 1 then + append(ret, { + "--vf-add=setpts=PTS/" .. tostring(speed), + "--af-add=atempo=" .. tostring(speed), + "--sub-speed=1/" .. tostring(speed) + }) + end + return ret +end +local get_metadata_flags +get_metadata_flags = function() + local title = mp.get_property("filename/no-ext") + return { + "--oset-metadata=title=%" .. tostring(string.len(title)) .. "%" .. tostring(title) + } +end +local apply_current_filters +apply_current_filters = function(filters) + local vf = mp.get_property_native("vf") + msg.verbose("apply_current_filters: got " .. tostring(#vf) .. " currently applied.") + for _index_0 = 1, #vf do + local _continue_0 = false + repeat + local filter = vf[_index_0] + msg.verbose("apply_current_filters: filter name: " .. tostring(filter['name'])) + if filter["enabled"] == false then + _continue_0 = true + break + end + local str = filter["name"] + local params = filter["params"] or { } + for k, v in pairs(params) do + str = str .. ":" .. tostring(k) .. "=%" .. tostring(string.len(v)) .. "%" .. tostring(v) + end + append(filters, { + str + }) + _continue_0 = true + until true + if not _continue_0 then + break + end + end +end +local get_video_filters +get_video_filters = function(format, region) + local filters = { } + append(filters, format:getPreFilters()) + if options.apply_current_filters then + apply_current_filters(filters) + end + if region and region:is_valid() then + append(filters, { + "lavfi-crop=" .. tostring(region.w) .. ":" .. tostring(region.h) .. ":" .. tostring(region.x) .. ":" .. tostring(region.y) + }) + end + append(filters, get_scale_filters()) + append(filters, get_fps_filters()) + append(filters, get_contrast_brightness_and_saturation_filters()) + append(filters, format:getPostFilters()) + return filters +end +local get_video_encode_flags +get_video_encode_flags = function(format, region) + local flags = { } + append(flags, get_playback_options()) + local filters = get_video_filters(format, region) + for _index_0 = 1, #filters do + local f = filters[_index_0] + append(flags, { + "--vf-add=" .. tostring(f) + }) + end + append(flags, get_speed_flags()) + return flags +end +local calculate_bitrate +calculate_bitrate = function(active_tracks, format, length) + if format.videoCodec == "" then + return nil, options.target_filesize * 8 / length + end + local video_kilobits = options.target_filesize * 8 + local audio_kilobits = nil + local has_audio_track = #active_tracks["audio"] > 0 + if options.strict_filesize_constraint and has_audio_track then + audio_kilobits = length * options.strict_audio_bitrate + video_kilobits = video_kilobits - audio_kilobits + end + local video_bitrate = math.floor(video_kilobits / length) + local audio_bitrate = audio_kilobits and math.floor(audio_kilobits / length) or nil + return video_bitrate, audio_bitrate +end +local find_path +find_path = function(startTime, endTime) + local path = mp.get_property('path') + if not path then + return nil, nil, nil, nil, nil + end + local is_stream = not file_exists(path) + local is_temporary = false + if is_stream then + if mp.get_property('file-format') == 'hls' then + path = utils.join_path(parse_directory('~'), 'cache_dump.ts') + mp.command_native({ + 'dump_cache', + seconds_to_time_string(startTime, false, true), + seconds_to_time_string(endTime + 5, false, true), + path + }) + endTime = endTime - startTime + startTime = 0 + is_temporary = true + end + end + return path, is_stream, is_temporary, startTime, endTime +end +local encode +encode = function(region, startTime, endTime) + local format = formats[options.output_format] + local originalStartTime = startTime + local originalEndTime = endTime + local path, is_stream, is_temporary + path, is_stream, is_temporary, startTime, endTime = find_path(startTime, endTime) + if not path then + message("No file is being played") + return + end + local command = { + "mpv", + path, + "--start=" .. seconds_to_time_string(startTime, false, true), + "--end=" .. seconds_to_time_string(endTime, false, true), + "--loop-file=no", + "--no-pause" + } + append(command, format:getCodecFlags()) + local active_tracks = get_active_tracks() + local supported_active_tracks = filter_tracks_supported_by_format(active_tracks, format) + for track_type, tracks in pairs(supported_active_tracks) do + if track_type == "audio" then + append_audio_tracks(command, tracks) + else + for _index_0 = 1, #tracks do + local track = tracks[_index_0] + append_track(command, track) + end + end + end + for track_type, tracks in pairs(supported_active_tracks) do + local _continue_0 = false + repeat + if #tracks > 0 then + _continue_0 = true + break + end + local _exp_0 = track_type + if "video" == _exp_0 then + append(command, { + "--vid=no" + }) + elseif "audio" == _exp_0 then + append(command, { + "--aid=no" + }) + elseif "sub" == _exp_0 then + append(command, { + "--sid=no" + }) + end + _continue_0 = true + until true + if not _continue_0 then + break + end + end + if format.videoCodec ~= "" then + append(command, get_video_encode_flags(format, region)) + end + append(command, format:getFlags()) + if options.write_filename_on_metadata then + append(command, get_metadata_flags()) + end + if format.acceptsBitrate then + if options.target_filesize > 0 then + local length = endTime - startTime + local video_bitrate, audio_bitrate = calculate_bitrate(supported_active_tracks, format, length) + if video_bitrate then + append(command, { + "--ovcopts-add=b=" .. tostring(video_bitrate) .. "k" + }) + end + if audio_bitrate then + append(command, { + "--oacopts-add=b=" .. tostring(audio_bitrate) .. "k" + }) + end + if options.strict_filesize_constraint then + local type = format.videoCodec ~= "" and "ovc" or "oac" + append(command, { + "--" .. tostring(type) .. "opts-add=minrate=" .. tostring(bitrate) .. "k", + "--" .. tostring(type) .. "opts-add=maxrate=" .. tostring(bitrate) .. "k" + }) + end + else + local type = format.videoCodec ~= "" and "ovc" or "oac" + append(command, { + "--" .. tostring(type) .. "opts-add=b=0" + }) + end + end + for token in string.gmatch(options.additional_flags, "[^%s]+") do + command[#command + 1] = token + end + if not options.strict_filesize_constraint then + for token in string.gmatch(options.non_strict_additional_flags, "[^%s]+") do + command[#command + 1] = token + end + if options.crf >= 0 then + append(command, { + "--ovcopts-add=crf=" .. tostring(options.crf) + }) + end + end + local dir = "" + if is_stream then + dir = parse_directory("~") + else + local _ + dir, _ = utils.split_path(path) + end + if options.output_directory ~= "" then + dir = parse_directory(options.output_directory) + end + local formatted_filename = format_filename(originalStartTime, originalEndTime, format) + local out_path = utils.join_path(dir, formatted_filename) + append(command, { + "--o=" .. tostring(out_path) + }) + emit_event("encode-started") + if options.twopass and format.supportsTwopass and not is_stream then + local first_pass_cmdline + do + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #command do + local arg = command[_index_0] + _accum_0[_len_0] = arg + _len_0 = _len_0 + 1 + end + first_pass_cmdline = _accum_0 + end + append(first_pass_cmdline, { + "--ovcopts-add=flags=+pass1" + }) + message("Starting first pass...") + msg.verbose("First-pass command line: ", table.concat(first_pass_cmdline, " ")) + local res = run_subprocess({ + args = first_pass_cmdline, + cancellable = false + }) + if not res then + message("First pass failed! Check the logs for details.") + emit_event("encode-finished", "fail") + return + end + append(command, { + "--ovcopts-add=flags=+pass2" + }) + if format.videoCodec == "libvpx" then + msg.verbose("Patching libvpx pass log file...") + vp8_patch_logfile(get_pass_logfile_path(out_path), endTime - startTime) + end + end + command = format:postCommandModifier(command, region, startTime, endTime) + msg.info("Encoding to", out_path) + msg.verbose("Command line:", table.concat(command, " ")) + if options.run_detached then + message("Started encode, process was detached.") + return utils.subprocess_detached({ + args = command + }) + else + local res = false + if not should_display_progress() then + message("Started encode...") + res = run_subprocess({ + args = command, + cancellable = false + }) + else + local ewp = EncodeWithProgress(startTime, endTime) + res = ewp:startEncode(command) + end + if res then + message("Encoded successfully! Saved to\\N" .. tostring(bold(out_path))) + emit_event("encode-finished", "success") + else + message("Encode failed! Check the logs for details.") + emit_event("encode-finished", "fail") + end + os.remove(get_pass_logfile_path(out_path)) + if is_temporary then + return os.remove(path) + end + end +end +local CropPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + reset = function(self) + local dimensions = get_video_dimensions() + local xa, ya + do + local _obj_0 = dimensions.top_left + xa, ya = _obj_0.x, _obj_0.y + end + self.pointA:set_from_screen(xa, ya) + local xb, yb + do + local _obj_0 = dimensions.bottom_right + xb, yb = _obj_0.x, _obj_0.y + end + self.pointB:set_from_screen(xb, yb) + if self.visible then + return self:draw() + end + end, + setPointA = function(self) + local posX, posY = mp.get_mouse_pos() + self.pointA:set_from_screen(posX, posY) + if self.visible then + return self:draw() + end + end, + setPointB = function(self) + local posX, posY = mp.get_mouse_pos() + self.pointB:set_from_screen(posX, posY) + if self.visible then + return self:draw() + end + end, + cancel = function(self) + self:hide() + return self.callback(false, nil) + end, + finish = function(self) + local region = Region() + region:set_from_points(self.pointA, self.pointB) + self:hide() + return self.callback(true, region) + end, + draw_box = function(self, ass) + local region = Region() + region:set_from_points(self.pointA:to_screen(), self.pointB:to_screen()) + local d = get_video_dimensions() + ass:new_event() + ass:append("{\\an7}") + ass:pos(0, 0) + ass:append('{\\bord0}') + ass:append('{\\shad0}') + ass:append('{\\c&H000000&}') + ass:append('{\\alpha&H77}') + ass:draw_start() + ass:rect_cw(d.top_left.x, d.top_left.y, region.x, region.y + region.h) + ass:rect_cw(region.x, d.top_left.y, d.bottom_right.x, region.y) + ass:rect_cw(d.top_left.x, region.y + region.h, region.x + region.w, d.bottom_right.y) + ass:rect_cw(region.x + region.w, region.y, d.bottom_right.x, d.bottom_right.y) + return ass:draw_stop() + end, + draw = function(self) + local window = { } + window.w, window.h = mp.get_osd_size() + local ass = assdraw.ass_new() + self:draw_box(ass) + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Crop:')) .. "\\N") + ass:append(tostring(bold('1:')) .. " change point A (" .. tostring(self.pointA.x) .. ", " .. tostring(self.pointA.y) .. ")\\N") + ass:append(tostring(bold('2:')) .. " change point B (" .. tostring(self.pointB.x) .. ", " .. tostring(self.pointB.y) .. ")\\N") + ass:append(tostring(bold('r:')) .. " reset to whole screen\\N") + ass:append(tostring(bold('ESC:')) .. " cancel crop\\N") + local width, height = math.abs(self.pointA.x - self.pointB.x), math.abs(self.pointA.y - self.pointB.y) + ass:append(tostring(bold('ENTER:')) .. " confirm crop (" .. tostring(width) .. "x" .. tostring(height) .. ")\\N") + return mp.set_osd_ass(window.w, window.h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region) + self.pointA = VideoPoint() + self.pointB = VideoPoint() + self.keybinds = { + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointA + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setPointB + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["r"] = (function() + local _base_1 = self + local _fn_0 = _base_1.reset + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.finish + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self:reset() + self.callback = callback + if region and region:is_valid() then + self.pointA.x = region.x + self.pointA.y = region.y + self.pointB.x = region.x + region.w + self.pointB.y = region.y + region.h + end + end, + __base = _base_0, + __name = "CropPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + CropPage = _class_0 +end +local Option +do + local _class_0 + local _base_0 = { + hasPrevious = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.min then + return self.value > self.opts.min + else + return true + end + elseif "list" == _exp_0 then + return self.value > 1 + end + end, + hasNext = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return true + elseif "int" == _exp_0 then + if self.opts.max then + return self.value < self.opts.max + else + return true + end + elseif "list" == _exp_0 then + return self.value < #self.opts.possibleValues + end + end, + leftKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value - self.opts.step + if self.opts.min and self.opts.min > self.value then + self.value = self.opts.min + end + elseif "list" == _exp_0 then + if self.value > 1 then + self.value = self.value - 1 + end + end + end, + rightKey = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = not self.value + elseif "int" == _exp_0 then + self.value = self.value + self.opts.step + if self.opts.max and self.opts.max < self.value then + self.value = self.opts.max + end + elseif "list" == _exp_0 then + if self.value < #self.opts.possibleValues then + self.value = self.value + 1 + end + end + end, + getValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value + elseif "int" == _exp_0 then + return self.value + elseif "list" == _exp_0 then + local value, _ + do + local _obj_0 = self.opts.possibleValues[self.value] + value, _ = _obj_0[1], _obj_0[2] + end + return value + end + end, + setValue = function(self, value) + local _exp_0 = self.optType + if "bool" == _exp_0 then + self.value = value + elseif "int" == _exp_0 then + self.value = value + elseif "list" == _exp_0 then + local set = false + for i, possiblePair in ipairs(self.opts.possibleValues) do + local possibleValue, _ + possibleValue, _ = possiblePair[1], possiblePair[2] + if possibleValue == value then + set = true + self.value = i + break + end + end + if not set then + return msg.warn("Tried to set invalid value " .. tostring(value) .. " to " .. tostring(self.displayText) .. " option.") + end + end + end, + getDisplayValue = function(self) + local _exp_0 = self.optType + if "bool" == _exp_0 then + return self.value and "yes" or "no" + elseif "int" == _exp_0 then + if self.opts.altDisplayNames and self.opts.altDisplayNames[self.value] then + return self.opts.altDisplayNames[self.value] + else + return tostring(self.value) + end + elseif "list" == _exp_0 then + local value, displayValue + do + local _obj_0 = self.opts.possibleValues[self.value] + value, displayValue = _obj_0[1], _obj_0[2] + end + return displayValue or value + end + end, + draw = function(self, ass, selected) + if selected then + ass:append(tostring(bold(self.displayText)) .. ": ") + else + ass:append(tostring(self.displayText) .. ": ") + end + if self:hasPrevious() then + ass:append("◀ ") + end + ass:append(self:getDisplayValue()) + if self:hasNext() then + ass:append(" ▶") + end + return ass:append("\\N") + end, + optVisible = function(self) + if self.visibleCheckFn == nil then + return true + else + return self.visibleCheckFn() + end + end + } + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, optType, displayText, value, opts, visibleCheckFn) + self.optType = optType + self.displayText = displayText + self.opts = opts + self.value = 1 + self.visibleCheckFn = visibleCheckFn + return self:setValue(value) + end, + __base = _base_0, + __name = "Option" + }, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Option = _class_0 +end +local EncodeOptionsPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + getCurrentOption = function(self) + return self.options[self.currentOption][2] + end, + leftKey = function(self) + (self:getCurrentOption()):leftKey() + return self:draw() + end, + rightKey = function(self) + (self:getCurrentOption()):rightKey() + return self:draw() + end, + prevOpt = function(self) + for i = self.currentOption - 1, 1, -1 do + if self.options[i][2]:optVisible() then + self.currentOption = i + break + end + end + return self:draw() + end, + nextOpt = function(self) + for i = self.currentOption + 1, #self.options do + if self.options[i][2]:optVisible() then + self.currentOption = i + break + end + end + return self:draw() + end, + confirmOpts = function(self) + for _, optPair in ipairs(self.options) do + local optName, opt + optName, opt = optPair[1], optPair[2] + options[optName] = opt:getValue() + end + self:hide() + return self.callback(true) + end, + cancelOpts = function(self) + self:hide() + return self.callback(false) + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('Options:')) .. "\\N\\N") + for i, optPair in ipairs(self.options) do + local opt = optPair[2] + if opt:optVisible() then + opt:draw(ass, self.currentOption == i) + end + end + ass:append("\\N▲ / ▼: navigate\\N") + ass:append(tostring(bold('ENTER:')) .. " confirm options\\N") + ass:append(tostring(bold('ESC:')) .. " cancel\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback) + self.callback = callback + self.currentOption = 1 + local scaleHeightOpts = { + possibleValues = { + { + -1, + "no" + }, + { + 144 + }, + { + 240 + }, + { + 360 + }, + { + 480 + }, + { + 540 + }, + { + 720 + }, + { + 1080 + }, + { + 1440 + }, + { + 2160 + } + } + } + local filesizeOpts = { + step = 250, + min = 0, + altDisplayNames = { + [0] = "0 (constant quality)" + } + } + local crfOpts = { + step = 1, + min = -1, + altDisplayNames = { + [-1] = "disabled" + } + } + local fpsOpts = { + possibleValues = { + { + -1, + "source" + }, + { + 15 + }, + { + 24 + }, + { + 30 + }, + { + 48 + }, + { + 50 + }, + { + 60 + }, + { + 120 + }, + { + 240 + } + } + } + local formatIds = { + "webm-vp8", + "webm-vp9", + "mp4", + "mp4-nvenc", + "raw", + "mp3", + "gif" + } + local formatOpts = { + possibleValues = (function() + local _accum_0 = { } + local _len_0 = 1 + for _index_0 = 1, #formatIds do + local fId = formatIds[_index_0] + _accum_0[_len_0] = { + fId, + formats[fId].displayName + } + _len_0 = _len_0 + 1 + end + return _accum_0 + end)() + } + local gifDitherOpts = { + possibleValues = { + { + 0, + "bayer_scale 0" + }, + { + 1, + "bayer_scale 1" + }, + { + 2, + "bayer_scale 2" + }, + { + 3, + "bayer_scale 3" + }, + { + 4, + "bayer_scale 4" + }, + { + 5, + "bayer_scale 5" + }, + { + 6, + "sierra2_4a" + } + } + } + self.options = { + { + "output_format", + Option("list", "Output Format", options.output_format, formatOpts) + }, + { + "twopass", + Option("bool", "Two Pass", options.twopass) + }, + { + "apply_current_filters", + Option("bool", "Apply Current Video Filters", options.apply_current_filters) + }, + { + "scale_height", + Option("list", "Scale Height", options.scale_height, scaleHeightOpts) + }, + { + "strict_filesize_constraint", + Option("bool", "Strict Filesize Constraint", options.strict_filesize_constraint) + }, + { + "write_filename_on_metadata", + Option("bool", "Write Filename on Metadata", options.write_filename_on_metadata) + }, + { + "target_filesize", + Option("int", "Target Filesize", options.target_filesize, filesizeOpts) + }, + { + "crf", + Option("int", "CRF", options.crf, crfOpts) + }, + { + "fps", + Option("list", "FPS", options.fps, fpsOpts) + }, + { + "gif_dither", + Option("list", "GIF Dither Type", options.gif_dither, gifDitherOpts, function() + return self.options[1][2]:getValue() == "gif" + end) + }, + { + "force_square_pixels", + Option("bool", "Force Square Pixels", options.force_square_pixels) + } + } + self.keybinds = { + ["LEFT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.leftKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["RIGHT"] = (function() + local _base_1 = self + local _fn_0 = _base_1.rightKey + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["UP"] = (function() + local _base_1 = self + local _fn_0 = _base_1.prevOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["DOWN"] = (function() + local _base_1 = self + local _fn_0 = _base_1.nextOpt + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ENTER"] = (function() + local _base_1 = self + local _fn_0 = _base_1.confirmOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancelOpts + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + end, + __base = _base_0, + __name = "EncodeOptionsPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + EncodeOptionsPage = _class_0 +end +local PreviewPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + prepare = function(self) + local vf = mp.get_property_native("vf") + vf[#vf + 1] = { + name = "sub" + } + if self.region:is_valid() then + vf[#vf + 1] = { + name = "crop", + params = { + w = tostring(self.region.w), + h = tostring(self.region.h), + x = tostring(self.region.x), + y = tostring(self.region.y) + } + } + end + mp.set_property_native("vf", vf) + if self.startTime > -1 and self.endTime > -1 then + mp.set_property_native("ab-loop-a", self.startTime) + mp.set_property_native("ab-loop-b", self.endTime) + mp.set_property_native("time-pos", self.startTime) + end + return mp.set_property_native("pause", false) + end, + dispose = function(self) + mp.set_property("ab-loop-a", "no") + mp.set_property("ab-loop-b", "no") + for prop, value in pairs(self.originalProperties) do + mp.set_property_native(prop, value) + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append("Press " .. tostring(bold('ESC')) .. " to exit preview.\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + cancel = function(self) + self:hide() + return self.callback() + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self, callback, region, startTime, endTime) + self.callback = callback + self.originalProperties = { + ["vf"] = mp.get_property_native("vf"), + ["time-pos"] = mp.get_property_native("time-pos"), + ["pause"] = mp.get_property_native("pause") + } + self.keybinds = { + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.cancel + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.region = region + self.startTime = startTime + self.endTime = endTime + self.isLoop = false + end, + __base = _base_0, + __name = "PreviewPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + PreviewPage = _class_0 +end +local MainPage +do + local _class_0 + local _parent_0 = Page + local _base_0 = { + setStartTime = function(self) + self.startTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setEndTime = function(self) + self.endTime = mp.get_property_number("time-pos") + if self.visible then + self:clear() + return self:draw() + end + end, + setupStartAndEndTimes = function(self) + if mp.get_property_native("duration") then + self.startTime = 0 + self.endTime = mp.get_property_native("duration") + else + self.startTime = -1 + self.endTime = -1 + end + if self.visible then + self:clear() + return self:draw() + end + end, + draw = function(self) + local window_w, window_h = mp.get_osd_size() + local ass = assdraw.ass_new() + ass:new_event() + self:setup_text(ass) + ass:append(tostring(bold('WebM maker')) .. "\\N\\N") + ass:append(tostring(bold('c:')) .. " crop\\N") + ass:append(tostring(bold('1:')) .. " set start time (current is " .. tostring(seconds_to_time_string(self.startTime)) .. ")\\N") + ass:append(tostring(bold('2:')) .. " set end time (current is " .. tostring(seconds_to_time_string(self.endTime)) .. ")\\N") + ass:append(tostring(bold('o:')) .. " change encode options\\N") + ass:append(tostring(bold('p:')) .. " preview\\N") + ass:append(tostring(bold('e:')) .. " encode\\N\\N") + ass:append(tostring(bold('ESC:')) .. " close\\N") + return mp.set_osd_ass(window_w, window_h, ass.text) + end, + show = function(self) + _class_0.__parent.show(self) + return emit_event("show-main-page") + end, + onUpdateCropRegion = function(self, updated, newRegion) + if updated then + self.region = newRegion + end + return self:show() + end, + crop = function(self) + self:hide() + local cropPage = CropPage((function() + local _base_1 = self + local _fn_0 = _base_1.onUpdateCropRegion + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region) + return cropPage:show() + end, + onOptionsChanged = function(self, updated) + return self:show() + end, + changeOptions = function(self) + self:hide() + local encodeOptsPage = EncodeOptionsPage((function() + local _base_1 = self + local _fn_0 = _base_1.onOptionsChanged + return function(...) + return _fn_0(_base_1, ...) + end + end)()) + return encodeOptsPage:show() + end, + onPreviewEnded = function(self) + return self:show() + end, + preview = function(self) + self:hide() + local previewPage = PreviewPage((function() + local _base_1 = self + local _fn_0 = _base_1.onPreviewEnded + return function(...) + return _fn_0(_base_1, ...) + end + end)(), self.region, self.startTime, self.endTime) + return previewPage:show() + end, + encode = function(self) + self:hide() + if self.startTime < 0 then + message("No start time, aborting") + return + end + if self.endTime < 0 then + message("No end time, aborting") + return + end + if self.startTime >= self.endTime then + message("Start time is ahead of end time, aborting") + return + end + return encode(self.region, self.startTime, self.endTime) + end + } + _base_0.__index = _base_0 + setmetatable(_base_0, _parent_0.__base) + _class_0 = setmetatable({ + __init = function(self) + self.keybinds = { + ["c"] = (function() + local _base_1 = self + local _fn_0 = _base_1.crop + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["1"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setStartTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["2"] = (function() + local _base_1 = self + local _fn_0 = _base_1.setEndTime + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["o"] = (function() + local _base_1 = self + local _fn_0 = _base_1.changeOptions + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["p"] = (function() + local _base_1 = self + local _fn_0 = _base_1.preview + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["e"] = (function() + local _base_1 = self + local _fn_0 = _base_1.encode + return function(...) + return _fn_0(_base_1, ...) + end + end)(), + ["ESC"] = (function() + local _base_1 = self + local _fn_0 = _base_1.hide + return function(...) + return _fn_0(_base_1, ...) + end + end)() + } + self.startTime = -1 + self.endTime = -1 + self.region = Region() + end, + __base = _base_0, + __name = "MainPage", + __parent = _parent_0 + }, { + __index = function(cls, name) + local val = rawget(_base_0, name) + if val == nil then + local parent = rawget(cls, "__parent") + if parent then + return parent[name] + end + else + return val + end + end, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + if _parent_0.__inherited then + _parent_0.__inherited(_parent_0, _class_0) + end + MainPage = _class_0 +end +monitor_dimensions() +local mainPage = MainPage() +mp.add_key_binding(options.keybind, "display-webm-encoder", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.show + return function(...) + return _fn_0(_base_0, ...) + end +end)(), { + repeatable = false +}) +mp.register_event("file-loaded", (function() + local _base_0 = mainPage + local _fn_0 = _base_0.setupStartAndEndTimes + return function(...) + return _fn_0(_base_0, ...) + end +end)()) +msg.verbose("Loaded mpv-webm script!") +return emit_event("script-loaded")