├── README.md └── bookmarker-menu.lua /README.md: -------------------------------------------------------------------------------- 1 | # Bookmarker Menu for mpv v1.3.1 2 | 3 | A bookmarker menu to manage all your bookmarks in MPV. This script is based on [mpv-bookmarker](https://github.com/nimatrueway/mpv-bookmark-lua-script) and has been rewritten to include a bookmarker menu. All of the code has been written from scratch, aside from the general file/JSON management utilities. 4 | 5 | **Notice:** Bookmarks created with [mpv-bookmarker](https://github.com/nimatrueway/mpv-bookmark-lua-script) can be loaded by this script and will be automatically converted. However, the bookmarks created with this script are not compatible with those created with [mpv-bookmarker](https://github.com/nimatrueway/mpv-bookmark-lua-script). 6 | 7 | ## New in version 1.3.1 8 | 9 | * `DEL` button now deletes the character after the cursor instead of clearing the whole string 10 | * Styling added for certain lines 11 | * Currently selected line is now bold and yellow 12 | * Typer cursor changed from `;` to a bold and yellow `|` 13 | 14 | ## Planned features 15 | 16 | * Allow custom tags for bookmark styling 17 | * Undo function 18 | * Redo function 19 | 20 | ## Installation 21 | 22 | Copy `bookmarker-menu.lua` to the scripts folder for mpv then add the following lines to `input.conf`: 23 | 24 | ``` 25 | B script_message bookmarker-menu 26 | b script_message bookmarker-quick-save 27 | ctrl+b script_message bookmarker-quick-load 28 | ``` 29 | 30 | The keys are only a suggestion, and can be changed to something else. 31 | 32 | Open `bookmarker-menu.lua` in a text editor, and you can easily change these settings: 33 | 34 | ```lua 35 | -- Maximum number of characters for bookmark name 36 | local maxChar = 100 37 | -- Number of bookmarks to be displayed per page 38 | local bookmarksPerPage = 10 39 | -- Whether to close the Bookmarker menu after loading a bookmark 40 | local closeAfterLoad = true 41 | -- Whether to close the Bookmarker menu after replacing a bookmark 42 | local closeAfterReplace = true 43 | -- Whether to ask for confirmation to replace a bookmark (Uses the Typer for confirmation) 44 | local confirmReplace = false 45 | -- Whether to ask for confirmation to delete a bookmark (Uses the Typer for confirmation) 46 | local confirmDelete = false 47 | -- The rate (in seconds) at which the bookmarker needs to refresh its interface; lower is more frequent 48 | local rate = 1.5 49 | -- The filename for the bookmarks file 50 | local bookmarkerName = "bookmarker.json" 51 | ``` 52 | 53 | It's recommended not to touch `bookmarkerName` but it's there to be changed in case you already have a file called `bookmarker.json` and don't want that to be overwritten, or to change it to `bookmarks.json` to convert bookmarks created by [mpv-bookmarker](https://github.com/nimatrueway/mpv-bookmark-lua-script). 54 | 55 | ## Usage 56 | 57 | #### When the Bookmarker menu is closed 58 | 59 | * *`B` or whichever key you configured in `input.conf`*: Pull up the Bookmarker menu 60 | * *`b` or whichever key you configured in `input.conf`*: Quickly add a new bookmark 61 | * *`ctrl+b` or whichever key you configured in `input.conf`*: Quickly load the latest bookmark 62 | 63 | #### When the Bookmarker menu is open 64 | 65 | * *`B` or whichever key you configured in `input.conf`*: Close the Bookmarker menu 66 | * `ESC`: Close the Bookmarker menu 67 | * `UP/DOWN`: Navigate through the bookmarks on the current page (Hold to quickly scroll) 68 | * `LEFT/RIGHT`: Navigate through pages of bookmarks (Hold to quickly scroll) 69 | * `ENTER`: Load the currently selected bookmark 70 | * `DELETE`: Delete the currently selected bookmark 71 | * `s`: Save a bookmark of the current media file and position 72 | * `shift+s`: Save a bookmark of the current media file and position (shows a text input, allowing you to type) 73 | * `p`: Replace the currently selected bookmark with the current media file and position 74 | * `r`: Rename the currently selected bookmark (shows a text input, allowing you to type) 75 | * `f`: Change the filepath of the currently selected bookmark (shows a text input, allowing you to type) 76 | * `m`: Move the currently selected bookmark 77 | 78 | Replacing a bookmark is intended for when you have a bookmark for your current progress in a TV series. When you've finished a new episode, you can select this bookmark and press `p` to instantly rewrite that bookmark with your current progress, leaving the name and its position in the list of bookmarks intact. 79 | Changing the filepath of a bookmark is intended to quickly change a bookmark in case you moved the media file to a different folder, or perhaps the drive letter of your external drive changed. 80 | 81 | #### When allowing text input 82 | 83 | The Typer (as I named it) allows you to type text for various ends, like renaming a bookmark or changing its filepath. 84 | 85 | * `ESC`: Cancel text input and return to the Bookmarker menu 86 | * `ENTER`: Confirm text input and save/rename the bookmark 87 | * `LEFT/RIGHT`: Move the cursor through the text, allowing you to input text in different places (Hold to quickly scroll) 88 | * `BACKSPACE`: Remove the character preceding the cursor (Hold to rapidly remove multiple) 89 | * `DELETE`: Remove the character after the cursor (Hold to rapidly remove multiple) 90 | * `Any text character`: Type for the text input. Allows special characters, spaces, numbers. Does not allow letters with accents (Hold to rapidly add characters) 91 | 92 | During text input for a bookmark's name, you can write `%t` or `%p` to input a timestamp in the name. (Note: This does not work for a bookmark's filepath.) 93 | 94 | * `%t` is a timestamp in the format of hh:mm:ss.mmm 95 | * `%p` is a timestamp in the format of S.mmm 96 | 97 | For example, `Awesome moment @ %t` will show up as `Awesome moment @ 00:13:41.673` in the menu 98 | 99 | #### When moving bookmarks 100 | 101 | * `ESC`: Cancel moving and return to the Bookmarker menu 102 | * `ENTER`: Confirm moving the bookmark 103 | * `m`: Confirm moving the bookmark 104 | * `s`: Save a bookmark of the current file and position 105 | * `UP/DOWN`: Navigate through the bookmarks on the current page (Hold to quickly scroll) 106 | * `LEFT/RIGHT`: Navigate through pages of bookmarks (Hold to quickly scroll) 107 | 108 | ## Testing 109 | 110 | This has been tested on Windows. In theory, it should also work for Unix systems, but it hasn't been tested on those. 111 | 112 | ## Changelog 113 | 114 | #### Version 1.3.1 115 | 116 | * `DEL` button now deletes the character after the cursor instead of clearing the whole string 117 | * Styling added for certain lines 118 | * Currently selected line is now bold and yellow 119 | * Typer cursor changed from `;` to a bold and yellow `|` 120 | 121 | #### Version 1.3.0 122 | 123 | * Added the ability to replace bookmarks with the currently playing file and position (See Usage for more info) 124 | * Bookmarks from [mpv-bookmarker](https://github.com/nimatrueway/mpv-bookmark-lua-script) can now be loaded and will be automatically converted 125 | * Certain keys are now repeated while holding the button, eliminating the need to constantly tap the same button 126 | * Added the option to automatically close the menu after replacing a bookmark 127 | * Added the option whether to ask for confirmation before replacing/deleting a bookmark 128 | * Added more error messages 129 | * Slightly changed up how forced controls are programmed and activated to accomodate the repeated keys 130 | * Slightly changed up how bookmarks are created in the code to accomodate replacing them 131 | 132 | #### Version 1.2.0 133 | 134 | * Added a cursor to the Typer, allowing you to insert text in other places than the end of the line 135 | * Added the ability to change the filepath of a bookmark, in case you moved files to a different folder, or your external drive is suddenly assigned a different drive letter 136 | * Changed the way filepaths are saved and loaded, to accomodate the ability to edit them 137 | * Introduced version numbers to the bookmarks for potential backward compatibility 138 | * Because of this, all older bookmarks should still be compatible with version 1.2.0 139 | * The default bookmark name now uses `media-title` instead of `filename` 140 | * Changed some of the messages and added a few more error messages 141 | 142 | #### Version 1.1.0 143 | 144 | * Added minor property expansion to the Typer 145 | * You can type `%t` or `%p` for the bookmark name and have it expand into the time or position respectively 146 | * Added checks for all faulty behavior that I could think of and added error messages for those situations 147 | * Cleaned up the code a bit 148 | 149 | #### Version 1.0.2 150 | 151 | * Added keypad keys to the Typer 152 | * It's possible to commit moving files with the keypad enter key as well 153 | 154 | #### Version 1.0.1 155 | 156 | * Added the option to close the bookmarker menu after loading a bookmark 157 | 158 | #### Version 1.0.0 159 | 160 | * Initial release -------------------------------------------------------------------------------- /bookmarker-menu.lua: -------------------------------------------------------------------------------- 1 | -- // Bookmarker Menu v1.3.1 for mpv \\ -- 2 | -- See README.md for instructions 3 | 4 | -- Maximum number of characters for bookmark name 5 | local maxChar = 100 6 | -- Number of bookmarks to be displayed per page 7 | local bookmarksPerPage = 10 8 | -- Whether to close the Bookmarker menu after loading a bookmark 9 | local closeAfterLoad = true 10 | -- Whether to close the Bookmarker menu after replacing a bookmark 11 | local closeAfterReplace = true 12 | -- Whether to ask for confirmation to replace a bookmark (Uses the Typer for confirmation) 13 | local confirmReplace = false 14 | -- Whether to ask for confirmation to delete a bookmark (Uses the Typer for confirmation) 15 | local confirmDelete = false 16 | -- The rate (in seconds) at which the bookmarker needs to refresh its interface; lower is more frequent 17 | local rate = 1.5 18 | -- The filename for the bookmarks file 19 | local bookmarkerName = "bookmarker.json" 20 | 21 | -- All the "global" variables and utilities; don't touch these 22 | local utils = require 'mp.utils' 23 | local styleOn = mp.get_property("osd-ass-cc/0") 24 | local styleOff = mp.get_property("osd-ass-cc/1") 25 | local bookmarks = {} 26 | local currentSlot = 0 27 | local currentPage = 1 28 | local maxPage = 1 29 | local active = false 30 | local mode = "none" 31 | local bookmarkStore = {} 32 | local oldSlot = 0 33 | 34 | -- // Controls \\ -- 35 | 36 | -- List of custom controls and their function 37 | local bookmarkerControls = { 38 | ESC = function() abort("") end, 39 | DOWN = function() jumpSlot(1) end, 40 | UP = function() jumpSlot(-1) end, 41 | RIGHT = function() jumpPage(1) end, 42 | LEFT = function() jumpPage(-1) end, 43 | s = function() addBookmark() end, 44 | S = function() mode="save" typerStart() end, 45 | p = function() mode="replace" typerStart() end, 46 | r = function() mode="rename" typerStart() end, 47 | f = function() mode="filepath" typerStart() end, 48 | m = function() mode="move" moverStart() end, 49 | DEL = function() mode="delete" typerStart() end, 50 | ENTER = function() jumpToBookmark(currentSlot) end, 51 | KP_ENTER = function() jumpToBookmark(currentSlot) end 52 | } 53 | 54 | local bookmarkerFlags = { 55 | DOWN = {repeatable = true}, 56 | UP = {repeatable = true}, 57 | RIGHT = {repeatable = true}, 58 | LEFT = {repeatable = true} 59 | } 60 | 61 | -- Activate the custom controls 62 | function activateControls(name, controls, flags) 63 | for key, func in pairs(controls) do 64 | mp.add_forced_key_binding(key, name..key, func, flags[key]) 65 | end 66 | end 67 | 68 | -- Deactivate the custom controls 69 | function deactivateControls(name, controls) 70 | for key, _ in pairs(controls) do 71 | mp.remove_key_binding(name..key) 72 | end 73 | end 74 | 75 | -- // Typer \\ -- 76 | 77 | -- Controls for the Typer 78 | local typerControls = { 79 | ESC = function() typerExit() end, 80 | ENTER = function() typerCommit() end, 81 | KP_ENTER = function() typerCommit() end, 82 | RIGHT = function() typerCursor(1) end, 83 | LEFT = function() typerCursor(-1) end, 84 | BS = function() typer("backspace") end, 85 | DEL = function() typer("delete") end, 86 | SPACE = function() typer(" ") end, 87 | SHARP = function() typer("#") end, 88 | KP0 = function() typer("0") end, 89 | KP1 = function() typer("1") end, 90 | KP2 = function() typer("2") end, 91 | KP3 = function() typer("3") end, 92 | KP4 = function() typer("4") end, 93 | KP5 = function() typer("5") end, 94 | KP6 = function() typer("6") end, 95 | KP7 = function() typer("7") end, 96 | KP8 = function() typer("8") end, 97 | KP9 = function() typer("9") end, 98 | KP_DEC = function() typer(".") end 99 | } 100 | 101 | -- All standard keys for the Typer 102 | local typerKeys = {"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","1","2","3","4","5","6","7","8","9","0","!","@","$","%","^","&","*","(",")","-","_","=","+","[","]","{","}","\\","|",";",":","'","\"",",",".","<",">","/","?","`","~"} 103 | -- For some reason, semicolon is not possible, but it's listed there just in case anyway 104 | 105 | local typerText = "" 106 | local typerPos = 0 107 | local typerActive = false 108 | 109 | -- Function to activate the Typer 110 | -- use typerStart() for custom controls around activating the Typer 111 | function activateTyper() 112 | for key, func in pairs(typerControls) do 113 | mp.add_forced_key_binding(key, "typer"..key, func, {repeatable=true}) 114 | end 115 | for i, key in ipairs(typerKeys) do 116 | mp.add_forced_key_binding(key, "typer"..key, function() typer(key) end, {repeatable=true}) 117 | end 118 | typerText = "" 119 | typerActive = true 120 | end 121 | 122 | -- Function to deactivate the Typer 123 | -- use typerExit() for custom controls around deactivating the Typer 124 | function deactivateTyper() 125 | for key, _ in pairs(typerControls) do 126 | mp.remove_key_binding("typer"..key) 127 | end 128 | for i, key in ipairs(typerKeys) do 129 | mp.remove_key_binding("typer"..key) 130 | end 131 | typerActive = false 132 | return typerText 133 | end 134 | 135 | -- Function to move the cursor of the typer; can wrap around 136 | function typerCursor(direction) 137 | typerPos = typerPos + direction 138 | if typerPos < 0 then typerPos = typerText:len() end 139 | if typerPos > typerText:len() then typerPos = 0 end 140 | typer("") 141 | end 142 | 143 | -- Function for handling the text as it is being typed 144 | function typer(s) 145 | -- Don't touch this part 146 | if s == "backspace" then 147 | if typerPos > 0 then 148 | typerText = typerText:sub(1, typerPos - 1) .. typerText:sub(typerPos + 1) 149 | typerPos = typerPos - 1 150 | end 151 | elseif s == "delete" then 152 | if typerPos < typerText:len() then 153 | typerText = typerText:sub(1, typerPos) .. typerText:sub(typerPos + 2) 154 | end 155 | else 156 | if mode == "filepath" or typerText:len() < maxChar then 157 | typerText = typerText:sub(1, typerPos) .. s .. typerText:sub(typerPos + 1) 158 | typerPos = typerPos + s:len() 159 | end 160 | end 161 | 162 | -- Enter custom script and display message here 163 | local preMessage = "Enter a bookmark name:" 164 | if mode == "save" then 165 | preMessage = styleOn.."{\\b1}Save a new bookmark with custom name:{\\b0}"..styleOff 166 | elseif mode == "replace" then 167 | preMessage = styleOn.."{\\b1}Type \"y\" to replace the following bookmark:{\\b0}\n"..displayName(bookmarks[currentSlot]["name"])..styleOff 168 | elseif mode == "delete" then 169 | preMessage = styleOn.."{\\b1}Type \"y\" to delete the following bookmark:{\\b0}\n"..displayName(bookmarks[currentSlot]["name"])..styleOff 170 | elseif mode == "rename" then 171 | preMessage = styleOn.."{\\b1}Rename an existing bookmark:{\\b0}"..styleOff 172 | elseif mode == "filepath" then 173 | preMessage = styleOn.."{\\b1}Change the bookmark's filepath:{\\b0}"..styleOff 174 | end 175 | 176 | local postMessage = "" 177 | local split = typerPos + math.floor(typerPos / maxChar) 178 | local messageLines = math.floor((typerText:len() - 1) / maxChar) + 1 179 | for i = 1, messageLines do 180 | postMessage = postMessage .. typerText:sub((i-1) * maxChar + 1, i * maxChar) .. "\n" 181 | end 182 | postMessage = postMessage:sub(1,postMessage:len()-1) 183 | 184 | mp.osd_message(preMessage.."\n"..postMessage:sub(1,split)..styleOn.."{\\c&H00FFFF&}{\\b1}|{\\r}"..styleOff..postMessage:sub(split+1), 9999) 185 | end 186 | 187 | -- // Mover \\ -- 188 | 189 | -- Controls for the Mover 190 | local moverControls = { 191 | ESC = function() moverExit() end, 192 | DOWN = function() jumpSlot(1) end, 193 | UP = function() jumpSlot(-1) end, 194 | RIGHT = function() jumpPage(1) end, 195 | LEFT = function() jumpPage(-1) end, 196 | s = function() addBookmark() end, 197 | m = function() moverCommit() end, 198 | ENTER = function() moverCommit() end, 199 | KP_ENTER = function() moverCommit() end 200 | } 201 | 202 | local moverFlags = { 203 | DOWN = {repeatable = true}, 204 | UP = {repeatable = true}, 205 | RIGHT = {repeatable = true}, 206 | LEFT = {repeatable = true} 207 | } 208 | 209 | -- Function to activate the Mover 210 | function moverStart() 211 | if bookmarkExists(currentSlot) then 212 | deactivateControls("bookmarker", bookmarkerControls) 213 | activateControls("mover", moverControls, moverFlags) 214 | displayBookmarks() 215 | else 216 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the bookmark at slot "..currentSlot) 217 | end 218 | end 219 | 220 | -- Function to commit the action of the Mover 221 | function moverCommit() 222 | saveBookmarks() 223 | moverExit() 224 | end 225 | 226 | -- Function to deactivate the Mover 227 | -- If isError is set, then it'll abort 228 | function moverExit(isError) 229 | deactivateControls("mover", moverControls) 230 | mode = "none" 231 | if not isError then 232 | loadBookmarks() 233 | displayBookmarks() 234 | activateControls("bookmarker", bookmarkerControls, bookmarkerFlags) 235 | end 236 | end 237 | 238 | -- // General utilities \\ -- 239 | 240 | -- Check if the operating system is Mac OS 241 | function isMacOS() 242 | local homedir = os.getenv("HOME") 243 | return (homedir ~= nil and string.sub(homedir,1,6) == "/Users") 244 | end 245 | 246 | -- Check if the operating system is Windows 247 | function isWindows() 248 | local windir = os.getenv("windir") 249 | return (windir~=nil) 250 | end 251 | 252 | -- Check whether a certain file exists 253 | function fileExists(path) 254 | local f = io.open(path,"r") 255 | if f~=nil then 256 | io.close(f) 257 | return true 258 | else 259 | return false 260 | end 261 | end 262 | 263 | -- Get the filepath of a file from the mpv config folder 264 | function getFilepath(filename) 265 | if isWindows() then 266 | return os.getenv("APPDATA"):gsub("\\", "/") .. "/mpv/" .. filename 267 | else 268 | return os.getenv("HOME") .. "/.config/mpv/" .. filename 269 | end 270 | end 271 | 272 | -- Load a table from a JSON file 273 | -- Returns nil if the file can't be found 274 | function loadTable(path) 275 | local contents = "" 276 | local myTable = {} 277 | local file = io.open( path, "r" ) 278 | if file then 279 | local contents = file:read( "*a" ) 280 | myTable = utils.parse_json(contents); 281 | io.close(file) 282 | return myTable 283 | end 284 | return nil 285 | end 286 | 287 | -- Save a table as a JSON file file 288 | -- Returns true if successful 289 | function saveTable(t, path) 290 | local contents = utils.format_json(t) 291 | local file = io.open(path .. ".tmp", "wb") 292 | file:write(contents) 293 | io.close(file) 294 | os.remove(path) 295 | os.rename(path .. ".tmp", path) 296 | return true 297 | end 298 | 299 | -- Convert a pos (seconds) to a hh:mm:ss.mmm format 300 | function parseTime(pos) 301 | local hours = math.floor(pos/3600) 302 | local minutes = math.floor((pos % 3600)/60) 303 | local seconds = math.floor((pos % 60)) 304 | local milliseconds = math.floor(pos % 1 * 1000) 305 | return string.format("%02d:%02d:%02d.%03d",hours,minutes,seconds,milliseconds) 306 | end 307 | 308 | -- // Bookmark functions \\ -- 309 | 310 | -- Checks whether the specified bookmark exists 311 | function bookmarkExists(slot) 312 | return (slot >= 1 and slot <= #bookmarks) 313 | end 314 | 315 | -- Calculates the current page and the total number of pages 316 | function calcPages() 317 | currentPage = math.floor((currentSlot - 1) / bookmarksPerPage) + 1 318 | if currentPage == 0 then currentPage = 1 end 319 | maxPage = math.floor((#bookmarks - 1) / bookmarksPerPage) + 1 320 | if maxPage == 0 then maxPage = 1 end 321 | end 322 | 323 | -- Get the amount of bookmarks on the specified page 324 | function getAmountBookmarksOnPage(page) 325 | local n = bookmarksPerPage 326 | if page == maxPage then n = #bookmarks % bookmarksPerPage end 327 | if n == 0 then n = bookmarksPerPage end 328 | if #bookmarks == 0 then n = 0 end 329 | return n 330 | end 331 | 332 | -- Get the index of the first slot on the specified page 333 | function getFirstSlotOnPage(page) 334 | return (page - 1) * bookmarksPerPage + 1 335 | end 336 | 337 | -- Get the index of the last slot on the specified page 338 | function getLastSlotOnPage(page) 339 | local endSlot = getFirstSlotOnPage(page) + getAmountBookmarksOnPage(page) - 1 340 | if endSlot > #bookmarks then endSlot = #bookmarks end 341 | return endSlot 342 | end 343 | 344 | -- Jumps a certain amount of slots forward or backwards in the bookmarks list 345 | -- Keeps in mind if the current mode is to move bookmarks 346 | function jumpSlot(i) 347 | if mode == "move" then 348 | oldSlot = currentSlot 349 | bookmarkStore = bookmarks[oldSlot] 350 | end 351 | 352 | currentSlot = currentSlot + i 353 | local startSlot = getFirstSlotOnPage(currentPage) 354 | local endSlot = getLastSlotOnPage(currentPage) 355 | 356 | if currentSlot < startSlot then currentSlot = endSlot end 357 | if currentSlot > endSlot then currentSlot = startSlot end 358 | 359 | if mode == "move" then 360 | table.remove(bookmarks, oldSlot) 361 | table.insert(bookmarks, currentSlot, bookmarkStore) 362 | end 363 | 364 | displayBookmarks() 365 | end 366 | 367 | -- Jumps a certain amount of pages forward or backwards in the bookmarks list 368 | -- Keeps in mind if the current mode is to move bookmarks 369 | function jumpPage(i) 370 | if mode == "move" then 371 | oldSlot = currentSlot 372 | bookmarkStore = bookmarks[oldSlot] 373 | end 374 | 375 | local oldPos = currentSlot - getFirstSlotOnPage(currentPage) + 1 376 | currentPage = currentPage + i 377 | if currentPage < 1 then currentPage = maxPage + currentPage end 378 | if currentPage > maxPage then currentPage = currentPage - maxPage end 379 | 380 | local bookmarksOnPage = getAmountBookmarksOnPage(currentPage) 381 | if oldPos > bookmarksOnPage then oldPos = bookmarksOnPage end 382 | currentSlot = getFirstSlotOnPage(currentPage) + oldPos - 1 383 | 384 | if mode == "move" then 385 | table.remove(bookmarks, oldSlot) 386 | table.insert(bookmarks, currentSlot, bookmarkStore) 387 | end 388 | 389 | displayBookmarks() 390 | end 391 | 392 | -- Parses a bookmark name for storing, also trimming it 393 | -- Replaces %t with the timestamp of the bookmark 394 | -- Replaces %p with the time position of the bookmark 395 | function parseName(name) 396 | local pos = 0 397 | if mode == "rename" then pos = bookmarks[currentSlot]["pos"] else pos = mp.get_property_number("time-pos") end 398 | name, _ = name:gsub("%%t", parseTime(pos)) 399 | name, _ = name:gsub("%%p", pos) 400 | name = trimName(name) 401 | return name 402 | end 403 | 404 | -- Parses a bookmark name for displaying, also trimming it 405 | -- Replaces all { with an escaped { so it won't be interpreted as a tag 406 | function displayName(name) 407 | name, _ = name:gsub("{", "\\{") 408 | name = trimName(name) 409 | return name 410 | end 411 | 412 | -- Trims a name to the max number of characters 413 | function trimName(name) 414 | if name:len() > maxChar then name = name:sub(1,maxChar) end 415 | return name 416 | end 417 | 418 | -- Parses a Windows path with backslashes to one with normal slashes 419 | function parsePath(path) 420 | if type(path) == "string" then path, _ = path:gsub("\\", "/") end 421 | return path 422 | end 423 | 424 | -- Loads all the bookmarks in the global table and sets the current page and total number of pages 425 | -- Also checks for older versions of bookmarks and "updates" them 426 | -- Also checks for bookmarks made by "mpv-bookmarker" and converts them 427 | -- Also removes anything it doesn't recognize as a bookmark 428 | function loadBookmarks() 429 | bookmarks = loadTable(getFilepath(bookmarkerName)) 430 | if bookmarks == nil then bookmarks = {} end 431 | 432 | local doSave = false 433 | local doEject = false 434 | local doReplace = false 435 | local ejects = {} 436 | local newmarks = {} 437 | 438 | for key, bookmark in pairs(bookmarks) do 439 | if type(key) == "number" then 440 | if bookmark.version == nil or bookmark.version == 1 then 441 | if bookmark.name ~= nil and bookmark.path ~= nil and bookmark.pos ~= nil then 442 | bookmark.path = parsePath(bookmark.path) 443 | bookmark.version = 2 444 | doSave = true 445 | else 446 | table.insert(ejects, key) 447 | doEject = true 448 | end 449 | end 450 | else 451 | if bookmark.filename ~= nil and bookmark.pos ~= nil and bookmark.filepath ~= nil then 452 | local newmark = { 453 | name = trimName(""..bookmark.filename.." @ "..parseTime(bookmark.pos)), 454 | pos = bookmark.pos, 455 | path = parsePath(bookmark.filepath), 456 | version = 2 457 | } 458 | table.insert(newmarks, newmark) 459 | end 460 | doReplace = true 461 | doSave = true 462 | end 463 | end 464 | 465 | if doEject then 466 | for i = #ejects, 1, -1 do table.remove(bookmarks, ejects[i]) end 467 | doSave = true 468 | end 469 | 470 | if doReplace then bookmarks = newmarks end 471 | if doSave then saveBookmarks() end 472 | 473 | if #bookmarks > 0 and currentSlot == 0 then currentSlot = 1 end 474 | calcPages() 475 | end 476 | 477 | -- Save the globally loaded bookmarks to the JSON file 478 | function saveBookmarks() 479 | saveTable(bookmarks, getFilepath(bookmarkerName)) 480 | end 481 | 482 | -- Make a bookmark of the current media file, position and name 483 | -- Name can be specified or left blank to automake a name 484 | -- Returns the bookmark if successful or nil if it can't make a bookmark 485 | function makeBookmark(bname) 486 | if mp.get_property("path") ~= nil then 487 | if bname == nil then bname = mp.get_property("media-title").." @ %t" end 488 | local bookmark = { 489 | name = parseName(bname), 490 | pos = mp.get_property_number("time-pos"), 491 | path = parsePath(mp.get_property("path")), 492 | version = 2 493 | } 494 | return bookmark 495 | else 496 | return nil 497 | end 498 | end 499 | 500 | -- Add the current position as a bookmark to the global table and then saves it 501 | -- Returns the slot of the newly added bookmark 502 | -- Returns -1 if there's an error 503 | function addBookmark(name) 504 | local bookmark = makeBookmark(name) 505 | if bookmark == nil then 506 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the media file to create the bookmark for") 507 | return -1 508 | end 509 | table.insert(bookmarks, bookmark) 510 | 511 | if #bookmarks == 1 then currentSlot = 1 end 512 | 513 | calcPages() 514 | saveBookmarks() 515 | displayBookmarks() 516 | return #bookmarks 517 | end 518 | 519 | -- Edit a property of a bookmark at the specified slot 520 | -- Returns -1 if there's an error 521 | function editBookmark(slot, property, value) 522 | if bookmarkExists(slot) then 523 | if property == "name" then value = parseName(value) end 524 | bookmarks[slot][property] = value 525 | saveBookmarks() 526 | else 527 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the bookmark at slot "..slot) 528 | return -1 529 | end 530 | end 531 | 532 | -- Replaces the bookmark at the specified slot with a provided bookmark 533 | -- Keeps the name and its position in the list 534 | -- If the slot is not specified, picks the currently selected bookmark to replace 535 | -- If a bookmark is not provided, generates a new bookmark 536 | function replaceBookmark(slot) 537 | if slot == nil then slot = currentSlot end 538 | if bookmarkExists(slot) then 539 | local bookmark = makeBookmark(bookmarks[slot]["name"]) 540 | if bookmark == nil then 541 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the media file to create the bookmark for") 542 | return -1 543 | end 544 | bookmarks[slot] = bookmark 545 | saveBookmarks() 546 | if closeAfterReplace then 547 | abort(styleOn.."{\\c&H00FF00&}{\\b1}Successfully replaced bookmark:{\\r}\n"..displayName(bookmark["name"])) 548 | return -1 549 | end 550 | return 1 551 | else 552 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the bookmark at slot "..slot) 553 | return -1 554 | end 555 | end 556 | 557 | -- Quickly saves a bookmark without bringing up the menu 558 | function quickSave() 559 | if not active then 560 | loadBookmarks() 561 | local slot = addBookmark() 562 | if slot > 0 then mp.osd_message("Saved new bookmark at slot " .. slot) end 563 | end 564 | end 565 | 566 | -- Quickly loads the last bookmark without bringing up the menu 567 | function quickLoad() 568 | if not active then 569 | loadBookmarks() 570 | local slot = #bookmarks 571 | if slot > 0 then mp.osd_message("Loaded bookmark at slot " .. slot) end 572 | jumpToBookmark(slot) 573 | end 574 | end 575 | 576 | -- Deletes the bookmark in the specified slot from the global table and then saves it 577 | function deleteBookmark(slot) 578 | table.remove(bookmarks, slot) 579 | if currentSlot > #bookmarks then currentSlot = #bookmarks end 580 | 581 | calcPages() 582 | saveBookmarks() 583 | displayBookmarks() 584 | end 585 | 586 | -- Jump to the specified bookmark 587 | -- This means loading it, reading it, and jumping to the file + position in the bookmark 588 | function jumpToBookmark(slot) 589 | if bookmarkExists(slot) then 590 | local bookmark = bookmarks[slot] 591 | if string.sub(bookmark["path"], 1, 4) == "http" or fileExists(bookmark["path"]) then 592 | if parsePath(mp.get_property("path")) == bookmark["path"] then 593 | mp.set_property_number("time-pos", bookmark["pos"]) 594 | else 595 | mp.commandv("loadfile", parsePath(bookmark["path"]), "replace", "start="..bookmark["pos"]) 596 | end 597 | if closeAfterLoad then abort(styleOn.."{\\c&H00FF00&}{\\b1}Successfully found file for bookmark:{\\r}\n"..displayName(bookmark["name"])) end 598 | else 599 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find file for bookmark:\n" .. displayName(bookmark["name"])) 600 | end 601 | else 602 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the bookmark at slot " .. slot) 603 | end 604 | end 605 | 606 | -- Displays the current page of bookmarks 607 | function displayBookmarks() 608 | -- Determine which slot is the first and last on the current page 609 | local startSlot = getFirstSlotOnPage(currentPage) 610 | local endSlot = getLastSlotOnPage(currentPage) 611 | 612 | -- Prepare the text to display and display it 613 | local display = styleOn .. "{\\b1}Bookmarks page " .. currentPage .. "/" .. maxPage .. ":{\\b0}" 614 | for i = startSlot, endSlot do 615 | local btext = displayName(bookmarks[i]["name"]) 616 | local selection = "" 617 | if i == currentSlot then 618 | selection = "{\\b1}{\\c&H00FFFF&}>" 619 | if mode == "move" then btext = "----------------" end 620 | btext = btext 621 | end 622 | display = display .. "\n" .. selection .. i .. ": " .. btext .. "{\\r}" 623 | end 624 | mp.osd_message(display, rate) 625 | end 626 | 627 | local timer = mp.add_periodic_timer(rate * 0.95, displayBookmarks) 628 | timer:kill() 629 | 630 | -- Commits the message entered with the Typer with custom scripts preceding it 631 | -- Should typically end with typerExit() 632 | function typerCommit() 633 | local status = 0 634 | if mode == "save" then 635 | status = addBookmark(typerText) 636 | elseif mode == "replace" and typerText == "y" then 637 | status = replaceBookmark(currentSlot, makeBookmark(bookmarks[currentSlot]["name"])) 638 | elseif mode == "delete" and typerText == "y" then 639 | deleteBookmark(currentSlot) 640 | elseif mode == "rename" then 641 | editBookmark(currentSlot, "name", typerText) 642 | elseif mode == "filepath" then 643 | editBookmark(currentSlot, "path", typerText) 644 | end 645 | if status >= 0 then typerExit() end 646 | end 647 | 648 | -- Exits the Typer without committing with custom scripts preceding it 649 | function typerExit() 650 | deactivateTyper() 651 | displayBookmarks() 652 | timer:resume() 653 | mode = "none" 654 | activateControls("bookmarker", bookmarkerControls, bookmarkerFlags) 655 | end 656 | 657 | -- Starts the Typer with custom scripts preceding it 658 | function typerStart() 659 | if (mode == "save" or mode=="replace") and mp.get_property("path") == nil then 660 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the media file to create the bookmark for") 661 | return -1 662 | end 663 | if (mode == "replace" or mode == "rename" or mode == "filepath" or mode == "delete") and not bookmarkExists(currentSlot) then 664 | abort(styleOn.."{\\c&H0000FF&}{\\b1}Can't find the bookmark at slot "..currentSlot) 665 | return -1 666 | end 667 | if (mode == "replace" and not confirmReplace) or (mode == "delete" and not confirmDelete) then 668 | typerText = "y" 669 | typerCommit() 670 | return 671 | end 672 | 673 | deactivateControls("bookmarker", bookmarkerControls) 674 | timer:kill() 675 | activateTyper() 676 | if mode == "rename" then typerText = bookmarks[currentSlot]["name"] end 677 | if mode == "filepath" then typerText = bookmarks[currentSlot]["path"] end 678 | typerPos = typerText:len() 679 | typer("") 680 | end 681 | 682 | -- Aborts the program with an optional error message 683 | function abort(message) 684 | mode = "none" 685 | moverExit(true) 686 | deactivateTyper() 687 | deactivateControls("bookmarker", bookmarkerControls) 688 | timer:kill() 689 | mp.osd_message(message) 690 | active = false 691 | end 692 | 693 | -- Handles the state of the bookmarker 694 | function handler() 695 | if active then 696 | abort("") 697 | else 698 | activateControls("bookmarker", bookmarkerControls, bookmarkerFlags) 699 | loadBookmarks() 700 | displayBookmarks() 701 | timer:resume() 702 | active = true 703 | end 704 | end 705 | 706 | mp.register_script_message("bookmarker-menu", handler) 707 | mp.register_script_message("bookmarker-quick-save", quickSave) 708 | mp.register_script_message("bookmarker-quick-load", quickLoad) 709 | --------------------------------------------------------------------------------