├── .gitignore ├── LICENSE ├── README.md ├── installer ├── mpv-icon.ico ├── mpv-install.bat ├── mpv-uninstall.bat └── updater.ps1 ├── mpv └── fonts.conf ├── portable_config ├── input.conf ├── mpv.conf ├── script-modules │ └── extended-menu.lua ├── script-opts │ ├── command_palette.conf │ ├── dyn_menu.conf │ ├── recentmenu.conf │ ├── thumbfast.conf │ └── uosc.conf └── scripts │ ├── command_palette.lua │ ├── dialog.lua │ ├── dyn_menu.lua │ ├── recentmenu.lua │ └── thumbfast.lua ├── screenshot.webp └── updater.bat /.gitignore: -------------------------------------------------------------------------------- 1 | d3dcompiler_43.dll 2 | mpv.com 3 | mpv.exe 4 | yt-dlp.exe 5 | portable_config/recent.json 6 | portable_config/cache 7 | portable_config/scripts/uosc 8 | portable_config/fonts 9 | portable_config/scripts/menu.dll 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # mpv-hero 3 | 4 | mpv media player from zero to hero. Get quickly and easily started with the popular mpv media player. 5 | 6 | So far mpv-hero is only available for Windows. 7 | 8 | mpv-hero consists of: 9 | 10 | - Original/Vanilla mpv ([Website](https://mpv.io), [Download](https://github.com/stax76/awesome-mpv?tab=readme-ov-file#installationdownload)). 11 | - Popular mpv user scripts and tools that enhance mpv. 12 | - mpv and user script config files. 13 | 14 | Included user scripts and tools: 15 | 16 | - [mpv-menu-plugin](https://github.com/tsl0922/mpv-menu-plugin) - The context menu. 17 | - [uosc](https://github.com/tomasklaen/uosc) - Feature-rich minimalist proximity-based UI for MPV player. 18 | - [thumbfast](https://github.com/po5/thumbfast) - High-performance on-the-fly thumbnailer script. 19 | - [recent-menu](https://github.com/natural-harmonia-gropius/recent-menu) - Showing recently played files. 20 | - [command_palette](https://github.com/stax76/mpv-scripts) - A searchable menu. 21 | - [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader. 22 | 23 | ![](screenshot.webp) 24 | 25 | For a full list of available mpv user scripts and tools visit: 26 | 27 | https://github.com/stax76/awesome-mpv 28 | -------------------------------------------------------------------------------- /installer/mpv-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stax76/mpv-hero/f612b6e9d46adf823ad938547f129405e6744c88/installer/mpv-icon.ico -------------------------------------------------------------------------------- /installer/mpv-install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions enabledelayedexpansion 3 | path %SystemRoot%\System32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SystemRoot%\System32\WindowsPowerShell\v1.0\ 4 | 5 | :: Unattended install flag. When set, the script will not require user input. 6 | set unattended=no 7 | if "%1"=="/u" set unattended=yes 8 | 9 | :: Make sure this is Windows Vista or later 10 | call :ensure_vista 11 | 12 | :: Make sure the script is running as admin 13 | call :ensure_admin 14 | 15 | :: Command line arguments to use when launching mpv from a file association 16 | set mpv_args= 17 | 18 | :: Get mpv.exe location 19 | cd /D %~dp0\.. 20 | set mpv_path=%cd%\mpv.exe 21 | if not exist "%mpv_path%" call :die "mpv.exe not found" 22 | 23 | :: Get mpv-icon.ico location 24 | set icon_path=%~dp0mpv-icon.ico 25 | if not exist "%icon_path%" call :die "mpv-icon.ico not found" 26 | 27 | :: Register mpv.exe under the "App Paths" key, so it can be found by 28 | :: ShellExecute, the run command, the start menu, etc. 29 | set app_paths_key=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\mpv.exe 30 | call :reg add "%app_paths_key%" /d "%mpv_path%" /f 31 | call :reg add "%app_paths_key%" /v "UseUrl" /t REG_DWORD /d 1 /f 32 | 33 | :: Register mpv.exe under the "Applications" key to add some default verbs for 34 | :: when mpv is used from the "Open with" menu 35 | set classes_root_key=HKLM\SOFTWARE\Classes 36 | set app_key=%classes_root_key%\Applications\mpv.exe 37 | call :reg add "%app_key%" /v "FriendlyAppName" /d "mpv" /f 38 | call :add_verbs "%app_key%" 39 | 40 | :: Add mpv to the "Open with" list for all video and audio file types 41 | call :reg add "%classes_root_key%\SystemFileAssociations\video\OpenWithList\mpv.exe" /d "" /f 42 | call :reg add "%classes_root_key%\SystemFileAssociations\audio\OpenWithList\mpv.exe" /d "" /f 43 | 44 | :: Add DVD AutoPlay handler 45 | set autoplay_key=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers 46 | call :reg add "%classes_root_key%\io.mpv.dvd\shell\play" /d "&Play" /f 47 | call :reg add "%classes_root_key%\io.mpv.dvd\shell\play\command" /d "\"%mpv_path%\" %mpv_args% dvd:// --dvd-device=\"%%%%L\"" /f 48 | call :reg add "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /v "Action" /d "Play DVD movie" /f 49 | call :reg add "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /v "DefaultIcon" /d "%mpv_path%,0" /f 50 | call :reg add "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /v "InvokeProgID" /d "io.mpv.dvd" /f 51 | call :reg add "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /v "InvokeVerb" /d "play" /f 52 | call :reg add "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /v "Provider" /d "mpv" /f 53 | call :reg add "%autoplay_key%\EventHandlers\PlayDVDMovieOnArrival" /v "MpvPlayDVDMovieOnArrival" /f 54 | 55 | :: Add Blu-ray AutoPlay handler 56 | call :reg add "%classes_root_key%\io.mpv.bluray\shell\play" /d "&Play" /f 57 | call :reg add "%classes_root_key%\io.mpv.bluray\shell\play\command" /d "\"%mpv_path%\" %mpv_args% bd:// --bluray-device=\"%%%%L\"" /f 58 | call :reg add "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /v "Action" /d "Play Blu-ray movie" /f 59 | call :reg add "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /v "DefaultIcon" /d "%mpv_path%,0" /f 60 | call :reg add "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /v "InvokeProgID" /d "io.mpv.bluray" /f 61 | call :reg add "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /v "InvokeVerb" /d "play" /f 62 | call :reg add "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /v "Provider" /d "mpv" /f 63 | call :reg add "%autoplay_key%\EventHandlers\PlayBluRayOnArrival" /v "MpvPlayBluRayOnArrival" /f 64 | 65 | :: Add a capabilities key for mpv, which is registered later on for use in the 66 | :: "Default Programs" control panel 67 | set capabilities_key=HKLM\SOFTWARE\Clients\Media\mpv\Capabilities 68 | call :reg add "%capabilities_key%" /v "ApplicationName" /d "mpv" /f 69 | call :reg add "%capabilities_key%" /v "ApplicationDescription" /d "mpv media player" /f 70 | 71 | :: Add file types 72 | set supported_types_key=%app_key%\SupportedTypes 73 | set file_associations_key=%capabilities_key%\FileAssociations 74 | :: DVD/Blu-ray audio formats 75 | call :add_type "audio/ac3" "audio" "AC-3 Audio" ".ac3" ".a52" 76 | call :add_type "audio/eac3" "audio" "E-AC-3 Audio" ".eac3" 77 | call :add_type "audio/vnd.dolby.mlp" "audio" "MLP Audio" ".mlp" 78 | call :add_type "audio/vnd.dts" "audio" "DTS Audio" ".dts" 79 | call :add_type "audio/vnd.dts.hd" "audio" "DTS-HD Audio" ".dts-hd" ".dtshd" 80 | call :add_type "" "audio" "TrueHD Audio" ".true-hd" ".thd" ".truehd" ".thd+ac3" 81 | call :add_type "" "audio" "True Audio" ".tta" 82 | :: Uncompressed formats 83 | call :add_type "" "audio" "PCM Audio" ".pcm" 84 | call :add_type "audio/wav" "audio" "Wave Audio" ".wav" 85 | call :add_type "audio/aiff" "audio" "AIFF Audio" ".aiff" ".aif" ".aifc" 86 | call :add_type "audio/amr" "audio" "AMR Audio" ".amr" 87 | call :add_type "audio/amr-wb" "audio" "AMR-WB Audio" ".awb" 88 | call :add_type "audio/basic" "audio" "AU Audio" ".au" ".snd" 89 | call :add_type "" "audio" "Linear PCM Audio" ".lpcm" 90 | call :add_type "" "video" "Raw YUV Video" ".yuv" 91 | call :add_type "" "video" "YUV4MPEG2 Video" ".y4m" 92 | :: Free lossless formats 93 | call :add_type "audio/x-ape" "audio" "Monkey's Audio" ".ape" 94 | call :add_type "audio/x-wavpack" "audio" "WavPack Audio" ".wv" 95 | call :add_type "audio/x-shorten" "audio" "Shorten Audio" ".shn" 96 | :: MPEG formats 97 | call :add_type "video/vnd.dlna.mpeg-tts" "video" "MPEG-2 Transport Stream" ".m2ts" ".m2t" ".mts" ".mtv" ".ts" ".tsv" ".tsa" ".tts" ".trp" 98 | call :add_type "audio/vnd.dlna.adts" "audio" "ADTS Audio" ".adts" ".adt" 99 | call :add_type "audio/mpeg" "audio" "MPEG Audio" ".mpa" ".m1a" ".m2a" ".mp1" ".mp2" 100 | call :add_type "audio/mpeg" "audio" "MP3 Audio" ".mp3" 101 | call :add_type "video/mpeg" "video" "MPEG Video" ".mpeg" ".mpg" ".mpe" ".mpeg2" ".m1v" ".m2v" ".mp2v" ".mpv" ".mpv2" ".mod" ".tod" 102 | call :add_type "video/dvd" "video" "Video Object" ".vob" ".vro" 103 | call :add_type "" "video" "Enhanced VOB" ".evob" ".evo" 104 | call :add_type "video/mp4" "video" "MPEG-4 Video" ".mpeg4" ".m4v" ".mp4" ".mp4v" ".mpg4" 105 | call :add_type "audio/mp4" "audio" "MPEG-4 Audio" ".m4a" 106 | call :add_type "audio/aac" "audio" "Raw AAC Audio" ".aac" 107 | call :add_type "" "video" "Raw H.264/AVC Video" ".h264" ".avc" ".x264" ".264" 108 | call :add_type "" "video" "Raw H.265/HEVC Video" ".hevc" ".h265" ".x265" ".265" 109 | :: Xiph formats 110 | call :add_type "audio/flac" "audio" "FLAC Audio" ".flac" 111 | call :add_type "audio/ogg" "audio" "Ogg Audio" ".oga" ".ogg" 112 | call :add_type "audio/ogg" "audio" "Opus Audio" ".opus" 113 | call :add_type "audio/ogg" "audio" "Speex Audio" ".spx" 114 | call :add_type "video/ogg" "video" "Ogg Video" ".ogv" ".ogm" 115 | call :add_type "application/ogg" "video" "Ogg Video" ".ogx" 116 | :: Matroska formats 117 | call :add_type "video/x-matroska" "video" "Matroska Video" ".mkv" 118 | call :add_type "video/x-matroska" "video" "Matroska 3D Video" ".mk3d" 119 | call :add_type "audio/x-matroska" "audio" "Matroska Audio" ".mka" 120 | call :add_type "video/webm" "video" "WebM Video" ".webm" 121 | call :add_type "audio/webm" "audio" "WebM Audio" ".weba" 122 | :: Misc formats 123 | call :add_type "video/avi" "video" "Video Clip" ".avi" ".vfw" 124 | call :add_type "" "video" "DivX Video" ".divx" 125 | call :add_type "" "video" "3ivx Video" ".3iv" 126 | call :add_type "" "video" "XVID Video" ".xvid" 127 | call :add_type "" "video" "NUT Video" ".nut" 128 | call :add_type "video/flc" "video" "FLIC Video" ".flic" ".fli" ".flc" 129 | call :add_type "" "video" "Nullsoft Streaming Video" ".nsv" 130 | call :add_type "application/gxf" "video" "General Exchange Format" ".gxf" 131 | call :add_type "application/mxf" "video" "Material Exchange Format" ".mxf" 132 | :: Windows Media formats 133 | call :add_type "audio/x-ms-wma" "audio" "Windows Media Audio" ".wma" 134 | call :add_type "video/x-ms-wm" "video" "Windows Media Video" ".wm" 135 | call :add_type "video/x-ms-wmv" "video" "Windows Media Video" ".wmv" 136 | call :add_type "video/x-ms-asf" "video" "Windows Media Video" ".asf" 137 | call :add_type "" "video" "Microsoft Recorded TV Show" ".dvr-ms" ".dvr" 138 | call :add_type "" "video" "Windows Recorded TV Show" ".wtv" 139 | :: DV formats 140 | call :add_type "" "video" "DV Video" ".dv" ".hdv" 141 | :: Flash Video formats 142 | call :add_type "video/x-flv" "video" "Flash Video" ".flv" 143 | call :add_type "video/mp4" "video" "Flash Video" ".f4v" 144 | call :add_type "audio/mp4" "audio" "Flash Audio" ".f4a" 145 | :: QuickTime formats 146 | call :add_type "video/quicktime" "video" "QuickTime Video" ".qt" ".mov" 147 | call :add_type "video/quicktime" "video" "QuickTime HD Video" ".hdmov" 148 | :: Real Media formats 149 | call :add_type "application/vnd.rn-realmedia" "video" "Real Media Video" ".rm" 150 | call :add_type "application/vnd.rn-realmedia-vbr" "video" "Real Media Video" ".rmvb" 151 | call :add_type "audio/vnd.rn-realaudio" "audio" "Real Media Audio" ".ra" ".ram" 152 | :: 3GPP formats 153 | call :add_type "audio/3gpp" "audio" "3GPP Audio" ".3ga" 154 | call :add_type "audio/3gpp2" "audio" "3GPP Audio" ".3ga2" 155 | call :add_type "video/3gpp" "video" "3GPP Video" ".3gpp" ".3gp" 156 | call :add_type "video/3gpp2" "video" "3GPP Video" ".3gp2" ".3g2" 157 | :: Video game formats 158 | call :add_type "" "audio" "AY Audio" ".ay" 159 | call :add_type "" "audio" "GBS Audio" ".gbs" 160 | call :add_type "" "audio" "GYM Audio" ".gym" 161 | call :add_type "" "audio" "HES Audio" ".hes" 162 | call :add_type "" "audio" "KSS Audio" ".kss" 163 | call :add_type "" "audio" "NSF Audio" ".nsf" 164 | call :add_type "" "audio" "NSFE Audio" ".nsfe" 165 | call :add_type "" "audio" "SAP Audio" ".sap" 166 | call :add_type "" "audio" "SPC Audio" ".spc" 167 | call :add_type "" "audio" "VGM Audio" ".vgm" 168 | call :add_type "" "audio" "VGZ Audio" ".vgz" 169 | :: Playlist formats 170 | call :add_type "audio/x-mpegurl" "audio" "M3U Playlist" ".m3u" ".m3u8" 171 | call :add_type "audio/x-scpls" "audio" "PLS Playlist" ".pls" 172 | call :add_type "" "audio" "CUE Sheet" ".cue" 173 | 174 | :: Register "Default Programs" entry 175 | call :reg add "HKLM\SOFTWARE\RegisteredApplications" /v "mpv" /d "SOFTWARE\Clients\Media\mpv\Capabilities" /f 176 | 177 | :: Enable long paths in Windows 10 178 | call :reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v "LongPathsEnabled" /t REG_DWORD /d 1 /f 179 | 180 | :: Add start menu link 181 | powershell "$s=(New-Object -COM WScript.Shell).CreateShortcut('%ProgramData%\Microsoft\Windows\Start Menu\Programs\mpv.lnk');$s.TargetPath='%mpv_path%';$s.Save()" 182 | 183 | echo. 184 | echo Installed successfully^^! You can now configure mpv's file associations in the 185 | echo Default Programs control panel. 186 | echo. 187 | if [%unattended%] == [yes] exit 0 188 | nul 190 | control /name Microsoft.DefaultPrograms 191 | exit 0 192 | 193 | :die 194 | if not [%1] == [] echo %~1 195 | if [%unattended%] == [yes] exit 1 196 | pause 197 | exit 1 198 | 199 | :ensure_admin 200 | :: 'openfiles' is just a commmand that is present on all supported Windows 201 | :: versions, requires admin privileges and has no side effects, see: 202 | :: https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights 203 | openfiles >nul 2>&1 204 | if errorlevel 1 ( 205 | echo This batch script requires administrator privileges. Right-click on 206 | echo mpv-install.bat and select "Run as administrator". 207 | call :die 208 | ) 209 | goto :EOF 210 | 211 | :ensure_vista 212 | ver | find "XP" >nul 213 | if not errorlevel 1 ( 214 | echo This batch script only works on Windows Vista and later. To create file 215 | echo associations on Windows XP, right click on a video file and use "Open with...". 216 | call :die 217 | ) 218 | goto :EOF 219 | 220 | :reg 221 | :: Wrap the reg command to check for errors 222 | >nul reg %* 223 | if errorlevel 1 set error=yes 224 | if [%error%] == [yes] echo Error in command: reg %* 225 | if [%error%] == [yes] call :die 226 | goto :EOF 227 | 228 | :reg_set_opt 229 | :: Set a value in the registry if it doesn't already exist 230 | set key=%~1 231 | set value=%~2 232 | set data=%~3 233 | 234 | reg query "%key%" /v "%value%" >nul 2>&1 235 | if errorlevel 1 call :reg add "%key%" /v "%value%" /d "%data%" 236 | goto :EOF 237 | 238 | :add_verbs 239 | set key=%~1 240 | 241 | :: Set the default verb to "play" 242 | call :reg add "%key%\shell" /d "play" /f 243 | 244 | :: Hide the "open" verb from the context menu, since it's the same as "play" 245 | call :reg add "%key%\shell\open" /v "LegacyDisable" /f 246 | 247 | :: Set open command 248 | call :reg add "%key%\shell\open\command" /d "\"%mpv_path%\" %mpv_args% -- \"%%%%L\"" /f 249 | 250 | :: Add "play" verb 251 | call :reg add "%key%\shell\play" /d "&Play" /f 252 | call :reg add "%key%\shell\play\command" /d "\"%mpv_path%\" %mpv_args% -- \"%%%%L\"" /f 253 | 254 | goto :EOF 255 | 256 | :add_progid 257 | set prog_id=%~1 258 | set friendly_name=%~2 259 | 260 | :: Add ProgId, edit flags are FTA_OpenIsSafe 261 | set prog_id_key=%classes_root_key%\%prog_id% 262 | call :reg add "%prog_id_key%" /d "%friendly_name%" /f 263 | call :reg add "%prog_id_key%" /v "EditFlags" /t REG_DWORD /d 65536 /f 264 | call :reg add "%prog_id_key%" /v "FriendlyTypeName" /d "%friendly_name%" /f 265 | call :reg add "%prog_id_key%\DefaultIcon" /d "%icon_path%" /f 266 | call :add_verbs "%prog_id_key%" 267 | 268 | goto :EOF 269 | 270 | :update_extension 271 | set extension=%~1 272 | set prog_id=%~2 273 | set mime_type=%~3 274 | set perceived_type=%~4 275 | 276 | :: Add information about the file extension, if not already present 277 | set extension_key=%classes_root_key%\%extension% 278 | if not [%mime_type%] == [] call :reg_set_opt "%extension_key%" "Content Type" "%mime_type%" 279 | if not [%perceived_type%] == [] call :reg_set_opt "%extension_key%" "PerceivedType" "%perceived_type%" 280 | call :reg add "%extension_key%\OpenWithProgIds" /v "%prog_id%" /f 281 | 282 | :: Add type to SupportedTypes 283 | call :reg add "%supported_types_key%" /v "%extension%" /f 284 | 285 | :: Add type to the Default Programs control panel 286 | call :reg add "%file_associations_key%" /v "%extension%" /d "%prog_id%" /f 287 | 288 | goto :EOF 289 | 290 | :add_type 291 | set mime_type=%~1 292 | set perceived_type=%~2 293 | set friendly_name=%~3 294 | set extension=%~4 295 | 296 | echo Adding "%extension%" file type 297 | 298 | :: Add ProgId 299 | set prog_id=io.mpv%extension% 300 | call :add_progid "%prog_id%" "%friendly_name%" 301 | 302 | :: Add extensions 303 | :extension_loop 304 | call :update_extension "%extension%" "%prog_id%" "%mime_type%" "%perceived_type%" 305 | 306 | :: Trailing parameters are additional extensions 307 | shift /4 308 | set extension=%~4 309 | if not [%extension%] == [] goto extension_loop 310 | 311 | goto :EOF 312 | -------------------------------------------------------------------------------- /installer/mpv-uninstall.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableextensions enabledelayedexpansion 3 | path %SystemRoot%\System32;%SystemRoot%;%SystemRoot%\System32\Wbem 4 | 5 | :: Unattended install flag. When set, the script will not require user input. 6 | set unattended=no 7 | if "%1"=="/u" set unattended=yes 8 | 9 | :: Make sure the script is running as admin 10 | call :ensure_admin 11 | 12 | :: Delete "App Paths" entry 13 | reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\mpv.exe" /f >nul 14 | 15 | :: Delete HKCR subkeys 16 | set classes_root_key=HKLM\SOFTWARE\Classes 17 | reg delete "%classes_root_key%\Applications\mpv.exe" /f >nul 18 | reg delete "%classes_root_key%\SystemFileAssociations\video\OpenWithList\mpv.exe" /f >nul 19 | reg delete "%classes_root_key%\SystemFileAssociations\audio\OpenWithList\mpv.exe" /f >nul 20 | 21 | :: Delete AutoPlay handlers 22 | set autoplay_key=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers 23 | reg delete "%autoplay_key%\Handlers\MpvPlayDVDMovieOnArrival" /f >nul 24 | reg delete "%autoplay_key%\EventHandlers\PlayDVDMovieOnArrival" /v "MpvPlayDVDMovieOnArrival" /f >nul 25 | reg delete "%autoplay_key%\Handlers\MpvPlayBluRayOnArrival" /f >nul 26 | reg delete "%autoplay_key%\EventHandlers\PlayBluRayOnArrival" /v "MpvPlayBluRayOnArrival" /f >nul 27 | 28 | :: Delete "Default Programs" entry 29 | reg delete "HKLM\SOFTWARE\RegisteredApplications" /v "mpv" /f >nul 30 | reg delete "HKLM\SOFTWARE\Clients\Media\mpv\Capabilities" /f >nul 31 | 32 | :: Delete all OpenWithProgIds referencing ProgIds that start with io.mpv. 33 | for /f "usebackq eol= delims=" %%k in (`reg query "%classes_root_key%" /f "io.mpv.*" /s /v /c`) do ( 34 | set "key=%%k" 35 | echo !key!| findstr /r /i "^HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\\.[^\\][^\\]*\\OpenWithProgIds$" >nul 36 | if not errorlevel 1 ( 37 | for /f "usebackq eol= tokens=1" %%v in (`reg query "!key!" /f "io.mpv.*" /v /c`) do ( 38 | set "value=%%v" 39 | echo !value!| findstr /r /i "^io\.mpv\.[^\\][^\\]*$" >nul 40 | if not errorlevel 1 ( 41 | echo Deleting !key!\!value! 42 | reg delete "!key!" /v "!value!" /f >nul 43 | ) 44 | ) 45 | ) 46 | ) 47 | 48 | :: Delete all ProgIds starting with io.mpv. 49 | for /f "usebackq eol= delims=" %%k in (`reg query "%classes_root_key%" /f "io.mpv.*" /k /c`) do ( 50 | set "key=%%k" 51 | echo !key!| findstr /r /i "^HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\io\.mpv\.[^\\][^\\]*$" >nul 52 | if not errorlevel 1 ( 53 | echo Deleting !key! 54 | reg delete "!key!" /f >nul 55 | ) 56 | ) 57 | 58 | :: Delete start menu link 59 | del "%ProgramData%\Microsoft\Windows\Start Menu\Programs\mpv.lnk" 60 | 61 | echo Uninstalled successfully 62 | if [%unattended%] == [yes] exit 0 63 | pause 64 | exit 0 65 | 66 | :die 67 | if not [%1] == [] echo %~1 68 | if [%unattended%] == [yes] exit 1 69 | pause 70 | exit 1 71 | 72 | :ensure_admin 73 | :: 'openfiles' is just a commmand that is present on all supported Windows 74 | :: versions, requires admin privileges and has no side effects, see: 75 | :: https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights 76 | openfiles >nul 2>&1 77 | if errorlevel 1 ( 78 | echo This batch script requires administrator privileges. Right-click on 79 | echo mpv-uninstall.bat and select "Run as administrator". 80 | call :die 81 | ) 82 | goto :EOF 83 | -------------------------------------------------------------------------------- /installer/updater.ps1: -------------------------------------------------------------------------------- 1 | $fallback7z = Join-Path (Get-Location) "\7z\7zr.exe"; 2 | $useragent = "mpv-win-updater" 3 | 4 | function Get-7z { 5 | $7z_command = Get-Command -CommandType Application -ErrorAction Ignore 7z.exe | Select-Object -Last 1 6 | if ($7z_command) { 7 | return $7z_command.Source 8 | } 9 | $7zdir = Get-ItemPropertyValue -ErrorAction Ignore "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\7-Zip" "InstallLocation" 10 | if ($7zdir -and (Test-Path (Join-Path $7zdir "7z.exe"))) { 11 | return Join-Path $7zdir "7z.exe" 12 | } 13 | if (Test-Path $fallback7z) { 14 | return $fallback7z 15 | } 16 | return $null 17 | } 18 | 19 | function Check-7z { 20 | if (-not (Get-7z)) 21 | { 22 | $null = New-Item -ItemType Directory -Force (Split-Path $fallback7z) 23 | $download_file = $fallback7z 24 | Write-Host "Downloading 7zr.exe" -ForegroundColor Green 25 | Invoke-WebRequest -Uri "https://www.7-zip.org/a/7zr.exe" -UserAgent $useragent -OutFile $download_file 26 | } 27 | else 28 | { 29 | Write-Host "7z already exist. Skipped download" -ForegroundColor Green 30 | } 31 | } 32 | 33 | function Check-PowershellVersion { 34 | $version = $PSVersionTable.PSVersion.Major 35 | Write-Host "Checking Windows PowerShell version -- $version" -ForegroundColor Green 36 | if ($version -le 2) 37 | { 38 | Write-Host "Using Windows PowerShell $version is unsupported. Upgrade your Windows PowerShell." -ForegroundColor Red 39 | throw 40 | } 41 | } 42 | 43 | function Check-Ytplugin { 44 | $ytdlp = Get-ChildItem "yt-dlp*.exe" -ErrorAction Ignore 45 | $youtubedl = Get-ChildItem "youtube-dl.exe" -ErrorAction Ignore 46 | if ($ytdlp) { 47 | return $ytdlp.ToString() 48 | } 49 | elseif ($youtubedl) { 50 | return $youtubedl.ToString() 51 | } 52 | else { 53 | return $null 54 | } 55 | } 56 | 57 | function Check-Ytplugin-In-System { 58 | $ytp = Get-Command -CommandType Application -ErrorAction Ignore yt-dlp.exe | Select-Object -Last 1 59 | if (-not $ytp) { 60 | $ytp = Get-Command -CommandType Application -ErrorAction Ignore youtube-dl.exe | Select-Object -Last 1 61 | } 62 | return [bool]($ytp -and ((Split-Path $ytp.Source) -ne (Get-Location))) 63 | } 64 | 65 | function Check-Mpv { 66 | $mpv = (Get-Location).Path + "\mpv.exe" 67 | $is_exist = Test-Path $mpv 68 | return $is_exist 69 | } 70 | 71 | function Download-Archive ($filename, $link) { 72 | Write-Host "Downloading" $filename -ForegroundColor Green 73 | Invoke-WebRequest -Uri $link -UserAgent $useragent -OutFile $filename 74 | } 75 | 76 | function Download-Ytplugin ($plugin, $version) { 77 | $link = "" 78 | $plugin_exe = "" 79 | switch -wildcard ($plugin) { 80 | "yt-dlp*" { 81 | Write-Host "Downloading $plugin ($version)" -ForegroundColor Green 82 | $32bit = "" 83 | if (-Not (Test-Path (Join-Path $env:windir "SysWow64"))) { 84 | $32bit = "_x86" 85 | } 86 | $ytdlp_channel = Check-Ytdlp-Channel 87 | if ($ytdlp_channel -eq 'stable') { 88 | $repo = "https://github.com/yt-dlp/yt-dlp" 89 | } 90 | elseif ($ytdlp_channel -eq 'nightly') { 91 | $repo = "https://github.com/yt-dlp/yt-dlp-nightly-builds" 92 | } 93 | elseif ($ytdlp_channel -eq 'master') { 94 | $repo = "https://github.com/yt-dlp/yt-dlp-master-builds" 95 | } 96 | $link = -join($repo, "/releases/download/", $version, "/", $plugin, $32bit, ".exe") 97 | $plugin_exe = -join($plugin, $32bit, ".exe") 98 | } 99 | "youtube-dl" { 100 | Write-Host "Downloading $plugin ($version)" -ForegroundColor Green 101 | $link = -join("https://yt-dl.org/downloads/", $version, "/youtube-dl.exe") 102 | $plugin_exe = "youtube-dl.exe" 103 | } 104 | } 105 | Invoke-WebRequest -Uri $link -UserAgent $useragent -OutFile $plugin_exe 106 | } 107 | 108 | function Extract-Archive ($file) { 109 | $7z = Get-7z 110 | Write-Host "Extracting" $file -ForegroundColor Green 111 | & $7z x -y $file 112 | } 113 | 114 | function Get-Latest-Mpv($Arch) { 115 | $filename = "" 116 | $download_link = "" 117 | $api_gh = "https://api.github.com/repos/zhongfly/mpv-winbuild/releases/latest" 118 | $json = Invoke-WebRequest $api_gh -MaximumRedirection 0 -ErrorAction Ignore -UseBasicParsing | ConvertFrom-Json 119 | $filename = $json.assets | where { $_.name -Match "mpv-$Arch-[0-9]{8}" } | Select-Object -ExpandProperty name 120 | $download_link = $json.assets | where { $_.name -Match "mpv-$Arch-[0-9]{8}" } | Select-Object -ExpandProperty browser_download_url 121 | if ($filename -is [array]) { 122 | return $filename[0], $download_link[0] 123 | } 124 | else { 125 | return $filename, $download_link 126 | } 127 | } 128 | 129 | function Get-Latest-Ytplugin ($plugin) { 130 | switch -wildcard ($plugin) { 131 | "yt-dlp*" { 132 | $ytdlp_channel = Check-Ytdlp-Channel 133 | if ($ytdlp_channel -eq 'stable') { 134 | $repo = "https://github.com/yt-dlp/yt-dlp" 135 | } 136 | elseif ($ytdlp_channel -eq 'nightly') { 137 | $repo = "https://github.com/yt-dlp/yt-dlp-nightly-builds" 138 | } 139 | elseif ($ytdlp_channel -eq 'master') { 140 | $repo = "https://github.com/yt-dlp/yt-dlp-master-builds" 141 | } 142 | $link = -join($repo, "/releases.atom") 143 | Write-Host "Fetching RSS feed for yt-dlp $ytdlp_channel" -ForegroundColor Green 144 | $resp = [xml](Invoke-WebRequest $link -MaximumRedirection 0 -ErrorAction Ignore -UseBasicParsing).Content 145 | $link = $resp.feed.entry[0].link.href 146 | $version = $link.split("/")[-1] 147 | return $version 148 | } 149 | "youtube-dl" { 150 | $link = "https://yt-dl.org/downloads/latest/youtube-dl.exe" 151 | Write-Host "Fetching RSS feed for youtube-dl" -ForegroundColor Green 152 | $resp = Invoke-WebRequest $link -MaximumRedirection 0 -ErrorAction Ignore -UseBasicParsing 153 | $redirect_link = $resp.Headers.Location 154 | $version = $redirect_link.split("/")[4] 155 | return $version 156 | } 157 | } 158 | } 159 | 160 | function Get-Latest-FFmpeg ($Arch) { 161 | $api_gh = "https://api.github.com/repos/zhongfly/mpv-winbuild/releases/latest" 162 | $json = Invoke-WebRequest $api_gh -MaximumRedirection 0 -ErrorAction Ignore -UseBasicParsing | ConvertFrom-Json 163 | $filename = $json.assets | where { $_.name -Match "ffmpeg-$Arch-git-" } | Select-Object -ExpandProperty name 164 | $download_link = $json.assets | where { $_.name -Match "ffmpeg-$Arch-git-" } | Select-Object -ExpandProperty browser_download_url 165 | if ($filename -is [array]) { 166 | return $filename[0], $download_link[0] 167 | } 168 | else { 169 | return $filename, $download_link 170 | } 171 | } 172 | 173 | function Get-Arch { 174 | # Reference: http://superuser.com/a/891443 175 | $FilePath = [System.IO.Path]::Combine((Get-Location).Path, 'mpv.exe') 176 | [int32]$MACHINE_OFFSET = 4 177 | [int32]$PE_POINTER_OFFSET = 60 178 | 179 | [byte[]]$data = New-Object -TypeName System.Byte[] -ArgumentList 4096 180 | $stream = New-Object -TypeName System.IO.FileStream -ArgumentList ($FilePath, 'Open', 'Read') 181 | $stream.Read($data, 0, 4096) | Out-Null 182 | 183 | # DOS header is 64 bytes, last element, long (4 bytes) is the address of the PE header 184 | [int32]$PE_HEADER_ADDR = [System.BitConverter]::ToInt32($data, $PE_POINTER_OFFSET) 185 | [int32]$machineUint = [System.BitConverter]::ToUInt16($data, $PE_HEADER_ADDR + $MACHINE_OFFSET) 186 | 187 | $result = "" | select FilePath, FileType 188 | $result.FilePath = $FilePath 189 | 190 | switch ($machineUint) 191 | { 192 | 0 { $result.FileType = 'Native' } 193 | 0x014c { $result.FileType = 'i686' } # 32bit 194 | 0x0200 { $result.FileType = 'Itanium' } 195 | 0x8664 { $result.FileType = 'x86_64' } # 64bit 196 | } 197 | 198 | $stream.Close() 199 | $result 200 | } 201 | 202 | function ExtractGitFromFile { 203 | $stripped = .\mpv --no-config | select-string "mpv" | select-object -First 1 204 | $pattern = "-g([a-z0-9-]{7})" 205 | $bool = $stripped -match $pattern 206 | return $matches[1] 207 | } 208 | 209 | function ExtractGitFromURL($filename) { 210 | $pattern = "-git-([a-z0-9-]{7})" 211 | $bool = $filename -match $pattern 212 | return $matches[1] 213 | } 214 | 215 | function ExtractDateFromFile { 216 | $date = (Get-Item ./mpv.exe).LastWriteTimeUtc 217 | $day = $date.Day.ToString("00") 218 | $month = $date.Month.ToString("00") 219 | $year = $date.Year.ToString("0000") 220 | return "$year$month$day" 221 | } 222 | 223 | function ExtractDateFromURL($filename) { 224 | $pattern = "mpv-[xi864_].*-([0-9]{8})-git-([a-z0-9-]{7})" 225 | $bool = $filename -match $pattern 226 | return $matches[1] 227 | } 228 | 229 | function Test-Admin 230 | { 231 | $user = [Security.Principal.WindowsIdentity]::GetCurrent(); 232 | (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) 233 | } 234 | 235 | function Create-XML{ 236 | @" 237 | 238 | unset 239 | unset 240 | unset 241 | unset 242 | unset 243 | 244 | "@ | Set-Content "settings.xml" -Encoding UTF8 245 | } 246 | 247 | function Check-Arch($arch) { 248 | $get_arch = "" 249 | $file = "settings.xml" 250 | 251 | if (-not (Test-Path $file)) { Create-XML } 252 | [xml]$doc = Get-Content $file 253 | if ($doc.settings.arch -eq "unset") { 254 | if ($arch -eq "i686") { 255 | $get_arch = "i686" 256 | } 257 | else { 258 | $result = Read-KeyOrTimeout "Choose variant for 64bit builds: x86_64 or x86_64-v3 (for cpu with AVX2 support) [1=x86_64 / 2=x86_64-v3 (default=1)" "D1" 259 | Write-Host "" 260 | if ($result -eq 'D1') { 261 | $get_arch = "x86_64" 262 | } 263 | elseif ($result -eq 'D2') { 264 | $get_arch = "x86_64-v3" 265 | } 266 | else { 267 | throw "Please enter valid input key." 268 | } 269 | } 270 | $doc.settings.arch = $get_arch 271 | $doc.Save($file) 272 | } 273 | else { 274 | $get_arch = $doc.settings.arch 275 | } 276 | return $get_arch 277 | } 278 | 279 | function Check-Autodelete($archive) { 280 | $autodelete = "" 281 | $file = "settings.xml" 282 | 283 | if (-not (Test-Path $file)) { exit } 284 | [xml]$doc = Get-Content $file 285 | if ($doc.settings.autodelete -eq "unset") { 286 | $result = Read-KeyOrTimeout "Delete archives after extract? [Y/n] (default=Y)" "Y" 287 | Write-Host "" 288 | if ($result -eq 'Y') { 289 | $autodelete = "true" 290 | } 291 | elseif ($result -eq 'N') { 292 | $autodelete = "false" 293 | } 294 | else { 295 | throw "Please enter valid input key." 296 | } 297 | $doc.settings.autodelete = $autodelete 298 | $doc.Save($file) 299 | } 300 | if ($doc.settings.autodelete -eq "true") { 301 | if (Test-Path $archive) 302 | { 303 | Remove-Item -Force $archive 304 | } 305 | } 306 | } 307 | 308 | function Check-GetFFmpeg() { 309 | $get_ffmpeg = "" 310 | $file = "settings.xml" 311 | 312 | if (-not (Test-Path $file)) { exit } 313 | [xml]$doc = Get-Content $file 314 | if ($doc.settings.getffmpeg -eq "unset") { 315 | Write-Host "FFmpeg doesn't exist. " -ForegroundColor Green -NoNewline 316 | $result = Read-KeyOrTimeout "Proceed with downloading? [Y/n] (default=n)" "N" 317 | Write-Host "" 318 | if ($result -eq 'Y') { 319 | $get_ffmpeg = "true" 320 | } 321 | elseif ($result -eq 'N') { 322 | $get_ffmpeg = "false" 323 | } 324 | else { 325 | throw "Please enter valid input key." 326 | } 327 | $doc.settings.getffmpeg = $get_ffmpeg 328 | $doc.Save($file) 329 | } 330 | else { 331 | $get_ffmpeg = $doc.settings.getffmpeg 332 | } 333 | return $get_ffmpeg 334 | } 335 | 336 | function Check-GetYTDL() { 337 | $get_ytdl = "" 338 | $file = "settings.xml" 339 | 340 | if (-not (Test-Path $file)) { exit } 341 | [xml]$doc = Get-Content $file 342 | if ($null -eq $doc.settings.getytdl) { 343 | $yt = Check-Ytplugin 344 | if ($yt -eq $null){ 345 | $get_ytdl = "unset" 346 | } 347 | elseif ((Get-Item $yt).BaseName -Match "yt-dlp*") { 348 | $get_ytdl = "ytdlp" 349 | } 350 | else { 351 | $get_ytdl = "youtubedl" 352 | } 353 | $newNode = $doc.CreateElement("getytdl") 354 | $newNode.AppendChild($doc.CreateTextNode($get_ytdl)) | out-null 355 | $doc.settings.appendchild($newNode) | out-null 356 | $doc.Save($file) 357 | } 358 | else { 359 | $get_ytdl = $doc.settings.getytdl 360 | } 361 | 362 | if ($get_ytdl -eq "unset") { 363 | $result = Read-KeyOrTimeout "Download ytdlp or youtubedl? [1=ytdlp/2=youtubedl/N] (default=1)" "D1" 364 | Write-Host "" 365 | if ($result -eq 'D1') { 366 | $get_ytdl = "ytdlp" 367 | } 368 | elseif ($result -eq 'D2') { 369 | $get_ytdl = "youtubedl" 370 | } 371 | elseif ($result -eq 'N') { 372 | $get_ytdl = "false" 373 | } 374 | else { 375 | throw "Please enter valid input key." 376 | } 377 | $doc.settings.getytdl = $get_ytdl 378 | $doc.Save($file) 379 | } 380 | 381 | return $get_ytdl 382 | } 383 | 384 | function Check-Ytdlp-Channel() { 385 | $ytdlp_channel = "" 386 | $file = "settings.xml" 387 | 388 | if (-not (Test-Path $file)) { exit } 389 | [xml]$doc = Get-Content $file 390 | if ($null -eq $doc.settings.ytdlpchannel) { 391 | $newNode = $doc.CreateElement("ytdlpchannel") 392 | $newNode.AppendChild($doc.CreateTextNode("unset")) | out-null 393 | $doc.settings.appendchild($newNode) | out-null 394 | } 395 | if ($doc.settings.ytdlpchannel -eq "unset") { 396 | $result = Read-KeyOrTimeout "Which update channel to update yt-dlp to? [1=stable/2=nightly/3=master] (default=1)" "D1" 397 | Write-Host "" 398 | if ($result -eq 'D1') { 399 | $ytdlp_channel = "stable" 400 | } 401 | elseif ($result -eq 'D2') { 402 | $ytdlp_channel = "nightly" 403 | } 404 | elseif ($result -eq 'D3') { 405 | $ytdlp_channel = "master" 406 | } 407 | else { 408 | throw "Please enter valid input key." 409 | } 410 | $doc.settings.ytdlpchannel = $ytdlp_channel 411 | $doc.Save($file) 412 | } 413 | else { 414 | $ytdlp_channel = $doc.settings.ytdlpchannel 415 | } 416 | return $ytdlp_channel 417 | } 418 | 419 | function Upgrade-Mpv { 420 | $need_download = $false 421 | $remoteName = "" 422 | $download_link = "" 423 | $arch = "" 424 | 425 | if (Check-Mpv) { 426 | $file_arch = (Get-Arch).FileType 427 | $arch = Check-Arch $file_arch 428 | $remoteName, $download_link = Get-Latest-Mpv $arch 429 | $localgit = ExtractGitFromFile 430 | $localdate = ExtractDateFromFile 431 | $remotegit = ExtractGitFromURL $remoteName 432 | $remotedate = ExtractDateFromURL $remoteName 433 | if ($localgit -match $remotegit) 434 | { 435 | if ($localdate -match $remotedate) 436 | { 437 | Write-Host "You are already using latest mpv build -- $remoteName" -ForegroundColor Green 438 | $need_download = $false 439 | } 440 | else { 441 | Write-Host "Newer mpv build available" -ForegroundColor Green 442 | $need_download = $true 443 | } 444 | } 445 | else { 446 | Write-Host "Newer mpv build available" -ForegroundColor Green 447 | $need_download = $true 448 | } 449 | } 450 | else { 451 | Write-Host "mpv doesn't exist. " -ForegroundColor Green -NoNewline 452 | $result = Read-KeyOrTimeout "Proceed with downloading? [Y/n] (default=y)" "Y" 453 | Write-Host "" 454 | 455 | if ($result -eq 'Y') { 456 | $need_download = $true 457 | if (Test-Path (Join-Path $env:windir "SysWow64")) { 458 | Write-Host "Detecting System Type is 64-bit" -ForegroundColor Green 459 | $original_arch = "x86_64" 460 | } 461 | else { 462 | Write-Host "Detecting System Type is 32-bit" -ForegroundColor Green 463 | $original_arch = "i686" 464 | } 465 | $arch = Check-Arch $original_arch 466 | $remoteName, $download_link = Get-Latest-Mpv $arch 467 | } 468 | elseif ($result -eq 'N') { 469 | $need_download = $false 470 | } 471 | else { 472 | throw "Please enter valid input key." 473 | } 474 | } 475 | 476 | if ($need_download) { 477 | Download-Archive $remoteName $download_link 478 | Check-7z 479 | Extract-Archive $remoteName 480 | } 481 | Check-Autodelete $remoteName 482 | } 483 | 484 | function Upgrade-Ytplugin { 485 | if (Check-Ytplugin-In-System) { 486 | Write-Host "yt-dlp.exe or youtube-dl.exe already exists in your system, skip the update check." -ForegroundColor Green 487 | return 488 | } 489 | $yt = Check-Ytplugin 490 | if ($yt) { 491 | $latest_release = Get-Latest-Ytplugin((Get-Item $yt).BaseName) 492 | if ((& $yt --version) -match ($latest_release)) { 493 | Write-Host "You are already using latest" (Get-Item $yt).BaseName "-- $latest_release" -ForegroundColor Green 494 | } 495 | else { 496 | Write-Host "Newer" (Get-Item $yt).BaseName "build available" -ForegroundColor Green 497 | if ((Get-Item $yt).BaseName -Match "yt-dlp*") { 498 | $ytdlp_channel = Check-Ytdlp-Channel 499 | & $yt --update-to $ytdlp_channel 500 | } 501 | else { 502 | & $yt --update 503 | } 504 | } 505 | } 506 | else { 507 | Write-Host "ytdlp or youtube-dl doesn't exist. " -ForegroundColor Green -NoNewline 508 | $ytdl = Check-GetYTDL 509 | if ($ytdl -eq 'ytdlp') { 510 | $latest_release = Get-Latest-Ytplugin "yt-dlp" 511 | Download-Ytplugin "yt-dlp" $latest_release 512 | } 513 | elseif ($ytdl -eq 'youtubedl') { 514 | $latest_release = Get-Latest-Ytplugin "youtube-dl" 515 | Download-Ytplugin "youtube-dl" $latest_release 516 | } 517 | elseif ($ytdl -ne 'false') { 518 | throw "Please enter valid input key." 519 | } 520 | } 521 | } 522 | 523 | function Upgrade-FFmpeg { 524 | $get_ffmpeg = Check-GetFFmpeg 525 | if ($get_ffmpeg -eq "false") { 526 | return 527 | } 528 | 529 | if (Test-Path (Join-Path $env:windir "SysWow64")) { 530 | $original_arch = "x86_64" 531 | $arch = Check-Arch $original_arch 532 | } 533 | else { 534 | $arch = "i686" 535 | } 536 | 537 | $need_download = $false 538 | $remote_name, $download_link = Get-Latest-FFmpeg $arch 539 | $ffmpeg = (Get-Location).Path + "\ffmpeg.exe" 540 | $ffmpeg_exist = Test-Path $ffmpeg 541 | 542 | if ($ffmpeg_exist) { 543 | $ffmpeg_file = .\ffmpeg -version | select-string "ffmpeg" | select-object -First 1 544 | $file_pattern_1 = "git-[0-9]{4}-[0-9]{2}-[0-9]{2}-(?[a-z0-9]+)" # git-2023-01-02-cc2b1a325 545 | $file_pattern_2 = "N-\d+-g(?[a-z0-9]+)" # N-109751-g9a820ec8b 546 | $file_pattern = $file_pattern_1, $file_pattern_2 -join '|' 547 | $url_pattern = "git-([a-z0-9]+)" 548 | $file_match= [Regex]::Matches($ffmpeg_file, $file_pattern) 549 | $remote_match = [Regex]::Matches($remote_name, $url_pattern) 550 | $local_git = $file_match[0].groups['commit'].value 551 | $remote_git = $remote_match[0].groups[1].value 552 | 553 | if ($local_git -match $remote_git) { 554 | Write-Host "You are already using latest ffmpeg build -- $remote_name" -ForegroundColor Green 555 | $need_download = $false 556 | } 557 | else { 558 | Write-Host "Newer ffmpeg build available" -ForegroundColor Green 559 | $need_download = $true 560 | } 561 | } 562 | else { 563 | $need_download = $true 564 | } 565 | 566 | if ($need_download) { 567 | Download-Archive $remote_name $download_link 568 | Check-7z 569 | Extract-Archive $remote_name 570 | } 571 | Check-Autodelete $remote_name 572 | } 573 | 574 | function Read-KeyOrTimeout ($prompt, $key){ 575 | $seconds = 9 576 | $startTime = Get-Date 577 | $timeOut = New-TimeSpan -Seconds $seconds 578 | 579 | Write-Host "$prompt " -ForegroundColor Green 580 | 581 | # Basic progress bar 582 | [Console]::CursorLeft = 0 583 | [Console]::Write("[") 584 | [Console]::CursorLeft = $seconds + 2 585 | [Console]::Write("]") 586 | [Console]::CursorLeft = 1 587 | 588 | while (-not [System.Console]::KeyAvailable) { 589 | $currentTime = Get-Date 590 | Start-Sleep -s 1 591 | Write-Host "#" -ForegroundColor Green -NoNewline 592 | if ($currentTime -gt $startTime + $timeOut) { 593 | Break 594 | } 595 | } 596 | if ([System.Console]::KeyAvailable) { 597 | $response = [System.Console]::ReadKey($true).Key 598 | } 599 | else { 600 | $response = $key 601 | } 602 | return $response.ToString() 603 | } 604 | 605 | # 606 | # Main script entry point 607 | # 608 | if (Test-Admin) { 609 | Write-Host "Running script with administrator privileges" -ForegroundColor Yellow 610 | } 611 | else { 612 | Write-Host "Running script without administrator privileges" -ForegroundColor Red 613 | } 614 | 615 | try { 616 | Check-PowershellVersion 617 | # Sourceforge only support TLS 1.2 618 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 619 | $global:progressPreference = 'silentlyContinue' 620 | Upgrade-Mpv 621 | Upgrade-Ytplugin 622 | Upgrade-FFmpeg 623 | Write-Host "Operation completed" -ForegroundColor Magenta 624 | } 625 | catch [System.Exception] { 626 | Write-Host $_.Exception.Message -ForegroundColor Red 627 | exit 1 628 | } 629 | -------------------------------------------------------------------------------- /mpv/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default configuration file 6 | 7 | 24 | 25 | 26 | 27 | WINDOWSFONTDIR 28 | WINDOWSUSERFONTDIR 29 | 30 | 31 | fonts 32 | 33 | ~/.fonts 34 | 35 | 38 | 39 | 40 | mono 41 | 42 | 43 | monospace 44 | 45 | 46 | 47 | 50 | 51 | 52 | sans serif 53 | 54 | 55 | sans-serif 56 | 57 | 58 | 59 | 62 | 63 | 64 | sans 65 | 66 | 67 | sans-serif 68 | 69 | 70 | 73 | 74 | 75 | system ui 76 | 77 | 78 | system-ui 79 | 80 | 81 | 82 | 85 | conf.d 86 | 87 | 88 | 89 | LOCAL_APPDATA_FONTCONFIG_CACHE 90 | fontconfig 91 | 92 | ~/.fontconfig 93 | 94 | 95 | 98 | 99 | 30 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /portable_config/input.conf: -------------------------------------------------------------------------------- 1 | ############################# 2 | # Internal default bindings # 3 | ############################# 4 | 5 | # mpv keybindings 6 | # 7 | # Location of user-defined bindings: ~/.config/mpv/input.conf 8 | # 9 | # Lines starting with # are comments. Use SHARP to assign the # key. 10 | # Copy this file and uncomment and edit the bindings you want to change. 11 | # 12 | # List of commands and further details: DOCS/man/input.rst 13 | # List of special keys: --input-keylist 14 | # Keybindings testing mode: mpv --input-test --force-window --idle 15 | # 16 | # Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore'). 17 | # 18 | # Strings need to be quoted and escaped: 19 | # KEY show-text "This is a single backslash: \\ and a quote: \" !" 20 | # 21 | # You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with 22 | # the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal). 23 | # 24 | # The default keybindings are hardcoded into the mpv binary. 25 | # You can disable them completely with: --no-input-default-bindings 26 | 27 | # Developer note: 28 | # On compilation, this file is baked into the mpv binary, and all lines are 29 | # uncommented (unless '#' is followed by a space) - thus this file defines the 30 | # default key bindings. 31 | 32 | # If this is enabled, treat all the following bindings as default. 33 | #default-bindings start 34 | 35 | #MBTN_LEFT ignore # don't do anything 36 | #MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen 37 | #MBTN_RIGHT cycle pause # toggle pause/playback mode 38 | #MBTN_BACK playlist-prev # skip to the previous file 39 | #MBTN_FORWARD playlist-next # skip to the next file 40 | 41 | # Mouse wheels, touchpad or other input devices that have axes 42 | # if the input devices supports precise scrolling it will also scale the 43 | # numeric value accordingly 44 | #WHEEL_UP add volume 2 45 | #WHEEL_DOWN add volume -2 46 | #WHEEL_LEFT seek -10 # seek 10 seconds backward 47 | #WHEEL_RIGHT seek 10 # seek 10 seconds forward 48 | 49 | ## Seek units are in seconds, but note that these are limited by keyframes 50 | #RIGHT seek 5 # seek 5 seconds forward 51 | #LEFT seek -5 # seek 5 seconds backward 52 | #UP seek 60 # seek 1 minute forward 53 | #DOWN seek -60 # seek 1 minute backward 54 | # Do smaller, always exact (non-keyframe-limited), seeks with shift. 55 | # Don't show them on the OSD (no-osd). 56 | #Shift+RIGHT no-osd seek 1 exact # seek exactly 1 second forward 57 | #Shift+LEFT no-osd seek -1 exact # seek exactly 1 second backward 58 | #Shift+UP no-osd seek 5 exact # seek exactly 5 seconds forward 59 | #Shift+DOWN no-osd seek -5 exact # seek exactly 5 seconds backward 60 | #Ctrl+LEFT no-osd sub-seek -1 # seek to the previous subtitle 61 | #Ctrl+RIGHT no-osd sub-seek 1 # seek to the next subtitle 62 | #Ctrl+Shift+LEFT sub-step -1 # change subtitle timing such that the previous subtitle is displayed 63 | #Ctrl+Shift+RIGHT sub-step 1 # change subtitle timing such that the next subtitle is displayed 64 | #Alt+left add video-pan-x 0.1 # move the video right 65 | #Alt+right add video-pan-x -0.1 # move the video left 66 | #Alt+up add video-pan-y 0.1 # move the video down 67 | #Alt+down add video-pan-y -0.1 # move the video up 68 | #Alt++ add video-zoom 0.1 # zoom in 69 | #ZOOMIN add video-zoom 0.1 # zoom in 70 | #Alt+- add video-zoom -0.1 # zoom out 71 | #ZOOMOUT add video-zoom -0.1 # zoom out 72 | #Ctrl+WHEEL_UP add video-zoom 0.1 # zoom in 73 | #Ctrl+WHEEL_DOWN add video-zoom -0.1 # zoom out 74 | #Alt+BS set video-zoom 0; set panscan 0; set video-pan-x 0; set video-pan-y 0 # reset zoom and pan settings 75 | #PGUP add chapter 1 # seek to the next chapter 76 | #PGDWN add chapter -1 # seek to the previous chapter 77 | #Shift+PGUP seek 600 # seek 10 minutes forward 78 | #Shift+PGDWN seek -600 # seek 10 minutes backward 79 | #[ multiply speed 1/1.1 # decrease the playback speed 80 | #] multiply speed 1.1 # increase the playback speed 81 | #{ multiply speed 0.5 # halve the playback speed 82 | #} multiply speed 2.0 # double the playback speed 83 | #BS set speed 1.0 # reset the speed to normal 84 | #Shift+BS revert-seek # undo the previous (or marked) seek 85 | #Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek 86 | #q quit 87 | #Q quit-watch-later # exit and remember the playback position 88 | #q {encode} quit 4 89 | #ESC set fullscreen no # leave fullscreen 90 | #ESC {encode} quit 4 91 | #p cycle pause # toggle pause/playback mode 92 | #. frame-step # advance one frame and pause 93 | #, frame-back-step # go back by one frame and pause 94 | #SPACE cycle pause # toggle pause/playback mode 95 | #> playlist-next # skip to the next file 96 | #ENTER playlist-next # skip to the next file 97 | #< playlist-prev # skip to the previous file 98 | #O no-osd cycle-values osd-level 3 1 # toggle displaying the OSD on user interaction or always 99 | #o show-progress # show playback progress 100 | #P show-progress # show playback progress 101 | #i script-binding stats/display-stats # display information and statistics 102 | #I script-binding stats/display-stats-toggle # toggle displaying information and statistics 103 | #` script-binding console/enable # open the console 104 | #z add sub-delay -0.1 # shift subtitles 100 ms earlier 105 | #Z add sub-delay +0.1 # delay subtitles by 100 ms 106 | #x add sub-delay +0.1 # delay subtitles by 100 ms 107 | #ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio 108 | #ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier 109 | #Shift+g add sub-scale +0.1 # increase the subtitle font size 110 | #Shift+f add sub-scale -0.1 # decrease the subtitle font size 111 | #9 add volume -2 112 | #/ add volume -2 113 | #KP_DIVIDE add volume -2 114 | #0 add volume 2 115 | #* add volume 2 116 | #KP_MULTIPLY add volume 2 117 | #m cycle mute # toggle mute 118 | #1 add contrast -1 119 | #2 add contrast 1 120 | #3 add brightness -1 121 | #4 add brightness 1 122 | #5 add gamma -1 123 | #6 add gamma 1 124 | #7 add saturation -1 125 | #8 add saturation 1 126 | #Alt+0 set window-scale 0.5 # halve the window size 127 | #Alt+1 set window-scale 1.0 # reset the window size 128 | #Alt+2 set window-scale 2.0 # double the window size 129 | #b cycle deband # toggle the debanding filter 130 | #d cycle deinterlace # cycle the deinterlacing filter 131 | #r add sub-pos -1 # move subtitles up 132 | #R add sub-pos +1 # move subtitles down 133 | #t add sub-pos +1 # move subtitles down 134 | #v cycle sub-visibility # hide or show the subtitles 135 | #Alt+v cycle secondary-sub-visibility # hide or show the secondary subtitles 136 | #V cycle sub-ass-vsfilter-aspect-compat # toggle stretching SSA/ASS subtitles with anamorphic videos to match the historical renderer 137 | #u cycle-values sub-ass-override "force" "scale" # toggle overriding SSA/ASS subtitle styles with the normal styles 138 | #j cycle sub # switch subtitle track 139 | #J cycle sub down # switch subtitle track backwards 140 | #SHARP cycle audio # switch audio track 141 | #_ cycle video # switch video track 142 | #T cycle ontop # toggle placing the video on top of other windows 143 | #f cycle fullscreen # toggle fullscreen 144 | #s screenshot # take a screenshot of the video in its original resolution with subtitles 145 | #S screenshot video # take a screenshot of the video in its original resolution without subtitles 146 | #Ctrl+s screenshot window # take a screenshot of the window with OSD and subtitles 147 | #Alt+s screenshot each-frame # automatically screenshot every frame; issue this command again to stop taking screenshots 148 | #w add panscan -0.1 # decrease panscan 149 | #W add panscan +0.1 # shrink black bars by cropping the video 150 | #e add panscan +0.1 # shrink black bars by cropping the video 151 | #A cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect) 152 | #POWER quit 153 | #PLAY cycle pause # toggle pause/playback mode 154 | #PAUSE cycle pause # toggle pause/playback mode 155 | #PLAYPAUSE cycle pause # toggle pause/playback mode 156 | #PLAYONLY set pause no # unpause 157 | #PAUSEONLY set pause yes # pause 158 | #STOP quit 159 | #FORWARD seek 60 # seek 1 minute forward 160 | #REWIND seek -60 # seek 1 minute backward 161 | #NEXT playlist-next # skip to the next file 162 | #PREV playlist-prev # skip to the previous file 163 | #VOLUME_UP add volume 2 164 | #VOLUME_DOWN add volume -2 165 | #MUTE cycle mute # toggle mute 166 | #CLOSE_WIN quit 167 | #CLOSE_WIN {encode} quit 4 168 | #ctrl+w quit 169 | #E cycle edition # switch edition 170 | #l ab-loop # set/clear A-B loop points 171 | #L cycle-values loop-file "inf" "no" # toggle infinite looping 172 | #ctrl+c quit 4 173 | #DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always 174 | #ctrl+h cycle-values hwdec "auto-safe" "no" # toggle hardware decoding 175 | #F8 show-text ${playlist} # show the playlist 176 | #F9 show-text ${track-list} # show the list of video, audio and sub tracks 177 | #g ignore 178 | #g-p script-binding select/select-playlist 179 | #g-s script-binding select/select-sid 180 | #g-S script-binding select/select-secondary-sid 181 | #g-a script-binding select/select-aid 182 | #g-v script-binding select/select-vid 183 | #g-t script-binding select/select-track 184 | #g-c script-binding select/select-chapter 185 | #g-l script-binding select/select-subtitle-line 186 | #g-d script-binding select/select-audio-device 187 | #g-b script-binding select/select-binding 188 | #g-r script-binding select/show-properties 189 | 190 | #Alt+KP1 add video-rotate -1 # rotate video counterclockwise by 1 degree 191 | #Alt+KP5 set video-rotate 0 # reset rotation 192 | #Alt+KP3 add video-rotate 1 # rotate video clockwise by 1 degree 193 | 194 | #KP1 add video-zoom -0.01 # zoom out video 195 | #KP2 add video-scale-y -0.01 # scale down video vertically 196 | #KP4 add video-scale-x -0.01 # scale down video horizontally 197 | #KP5 set video-scale-x 1.00; set video-scale-y 1; set video-zoom 0 # reset video scale 198 | #KP6 add video-scale-x 0.01 # scale up video horizontally 199 | #KP8 add video-scale-y 0.01 # scale up video vertically 200 | #KP9 add video-zoom 0.01 # zoom in video 201 | 202 | #Ctrl+KP1 add video-pan-x -0.01; add video-pan-y 0.01 # move video left and down 203 | #Ctrl+KP2 add video-pan-y 0.01 # move video down 204 | #Ctrl+KP3 add video-pan-x 0.01; add video-pan-y 0.01 # move video right and down 205 | #Ctrl+KP4 add video-pan-x -0.01 # move video left 206 | #Ctrl+KP5 set video-pan-x 0.00; set video-pan-y 0.00 # reset video position 207 | #Ctrl+KP6 add video-pan-x 0.01 # move video right 208 | #Ctrl+KP7 add video-pan-x -0.01; add video-pan-y -0.01 # move video left and up 209 | #Ctrl+KP8 add video-pan-y -0.01 # move video up 210 | #Ctrl+KP9 add video-pan-x 0.01; add video-pan-y -0.01 # move video right and up 211 | 212 | #Ctrl+KP_END add video-align-x -0.01; add video-align-y 0.01 # align video left and down 213 | #Ctrl+KP_DOWN add video-align-y 0.01 # align video down 214 | #Ctrl+KP_PGDWN add video-align-x 0.01; add video-align-y 0.01 # align video right and down 215 | #Ctrl+KP_LEFT add video-align-x -0.01 # align video left 216 | #Ctrl+KP_BEGIN set video-align-x 0.00; set video-align-y 0.00 # reset video alignment 217 | #Ctrl+KP_RIGHT add video-align-x 0.01 # align video right 218 | #Ctrl+KP_HOME add video-align-x -0.01; add video-align-y -0.01 # align video left and up 219 | #Ctrl+KP_UP add video-align-y -0.01 # align video up 220 | #Ctrl+KP_PGUP add video-align-x 0.01; add video-align-y -0.01 # align video right and up 221 | 222 | # 223 | # Legacy bindings (may or may not be removed in the future) 224 | # 225 | #! add chapter -1 # seek to the previous chapter 226 | #@ add chapter 1 # seek to the next chapter 227 | 228 | # 229 | # Not assigned by default 230 | # (not an exhaustive list of unbound commands) 231 | # 232 | 233 | # ? cycle sub-forced-events-only # display only DVD/PGS forced subtitle events 234 | # ? stop # stop playback (quit or enter idle mode) 235 | 236 | ################ 237 | # Context Menu # 238 | ################ 239 | 240 | Ctrl+o script-message-to dialog open #menu: Open > Files... 241 | Ctrl+O script-message-to dialog open-folder #menu: Open > Folder... 242 | _ script-message-to dialog open append #menu: Open > Add To Playlist... 243 | _ ignore #menu: Open > - 244 | _ script-message-to dialog open bd-iso #menu: Open > Bluray ISO... 245 | _ script-message-to dialog open dvd-iso #menu: Open > DVD ISO... 246 | _ ignore #menu: Open > - 247 | Ctrl+v script-message-to dialog open-clipboard #menu: Open > Clipboard 248 | _ ignore #menu: Open > - 249 | Ctrl+R script-binding recentmenu/open #menu: Open > Recently Played #@recent 250 | _ ignore #menu: - 251 | 252 | Space cycle pause #menu: Play #@state=(idle_active and 'disabled' or (pause or 'hidden')) 253 | Space cycle pause #menu: Pause #@state=((idle_active or pause) and 'hidden') 254 | Ctrl+s stop #menu: Stop #@state=(idle_active and 'disabled') 255 | _ ignore #menu: - 256 | 257 | F12 playlist-next #menu: Navigate > Next File 258 | F11 playlist-prev #menu: Navigate > Previous File 259 | _ ignore #menu: Navigate > - 260 | PGUP add chapter 1 #menu: Navigate > Next Chapter 261 | PGDWN add chapter -1 #menu: Navigate > Previous Chapter 262 | _ ignore #menu: Navigate > - 263 | . frame-step #menu: Navigate > Jump Next Frame 264 | , frame-back-step #menu: Navigate > Jump Previous Frame 265 | _ ignore #menu: Navigate > - 266 | Right seek 5 #menu: Navigate > Jump 5 sec forward 267 | Left seek -5 #menu: Navigate > Jump 5 sec backward 268 | _ ignore #menu: Navigate > - 269 | Up seek 30 #menu: Navigate > Jump 30 sec forward 270 | Down seek -30 #menu: Navigate > Jump 30 sec backward 271 | _ ignore #menu: Navigate > - 272 | Ctrl+Right seek 300 #menu: Navigate > Jump 5 min forward 273 | Ctrl+Left seek -300 #menu: Navigate > Jump 5 min backward 274 | _ ignore #menu: - 275 | 276 | _ ignore #menu: Chapters #@chapters 277 | _ ignore #menu: Tracks #@tracks 278 | _ ignore #menu: Editions #@editions 279 | _ ignore #menu: Playlist #@playlist 280 | _ ignore #menu: - 281 | 282 | KP8 cycle video #menu: Video > Tracks #@tracks/video 283 | _ script-message-to dialog open add-video #menu: Video > Add Tracks... #@state=(idle_active and 'disabled') 284 | _ ignore #menu: Video > - 285 | _ cycle video #menu: Video > Next Track 286 | Ctrl+1 add contrast -1 #menu: Video > Decrease Contrast 287 | Ctrl+2 add contrast 1 #menu: Video > Increase Contrast 288 | _ ignore #menu: Video > - 289 | Ctrl+3 add brightness -1 #menu: Video > Decrease Brightness 290 | Ctrl+4 add brightness 1 #menu: Video > Increase Brightness 291 | _ ignore #menu: Video > - 292 | Ctrl+5 add gamma -1 #menu: Video > Decrease Gamma 293 | Ctrl+6 add gamma 1 #menu: Video > Increase Gamma 294 | _ ignore #menu: Video > - 295 | Ctrl+7 add saturation -1 #menu: Video > Decrease Saturation 296 | Ctrl+8 add saturation 1 #menu: Video > Increase Saturation 297 | _ ignore #menu: Video > - 298 | s async screenshot #menu: Video > Take Screenshot #@state=(tonumber(vid) or 'disabled') 299 | S async screenshot video #menu: Video > Take Screenshot (without subtitles) #@state=(tonumber(vid) or 'disabled') 300 | _ ignore #menu: Video > - 301 | _ set video-aspect-override 16:9 #menu: Video > Aspect Ratio > 16:9 302 | _ set video-aspect-override 4:3 #menu: Video > Aspect Ratio > 4:3 303 | _ set video-aspect-override 2.35:1 #menu: Video > Aspect Ratio > 2.35:1 304 | _ set video-aspect-override -1 #menu: Video > Aspect Ratio > Reset 305 | a cycle-values video-aspect-override 16:9 4:3 2.35:1 -1 306 | Ctrl+q script-binding uosc/stream-quality #menu: Video > Stream Quality 307 | Ctrl+r cycle-values video-rotate 90 180 270 0 #menu: Video > Rotate Video 308 | D cycle deband #menu: Video > Toggle Deband #@state=(deband and 'checked') 309 | d cycle deinterlace #menu: Video > Toggle Deinterlace #@state=(deinterlace and 'checked') 310 | Ctrl+I cycle icc-profile-auto #menu: Video > Toggle Auto ICC Profile #@state=(icc_profile_auto and 'checked') 311 | 312 | KP7 cycle audio #menu: Audio > Tracks #@tracks/audio 313 | _ script-message-to dialog open add-audio #menu: Audio > Add Tracks... #@state=(idle_active and 'disabled') 314 | _ ignore #menu: Audio > - 315 | Ctrl+d add audio-delay 0.1 #menu: Audio > Delay +0.1 316 | Ctrl+D add audio-delay -0.1 #menu: Audio > Delay -0.1 317 | _ ignore #menu: Audio > - 318 | _ ignore #menu: Audio > Devices #@audio-devices 319 | 320 | j cycle sub #menu: Subtitle > Main Subtitle #@tracks/sub 321 | v cycle sub-visibility #menu: Subtitle > Main Subtitle Options > Visibility #@state=(sub_visibility and 'checked') 322 | _ ignore #menu: Subtitle > Main Subtitle Options > - 323 | z add sub-delay -0.1 #menu: Subtitle > Main Subtitle Options > Delay -0.1 324 | Z add sub-delay 0.1 #menu: Subtitle > Main Subtitle Options > Delay +0.1 325 | _ ignore #menu: Subtitle > Main Subtitle Options > - 326 | r add sub-pos -1 #menu: Subtitle > Main Subtitle Options > Move Up 327 | R add sub-pos +1 #menu: Subtitle > Main Subtitle Options > Move Down 328 | Alt+j cycle secondary-sid #menu: Subtitle > Secondary Subtitle #@tracks/sub-secondary 329 | Alt+v cycle secondary-sub-visibility #menu: Subtitle > Secondary Subtitle Options > Visibility #@state=(secondary_sub_visibility and 'checked') 330 | _ ignore #menu: Subtitle > Secondary Subtitle Options > - 331 | Alt+z add secondary-sub-delay -0.1 #menu: Subtitle > Secondary Subtitle Options > Delay -0.1 332 | Alt+Z add secondary-sub-delay 0.1 #menu: Subtitle > Secondary Subtitle Options > Delay +0.1 333 | _ ignore #menu: Subtitle > Secondary Subtitle Options > - 334 | Alt+r add secondary-sub-pos -1 #menu: Subtitle > Secondary Subtitle Options > Move Up 335 | Alt+R add secondary-sub-pos +1 #menu: Subtitle > Secondary Subtitle Options > Move Down 336 | _ script-message-to dialog open add-sub #menu: Subtitle > Add Tracks... #@state=(idle_active and 'disabled') 337 | _ ignore #menu: Subtitle > - 338 | F add sub-scale -0.1 #menu: Subtitle > Decrease Subtitle Font Size 339 | G add sub-scale 0.1 #menu: Subtitle > Increase Subtitle Font Size 340 | _ ignore #menu: - 341 | 342 | Ctrl++ add video-zoom 0.1 #menu: Pan & Scan > Increase Size 343 | Ctrl+- add video-zoom -0.1 #menu: Pan & Scan > Decrease Size 344 | _ ignore #menu: Pan & Scan > - 345 | Ctrl+KP4 add video-pan-x -0.01 #menu: Pan & Scan > Move Left 346 | Ctrl+KP6 add video-pan-x 0.01 #menu: Pan & Scan > Move Right 347 | _ ignore #menu: Pan & Scan > - 348 | Ctrl+KP8 add video-pan-y -0.01 #menu: Pan & Scan > Move Up 349 | Ctrl+KP2 add video-pan-y 0.01 #menu: Pan & Scan > Move Down 350 | _ ignore #menu: Pan & Scan > - 351 | w add panscan -0.1 #menu: Pan & Scan > Decrease Height 352 | W add panscan 0.1 #menu: Pan & Scan > Increase Height 353 | _ ignore #menu: Pan & Scan > - 354 | Ctrl+BS set video-zoom 0; set video-pan-x 0; set video-pan-y 0 #menu: Pan & Scan > Reset 355 | 356 | [ multiply speed 1/1.1 #menu: Speed > -10% 357 | ] multiply speed 1.1 #menu: Speed > +10% 358 | _ ignore #menu: Speed > - 359 | { multiply speed 0.5 #menu: Speed > Half 360 | } multiply speed 2.0 #menu: Speed > Double 361 | _ ignore #menu: Speed > - 362 | _ set speed 0.5 #menu: Speed > 0.5x 363 | _ set speed 0.75 #menu: Speed > 0.75x 364 | _ set speed 1.0 #menu: Speed > 1.0x 365 | _ set speed 1.25 #menu: Speed > 1.25x 366 | _ set speed 1.5 #menu: Speed > 1.5x 367 | _ set speed 2.0 #menu: Speed > 2.0x 368 | _ ignore #menu: Speed > - 369 | BS set speed 1 #menu: Speed > Reset 370 | 371 | + add volume 2 #menu: Volume > Up 372 | - add volume -2 #menu: Volume > Down 373 | _ ignore #menu: Volume > - 374 | m cycle mute #menu: Volume > Mute #@state=(mute and 'checked') 375 | _ ignore #menu: - 376 | 377 | Ctrl+p script-message-to command_palette show-command-palette "Command Palette" # Command Palette #menu: View > Command Palette > Command Palette 378 | F1 script-message-to command_palette show-command-palette "Bindings" # Bindings #menu: View > Command Palette > Bindings 379 | F2 script-message-to command_palette show-command-palette "Commands" # Commands #menu: View > Command Palette > Commands 380 | F3 script-message-to command_palette show-command-palette "Properties" # Properties #menu: View > Command Palette > Properties 381 | F4 script-message-to command_palette show-command-palette "Options" # Options #menu: View > Command Palette > Options 382 | F8 script-message-to command_palette show-command-palette "Playlist" # Playlist #menu: View > Command Palette > Playlist 383 | F9 script-message-to command_palette show-command-palette "Tracks" # Tracks #menu: View > Command Palette > Tracks 384 | Alt+a script-message-to command_palette show-command-palette "Audio Tracks" # Audio Tracks #menu: View > Command Palette > Audio Tracks 385 | Alt+s script-message-to command_palette show-command-palette "Subtitle Tracks" # Subtitle Tracks #menu: View > Command Palette > Subtitle Tracks 386 | Alt+b script-message-to command_palette show-command-palette "Secondary Subtitle" # Secondary Subtitle #menu: View > Command Palette > Secondary Subtitle 387 | _ script-message-to command_palette show-command-palette "Video Tracks" # Video Tracks #menu: View > Command Palette > Video Tracks 388 | Alt+c script-message-to command_palette show-command-palette "Chapters" # Chapters #menu: View > Command Palette > Chapters 389 | Alt+p script-message-to command_palette show-command-palette "Profiles" # Profiles #menu: View > Command Palette > Profiles 390 | Alt+d script-message-to command_palette show-command-palette "Audio Devices" # Audio Devices #menu: View > Command Palette > Audio Devices 391 | Alt+l script-message-to command_palette show-command-palette "Subtitle Line" # Subtitle Line #menu: View > Command Palette > Subtitle Line 392 | Alt+t script-message-to command_palette show-command-palette "Blu-ray Titles" # Blu-ray Titles #menu: View > Command Palette > Blu-ray Titles 393 | Alt+q script-message-to command_palette show-command-palette "Stream Quality" # Stream Quality #menu: View > Command Palette > Stream Quality 394 | _ script-message-to command_palette show-command-palette "Aspect Ratio" # Aspect Ratio #menu: View > Command Palette > Aspect Ratio 395 | Alt+e script-message-to command_palette show-command-palette "Recent Files" # Recent Files #menu: View > Command Palette > Recent Files 396 | 397 | _ script-binding uosc/items #menu: View > Playlist 398 | p show-progress #menu: View > Progress 399 | c script-binding uosc/chapters #menu: View > Chapters 400 | alt+i script-binding uosc/keybinds #menu: View > Bindings 401 | ` script-binding console/enable #menu: View > Console 402 | _ ignore #menu: View > - 403 | Alt++ add window-scale 0.1 #menu: View > Zoom > Enlarge 404 | Alt+- add window-scale -0.1 #menu: View > Zoom > Shrink 405 | _ ignore #menu: View > Zoom > - 406 | Alt+0 set window-scale 0.5 #menu: View > Zoom > 50 % 407 | Alt+1 set window-scale 1.0 #menu: View > Zoom > 100 % 408 | Alt+2 set window-scale 2.0 #menu: View > Zoom > 200 % 409 | Alt+3 set window-scale 3.0 #menu: View > Zoom > 300 % 410 | g-p script-binding select/select-playlist #menu: View > Select On-Screen Menu > Select Playlist 411 | g-s script-binding select/select-sid #menu: View > Select On-Screen Menu > Select Subtitle Track 412 | g-S script-binding select/select-secondary-sid #menu: View > Select On-Screen Menu > Select Secondary Subtitle 413 | g-a script-binding select/select-aid #menu: View > Select On-Screen Menu > Select Audio Track 414 | g-v script-binding select/select-vid #menu: View > Select On-Screen Menu > Select Video Track 415 | g-t script-binding select/select-track #menu: View > Select On-Screen Menu > Select Track 416 | g-c script-binding select/select-chapter #menu: View > Select On-Screen Menu > Select Chapter 417 | g-l script-binding select/select-subtitle-line #menu: View > Select On-Screen Menu > Select Subtitle Line 418 | g-d script-binding select/select-audio-device #menu: View > Select On-Screen Menu > Select Audio Device 419 | g-b script-binding select/select-binding #menu: View > Select On-Screen Menu > Select Bindings 420 | g-r script-binding select/show-properties #menu: View > Select On-Screen Menu > Select Properties 421 | _ ignore #menu: View > - 422 | t script-binding stats/display-stats-toggle #menu: View > Toggle Statistics 423 | O no-osd cycle-values osd-level 3 1 #menu: View > Toggle Time OSD 424 | Del script-binding osc/visibility #menu: View > Toggle OSC Visibility 425 | 426 | 427 | Enter cycle fullscreen #menu: Window > Fullscreen #@state=(fullscreen and 'checked') 428 | Ctrl+S screenshot window #menu: Window > Take Screenshot #@state=(tonumber(vid) or 'disabled') 429 | _ script-message-to dialog save screenshot #menu: Window > Export Screenshot File #@state=(tonumber(vid) or 'disabled') 430 | _ ignore #menu: Window > - 431 | b cycle border #menu: Window > Toggle Border #@state=(border and 'checked') 432 | Ctrl+t cycle ontop #menu: Window > Toggle On Top #@ontop:check #@state=(ontop and 'checked') 433 | 434 | _ script-message-to dialog set-clipboard ${path} #menu: Tools > Copy File Path 435 | _ script-message-to dialog set-clipboard ${metadata} #menu: Tools > Copy Metadata 436 | _ ignore #menu: Tools > - 437 | _ playlist-shuffle #menu: Tools > Shuffle Playlist 438 | _ script-message-to dialog save playlist #menu: Tools > Export Playlist 439 | _ ignore #menu: Tools > - 440 | l ab-loop #menu: Tools > Set/clear A-B loop points 441 | L cycle-values loop-file inf no #menu: Tools > Toggle infinite file looping 442 | Ctrl+h cycle-values hwdec auto no #menu: Tools > Toggle Hardware Decoding 443 | e script-binding uosc/show-in-directory #menu: Tools > Show in directory 444 | Ctrl+f script-binding uosc/open-config-directory #menu: Tools > Open config directory 445 | # script-binding uosc/update #menu: Tools > Update uosc 446 | _ ignore #menu: Tools > - 447 | _ ignore #menu: Tools > Profiles #@profiles 448 | alt+> script-binding uosc/delete-file-next #menu: Tools > Delete > Delete file & Next 449 | alt+< script-binding uosc/delete-file-prev #menu: Tools > Delete > Delete file & Prev 450 | alt+esc script-binding uosc/delete-file-quit #menu: Tools > Delete > Delete file & Quit 451 | _ ignore #menu: Tools > - 452 | Q quit-watch-later #menu: Tools > Exit Watch Later 453 | 454 | _ ignore #menu: - 455 | q quit #menu: Exit 456 | 457 | Esc quit 458 | -------------------------------------------------------------------------------- /portable_config/mpv.conf: -------------------------------------------------------------------------------- 1 | 2 | # mpv config file 3 | 4 | # https://mpv.io/manual/master/#options 5 | 6 | ########## 7 | # Window # 8 | ########## 9 | 10 | # Set window size to 60 % of screen 11 | autofit = 60%x60% 12 | 13 | # The Screen where to start 14 | screen = 0 15 | 16 | # Snap the player window to screen edges 17 | snap-window = yes 18 | 19 | # Set the window title. This is used for the video window 20 | title = ${filename} 21 | 22 | # Disable the title bar 23 | title-bar = no 24 | 25 | # Create a video output window even if there is no video. 26 | # This can be useful when pretending that mpv is a GUI application. 27 | force-window = yes 28 | 29 | ######### 30 | # Audio # 31 | ######### 32 | 33 | # Set volume to 50 34 | volume = 50 35 | 36 | # Set preferred audio language to German and English 37 | alang = de,deu,ger,en,eng 38 | 39 | ############ 40 | # Subtitle # 41 | ############ 42 | 43 | # Load all external subs containing the media filename 44 | sub-auto = fuzzy 45 | 46 | # Show no subtitle 47 | sid = no 48 | 49 | # Set preferred subtitle language to English and German 50 | slang = en,eng,de,deu,ger 51 | 52 | ######### 53 | # Video # 54 | ######### 55 | 56 | # Disable hardware decoding 57 | hwdec = no 58 | 59 | ############ 60 | # Playback # 61 | ############ 62 | 63 | # no: If the current file ends, go to the next file or terminate. (Default.) 64 | # yes: Don't terminate if the current file is the last playlist entry. Equivalent to --keep-open without arg 65 | # always: Playback will never automatically advance to the next file. 66 | keep-open=yes 67 | 68 | # If set to no, instead of pausing when --keep-open is active, 69 | # just stop at end of file and continue playing forward when you 70 | # seek backwards until end where it stops again. Default: yes. 71 | keep-open-pause = no 72 | 73 | ######### 74 | # OSD # 75 | ######### 76 | 77 | # Disable internal OSC script, uosc is used instead 78 | osc = no 79 | 80 | # Set the duration of the OSD messages in ms (default: 1000) 81 | osd-duration = 2000 82 | 83 | # Show a message on OSD when playback starts 84 | osd-playing-msg = ${filename} 85 | 86 | #################### 87 | # Program Behavior # 88 | #################### 89 | 90 | # Makes mpv wait idly instead of quitting when there is no file to play. 91 | idle = yes 92 | 93 | # Properties to reset when next file starts 94 | reset-on-next-file = video-zoom,video-pan-y,pause 95 | 96 | # The options that are saved in "watch later" files if 97 | # they have been changed since when mpv started 98 | watch-later-options = pause 99 | 100 | # Create a playlist from the parent directory with files matching --directory-filter-types 101 | autocreate-playlist = filter 102 | 103 | ############## 104 | # Screenshot # 105 | ############## 106 | 107 | # Directory to save screenshots 108 | screenshot-dir = ~~desktop/ 109 | 110 | # File name to use for screenshots 111 | screenshot-template = mpv screenshot %p %f 112 | 113 | ######### 114 | # Input # 115 | ######### 116 | 117 | # Delay in milliseconds before we start to autorepeat a key (default: 200) 118 | input-ar-delay = 500 119 | 120 | # Number of key presses to generate per second on autorepeat (default: 40) 121 | input-ar-rate = 20 122 | -------------------------------------------------------------------------------- /portable_config/script-modules/extended-menu.lua: -------------------------------------------------------------------------------- 1 | local mp = require 'mp' 2 | local utils = require 'mp.utils' 3 | local assdraw = require 'mp.assdraw' 4 | 5 | -- create namespace with default values 6 | local em = { 7 | 8 | -- customisable values ------------------------------------------------------ 9 | 10 | lines_to_show = 17, -- NOT including search line 11 | pause_on_open = true, 12 | resume_on_exit = "only-if-was-paused", -- another possible value is true 13 | 14 | -- styles (earlyer it was a table, but required many more steps to pass def-s 15 | -- here from .conf file) 16 | font_size = 21, 17 | --font size scales by window 18 | scale_by_window = false, 19 | -- cursor 'width', useful to change if you have hidpi monitor 20 | cursor_x_border = 0.3, 21 | line_bottom_margin = 1, -- basically space between lines 22 | text_color = { 23 | default = 'ffffff', 24 | accent = 'd8a07b', 25 | current = 'aaaaaa', 26 | comment = '636363', 27 | }, 28 | menu_x_padding = 5, -- this padding for now applies only to 'left', not x 29 | menu_y_padding = 2, -- but this one applies to both - top & bottom 30 | 31 | 32 | -- values that should be passed from main script ---------------------------- 33 | 34 | search_heading = 'Default search heading', 35 | -- 'full' is required from main script, 'current_i' is optional 36 | -- others are 'private' 37 | list = { 38 | full = {}, filtered = {}, current_i = nil, pointer_i = 1, show_from_to = {} 39 | }, 40 | -- field to compare with when searching for 'current value' by 'current_i' 41 | index_field = 'index', 42 | -- fields to use when searching for string match / any other custom searching 43 | -- if value has 0 length, then search list item itself 44 | filter_by_fields = {}, 45 | 46 | 47 | -- 'private' values that are not supposed to be changed from the outside ---- 48 | 49 | is_active = false, 50 | -- https://mpv.io/manual/master/#lua-scripting-mp-create-osd-overlay(format) 51 | ass = mp.create_osd_overlay("ass-events"), 52 | was_paused = false, -- flag that indicates that vid was paused by this script 53 | 54 | line = '', 55 | -- if there was no cursor it wouldn't have been needed, but for now we need 56 | -- variable below only to compare it with 'line' and see if we need to filter 57 | prev_line = '', 58 | cursor = 1, 59 | history = {}, 60 | history_pos = 1, 61 | key_bindings = {}, 62 | insert_mode = false, 63 | 64 | -- used only in 'update' func to get error text msgs 65 | error_codes = { 66 | no_match = 'Match required', 67 | no_submit_provided = 'No submit function provided' 68 | } 69 | } 70 | 71 | -- PRIVATE METHODS ------------------------------------------------------------ 72 | 73 | -- declare constructor function 74 | function em:new(o) 75 | o = o or {} 76 | setmetatable(o, self) 77 | self.__index = self 78 | 79 | -- some options might be customised by user in .conf file and read as strings 80 | -- in that case parse those 81 | if type(o.filter_by_fields) == 'string' then 82 | o.filter_by_fields = utils.parse_json(o.filter_by_fields) 83 | end 84 | 85 | if type(o.text_color) == 'string' then 86 | o.text_color = utils.parse_json(o.text_color) 87 | end 88 | 89 | return o 90 | end 91 | 92 | -- this func is just a getter of a current list depending on search line 93 | function em:current() 94 | return self.line == '' and self.list.full or self.list.filtered 95 | end 96 | 97 | -- REVIEW: how to get rid of this wrapper and handle filter func sideeffects 98 | -- in a more elegant way? 99 | function em:filter_wrapper() 100 | -- handles sideeffect that are needed to be run on filtering list 101 | -- cuz the filter func may be redefined in main script and therefore needs 102 | -- to be straight forward - only doing filtering and returning the table 103 | 104 | -- passing current query just in case, so ppl can use it in their custom funcs 105 | self.list.filtered = self:filter(self.line) 106 | 107 | self.prev_line = self.line 108 | self.list.pointer_i = 1 109 | self:set_from_to(true) 110 | end 111 | 112 | function em:set_from_to(reset_flag) 113 | -- additional variables just for shorter var name 114 | local i = self.list.pointer_i 115 | local to_show = self.lines_to_show 116 | local total = #self:current() 117 | 118 | if reset_flag or to_show >= total then 119 | self.list.show_from_to = { 1, math.min(to_show, total) } 120 | return 121 | end 122 | 123 | -- If menu is opened with something already selected we want this 'selected' 124 | -- to be displayed close to the middle of the menu. That's why 'show_from_to' 125 | -- is not initially set, so we can know - if show_from_to length is 0 - it is 126 | -- first call of this func in cur. init 127 | if #self.list.show_from_to == 0 then 128 | -- set show_from_to so chosen item will be displayed close to middle 129 | local half_list = math.ceil(to_show / 2) 130 | if i < half_list then 131 | self.list.show_from_to = { 1, to_show } 132 | elseif total - i < half_list then 133 | self.list.show_from_to = { total - to_show + 1, total } 134 | else 135 | self.list.show_from_to = { i - half_list + 1, i - half_list + to_show } 136 | end 137 | else 138 | table.unpack = table.unpack or unpack -- 5.1 compatibility 139 | local first, last = table.unpack(self.list.show_from_to) 140 | 141 | -- handle cursor moving towards start / end bondary 142 | if first ~= 1 and i - first < 2 then 143 | self.list.show_from_to = { first - 1, last - 1 } 144 | end 145 | if last ~= total and last - i < 2 then 146 | self.list.show_from_to = { first + 1, last + 1 } 147 | end 148 | 149 | -- handle index jumps from beginning to end and backwards 150 | if i > last then 151 | self.list.show_from_to = { i - to_show + 1, i } 152 | end 153 | if i < first then self.list.show_from_to = { 1, to_show } end 154 | end 155 | end 156 | 157 | function em:change_selected_index(num) 158 | self.list.pointer_i = self.list.pointer_i + num 159 | if self.list.pointer_i < 1 then 160 | self.list.pointer_i = #self:current() 161 | elseif self.list.pointer_i > #self:current() then 162 | self.list.pointer_i = 1 163 | end 164 | self:set_from_to() 165 | self:update() 166 | end 167 | 168 | -- Render the REPL and console as an ASS OSD 169 | function em:update(err_code) 170 | -- ASS tags documentation here - https://aegi.vmoe.info/docs/3.0/ASS_Tags/ 171 | 172 | -- do not bother if function was called to close the menu.. 173 | if not self.is_active then 174 | em.ass:remove() 175 | return 176 | end 177 | 178 | local line_height = self.font_size + self.line_bottom_margin 179 | local _, h, aspect = mp.get_osd_size() 180 | local wh = self.scale_by_window and 720 or h 181 | local ww = wh * aspect 182 | 183 | -- '+ 1' below is a search string 184 | local menu_y_pos = 185 | wh - (line_height * (self.lines_to_show + 1) + self.menu_y_padding * 2) 186 | 187 | -- didn't find better place to handle filtered list update 188 | if self.line ~= self.prev_line then self:filter_wrapper() end 189 | 190 | local function get_background() 191 | local a = self:ass_new_wrapper() 192 | a:append('{\\1c&H1c1c1c\\1a&H19}') -- background color & opacity 193 | a:pos(0, 0) 194 | a:draw_start() 195 | a:rect_cw(0, menu_y_pos, ww, wh) 196 | a:draw_stop() 197 | return a.text 198 | end 199 | 200 | local function get_search_header() 201 | local a = self:ass_new_wrapper() 202 | 203 | a:pos(self.menu_x_padding, menu_y_pos + self.menu_y_padding) 204 | 205 | local search_prefix = table.concat({ 206 | self:get_font_color('accent'), 207 | (#self:current() ~= 0 and self.list.pointer_i or '!'), 208 | '/', #self:current(), '\\h\\h', self.search_heading, ':\\h' 209 | }); 210 | 211 | a:append(search_prefix) 212 | -- reset font color after search prefix 213 | a:append(self:get_font_color 'default') 214 | 215 | -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor 216 | -- inline with the surrounding text, but it sets the advance to the width 217 | -- of the drawing. So the cursor doesn't affect layout too much, make it as 218 | -- thin as possible and make it appear to be 1px wide by giving it 0.5px 219 | -- horizontal borders. 220 | local cheight = self.font_size * 8 221 | -- TODO: maybe do it using draw_rect from ass? 222 | local cglyph = '{\\r' .. -- styles reset 223 | '\\1c&Hffffff&\\3c&Hffffff' .. -- font color and border color 224 | '\\xbord' .. self.cursor_x_border .. '\\p4\\pbo24}' .. -- xborder, scale x8 and baseline offset 225 | 'm 0 0 l 0 ' .. cheight .. -- drawing just a line 226 | '{\\p0\\r}' -- finish drawing and reset styles 227 | local before_cur = self:ass_escape(self.line:sub(1, self.cursor - 1)) 228 | local after_cur = self:ass_escape(self.line:sub(self.cursor)) 229 | 230 | a:append(table.concat({ 231 | before_cur, cglyph, self:reset_styles(), 232 | self:get_font_color('default'), after_cur, 233 | (err_code and '\\h' .. self.error_codes[err_code] or "") 234 | })) 235 | 236 | return a.text 237 | 238 | -- NOTE: perhaps this commented code will some day help me in coding cursor 239 | -- like in M-x emacs menu: 240 | -- Redraw the cursor with the REPL text invisible. This will make the 241 | -- cursor appear in front of the text. 242 | -- ass:new_event() 243 | -- ass:an(1) 244 | -- ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur) 245 | -- ass:append(cglyph) 246 | -- ass:append(style .. '{\\alpha&HFF&}' .. after_cur) 247 | end 248 | 249 | local function get_list() 250 | local a = assdraw.ass_new() 251 | 252 | local function apply_highlighting(y) 253 | a:new_event() 254 | a:append(self:reset_styles()) 255 | a:append('{\\1c&Hffffff\\1a&HE6}') -- background color & opacity 256 | a:pos(0, 0) 257 | a:draw_start() 258 | a:rect_cw(0, y, ww, y + self.font_size) 259 | a:draw_stop() 260 | end 261 | 262 | -- REVIEW: maybe make another function 'get_line_str' and move there 263 | -- everything from this for loop? 264 | -- REVIEW: how to use something like table.unpack below? 265 | for i = self.list.show_from_to[1], self.list.show_from_to[2] do 266 | local value = assert(self:current()[i], 'no value with index ' .. i) 267 | local y_offset = menu_y_pos + self.menu_y_padding + 268 | (line_height * (i - self.list.show_from_to[1] + 1)) 269 | 270 | if i == self.list.pointer_i then apply_highlighting(y_offset) end 271 | 272 | a:new_event() 273 | a:append(self:reset_styles()) 274 | a:pos(self.menu_x_padding, y_offset) 275 | a:append(self:get_line(i, value)) 276 | end 277 | 278 | return a.text 279 | end 280 | 281 | em.ass.res_x = ww 282 | em.ass.res_y = wh 283 | em.ass.data = table.concat({ 284 | get_background(), 285 | get_search_header(), 286 | get_list() 287 | }, "\n") 288 | 289 | em.ass:update() 290 | end 291 | 292 | -- params: 293 | -- - data : {list: {}, [current_i] : num} 294 | function em:init(data) 295 | self.list.full = data.list or {} 296 | self.list.current_i = data.current_i or nil 297 | self.list.pointer_i = data.current_i or 1 298 | self:set_active(true) 299 | end 300 | 301 | function em:exit() 302 | self:undefine_key_bindings() 303 | collectgarbage() 304 | end 305 | 306 | -- TODO: write some idle func like this 307 | -- function idle() 308 | -- if pending_selection then 309 | -- gallery:set_selection(pending_selection) 310 | -- pending_selection = nil 311 | -- end 312 | -- if ass_changed or geometry_changed then 313 | -- local ww, wh = mp.get_osd_size() 314 | -- if geometry_changed then 315 | -- geometry_changed = false 316 | -- compute_geometry(ww, wh) 317 | -- end 318 | -- if ass_changed then 319 | -- ass_changed = false 320 | -- mp.set_osd_ass(ww, wh, ass) 321 | -- end 322 | -- end 323 | -- end 324 | -- ... 325 | -- and handle it as follows 326 | -- init(): 327 | -- mp.register_idle(idle) 328 | -- idle() 329 | -- exit(): 330 | -- mp.unregister_idle(idle) 331 | -- idle() 332 | -- And in these observers he is setting a flag, that's being checked in func above 333 | -- mp.observe_property("osd-width", "native", mark_geometry_stale) 334 | -- mp.observe_property("osd-height", "native", mark_geometry_stale) 335 | 336 | -- PRIVATE METHODS END -------------------------------------------------------- 337 | 338 | -- PUBLIC METHODS ------------------------------------------------------------- 339 | 340 | function em:filter() 341 | -- default filter func, might be redefined in main script 342 | local result = {} 343 | 344 | local function get_full_search_str(v) 345 | local str = '' 346 | for _, key in ipairs(self.filter_by_fields) do str = str .. (v[key] or '') end 347 | return str 348 | end 349 | 350 | for _, v in ipairs(self.list.full) do 351 | -- if filter_by_fields has 0 length, then search list item itself 352 | if #self.filter_by_fields == 0 then 353 | if self:search_method(v) then table.insert(result, v) end 354 | else 355 | -- NOTE: we might use search_method on fiels separately like this: 356 | -- for _,key in ipairs(self.filter_by_fields) do 357 | -- if self:search_method(v[key]) then table.insert(result, v) end 358 | -- end 359 | -- But since im planning to implement fuzzy search in future i need full 360 | -- search string here 361 | if self:search_method(get_full_search_str(v)) then 362 | table.insert(result, v) 363 | end 364 | end 365 | end 366 | return result 367 | end 368 | 369 | -- TODO: implement fuzzy search and maybe match highlights 370 | function em:search_method(str) 371 | -- also might be redefined by main script 372 | 373 | -- convert to string just to make sure.. 374 | return tostring(str):lower():find(self.line:lower(), 1, true) 375 | end 376 | 377 | -- this module requires submit function to be defined in main script 378 | function em:submit() self:update('no_submit_provided') end 379 | 380 | function em:update_list(list) 381 | -- for now this func doesn't handle cases when we have 'current_i' to update 382 | -- it 383 | self.list.full = list 384 | if self.line ~= self.prev_line then self:filter_wrapper() end 385 | end 386 | 387 | -- PUBLIC METHODS END --------------------------------------------------------- 388 | 389 | -- HELPER METHODS ------------------------------------------------------------- 390 | 391 | function em:get_line(_, v) -- [i]ndex, [v]alue 392 | -- this func might be redefined in main script to get a custom-formatted line 393 | -- default implementation of this func supposes that value.content field is a 394 | -- String 395 | local a = assdraw.ass_new() 396 | local style = (self.list.current_i == v[self.index_field]) 397 | and 'current' or 'default' 398 | 399 | a:append(self:reset_styles()) 400 | a:append(self:get_font_color(style)) 401 | -- content as default field, which is holding string 402 | -- no point in moving it to main object since content itself is being 403 | -- composed in THIS function, that might (and most likely, should) be 404 | -- redefined in main script 405 | a:append(v.content or 'Something is off in `get_line` func') 406 | return a.text 407 | end 408 | 409 | -- REVIEW: for now i don't see normal way of mergin this func with below one 410 | -- but it's being used only once 411 | function em:reset_styles() 412 | local a = assdraw.ass_new() 413 | -- alignment top left, no word wrapping, border 0, shadow 0 414 | a:append('{\\an7\\q2\\bord0\\shad0}') 415 | a:append('{\\fs' .. self.font_size .. '}') 416 | return a.text 417 | end 418 | 419 | -- function to get rid of some copypaste 420 | function em:ass_new_wrapper() 421 | local a = assdraw.ass_new() 422 | a:new_event() 423 | a:append(self:reset_styles()) 424 | return a 425 | end 426 | 427 | function em:get_font_color(style) 428 | return '{\\1c&H' .. self.text_color[style] .. '}' 429 | end 430 | 431 | -- HELPER METHODS END --------------------------------------------------------- 432 | 433 | 434 | --[[ 435 | The below code is a modified implementation of text input from mpv's console.lua: 436 | https://github.com/mpv-player/mpv/blob/87c9eefb2928252497f6141e847b74ad1158bc61/player/lua/console.lua 437 | 438 | I was too lazy to list all modifications i've done to the script, but if u 439 | rly need to see those - do diff with the original code 440 | ]] 441 | -- 442 | 443 | ------------------------------------------------------------------------------- 444 | -- START ORIGINAL MPV CODE -- 445 | ------------------------------------------------------------------------------- 446 | 447 | -- Copyright (C) 2019 the mpv developers 448 | -- 449 | -- Permission to use, copy, modify, and/or distribute this software for any 450 | -- purpose with or without fee is hereby granted, provided that the above 451 | -- copyright notice and this permission notice appear in all copies. 452 | -- 453 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 454 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 455 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 456 | -- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 457 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 458 | -- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 459 | -- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 460 | 461 | function em:detect_platform() 462 | local o = {} 463 | -- Kind of a dumb way of detecting the platform but whatever 464 | if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 465 | return 'windows' 466 | elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then 467 | return 'macos' 468 | elseif os.getenv('WAYLAND_DISPLAY') then 469 | return 'wayland' 470 | end 471 | return 'x11' 472 | end 473 | 474 | -- Escape a string for verbatim display on the OSD 475 | function em:ass_escape(str) 476 | -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if 477 | -- it isn't followed by a recognised character, so add a zero-width 478 | -- non-breaking space 479 | str = str:gsub('\\', '\\\239\187\191') 480 | str = str:gsub('{', '\\{') 481 | str = str:gsub('}', '\\}') 482 | -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of 483 | -- consecutive newlines 484 | str = str:gsub('\n', '\239\187\191\\N') 485 | -- Turn leading spaces into hard spaces to prevent ASS from stripping them 486 | str = str:gsub('\\N ', '\\N\\h') 487 | str = str:gsub('^ ', '\\h') 488 | return str 489 | end 490 | 491 | -- Set the REPL visibility ("enable", Esc) 492 | function em:set_active(active) 493 | if active == self.is_active then return end 494 | if active then 495 | self.is_active = true 496 | self.insert_mode = false 497 | mp.enable_messages('terminal-default') 498 | self:define_key_bindings() 499 | 500 | -- set flag 'was_paused' only if vid wasn't paused before EM init 501 | if self.pause_on_open and not mp.get_property_bool("pause", false) then 502 | mp.set_property_bool("pause", true) 503 | self.was_paused = true 504 | end 505 | 506 | self:set_from_to() 507 | self:update() 508 | else 509 | -- no need to call 'update' in this block cuz 'clear' method is calling it 510 | self.is_active = false 511 | self:undefine_key_bindings() 512 | 513 | if self.resume_on_exit == true or 514 | (self.resume_on_exit == "only-if-was-paused" and self.was_paused) then 515 | mp.set_property_bool("pause", false) 516 | end 517 | 518 | self:clear() 519 | collectgarbage() 520 | end 521 | end 522 | 523 | -- Naive helper function to find the next UTF-8 character in 'str' after 'pos' 524 | -- by skipping continuation bytes. Assumes 'str' contains valid UTF-8. 525 | function em:next_utf8(str, pos) 526 | if pos > str:len() then return pos end 527 | repeat 528 | pos = pos + 1 529 | until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf 530 | return pos 531 | end 532 | 533 | -- As above, but finds the previous UTF-8 charcter in 'str' before 'pos' 534 | function em:prev_utf8(str, pos) 535 | if pos <= 1 then return pos end 536 | repeat 537 | pos = pos - 1 538 | until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf 539 | return pos 540 | end 541 | 542 | -- Insert a character at the current cursor position (any_unicode) 543 | function em:handle_char_input(c) 544 | if self.insert_mode then 545 | self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self:next_utf8(self.line, self.cursor)) 546 | else 547 | self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self.cursor) 548 | end 549 | self.cursor = self.cursor + #c 550 | self:update() 551 | end 552 | 553 | -- Remove the character behind the cursor (Backspace) 554 | function em:handle_backspace() 555 | if self.cursor <= 1 then return end 556 | local prev = self:prev_utf8(self.line, self.cursor) 557 | self.line = self.line:sub(1, prev - 1) .. self.line:sub(self.cursor) 558 | self.cursor = prev 559 | self:update() 560 | end 561 | 562 | -- Remove the character in front of the cursor (Del) 563 | function em:handle_del() 564 | if self.cursor > self.line:len() then return end 565 | self.line = self.line:sub(1, self.cursor - 1) .. self.line:sub(self:next_utf8(self.line, self.cursor)) 566 | self:update() 567 | end 568 | 569 | -- Toggle insert mode (Ins) 570 | function em:handle_ins() 571 | self.insert_mode = not self.insert_mode 572 | end 573 | 574 | -- Move the cursor to the next character (Right) 575 | function em:next_char() 576 | self.cursor = self:next_utf8(self.line, self.cursor) 577 | self:update() 578 | end 579 | 580 | -- Move the cursor to the previous character (Left) 581 | function em:prev_char() 582 | self.cursor = self:prev_utf8(self.line, self.cursor) 583 | self:update() 584 | end 585 | 586 | -- Clear the current line (Ctrl+C) 587 | function em:clear() 588 | self.line = '' 589 | self.prev_line = '' 590 | 591 | self.list.current_i = nil 592 | self.list.pointer_i = 1 593 | self.list.filtered = {} 594 | self.list.show_from_to = {} 595 | 596 | self.was_paused = false 597 | 598 | self.cursor = 1 599 | self.insert_mode = false 600 | self.history_pos = #self.history + 1 601 | 602 | self:update() 603 | end 604 | 605 | -- Run the current command and clear the line (Enter) 606 | function em:handle_enter() 607 | if #self:current() == 0 then 608 | self:update('no_match') 609 | return 610 | end 611 | 612 | if self.history[#self.history] ~= self.line then 613 | self.history[#self.history + 1] = self.line 614 | end 615 | 616 | self:submit(self:current()[self.list.pointer_i]) 617 | self:set_active(false) 618 | end 619 | 620 | -- Go to the specified position in the command history 621 | function em:go_history(new_pos) 622 | local old_pos = self.history_pos 623 | self.history_pos = new_pos 624 | 625 | -- Restrict the position to a legal value 626 | if self.history_pos > #self.history + 1 then 627 | self.history_pos = #self.history + 1 628 | elseif self.history_pos < 1 then 629 | self.history_pos = 1 630 | end 631 | 632 | -- Do nothing if the history position didn't actually change 633 | if self.history_pos == old_pos then 634 | return 635 | end 636 | 637 | -- If the user was editing a non-history line, save it as the last history 638 | -- entry. This makes it much less frustrating to accidentally hit Up/Down 639 | -- while editing a line. 640 | if old_pos == #self.history + 1 and self.line ~= '' and self.history[#self.history] ~= self.line then 641 | self.history[#self.history + 1] = self.line 642 | end 643 | 644 | -- Now show the history line (or a blank line for #history + 1) 645 | if self.history_pos <= #self.history then 646 | self.line = self.history[self.history_pos] 647 | else 648 | self.line = '' 649 | end 650 | self.cursor = self.line:len() + 1 651 | self.insert_mode = false 652 | self:update() 653 | end 654 | 655 | -- Go to the specified relative position in the command history (Up, Down) 656 | function em:move_history(amount) 657 | self:go_history(self.history_pos + amount) 658 | end 659 | 660 | -- Go to the first command in the command history (PgUp) 661 | function em:handle_pgup() 662 | self:go_history(1) 663 | end 664 | 665 | -- Stop browsing history and start editing a blank line (PgDown) 666 | function em:handle_pgdown() 667 | self:go_history(#self.history + 1) 668 | end 669 | 670 | -- Move to the start of the current word, or if already at the start, the start 671 | -- of the previous word. (Ctrl+Left) 672 | function em:prev_word() 673 | -- This is basically the same as next_word() but backwards, so reverse the 674 | -- string in order to do a "backwards" find. This wouldn't be as annoying 675 | -- to do if Lua didn't insist on 1-based indexing. 676 | self.cursor = self.line:len() - select(2, self.line:reverse():find('%s*[^%s]*', self.line:len() - self.cursor + 2)) + 1 677 | self:update() 678 | end 679 | 680 | -- Move to the end of the current word, or if already at the end, the end of 681 | -- the next word. (Ctrl+Right) 682 | function em:next_word() 683 | self.cursor = select(2, self.line:find('%s*[^%s]*', self.cursor)) + 1 684 | self:update() 685 | end 686 | 687 | -- Move the cursor to the beginning of the line (HOME) 688 | function em:go_home() 689 | self.cursor = 1 690 | self:update() 691 | end 692 | 693 | -- Move the cursor to the end of the line (END) 694 | function em:go_end() 695 | self.cursor = self.line:len() + 1 696 | self:update() 697 | end 698 | 699 | -- Delete from the cursor to the beginning of the word (Ctrl+Backspace) 700 | function em:del_word() 701 | local before_cur = self.line:sub(1, self.cursor - 1) 702 | local after_cur = self.line:sub(self.cursor) 703 | 704 | before_cur = before_cur:gsub('[^%s]+%s*$', '', 1) 705 | self.line = before_cur .. after_cur 706 | self.cursor = before_cur:len() + 1 707 | self:update() 708 | end 709 | 710 | -- Delete from the cursor to the end of the word (Ctrl+Del) 711 | function em:del_next_word() 712 | if self.cursor > self.line:len() then return end 713 | 714 | local before_cur = self.line:sub(1, self.cursor - 1) 715 | local after_cur = self.line:sub(self.cursor) 716 | 717 | after_cur = after_cur:gsub('^%s*[^%s]+', '', 1) 718 | self.line = before_cur .. after_cur 719 | self:update() 720 | end 721 | 722 | -- Delete from the cursor to the end of the line (Ctrl+K) 723 | function em:del_to_eol() 724 | self.line = self.line:sub(1, self.cursor - 1) 725 | self:update() 726 | end 727 | 728 | -- Delete from the cursor back to the start of the line (Ctrl+U) 729 | function em:del_to_start() 730 | self.line = self.line:sub(self.cursor) 731 | self.cursor = 1 732 | self:update() 733 | end 734 | 735 | -- Returns a string of UTF-8 text from the clipboard (or the primary selection) 736 | function em:get_clipboard(clip) 737 | -- Pick a better default font for Windows and macOS 738 | local platform = self:detect_platform() 739 | 740 | if platform == 'x11' then 741 | local res = utils.subprocess({ 742 | args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' }, 743 | playback_only = false, 744 | }) 745 | if not res.error then 746 | return res.stdout 747 | end 748 | elseif platform == 'wayland' then 749 | local res = utils.subprocess({ 750 | args = { 'wl-paste', clip and '-n' or '-np' }, 751 | playback_only = false, 752 | }) 753 | if not res.error then 754 | return res.stdout 755 | end 756 | elseif platform == 'windows' then 757 | local res = utils.subprocess({ 758 | args = { 'powershell', '-NoProfile', '-Command', [[& { 759 | Trap { 760 | Write-Error -ErrorRecord $_ 761 | Exit 1 762 | } 763 | 764 | $clip = "" 765 | if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) { 766 | $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText 767 | } else { 768 | Add-Type -AssemblyName PresentationCore 769 | $clip = [Windows.Clipboard]::GetText() 770 | } 771 | 772 | $clip = $clip -Replace "`r","" 773 | $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip) 774 | [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length) 775 | }]] }, 776 | playback_only = false, 777 | }) 778 | if not res.error then 779 | return res.stdout 780 | end 781 | elseif platform == 'macos' then 782 | local res = utils.subprocess({ 783 | args = { 'pbpaste' }, 784 | playback_only = false, 785 | }) 786 | if not res.error then 787 | return res.stdout 788 | end 789 | end 790 | return '' 791 | end 792 | 793 | -- Paste text from the window-system's clipboard. 'clip' determines whether the 794 | -- clipboard or the primary selection buffer is used (on X11 and Wayland only.) 795 | function em:paste(clip) 796 | local text = self:get_clipboard(clip) 797 | local before_cur = self.line:sub(1, self.cursor - 1) 798 | local after_cur = self.line:sub(self.cursor) 799 | self.line = before_cur .. text .. after_cur 800 | self.cursor = self.cursor + text:len() 801 | self:update() 802 | end 803 | 804 | -- List of input bindings. This is a weird mashup between common GUI text-input 805 | -- bindings and readline bindings. 806 | function em:get_bindings() 807 | local bindings = { 808 | { 'ctrl+[', function() self:set_active(false) end }, 809 | { 'ctrl+g', function() self:set_active(false) end }, 810 | { 'esc', function() self:set_active(false) end }, 811 | { 'enter', function() self:handle_enter() end }, 812 | { 'kp_enter', function() self:handle_enter() end }, 813 | { 'ctrl+m', function() self:handle_enter() end }, 814 | { 'bs', function() self:handle_backspace() end }, 815 | { 'shift+bs', function() self:handle_backspace() end }, 816 | { 'ctrl+h', function() self:handle_backspace() end }, 817 | { 'del', function() self:handle_del() end }, 818 | { 'shift+del', function() self:handle_del() end }, 819 | { 'ins', function() self:handle_ins() end }, 820 | { 'shift+ins', function() self:paste(false) end }, 821 | { 'mbtn_mid', function() self:paste(false) end }, 822 | { 'left', function() self:prev_char() end }, 823 | { 'ctrl+b', function() self:prev_char() end }, 824 | { 'right', function() self:next_char() end }, 825 | { 'ctrl+f', function() self:next_char() end }, 826 | { 'ctrl+k', function() self:change_selected_index(-1) end }, 827 | { 'ctrl+p', function() self:change_selected_index(-1) end }, 828 | { 'ctrl+j', function() self:change_selected_index(1) end }, 829 | { 'ctrl+n', function() self:change_selected_index(1) end }, 830 | { 'up', function() self:move_history(-1) end }, 831 | { 'alt+p', function() self:move_history(-1) end }, 832 | { 'wheel_up', function() self:move_history(-1) end }, 833 | { 'down', function() self:move_history(1) end }, 834 | { 'alt+n', function() self:move_history(1) end }, 835 | { 'wheel_down', function() self:move_history(1) end }, 836 | { 'wheel_left', function() end }, 837 | { 'wheel_right', function() end }, 838 | { 'ctrl+left', function() self:prev_word() end }, 839 | { 'alt+b', function() self:prev_word() end }, 840 | { 'ctrl+right', function() self:next_word() end }, 841 | { 'alt+f', function() self:next_word() end }, 842 | { 'ctrl+a', function() self:go_home() end }, 843 | { 'home', function() self:go_home() end }, 844 | { 'ctrl+e', function() self:go_end() end }, 845 | { 'end', function() self:go_end() end }, 846 | { 'pgup', function() self:handle_pgup() end }, 847 | { 'pgdwn', function() self:handle_pgdown() end }, 848 | { 'ctrl+c', function() self:clear() end }, 849 | { 'ctrl+d', function() self:handle_del() end }, 850 | { 'ctrl+u', function() self:del_to_start() end }, 851 | { 'ctrl+v', function() self:paste(true) end }, 852 | { 'meta+v', function() self:paste(true) end }, 853 | { 'ctrl+bs', function() self:del_word() end }, 854 | { 'ctrl+w', function() self:del_word() end }, 855 | { 'ctrl+del', function() self:del_next_word() end }, 856 | { 'alt+d', function() self:del_next_word() end }, 857 | { 'kp_dec', function() self:handle_char_input('.') end }, 858 | } 859 | 860 | for i = 0, 9 do 861 | bindings[#bindings + 1] = 862 | { 'kp' .. i, function() self:handle_char_input('' .. i) end } 863 | end 864 | 865 | return bindings 866 | end 867 | 868 | function em:text_input(info) 869 | if info.key_text and (info.event == "press" or info.event == "down" 870 | or info.event == "repeat") 871 | then 872 | self:handle_char_input(info.key_text) 873 | end 874 | end 875 | 876 | function em:define_key_bindings() 877 | if #self.key_bindings > 0 then 878 | return 879 | end 880 | for _, bind in ipairs(self:get_bindings()) do 881 | -- Generate arbitrary name for removing the bindings later. 882 | local name = "search_" .. (#self.key_bindings + 1) 883 | self.key_bindings[#self.key_bindings + 1] = name 884 | mp.add_forced_key_binding(bind[1], name, bind[2], { repeatable = true }) 885 | end 886 | mp.add_forced_key_binding("any_unicode", "search_input", function(...) 887 | self:text_input(...) 888 | end, { repeatable = true, complex = true }) 889 | self.key_bindings[#self.key_bindings + 1] = "search_input" 890 | end 891 | 892 | function em:undefine_key_bindings() 893 | for _, name in ipairs(self.key_bindings) do 894 | mp.remove_key_binding(name) 895 | end 896 | self.key_bindings = {} 897 | end 898 | 899 | ------------------------------------------------------------------------------- 900 | -- END ORIGINAL MPV CODE -- 901 | ------------------------------------------------------------------------------- 902 | 903 | return em 904 | -------------------------------------------------------------------------------- /portable_config/script-opts/command_palette.conf: -------------------------------------------------------------------------------- 1 | font_size=44 2 | scale_by_window=no 3 | lines_to_show=12 4 | #pause_on_open=no 5 | #resume_on_exit=only-if-was-paused 6 | 7 | #line_bottom_margin=1 8 | #menu_x_padding=5 9 | #menu_y_padding=2 10 | 11 | #use_mediainfo=no 12 | #stream_quality_options=2160,1440,1080,720,480 13 | #aspect_ratios=4:3,16:9,2.35:1,1.36,1.82,0,-1 14 | -------------------------------------------------------------------------------- /portable_config/script-opts/dyn_menu.conf: -------------------------------------------------------------------------------- 1 | # use_mpv_impl=yes # use mpv's menu implementation if available 2 | # uosc_syntax=no # toggle uosc menu syntax support 3 | # escape_title=yes # escape & to && in menu title 4 | # max_title_length=80 # limit the title length, set to 0 to disable. 5 | # max_playlist_items=20 # limit the playlist items in submenu, set to 0 to disable. 6 | -------------------------------------------------------------------------------- /portable_config/script-opts/recentmenu.conf: -------------------------------------------------------------------------------- 1 | #enabled=yes 2 | #path="~~/recent.json" 3 | length=20 4 | #width=88 5 | #ignore_same_series=yes 6 | reduce_io=yes -------------------------------------------------------------------------------- /portable_config/script-opts/thumbfast.conf: -------------------------------------------------------------------------------- 1 | # Socket path (leave empty for auto) 2 | socket= 3 | 4 | # Thumbnail path (leave empty for auto) 5 | thumbnail= 6 | 7 | # Maximum thumbnail generation size in pixels (scaled down to fit) 8 | # Values are scaled when hidpi is enabled 9 | max_height=200 10 | max_width=200 11 | 12 | # Scale factor for thumbnail display size (requires mpv 0.38+) 13 | # Note that this is lower quality than increasing max_height and max_width 14 | scale_factor=1 15 | 16 | # Apply tone-mapping, no to disable 17 | tone_mapping=auto 18 | 19 | # Overlay id 20 | overlay_id=42 21 | 22 | # Spawn thumbnailer on file load for faster initial thumbnails 23 | spawn_first=no 24 | 25 | # Close thumbnailer process after an inactivity period in seconds, 0 to disable 26 | quit_after_inactivity=0 27 | 28 | # Enable on network playback 29 | network=no 30 | 31 | # Enable on audio playback 32 | audio=no 33 | 34 | # Enable hardware decoding 35 | hwdec=no 36 | 37 | # Windows only: use native Windows API to write to pipe (requires LuaJIT) 38 | direct_io=no 39 | 40 | # Custom path to the mpv executable 41 | mpv_path=mpv 42 | -------------------------------------------------------------------------------- /portable_config/script-opts/uosc.conf: -------------------------------------------------------------------------------- 1 | # Display style of current position. available: line, bar 2 | timeline_style=line 3 | # Line display style config 4 | timeline_line_width=2 5 | # Timeline size when fully expanded, in pixels, 0 to disable 6 | timeline_size=40 7 | # Comma separated states when element should always be fully visible. 8 | # Available: paused, audio, image, video, idle, windowed, fullscreen 9 | timeline_persistency= 10 | # Top border of background color to help visually separate timeline from video 11 | timeline_border=1 12 | # When scrolling above timeline, wheel will seek by this amount of seconds 13 | timeline_step=5 14 | # Render cache indicators for streaming content 15 | timeline_cache=yes 16 | 17 | # When to display an always visible progress bar (minimized timeline). Can be: windowed, fullscreen, always, never 18 | # Can also be toggled on demand with `toggle-progress` command. 19 | progress=windowed 20 | progress_size=2 21 | progress_line_width=20 22 | 23 | # A comma delimited list of controls above the timeline. Set to `never` to disable. 24 | # Parameter spec: enclosed in `{}` means value, enclosed in `[]` means optional 25 | # Full item syntax: `[<[!]{disposition1}[,[!]{dispositionN}]>]{element}[:{paramN}][#{badge}[>{limit}]][?{tooltip}]` 26 | # Common properties: 27 | # `{icon}` - parameter used to specify an icon name (example: `face`) 28 | # - pick here: https://fonts.google.com/icons?icon.platform=web&icon.set=Material+Icons&icon.style=Rounded 29 | # `{element}`s and their parameters: 30 | # `{shorthand}` - preconfigured shorthands: 31 | # `play-pause`, `menu`, `subtitles`, `audio`, `video`, `playlist`, 32 | # `chapters`, `editions`, `stream-quality`, `open-file`, `items`, 33 | # `next`, `prev`, `first`, `last`, `audio-device`, `fullscreen`, 34 | # `loop-playlist`, `loop-file`, `shuffle` 35 | # `speed[:{scale}]` - display speed slider, [{scale}] - factor of controls_size, default: 1.3 36 | # `command:{icon}:{command}` - button that executes a {command} when pressed 37 | # `toggle:{icon}:{prop}[@{owner}]` - button that toggles mpv property 38 | # `cycle:{default_icon}:{prop}[@{owner}]:{value1}[={icon1}][!]/{valueN}[={iconN}][!]` 39 | # - button that cycles mpv property between values, each optionally having different icon and active flag 40 | # - presence of `!` at the end will style the button as active 41 | # - `{owner}` is the name of a script that manages this property if any 42 | # `gap[:{scale}]` - display an empty gap 43 | # {scale} - factor of controls_size, default: 0.3 44 | # `space` - fills all available space between previous and next item, useful to align items to the right 45 | # - multiple spaces divide the available space among themselves, which can be used for centering 46 | # Item visibility control: 47 | # `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility 48 | # - `{disposition}` can be one of: 49 | # - `idle` - true if mpv is in idle mode (no file loaded) 50 | # - `image` - true if current file is a single image 51 | # - `audio` - true for audio only files 52 | # - `video` - true for files with a video track 53 | # - `has_many_video` - true for files with more than one video track 54 | # - `has_image` - true for files with a cover or other image track 55 | # - `has_audio` - true for files with an audio track 56 | # - `has_many_audio` - true for files with more than one audio track 57 | # - `has_sub` - true for files with an subtitle track 58 | # - `has_many_sub` - true for files with more than one subtitle track 59 | # - `has_many_edition` - true for files with more than one edition 60 | # - `has_chapter` - true for files with chapter list 61 | # - `stream` - true if current file is read from a stream 62 | # - `has_playlist` - true if current playlist has 2 or more items in it 63 | # - prefix with `!` to negate the required disposition 64 | # Examples: 65 | # - `stream-quality` - show stream quality button only for streams 66 | # - `audio` - show audio tracks button for all files that have 67 | # an audio track, but are not exclusively audio only files 68 | # Place `#{badge}[>{limit}]` after the element params to give it a badge. Available badges: 69 | # `sub`, `audio`, `video` - track type counters 70 | # `{mpv_prop}` - any mpv prop that makes sense to you: https://mpv.io/manual/master/#property-list 71 | # - if prop value is an array it'll display its size 72 | # `>{limit}` will display the badge only if it's numerical value is above this threshold. 73 | # Example: `#audio>1` 74 | # Place `?{tooltip}` after the element config to give it a tooltip. 75 | # Example implementations: 76 | # menu = command:menu:script-binding uosc/menu-blurred?Menu 77 | # subtitles = command:subtitles:script-binding uosc/subtitles#sub?Subtitles 78 | # fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen 79 | # loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist 80 | # toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes! 81 | controls=play-pause,menu,gap,subtitles,audio,video,editions,stream-quality,gap,space,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen 82 | controls_size=32 83 | controls_margin=8 84 | controls_spacing=2 85 | controls_persistency= 86 | 87 | # Where to display volume controls: none, left, right 88 | volume=right 89 | volume_size=40 90 | volume_border=1 91 | volume_step=1 92 | volume_persistency= 93 | 94 | # Playback speed widget: mouse drag or wheel to change, click to reset 95 | speed_step=0.1 96 | speed_step_is_factor=no 97 | speed_persistency= 98 | 99 | # Controls all menus, such as context menu, subtitle loader/selector, etc 100 | menu_item_height=36 101 | menu_min_width=260 102 | menu_padding=4 103 | # Determines if `/` or `ctrl+f` is required to activate the search, or if typing 104 | # any text is sufficient. 105 | # When enabled, you can no longer toggle a menu off with the same key that opened it, if the key is a unicode character. 106 | menu_type_to_search=yes 107 | 108 | # Top bar with window controls and media title 109 | # Can be: never, no-border, always 110 | top_bar=no-border 111 | top_bar_size=40 112 | top_bar_controls=yes 113 | # Can be: `no` (hide), `yes` (inherit title from mpv.conf), or a custom template string 114 | top_bar_title=yes 115 | # Template string to enable alternative top bar title. If alt title matches main title, 116 | # it'll be hidden. Tip: use `${media-title}` for main, and `${filename}` for alt title. 117 | top_bar_alt_title= 118 | # Can be: 119 | # `below` => display alt title below the main one 120 | # `toggle` => toggle the top bar title text between main and alt by clicking 121 | # the top bar, or calling `toggle-title` binding 122 | top_bar_alt_title_place=below 123 | # Flash top bar when any of these file types is loaded. Available: audio,image,video 124 | top_bar_flash_on=video,audio 125 | top_bar_persistency= 126 | 127 | # Window border drawn in no-border mode 128 | window_border_size=1 129 | 130 | # If there's no playlist and file ends, load next file in the directory 131 | # Requires `keep-open=yes` in `mpv.conf`. 132 | autoload=no 133 | # What types to accept as next item when autoloading or requesting to play next file 134 | # Can be: video, audio, image, subtitle 135 | autoload_types=video,audio,image 136 | # Enable uosc's playlist/directory shuffle mode 137 | # This simply makes the next selected playlist or directory item be random, just 138 | # like any other player in the world. It also has an easily togglable control button. 139 | shuffle=no 140 | 141 | # Scale the interface by this factor 142 | scale=1 143 | # Scale in fullscreen 144 | scale_fullscreen=1.3 145 | # Adjust the text scaling to fit your font 146 | font_scale=1 147 | # Border of text and icons when drawn directly on top of video 148 | text_border=1.2 149 | # Border radius of buttons, menus, and all other rectangles 150 | border_radius=4 151 | # A comma delimited list of color overrides in RGB HEX format. Defaults: 152 | # foreground=ffffff,foreground_text=000000,background=000000,background_text=ffffff,curtain=111111,success=a5e075,error=ff616e 153 | color= 154 | # A comma delimited list of opacity overrides for various UI element backgrounds and shapes. 155 | # This does not affect any text, which is always rendered fully opaque. Defaults: 156 | # timeline=0.9,position=1,chapters=0.8,slider=0.9,slider_gauge=1,controls=0,speed=0.6,menu=1,submenu=0.4,border=1,title=1,tooltip=1,thumbnail=1,curtain=0.8,idle_indicator=0.8,audio_indicator=0.5,buffering_indicator=0.3,playlist_position=0.8 157 | opacity= 158 | # A comma delimited list of features to refine at a cost of some performance impact. 159 | # text_width - Use a more accurate text width measurement that measures each text string individually 160 | # instead of just measuring the width of known letters once and adding them up. 161 | # sorting - Use filename sorting that handles non-english languages better, especially asian ones. 162 | # At the moment, this is only available on windows, and has no effect on other platforms. 163 | refine= 164 | # Duration of animations in milliseconds 165 | animation_duration=100 166 | # Execute command for background clicks shorter than this number of milliseconds, 0 to disable 167 | # Execution always waits for `input-doubleclick-time` to filter out double-clicks 168 | click_threshold=0 169 | click_command=cycle pause; script-binding uosc/flash-pause-indicator 170 | # Flash duration in milliseconds used by `flash-{element}` commands 171 | flash_duration=1000 172 | # Distances in pixels below which elements are fully faded in/out 173 | proximity_in=40 174 | proximity_out=120 175 | # Use only bold font weight throughout the whole UI 176 | font_bold=no 177 | # One of `total`, `playtime-remaining` (scaled by the current speed), `time-remaining` (remaining length of file) 178 | destination_time=playtime-remaining 179 | # Display sub second fraction in timestamps up to this precision 180 | time_precision=0 181 | # Display stream's buffered time in timeline if it's lower than this amount of seconds, 0 to disable 182 | buffered_time_threshold=60 183 | # Hide UI when mpv autohides the cursor. Timing is controlled by `cursor-autohide` in `mpv.conf` (in milliseconds). 184 | autohide=no 185 | # Can be: flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands) 186 | pause_indicator=flash 187 | # Sizes to list in stream quality menu 188 | stream_quality_options=4320,2160,1440,1080,720,480,360,240 189 | # Types to identify media files 190 | video_types=3g2,3gp,asf,avi,f4v,flv,h264,h265,m2ts,m4v,mkv,mov,mp4,mp4v,mpeg,mpg,ogm,ogv,rm,rmvb,ts,vob,webm,wmv,y4m 191 | audio_types=aac,ac3,aiff,ape,au,cue,dsf,dts,flac,m4a,mid,midi,mka,mp3,mp4a,oga,ogg,opus,spx,tak,tta,wav,weba,wma,wv 192 | image_types=apng,avif,bmp,gif,j2k,jp2,jfif,jpeg,jpg,jxl,mj2,png,svg,tga,tif,tiff,webp 193 | subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,sbv,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt 194 | # Default open-file menu directory 195 | default_directory=~/ 196 | # List hidden files when reading directories. Due to environment limitations, this currently only hides 197 | # files starting with a dot. Doesn't hide hidden files on windows (we have no way to tell they're hidden). 198 | show_hidden_files=no 199 | # Move files to trash (recycle bin) when deleting files. Dependencies: 200 | # - Linux: `sudo apt install trash-cli` 201 | # - MacOS: `brew install trash` 202 | use_trash=no 203 | # Adjusted osd margins based on the visibility of UI elements 204 | adjust_osd_margins=yes 205 | 206 | # Adds chapter range indicators to some common chapter types. 207 | # Additionally to displaying the start of the chapter as a diamond icon on top of the timeline, 208 | # the portion of the timeline of that chapter range is also colored based on the config below. 209 | # 210 | # The syntax is a comma-delimited list of `{type}:{color}` pairs, where: 211 | # `{type}` => range type. Currently supported ones are: 212 | # - `openings`, `endings` => anime openings/endings 213 | # - `intros`, `outros` => video intros/outros 214 | # - `ads` => segments created by sponsor-block software like https://github.com/po5/mpv_sponsorblock 215 | # `{color}` => an RGB(A) HEX color code (`rrggbb`, or `rrggbbaa`) 216 | # 217 | # To exclude marking any of the range types, simply remove them from the list. 218 | chapter_ranges=openings:30abf964,endings:30abf964,ads:c54e4e80 219 | # Add alternative lua patterns to identify beginnings of simple chapter ranges (except for `ads`) 220 | # Syntax: `{type}:{pattern}[,{patternN}][;{type}:{pattern}[,{patternN}]]` 221 | chapter_range_patterns=openings:オープニング;endings:エンディング 222 | 223 | # Localization language priority from highest to lowest. 224 | # Also controls what languages are fetched by `download-subtitles` menu. 225 | # Built in languages can be found in `uosc/intl`. 226 | # `slang` is a keyword to inherit values from `--slang` mpv config. 227 | # Supports paths to custom json files: `languages=~~/custom.json,slang,en` 228 | languages=slang,en 229 | 230 | # A comma separated list of element IDs to disable. Available IDs: 231 | # window_border, top_bar, timeline, controls, volume, 232 | # idle_indicator, audio_indicator, buffering_indicator, pause_indicator 233 | disable_elements= 234 | -------------------------------------------------------------------------------- /portable_config/scripts/dialog.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2023-2024 tsl0922. All rights reserved. 2 | -- SPDX-License-Identifier: GPL-2.0-only 3 | 4 | -- https://github.com/tsl0922/mpv-menu-plugin/blob/main/src/lua/dialog.lua 5 | 6 | local opts = require('mp.options') 7 | local utils = require('mp.utils') 8 | local msg = require('mp.msg') 9 | 10 | -- user options 11 | local o = { 12 | video_exts = '*.mp4;*.m4v;*.mkv;*.h264;*.h265;*.m2ts;*.mpeg;*.wmv;*.webm;*.avi;*.flv;*.mov;*.rm;*.rmvb;*.3gp', 13 | audio_exts = '*.mp3;*.m4a;*.aac;*.flac;*.ac3;*.ogg;*.wav;*.dts;*.tta;*.amr;*.ape;*.wv;*.mka;*.weba;*.wma;*.f4a', 14 | image_exts = '*.jpg;*.jpeg;*.bmp;*.png;*.apng;*.gif;*.tiff;*.webp', 15 | subtitle_exts = '*.srt;*.ass;*.idx;*.sub;*.sup;*.txt;*.ssa;*.smi;*.mks', 16 | playlist_exts = '*.m3u;*.m3u8;*.pls;*.cue', 17 | } 18 | opts.read_options(o) 19 | 20 | local menu_native = 'menu' 21 | local open_action = '' 22 | local save_action = '' 23 | local save_arg1 = nil 24 | 25 | -- show error message on screen and log 26 | local function show_error(message) 27 | msg.error(message) 28 | mp.osd_message('error: ' .. message) 29 | end 30 | 31 | -- mp.commandv with error check 32 | local function mp_commandv(...) 33 | local args = { ... } -- remove trailing nil 34 | local res, err = mp.commandv(unpack(args)) 35 | if not res then 36 | local cmd = table.concat(args, ' ') 37 | show_error(cmd .. ': ' .. err) 38 | end 39 | end 40 | 41 | -- open bluray iso or dir 42 | local function open_bluray(path) 43 | mp.commandv('set', 'bluray-device', path) 44 | mp.commandv('loadfile', 'bd://') 45 | end 46 | 47 | -- open dvd iso or dir 48 | local function open_dvd(path) 49 | mp.commandv('set', 'dvd-device', path) 50 | mp.commandv('loadfile', 'dvd://') 51 | end 52 | 53 | -- check if path is url 54 | local function check_url(path) 55 | return type(path) == 'string' and (path:find("^%a[%w.+-]-://") or path:find("^%a[%w.+-]-:%?")) 56 | end 57 | 58 | -- write m3u8 playlist 59 | local function write_playlist(path) 60 | local playlist = mp.get_property_native('playlist') 61 | 62 | local file, err = io.open(path, 'w') 63 | if not file then 64 | show_error('write playlist failed: ' .. err) 65 | return 66 | end 67 | file:write('#EXTM3U\n') 68 | local pwd = mp.get_property("working-directory") 69 | for _, item in ipairs(playlist) do 70 | local fullpath = item.filename 71 | if not check_url(fullpath) then 72 | fullpath = utils.join_path(pwd, fullpath) 73 | end 74 | if item.title and item.title ~= '' then 75 | file:write('#EXTINF:-1, ', item.title, '\n') 76 | end 77 | file:write(fullpath, '\n') 78 | end 79 | file:close() 80 | end 81 | 82 | -- open a single file 83 | local function open_file(i, path, action) 84 | if action == 'add-sub' then 85 | mp.commandv('sub-add', path) 86 | elseif action == 'add-video' then 87 | mp.commandv('video-add', path) 88 | elseif action == 'add-audio' then 89 | mp.commandv('audio-add', path) 90 | elseif action == 'bd-iso' then 91 | open_bluray(path) 92 | elseif action == 'dvd-iso' then 93 | open_dvd(path) 94 | elseif action == 'append' then 95 | mp.commandv('loadfile', path, 'append') 96 | else 97 | mp.commandv('loadfile', path, i > 1 and 'append-play' or 'replace') 98 | end 99 | end 100 | 101 | -- open callback 102 | local function open_cb(...) 103 | for i, v in ipairs({ ... }) do 104 | local path = tostring(v) 105 | open_file(i, path, open_action) 106 | end 107 | end 108 | 109 | -- open folder callback 110 | local function open_folder_cb(path) 111 | if utils.file_info(utils.join_path(path, 'BDMV')) then 112 | open_bluray(path) 113 | elseif utils.file_info(utils.join_path(path, 'VIDEO_TS')) then 114 | open_dvd(path) 115 | else 116 | mp.commandv('loadfile', path) 117 | end 118 | end 119 | 120 | -- save callback 121 | local function save_cb(path) 122 | if save_action == 'screenshot' then 123 | mp_commandv('screenshot-to-file', path, save_arg1) 124 | elseif save_action == 'playlist' then 125 | write_playlist(path) 126 | end 127 | end 128 | 129 | -- clipboard callback 130 | local function clipboard_cb(clipboard) 131 | mp.osd_message('clipboard: ' .. clipboard) 132 | local i = 1 133 | for line in string.gmatch(clipboard, '[^\r\n]+') do 134 | open_file(i, line, open_action) 135 | i = i + 1 136 | end 137 | end 138 | 139 | -- handle message replies 140 | mp.register_script_message('dialog-open-multi-reply', open_cb) 141 | mp.register_script_message('dialog-open-folder-reply', open_folder_cb) 142 | mp.register_script_message('dialog-save-reply', save_cb) 143 | mp.register_script_message('clipboard-get-reply', clipboard_cb) 144 | 145 | -- detect dll client name 146 | mp.register_script_message('menu-init', function(name) menu_native = name end) 147 | 148 | -- open dialog 149 | mp.register_script_message('open', function(action) 150 | local function append_raw(filters, name, spec) 151 | filters[#filters + 1] = { name = name, spec = spec } 152 | end 153 | local function append(filters, name, type) 154 | append_raw(filters, name, o[type]) 155 | end 156 | 157 | open_action = action or '' 158 | local filters = {} 159 | 160 | if open_action == '' or open_action == 'append' then 161 | append(filters, 'Video Files', 'video_exts') 162 | append(filters, 'Audio Files', 'audio_exts') 163 | append(filters, 'Image Files', 'image_exts') 164 | append(filters, 'Playlist Files', 'playlist_exts') 165 | elseif open_action == 'add-sub' then 166 | append(filters, 'Subtitle Files', 'subtitle_exts') 167 | elseif open_action == 'add-video' then 168 | append(filters, 'Video Files', 'video_exts') 169 | elseif open_action == 'add-audio' then 170 | append(filters, 'Audio Files', 'audio_exts') 171 | elseif open_action == 'bd-iso' or open_action == 'dvd-iso' then 172 | append_raw(filters, 'ISO Files', '*.iso') 173 | else 174 | mp.osd_message('unknown open action: ' .. open_action) 175 | return 176 | end 177 | 178 | if open_action ~= 'bd-iso' and open_action ~= 'dvd-iso' then 179 | append_raw(filters, 'All Files', '*.*') 180 | end 181 | 182 | mp.set_property_native('user-data/menu/dialog/filters', filters) 183 | mp.commandv('script-message-to', menu_native, 'dialog/open-multi', mp.get_script_name()) 184 | end) 185 | 186 | -- open folder dialog 187 | mp.register_script_message('open-folder', function() 188 | mp.commandv('script-message-to', menu_native, 'dialog/open-folder', mp.get_script_name()) 189 | end) 190 | 191 | -- save dialog 192 | mp.register_script_message('save', function(action, arg1) 193 | save_action = action or '' 194 | save_arg1 = arg1 195 | if save_action == 'screenshot' then 196 | if not mp.get_property_number('vid') then 197 | mp.osd_message('no video track') 198 | return 199 | end 200 | 201 | mp.set_property_native('user-data/menu/dialog/filters', { 202 | { name = 'JPEG Image', spec = '*.jpg' }, 203 | { name = 'PNG Image', spec = '*.png' }, 204 | { name = 'WebP Image', spec = '*.webp' }, 205 | }) 206 | local filename = mp.get_property('filename/no-ext') or ('screenshot-' .. os.time()) 207 | mp.set_property('user-data/menu/dialog/default-name', filename) 208 | elseif save_action == 'playlist' then 209 | if mp.get_property_number('playlist-count', 0) == 0 then 210 | mp.osd_message('playlist is empty') 211 | return 212 | end 213 | 214 | mp.set_property_native('user-data/menu/dialog/filters', { 215 | { name = 'M3U8 Playlist', spec = '*.m3u8' }, 216 | }) 217 | mp.set_property('user-data/menu/dialog/default-name', 'playlist-' .. os.time()) 218 | else 219 | mp.osd_message('unknown save action: ' .. save_action) 220 | return 221 | end 222 | mp.commandv('script-message-to', menu_native, 'dialog/save', mp.get_script_name()) 223 | end) 224 | 225 | -- open clipboard 226 | mp.register_script_message('open-clipboard', function(action) 227 | open_action = action 228 | mp.commandv('script-message-to', menu_native, 'clipboard/get', mp.get_script_name()) 229 | end) 230 | 231 | -- set clipboard 232 | mp.register_script_message('set-clipboard', function(text) 233 | if not text then return end 234 | local value = text:gsub('\xFD.-\xFE', '') 235 | mp.commandv('script-message-to', menu_native, 'clipboard/set', value) 236 | end) 237 | -------------------------------------------------------------------------------- /portable_config/scripts/dyn_menu.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2023-2024 tsl0922. All rights reserved. 2 | -- SPDX-License-Identifier: GPL-2.0-only 3 | 4 | -- https://github.com/tsl0922/mpv-menu-plugin/blob/main/src/lua/dyn_menu.lua 5 | 6 | local opts = require('mp.options') 7 | local utils = require('mp.utils') 8 | local msg = require('mp.msg') 9 | 10 | -- user options 11 | local o = { 12 | use_mpv_impl = true, -- use mpv's menu implementation if available 13 | uosc_syntax = false, -- toggle uosc menu syntax support 14 | escape_title = true, -- escape & to && in menu title 15 | max_title_length = 80, -- limit the title length, set to 0 to disable. 16 | max_playlist_items = 20, -- limit the playlist items in submenu, set to 0 to disable. 17 | } 18 | opts.read_options(o) 19 | 20 | local use_mpv_impl = o.use_mpv_impl and (mp.get_property_native('menu-data') ~= nil) 21 | local menu_prop = use_mpv_impl and 'menu-data' or 'user-data/menu/items' -- menu data property 22 | local menu_items = {} -- raw menu data 23 | local menu_items_dirty = false -- menu data dirty flag 24 | local dyn_menus = {} -- dynamic menu list 25 | local keyword_to_menu = {} -- keyword -> menu 26 | local has_uosc = false -- uosc installed flag 27 | 28 | -- lua expression compiler (copied from mpv auto_profiles.lua) 29 | ------------------------------------------------------------------------ 30 | local watched_properties = {} -- indexed by property name (used as a set) 31 | local cached_properties = {} -- property name -> last known raw value 32 | local properties_to_menus = {} -- property name -> set of menus using it 33 | local have_dirty_menus = false -- at least one menu is marked dirty 34 | 35 | -- Used during evaluation of the menu update 36 | local current_menu = nil 37 | 38 | -- Cached set of all top-level mpv properities. Only used for extra validation. 39 | local property_set = {} 40 | for _, property in pairs(mp.get_property_native("property-list")) do 41 | property_set[property] = true 42 | end 43 | 44 | local function on_property_change(name, val) 45 | cached_properties[name] = val 46 | -- Mark all menus reading this property as dirty, so they get re-evaluated 47 | -- the next time the script goes back to sleep. 48 | local dependent_menus = properties_to_menus[name] 49 | if dependent_menus then 50 | for menu, _ in pairs(dependent_menus) do 51 | menu.dirty = true 52 | have_dirty_menus = true 53 | end 54 | end 55 | end 56 | 57 | function get(name, default) 58 | -- Normally, we use the cached value only 59 | if not watched_properties[name] then 60 | watched_properties[name] = true 61 | local res, err = mp.get_property_native(name) 62 | -- Property has to not exist and the toplevel of property in the name must also 63 | -- not have an existing match in the property set for this to be considered an error. 64 | -- This allows things like user-data/test to still work. 65 | if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then 66 | msg.error("Property '" .. name .. "' was not found.") 67 | return default 68 | end 69 | cached_properties[name] = res 70 | mp.observe_property(name, "native", on_property_change) 71 | end 72 | -- The first time the property is read we need add it to the 73 | -- properties_to_menus table, which will be used to mark the menu 74 | -- dirty if a property referenced by it changes. 75 | if current_menu then 76 | local map = properties_to_menus[name] 77 | if not map then 78 | map = {} 79 | properties_to_menus[name] = map 80 | end 81 | map[current_menu] = true 82 | end 83 | local val = cached_properties[name] 84 | if val == nil then 85 | val = default 86 | end 87 | return val 88 | end 89 | 90 | local function magic_get(name) 91 | -- Lua identifiers can't contain "-", so in order to match with mpv 92 | -- property conventions, replace "_" to "-" 93 | name = string.gsub(name, "_", "-") 94 | return get(name, nil) 95 | end 96 | 97 | local evil_magic = {} 98 | setmetatable(evil_magic, { 99 | __index = function(table, key) 100 | -- interpret everything as property, unless it already exists as 101 | -- a non-nil global value 102 | local v = _G[key] 103 | if type(v) ~= "nil" then 104 | return v 105 | end 106 | return magic_get(key) 107 | end, 108 | }) 109 | 110 | p = {} 111 | setmetatable(p, { 112 | __index = function(table, key) 113 | return magic_get(key) 114 | end, 115 | }) 116 | 117 | local function compile_expr(name, s) 118 | local code, chunkname = "return " .. s, "expr " .. name 119 | local chunk, err 120 | if setfenv then -- lua 5.1 121 | chunk, err = loadstring(code, chunkname) 122 | if chunk then 123 | setfenv(chunk, evil_magic) 124 | end 125 | else -- lua 5.2 126 | chunk, err = load(code, chunkname, "t", evil_magic) 127 | end 128 | if not chunk then 129 | msg.error("expr '" .. name .. "' : " .. err) 130 | chunk = function() return false end 131 | end 132 | return chunk 133 | end 134 | ------------------------------------------------------------------------ 135 | 136 | -- append menu item to menu 137 | local function append_menu(menu, item) 138 | if (item.title and o.escape_title) then 139 | item.title = item.title:gsub('&', '&&') 140 | end 141 | menu[#menu + 1] = item 142 | end 143 | 144 | -- escape codec name to make it more readable 145 | local function escape_codec(str) 146 | if not str or str == '' then return '' end 147 | if str:find("mpeg2") then return "mpeg2" 148 | elseif str:find("dvvideo") then return "dv" 149 | elseif str:find("pcm") then return "pcm" 150 | elseif str:find("pgs") then return "pgs" 151 | elseif str:find("subrip") then return "srt" 152 | elseif str:find("vtt") then return "vtt" 153 | elseif str:find("dvd_sub") then return "vob" 154 | elseif str:find("dvb_sub") then return "dvb" 155 | elseif str:find("dvb_tele") then return "teletext" 156 | elseif str:find("arib") then return "arib" 157 | else return str end 158 | end 159 | 160 | -- from http://lua-users.org/wiki/LuaUnicode 161 | local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*' 162 | 163 | -- return a substring based on utf8 characters 164 | -- like string.sub, but negative index is not supported 165 | local function utf8_sub(s, i, j) 166 | local t = {} 167 | local idx = 1 168 | for match in s:gmatch(UTF8_PATTERN) do 169 | if j and idx > j then break end 170 | if idx >= i then t[#t + 1] = match end 171 | idx = idx + 1 172 | end 173 | return table.concat(t) 174 | end 175 | 176 | -- return the length of a utf8 string 177 | local function utf8_len(s) 178 | local _, count = s:gsub(UTF8_PATTERN, "") 179 | return count 180 | end 181 | 182 | -- abbreviate title if it's too long 183 | local function abbr_title(str) 184 | if not str or str == '' then return '' end 185 | if o.max_title_length > 0 and utf8_len(str) > o.max_title_length then 186 | return utf8_sub(str, 1, o.max_title_length) .. '...' 187 | end 188 | return str 189 | end 190 | 191 | -- build track title from track metadata 192 | -- 193 | -- example: 194 | -- V: Video 1 [h264, 1920x1080, 23.976 fps] (*) JPN 195 | -- | | | | | 196 | -- type title hints default lang 197 | local function build_track_title(track, prefix, filename) 198 | local type = track.type 199 | local title = track.title or '' 200 | local codec = escape_codec(track.codec) 201 | 202 | -- remove filename from title if it's external track 203 | if track.external and title ~= '' then 204 | if filename ~= '' then title = title:gsub(filename .. '%.?', '') end 205 | if title:lower() == codec:lower() then title = '' end 206 | end 207 | -- set a default title if it's empty 208 | if title == '' then 209 | local name = type:sub(1, 1):upper() .. type:sub(2, #type) 210 | title = string.format('%s %d', name, track.id) 211 | else 212 | title = abbr_title(title) 213 | end 214 | 215 | -- build hints from track metadata 216 | local hints = {} 217 | local function h(value) hints[#hints + 1] = value end 218 | if codec ~= '' then h(codec) end 219 | if track['demux-h'] then 220 | h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h'] or track['demux-h'] .. 'p')) 221 | end 222 | if track['demux-fps'] then h(string.format('%.5g fps', track['demux-fps'])) end 223 | if track['audio-channels'] then h(track['audio-channels'] .. ' ch') end 224 | if track['demux-samplerate'] then h(string.format('%.5g kHz', track['demux-samplerate'] / 1000)) end 225 | if track['demux-bitrate'] then h(string.format('%.5g kbps', track['demux-bitrate'] / 1000)) end 226 | if #hints > 0 then title = string.format('%s [%s]', title, table.concat(hints, ', ')) end 227 | 228 | -- put some important info at the end 229 | if track.forced then title = title .. ' (forced)' end 230 | if track.external then title = title .. ' (external)' end 231 | if track.default then title = title .. ' (*)' end 232 | 233 | -- prepend a 1-letter type prefix, used when displaying multiple track types 234 | if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end 235 | return title 236 | end 237 | 238 | -- build track menu items from track list for given type 239 | local function build_track_items(list, type, prop, prefix) 240 | local items = {} 241 | 242 | -- filename without extension, escaped for pattern matching 243 | local filename = get('filename/no-ext', ''):gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") 244 | local pos = tonumber(get(prop)) or -1 245 | 246 | for _, track in ipairs(list) do 247 | if track.type == type then 248 | local state = {} 249 | if track.selected and track.id == pos then 250 | state[#state + 1] = 'checked' 251 | if type == 'sub' then 252 | if (prop == 'sid' and not get('sub-visibility')) or 253 | (prop == 'secondary-sid' and not get('secondary-sub-visibility')) 254 | then 255 | state[#state + 1] = 'disabled' 256 | end 257 | end 258 | end 259 | 260 | items[#items + 1] = { 261 | title = build_track_title(track, prefix, filename), 262 | shortcut = (track.lang and track.lang ~= '') and track.lang or nil, 263 | cmd = string.format('set %s %d', prop, track.id), 264 | state = state, 265 | } 266 | end 267 | end 268 | 269 | -- add an extra item to disable or re-enable the track 270 | if #items > 0 then 271 | local title = pos > 0 and 'Off' or 'Auto' 272 | local value = pos > 0 and 'no' or 'auto' 273 | if prefix then title = string.format('%s: %s', type:sub(1, 1):upper(), title) end 274 | 275 | items[#items + 1] = { 276 | title = title, 277 | cmd = string.format('set %s %s', prop, value), 278 | } 279 | end 280 | 281 | return items 282 | end 283 | 284 | -- update menu item to a submenu 285 | local function to_submenu(item) 286 | item.type = 'submenu' 287 | item.submenu = {} 288 | item.cmd = nil 289 | 290 | menu_items_dirty = true 291 | 292 | return item.submenu 293 | end 294 | 295 | -- handle #@tracks menu update 296 | local function update_tracks_menu(menu) 297 | local submenu = to_submenu(menu.item) 298 | local track_list = get('track-list', {}) 299 | if #track_list == 0 then return end 300 | 301 | local items_v = build_track_items(track_list, 'video', 'vid', true) 302 | local items_a = build_track_items(track_list, 'audio', 'aid', true) 303 | local items_s = build_track_items(track_list, 'sub', 'sid', true) 304 | 305 | -- append video/audio/sub tracks into one submenu, separated by a separator 306 | for _, item in ipairs(items_v) do append_menu(submenu, item) end 307 | if #submenu > 0 and #items_a > 0 then append_menu(submenu, { type = 'separator' }) end 308 | for _, item in ipairs(items_a) do append_menu(submenu, item) end 309 | if #submenu > 0 and #items_s > 0 then append_menu(submenu, { type = 'separator' }) end 310 | for _, item in ipairs(items_s) do append_menu(submenu, item) end 311 | end 312 | 313 | -- handle #@tracks/ menu update for given type 314 | local function update_track_menu(menu, type, prop) 315 | local submenu = to_submenu(menu.item) 316 | local track_list = get('track-list', {}) 317 | if #track_list == 0 then return end 318 | 319 | local items = build_track_items(track_list, type, prop, false) 320 | for _, item in ipairs(items) do append_menu(submenu, item) end 321 | end 322 | 323 | -- handle #@chapters menu update 324 | local function update_chapters_menu(menu) 325 | local submenu = to_submenu(menu.item) 326 | local chapter_list = get('chapter-list', {}) 327 | if #chapter_list == 0 then return end 328 | 329 | local pos = get('chapter', -1) 330 | for id, chapter in ipairs(chapter_list) do 331 | local title = abbr_title(chapter.title) 332 | if title == '' then title = 'Chapter ' .. id end 333 | 334 | append_menu(submenu, { 335 | title = title, 336 | shortcut = string.format('[%02d:%02d:%02d]', chapter.time / 3600, chapter.time / 60 % 60, chapter.time % 60), 337 | cmd = string.format('seek %f absolute', chapter.time), 338 | state = id == pos + 1 and { 'checked' } or {}, 339 | }) 340 | end 341 | end 342 | 343 | -- handle #@edition menu update 344 | local function update_editions_menu(menu) 345 | local submenu = to_submenu(menu.item) 346 | local edition_list = get('edition-list', {}) 347 | if #edition_list == 0 then return end 348 | 349 | local current = get('current-edition', -1) 350 | for id, edition in ipairs(edition_list) do 351 | local title = abbr_title(edition.title) 352 | if title == '' then title = 'Edition ' .. id end 353 | if edition.default then title = title .. ' [default]' end 354 | append_menu(submenu, { 355 | title = title, 356 | cmd = string.format('set edition %d', id - 1), 357 | state = id == current + 1 and { 'checked' } or {}, 358 | }) 359 | end 360 | end 361 | 362 | -- handle #@audio-devices menu update 363 | local function update_audio_devices_menu(menu) 364 | local submenu = to_submenu(menu.item) 365 | local device_list = get('audio-device-list', {}) 366 | if #device_list == 0 then return end 367 | 368 | local current = get('audio-device', '') 369 | for _, device in ipairs(device_list) do 370 | append_menu(submenu, { 371 | title = device.description or device.name, 372 | cmd = string.format('set audio-device %s', device.name), 373 | state = device.name == current and { 'checked' } or {}, 374 | }) 375 | end 376 | end 377 | 378 | -- build playlist item title 379 | local function build_playlist_title(item, id) 380 | local title = item.title or '' 381 | local ext = '' 382 | if item.filename and item.filename ~= '' then 383 | local _, filename = utils.split_path(item.filename) 384 | local n, e = filename:match('^(.+)%.([%w-_]+)$') 385 | if title == '' then title = n and n or filename end 386 | if e then ext = e end 387 | end 388 | title = title ~= '' and abbr_title(title) or 'Item ' .. id 389 | return title, ext 390 | end 391 | 392 | -- handle #@playlist menu update 393 | local function update_playlist_menu(menu) 394 | local submenu = to_submenu(menu.item) 395 | local playlist = get('playlist', {}) 396 | if #playlist == 0 then return end 397 | 398 | local from, to = 1, #playlist 399 | if o.max_playlist_items > 0 then 400 | local pos = get('playlist-playing-pos', -1) 401 | if pos == -1 then pos = get('playlist-pos', -1) end 402 | local mid = math.floor(o.max_playlist_items / 2) 403 | from, to = pos + 1 - mid, pos + (o.max_playlist_items - mid) 404 | if from < 1 then from, to = 1, o.max_playlist_items end 405 | if to > #playlist then from, to = #playlist - o.max_playlist_items + 1, #playlist end 406 | end 407 | 408 | if from > 1 then 409 | append_menu(submenu, { 410 | title = '...', 411 | shortcut = string.format('[%d]', from - 1), 412 | cmd = has_uosc and 'script-message-to uosc playlist' or 'ignore', 413 | }) 414 | end 415 | 416 | for id = from, to do 417 | local item = playlist[id] 418 | if item then 419 | local title, ext = build_playlist_title(item, id - 1) 420 | append_menu(submenu, { 421 | title = build_playlist_title(item, id - 1), 422 | shortcut = (ext and ext ~= '') and ext:upper() or nil, 423 | cmd = string.format('playlist-play-index %d', id - 1), 424 | state = (item.playing or item.current) and { 'checked' } or {}, 425 | }) 426 | end 427 | end 428 | 429 | if to < #playlist then 430 | append_menu(submenu, { 431 | title = '...', 432 | shortcut = string.format('[%d]', #playlist - to), 433 | cmd = has_uosc and 'script-message-to uosc playlist' or 'ignore', 434 | }) 435 | end 436 | end 437 | 438 | -- handle #@profiles menu update 439 | local function update_profiles_menu(menu) 440 | local submenu = to_submenu(menu.item) 441 | local profile_list = get('profile-list', {}) 442 | if #profile_list == 0 then return end 443 | 444 | for _, profile in ipairs(profile_list) do 445 | if not (profile.name == 'default' or profile.name:find('gui') or 446 | profile.name == 'encoding' or profile.name == 'libmpv') then 447 | append_menu(submenu, { 448 | title = profile.name, 449 | cmd = string.format('show-text %s; apply-profile %s', profile.name, profile.name), 450 | }) 451 | end 452 | end 453 | end 454 | 455 | -- handle menu state update 456 | local function update_menu_state(menu) 457 | if not menu.state then return end 458 | local status, res = pcall(menu.state) 459 | if not status then 460 | msg.verbose("state expr error on evaluating: " .. res) 461 | return 462 | end 463 | 464 | local state = {} 465 | if type(res) == 'string' then 466 | for s in res:gmatch('[^,%s]+') do state[#state + 1] = s end 467 | end 468 | menu.item.state = state 469 | menu_items_dirty = true 470 | end 471 | 472 | -- dynamic menu updaters 473 | local dyn_updaters = { 474 | ['tracks'] = update_tracks_menu, 475 | ['tracks/video'] = function(menu) update_track_menu(menu, 'video', 'vid') end, 476 | ['tracks/audio'] = function(menu) update_track_menu(menu, 'audio', 'aid') end, 477 | ['tracks/sub'] = function(menu) update_track_menu(menu, 'sub', 'sid') end, 478 | ['tracks/sub-secondary'] = function(menu) update_track_menu(menu, 'sub', 'secondary-sid') end, 479 | ['chapters'] = update_chapters_menu, 480 | ['editions'] = update_editions_menu, 481 | ['audio-devices'] = update_audio_devices_menu, 482 | ['playlist'] = update_playlist_menu, 483 | ['profiles'] = update_profiles_menu, 484 | } 485 | 486 | -- handle dynamic menu update 487 | local function update_menu(menu) 488 | if menu.updater then 489 | msg.debug('update menu: ' .. menu.item.title) 490 | current_menu = menu 491 | menu.updater(menu) 492 | current_menu = nil 493 | end 494 | end 495 | 496 | -- load dynamic menu item 497 | local function dyn_menu_load(item, keyword) 498 | local menu = { 499 | item = item, 500 | updater = nil, 501 | state = nil, 502 | dirty = false, 503 | } 504 | dyn_menus[#dyn_menus + 1] = menu 505 | keyword_to_menu[keyword] = menu 506 | 507 | local expr = keyword:match('^state=(.-)%s*$') 508 | if expr then 509 | menu.updater = update_menu_state 510 | menu.state = compile_expr(string.format('[%s]:%s', item.title, keyword), expr) 511 | else 512 | keyword = keyword:match('^([%S]+).*$') 513 | menu.updater = dyn_updaters[keyword] 514 | end 515 | 516 | -- update menu immediately 517 | if menu.updater then update_menu(menu) end 518 | end 519 | 520 | -- find #@keyword for dynamic menu and handle updates 521 | -- 522 | -- cplugin will keep the trailing comments in the cmd field, so we can 523 | -- parse the keyword from it. 524 | -- 525 | -- example: ignore #menu: Chapters #@chapters # extra comment 526 | local function dyn_menu_check(items) 527 | if not items then return end 528 | for _, item in ipairs(items) do 529 | if item.type == 'submenu' then 530 | dyn_menu_check(item.submenu) 531 | else 532 | if item.type ~= 'separator' and item.cmd then 533 | local keyword = item.cmd:match('%s*#@(.-)%s*$') or '' 534 | if keyword ~= '' then 535 | msg.debug('load menu: ' .. item.title, ', keyword: ' .. keyword) 536 | dyn_menu_load(item, keyword) 537 | end 538 | end 539 | end 540 | end 541 | end 542 | 543 | -- load dynamic menus 544 | local function load_dyn_menus() 545 | dyn_menu_check(menu_items) 546 | 547 | -- broadcast menu ready message 548 | mp.commandv('script-message', 'menu-ready', mp.get_script_name()) 549 | end 550 | 551 | -- read input.conf content 552 | local function get_input_conf() 553 | local prop = mp.get_property_native('input-conf') 554 | if prop:sub(1, 9) == 'memory://' then return prop:sub(10) end 555 | 556 | prop = prop == '' and '~~/input.conf' or prop 557 | local conf_path = mp.command_native({ 'expand-path', prop }) 558 | 559 | local f, err = io.open(conf_path, 'rb') 560 | if not f then 561 | msg.error('failed to open file: ' .. conf_path) 562 | return nil 563 | end 564 | 565 | local conf = f:read('*all') 566 | f:close() 567 | return conf 568 | end 569 | 570 | -- parse input.conf, return menu items 571 | local function parse_input_conf(conf) 572 | local function parse_line(line) 573 | local c = line:match('^%s*#') 574 | if c and (not o.uosc_syntax) then return end 575 | local key, cmd = line:match('%s*([%S]+)%s+(.-)%s*$') 576 | if key and key:match('^#%S+') then return end 577 | return ((o.uosc_syntax and c) and '' or key), cmd 578 | end 579 | 580 | local function extract_title(cmd) 581 | if not cmd or cmd == '' then return '' end 582 | local title = cmd:match('#menu:%s*(.*)%s*') 583 | if not title and o.uosc_syntax then title = cmd:match('#!%s*(.*)%s*') end 584 | if title then title = title:match('(.-)%s*#.*$') or title end 585 | return title or '' 586 | end 587 | 588 | local function split_title(title) 589 | local list = {} 590 | if not title or title == '' then return list end 591 | 592 | local pattern = '(.-)%s*>%s*' 593 | local last_ends = 1 594 | local starts, ends, match = title:find(pattern) 595 | while starts do 596 | list[#list + 1] = match 597 | last_ends = ends + 1 598 | starts, ends, match = title:find(pattern, last_ends) 599 | end 600 | if last_ends < (#title + 1) then list[#list + 1] = title:sub(last_ends) end 601 | 602 | return list 603 | end 604 | 605 | local items = {} 606 | local by_id = {} 607 | 608 | for line in conf:gmatch('[^\r\n]+') do 609 | local key, cmd = parse_line(line) 610 | local list = split_title(extract_title(cmd)) 611 | 612 | local submenu_id = '' 613 | local target_menu = items 614 | 615 | for id, name in ipairs(list) do 616 | if id < #list then 617 | submenu_id = submenu_id .. name 618 | if not by_id[submenu_id] then 619 | local submenu = {} 620 | by_id[submenu_id] = submenu 621 | append_menu(target_menu, { type = 'submenu', title = name, submenu = submenu }) 622 | end 623 | target_menu = by_id[submenu_id] 624 | else 625 | if name == '-' or (o.uosc_syntax and name:sub(1, 3) == '---') then 626 | append_menu(target_menu, { type = 'separator' }) 627 | else 628 | local shortcut = (key ~= '' and key ~= '_') and key or nil 629 | append_menu(target_menu, { title = name, shortcut = shortcut, cmd = cmd }) 630 | end 631 | end 632 | end 633 | end 634 | 635 | return items 636 | end 637 | 638 | -- script message: get 639 | mp.register_script_message('get', function(keyword, src) 640 | if not src or src == '' then 641 | msg.debug('get: ignored message with empty src') 642 | return 643 | end 644 | 645 | local menu = keyword_to_menu[keyword] 646 | local reply = { keyword = keyword } 647 | if menu then reply.item = menu.item else reply.error = 'keyword not found' end 648 | mp.commandv('script-message-to', src, 'menu-get-reply', utils.format_json(reply)) 649 | end) 650 | 651 | -- script message: update 652 | mp.register_script_message('update', function(keyword, json) 653 | local menu = keyword_to_menu[keyword] 654 | if not menu then 655 | msg.debug('update: ignored message with invalid keyword:', keyword) 656 | return 657 | end 658 | 659 | local data, err = utils.parse_json(json) 660 | if err then msg.error('update: failed to parse json:', err) end 661 | if not data or next(data) == nil then 662 | msg.debug('update: ignored message with invalid json:', json) 663 | return 664 | end 665 | 666 | local item = menu.item 667 | if not data.title or data.title == '' then data.title = item.title end 668 | if not data.type or data.type == '' then data.type = item.type end 669 | 670 | for k, _ in pairs(item) do item[k] = nil end 671 | for k, v in pairs(data) do item[k] = v end 672 | 673 | menu_items_dirty = true 674 | end) 675 | 676 | -- detect uosc installation 677 | mp.register_script_message('uosc-version', function() has_uosc = true end) 678 | 679 | -- update menu on idle, this reduces the update frequency 680 | mp.register_idle(function() 681 | if have_dirty_menus then 682 | for _, menu in ipairs(dyn_menus) do 683 | if menu.dirty then 684 | update_menu(menu) 685 | menu.dirty = false 686 | end 687 | end 688 | have_dirty_menus = false 689 | end 690 | 691 | if menu_items_dirty then 692 | msg.debug('commit menu items: ' .. menu_prop) 693 | mp.set_property_native(menu_prop, menu_items) 694 | menu_items_dirty = false 695 | end 696 | end) 697 | 698 | -- menu implementation related initialization 699 | if use_mpv_impl then 700 | -- IMPORTANT: make menu work on vo change 701 | mp.observe_property('current-vo', 'native', function(name, val) 702 | if val then menu_items_dirty = true end 703 | end) 704 | 705 | mp.add_key_binding('MBTN_RIGHT', nil, function() 706 | mp.commandv('context-menu') 707 | end) 708 | else 709 | local menu_native = 'menu' 710 | 711 | mp.register_script_message('menu-init', function(name) 712 | menu_native = name 713 | end) 714 | 715 | mp.add_key_binding('MBTN_RIGHT', 'show', function() 716 | mp.commandv('script-message-to', menu_native, 'show') 717 | end) 718 | end 719 | 720 | -- load menu data from input.conf 721 | -- 722 | -- NOTE: to simplify the code, we don't watch for the menu data change event, this 723 | -- make it conflict with other scripts that also update the menu data property. 724 | local conf = get_input_conf() 725 | if conf then 726 | menu_items = parse_input_conf(conf) 727 | menu_items_dirty = true 728 | load_dyn_menus() 729 | end 730 | -------------------------------------------------------------------------------- /portable_config/scripts/recentmenu.lua: -------------------------------------------------------------------------------- 1 | local utils = require("mp.utils") 2 | local options = require("mp.options") 3 | local input_available, input = pcall(require, "mp.input") 4 | 5 | local o = { 6 | enabled = true, 7 | path = "~~/recent.json", 8 | length = 10, 9 | width = 88, 10 | ignore_same_series = true, 11 | reduce_io = false, 12 | } 13 | options.read_options(o, _, function() end) 14 | 15 | local path = mp.command_native({ "expand-path", o.path }) 16 | 17 | local uosc_available = false 18 | local command_palette_available = false 19 | 20 | local menu = { 21 | type = 'recent_menu', 22 | title = 'Recently played', 23 | items = {}, 24 | } 25 | 26 | local dyn_menu = { 27 | ready = false, 28 | script_name = 'dyn_menu', 29 | type = 'submenu', 30 | submenu = {} 31 | } 32 | 33 | local current_item = { nil, nil, nil } 34 | 35 | local locale = {} 36 | function t(text) return locale[text] or text end 37 | 38 | function utf8_char_bytes(str, i) 39 | local char_byte = str:byte(i) 40 | if char_byte < 0xC0 then 41 | return 1 42 | elseif char_byte < 0xE0 then 43 | return 2 44 | elseif char_byte < 0xF0 then 45 | return 3 46 | elseif char_byte < 0xF8 then 47 | return 4 48 | else 49 | return 1 50 | end 51 | end 52 | 53 | function utf8_iter(str) 54 | local byte_start = 1 55 | return function() 56 | local start = byte_start 57 | if #str < start then return nil end 58 | local byte_count = utf8_char_bytes(str, start) 59 | byte_start = start + byte_count 60 | return start, str:sub(start, start + byte_count - 1) 61 | end 62 | end 63 | 64 | function utf8_to_table(str) 65 | local t = {} 66 | for _, ch in utf8_iter(str) do 67 | t[#t + 1] = ch 68 | end 69 | return t 70 | end 71 | 72 | function utf8_substring(str, indexStart, indexEnd) 73 | if indexStart > indexEnd then 74 | return str 75 | end 76 | 77 | local index = 1 78 | local substr = "" 79 | for _, char in utf8_iter(str) do 80 | if indexStart <= index and index <= indexEnd then 81 | local width = #char > 2 and 2 or 1 82 | index = index + width 83 | substr = substr .. char 84 | end 85 | end 86 | return substr, index 87 | end 88 | 89 | function jaro(s1, s2) 90 | local match_window = math.floor(math.max(#s1, #s2) / 2.0) - 1 91 | local matches1 = {} 92 | local matches2 = {} 93 | 94 | local m = 0; 95 | local t = 0; 96 | 97 | for i = 0, #s1, 1 do 98 | local start = math.max(0, i - match_window) 99 | local final = math.min(i + match_window + 1, #s2) 100 | 101 | for k = start, final, 1 do 102 | if not (matches2[k] or s1[i] ~= s2[k]) then 103 | matches1[i] = true 104 | matches2[k] = true 105 | m = m + 1 106 | break 107 | end 108 | end 109 | end 110 | 111 | if m == 0 then 112 | return 0.0 113 | end 114 | 115 | local k = 0 116 | for i = 0, #s1, 1 do 117 | if matches1[i] then 118 | while not matches2[k] do 119 | k = k + 1 120 | end 121 | 122 | if s1[i] ~= s2[k] then 123 | t = t + 1 124 | end 125 | 126 | k = k + 1 127 | end 128 | end 129 | 130 | t = t / 2.0 131 | 132 | return (m / #s1 + m / #s2 + (m - t) / m) / 3.0 133 | end 134 | 135 | function jaro_winkler_distance(s1, s2) 136 | if #s1 + #s2 == 0 then 137 | return 0.0 138 | end 139 | 140 | if s1 == s2 then 141 | return 1.0 142 | end 143 | 144 | s1 = utf8_to_table(s1) 145 | s2 = utf8_to_table(s2) 146 | 147 | local d = jaro(s1, s2) 148 | local p = 0.1 149 | local l = 0; 150 | while (s1[l] == s2[l] and l < 4) do 151 | l = l + 1 152 | end 153 | 154 | return d + l * p * (1 - d) 155 | end 156 | 157 | function split_path(path) 158 | -- return path, filename, extension 159 | return path:match("(.-)([^\\/]-)%.?([^%.\\/]*)$") 160 | end 161 | 162 | function is_protocol(path) 163 | return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil) 164 | end 165 | 166 | function is_same_series(path1, path2) 167 | if not o.ignore_same_series then 168 | return false 169 | end 170 | 171 | local dir1, filename1, extension1 = split_path(path1) 172 | local dir2, filename2, extension2 = split_path(path2) 173 | 174 | -- don't remove files are not in same folder 175 | if dir1 ~= dir2 then 176 | return false 177 | end 178 | 179 | -- don't remove same filename but different extensions 180 | if filename1 == filename2 then 181 | return false 182 | end 183 | 184 | -- by episode 185 | local episode1 = filename1:gsub("^[%[%(]+.-[%]%)]+[%s%[]*", ""):match("(.-%D+)0*%d+") 186 | local episode2 = filename2:gsub("^[%[%(]+.-[%]%)]+[%s%[]*", ""):match("(.-%D+)0*%d+") 187 | if episode1 and episode2 and episode1 == episode2 then 188 | return true 189 | end 190 | 191 | -- by similarity 192 | local threshold = 0.8 193 | local similarity = jaro_winkler_distance(filename1, filename2) 194 | if similarity > threshold then 195 | return true 196 | end 197 | 198 | return false 199 | end 200 | 201 | function remove_deleted() 202 | local new_items = {} 203 | for _, item in ipairs(menu.items) do 204 | local path = item.value[2] 205 | local deleted = false 206 | 207 | if not is_protocol(path) then 208 | local meta, meta_error = utils.file_info(path) 209 | if not (meta and meta.is_file) then 210 | deleted = true 211 | end 212 | end 213 | 214 | if not deleted then 215 | new_items[#new_items + 1] = item 216 | end 217 | end 218 | 219 | if #menu.items ~= #new_items then 220 | menu.items = new_items 221 | write_json() 222 | end 223 | end 224 | 225 | function read_json(force) 226 | if o.reduce_io and not force then 227 | return 228 | end 229 | local meta, meta_error = utils.file_info(path) 230 | if not meta or not meta.is_file then 231 | menu.items = {} 232 | return 233 | end 234 | 235 | local json_file = io.open(path, "r") 236 | if not json_file then 237 | menu.items = {} 238 | return 239 | end 240 | 241 | local json = json_file:read("*all") 242 | json_file:close() 243 | 244 | menu.items = utils.parse_json(json) or {} 245 | remove_deleted() 246 | end 247 | 248 | function write_json(force) 249 | if o.reduce_io and not force then 250 | return 251 | end 252 | local json_file = io.open(path, "w") 253 | if not json_file then return end 254 | 255 | local json = utils.format_json(menu.items) 256 | 257 | json_file:write(json) 258 | json_file:close() 259 | 260 | if dyn_menu.ready then 261 | update_dyn_menu_items() 262 | end 263 | end 264 | 265 | function append_item(path, filename, title) 266 | local new_items = { { title = filename, hint = title, value = { "loadfile", path } } } 267 | read_json() 268 | for index, value in ipairs(menu.items) do 269 | local opath = value.value[2] 270 | if #new_items < o.length and 271 | path ~= opath and 272 | not is_same_series(path, opath) 273 | then 274 | new_items[#new_items + 1] = value 275 | end 276 | end 277 | menu.items = new_items 278 | write_json() 279 | end 280 | 281 | function open_menu_uosc() 282 | local json = utils.format_json(menu) 283 | mp.commandv('script-message-to', 'uosc', 'open-menu', json) 284 | end 285 | 286 | function open_menu_command_palette() 287 | local json = utils.format_json(menu) 288 | mp.commandv('script-message-to', 289 | 'command_palette', 290 | 'show-command-palette-json', json) 291 | end 292 | 293 | function open_menu_select() 294 | local item_titles, item_values = {}, {} 295 | for i, v in ipairs(menu.items) do 296 | item_titles[i] = v.title 297 | item_values[i] = v.value 298 | end 299 | mp.commandv('script-message-to', 'console', 'disable') 300 | input.select({ 301 | prompt = menu.title .. ':', 302 | items = item_titles, 303 | default_item = 1, 304 | submit = function(id) 305 | mp.commandv(unpack(item_values[id])) 306 | end, 307 | }) 308 | end 309 | 310 | function open_menu() 311 | read_json() 312 | if uosc_available then 313 | open_menu_uosc() 314 | elseif command_palette_available then 315 | open_menu_command_palette() 316 | elseif input_available then 317 | open_menu_select() 318 | return 319 | else 320 | mp.msg.warn("No menu providers available") 321 | end 322 | end 323 | 324 | function get_dyn_menu_title(title, hint, path) 325 | if is_protocol(path) then 326 | local protocol = path:match("^(%a[%w.+-]-)://") 327 | hint = protocol 328 | else 329 | local dir, filename, extension = split_path(path) 330 | title = filename 331 | hint = extension 332 | end 333 | local title_clip = utf8_substring(title, 1, o.width) 334 | if title ~= title_clip then 335 | title = utf8_substring(title_clip, 1, o.width - 2) .. "..." 336 | end 337 | return string.format('%s\t%s', title, hint:upper()) 338 | end 339 | 340 | function update_dyn_menu_items() 341 | if #menu.items == 0 then 342 | read_json() 343 | end 344 | local submenu = {} 345 | local menu_items = menu.items 346 | for _, item in ipairs(menu_items) do 347 | submenu[#submenu + 1] = { 348 | title = get_dyn_menu_title(item.title, item.hint, item.value[2]), 349 | cmd = string.format("%s \"%s\"", item.value[1], item.value[2]:gsub("\\", "\\\\")), 350 | } 351 | end 352 | dyn_menu.submenu = submenu 353 | mp.commandv('script-message-to', dyn_menu.script_name, 'update', 'recent', utils.format_json(dyn_menu)) 354 | end 355 | 356 | function play_last() 357 | read_json() 358 | if menu.items[1] then 359 | mp.command_native(menu.items[1].value) 360 | end 361 | end 362 | 363 | function on_load() 364 | current_item = { nil, nil, nil } 365 | if not o.enabled then return end 366 | local path = mp.get_property("path") 367 | if not path then return end 368 | if path:match("bd://") or path:match("dvd://") or path:match("dvb://") or path:match("cdda://") then return end 369 | local filename = mp.get_property("filename") 370 | local dir, filename_without_ext, ext = split_path(filename) 371 | local title = mp.get_property("media-title") or path 372 | if filename == title or filename_without_ext == title then 373 | title = "" 374 | end 375 | if is_protocol(path) and title and title ~= "" then 376 | filename, title = title, filename 377 | end 378 | if title and title ~= "" then 379 | local width 380 | filename, width = utf8_substring(filename, 1, o.width * 0.618) 381 | title = utf8_substring(title, 1, o.width - width) 382 | else 383 | filename = utf8_substring(filename, 1, o.width) 384 | end 385 | current_item = { path, filename, title } 386 | append_item(unpack(current_item)) 387 | end 388 | 389 | function on_end(e) 390 | if not (e and e.reason and e.reason == "quit") then 391 | return 392 | end 393 | if not current_item[1] then 394 | return 395 | end 396 | append_item(unpack(current_item)) 397 | end 398 | 399 | mp.add_key_binding(nil, "open", open_menu) 400 | mp.add_key_binding(nil, "last", play_last) 401 | mp.register_event("file-loaded", on_load) 402 | mp.register_event("end-file", on_end) 403 | 404 | mp.register_script_message('open-recent-menu', function(provider) 405 | if provider == nil then 406 | open_menu() 407 | elseif provider == "uosc" then 408 | open_menu_uosc() 409 | elseif provider == "command-palette" then 410 | open_menu_command_palette() 411 | elseif provider == "select" then 412 | open_menu_select() 413 | else 414 | mp.msg.warn(provider .. " not available") 415 | end 416 | end) 417 | 418 | mp.register_script_message('uosc-version', function() 419 | uosc_available = true 420 | mp.commandv('script-message-to', 'uosc', 'get-locale', mp.get_script_name()) 421 | end) 422 | mp.register_script_message('uosc-locale', function(json) 423 | locale = utils.parse_json(json) 424 | menu.title = t(menu.title) 425 | end) 426 | 427 | mp.register_script_message('command-palette-version', function() 428 | command_palette_available = true 429 | end) 430 | 431 | mp.register_script_message('menu-ready', function(script_name) 432 | dyn_menu.ready = true 433 | dyn_menu.script_name = script_name 434 | update_dyn_menu_items() 435 | end) 436 | 437 | if o.reduce_io then 438 | read_json(true) 439 | mp.register_event("shutdown", function (e) 440 | write_json(true) 441 | end) 442 | end 443 | -------------------------------------------------------------------------------- /screenshot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stax76/mpv-hero/f612b6e9d46adf823ad938547f129405e6744c88/screenshot.webp -------------------------------------------------------------------------------- /updater.bat: -------------------------------------------------------------------------------- 1 | @echo OFF 2 | :: This batch file exists to run updater.ps1 without hassle 3 | pushd %~dp0 4 | if exist "%~dp0\installer\updater.ps1" ( 5 | set updater_script="%~dp0\installer\updater.ps1" 6 | ) else ( 7 | set updater_script="%~dp0\updater.ps1" 8 | ) 9 | 10 | :: Check if pwsh is in the system's PATH 11 | where pwsh >nul 2>nul 12 | if %errorlevel% equ 0 ( 13 | :: pwsh is in PATH, so run the script using Windows Powershell 14 | pwsh -NoProfile -NoLogo -ExecutionPolicy Bypass -File %updater_script% 15 | ) else ( 16 | :: pwsh is not in PATH, run the script using PowerShell Core 17 | powershell -NoProfile -NoLogo -ExecutionPolicy Bypass -File %updater_script% 18 | ) 19 | 20 | :: After update, updater.ps1 should not in same folder as mpv.exe 21 | if exist "%~dp0\installer\updater.ps1" if exist "%~dp0\updater.ps1" ( 22 | del "%~dp0\updater.ps1" 23 | ) 24 | timeout 5 25 | --------------------------------------------------------------------------------