├── .gitignore ├── README.md ├── README.zh-CN.md ├── history_bookmark.lua ├── res └── history-bookmark.png └── show_filename.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # This ignore file happens to be a document which 2 | # marks what other scripts I used before :D 3 | autoload.lua 4 | 5 | .vscode/* 6 | 7 | # Local History for Visual Studio Code 8 | .history/ 9 | 10 | # Built Visual Studio Code Extensions 11 | *.vsix 12 | 13 | # Swap 14 | [._]*.s[a-v][a-z] 15 | !*.svg # comment out if you don't need vector files 16 | [._]*.sw[a-p] 17 | [._]s[a-rt-v][a-z] 18 | [._]ss[a-gi-z] 19 | [._]sw[a-p] 20 | 21 | # Session 22 | Session.vim 23 | Sessionx.vim 24 | 25 | # Temporary 26 | .netrwhist 27 | *~ 28 | # Auto-generated tag files 29 | tags 30 | # Persistent undo 31 | [._]*.un~ 32 | 33 | # Compiled Lua sources 34 | luac.out 35 | 36 | # luarocks build files 37 | *.src.rock 38 | *.zip 39 | *.tar.gz 40 | 41 | # Object files 42 | *.o 43 | *.os 44 | *.ko 45 | *.obj 46 | *.elf 47 | 48 | # Precompiled Headers 49 | *.gch 50 | *.pch 51 | 52 | # Libraries 53 | *.lib 54 | *.a 55 | *.la 56 | *.lo 57 | *.def 58 | *.exp 59 | 60 | # Shared objects (inc. Windows DLLs) 61 | *.dll 62 | *.so 63 | *.so.* 64 | *.dylib 65 | 66 | # Executables 67 | *.exe 68 | *.out 69 | *.app 70 | *.i*86 71 | *.x86_64 72 | *.hex 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpv-scripts 2 | 3 | [![Static Badge](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87-blue)](./README.zh-CN.md) 4 | 5 | These are scripts written for mpv. 6 | 7 | ## How to use 8 | 9 | - Windows: move `*.lua` files into `/scripts/` 10 | - Linux: move `*.lua` files into `~/.config/mpv/scripts/` 11 | 12 | ## show_filename.lua 13 | 14 | A simple script to show the name of current playing file. To use this function, you need to press `SHIFT+ENTER`. For better experience, you could adjust the `osd-font-size` property in mpv.conf 15 | 16 | For example: 17 | 18 | ``` txt 19 | osd-font-size=30 20 | ``` 21 | 22 | ## history-bookmark.lua 23 | 24 | This script will create a history folder in `.config/mpv/history/` (unix like system) or `%APPDATA%\mpv\history\` (windows). The history folder contains records of the videos you have watched. The next time you want to continue to watch it, you can open any videos in the folder. The script will lead you to the video played last time. 25 | 26 | ![history-bookmark](./res/history-bookmark.png) 27 | 28 | As shown in the screenshot, last time we watched episode 1. Now we can press `ENTER` to jump to ep1, press `n` or do nothing to stay in the episode we are watching now. 29 | 30 | In order to resume from the exact point of the watching progress in target episode, you just need to add a line in the mpv.conf 31 | 32 | ``` txt 33 | save-position-on-quit 34 | ``` 35 | 36 | ## Other scripts I recommend 37 | 38 | [autoload](https://github.com/mpv-player/mpv/blob/master/TOOLS/lua/autoload.lua): Automatically load playlist entries before and after the currently playing file, by scanning the directory. 39 | 40 | ## Contributors 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # mpv-scripts 2 | 3 | [![Static Badge](https://img.shields.io/badge/README-English-blue)](./README.md) 4 | 5 | 这些是为 mpv 编写的脚本。 6 | 7 | ## 使用方法 8 | 9 | - Windows:将 `*.lua` 文件移动到 `/scripts/` 10 | - Linux:将 `*.lua` 文件移动到 `~/.config/mpv/scripts/` 11 | 12 | ## show_filename.lua 13 | 14 | 显示当前播放文件名的简单脚本。要使用此功能,您需要按 `SHIFT+ENTER`。为了更好的体验,您可以调整 mpv.conf 中的 `osd-font-size` 属性,例如: 15 | 16 | ``` txt 17 | osd-font-size=30 18 | ``` 19 | 20 | ## history-bookmark.lua 21 | 22 | 此脚本将在 `.config/mpv/history/`(类Unix系统)或 `%APPDATA%\mpv\history\`(Windows)中创建一个历史文件夹。历史文件夹包含您观看的视频的记录。下次您想继续观看它时,您可以在文件夹中打开任何视频。脚本将引导您到上次播放的视频。 23 | 24 | ![history-bookmark](./res/history-bookmark.png) 25 | 26 | 如屏幕截图所示,上次我们观看了第1集。现在我们可以按 `ENTER` 跳转到第1集,按 `n` 或什么都不做以停留在我们现在正在观看的集数。 27 | 28 | 为了从目标集数的观看进度的确切点继续观看,您只需要在 mpv.conf 中添加一行: 29 | 30 | ``` txt 31 | save-position-on-quit 32 | ``` 33 | 34 | ## 其他我推荐的脚本 35 | 36 | [autoload](https//github.com/mpv-player/mpv/blob/master/TOOLS/lua/autoload.lua):通过扫描目录,在当前播放文件之前和之后自动加载播放列表条目。 37 | -------------------------------------------------------------------------------- /history_bookmark.lua: -------------------------------------------------------------------------------- 1 | local mp = require 'mp' 2 | local mops = require 'mp.options' 3 | local utils = require 'mp.utils' 4 | local msg = require 'mp.msg' 5 | 6 | local M = {} 7 | 8 | local WINDOWS = 'windows' 9 | local LINUX = 'linux' 10 | local MACOSX = 'macosx' 11 | 12 | local o = { 13 | save_period = 30, -- time interval for saving history 14 | exclude_dir = {}, -- directories to be excluded 15 | exclude_proto = { 16 | 'https?://', 'magnet://', 'rtmp://', 'smb://', 'ftp://', 'bd://', 17 | 'dvb://', 'bluray://', 'dvd://', 'tv://', 'dshow://', 'cdda://', 18 | } -- protocols to be excluded 19 | } 20 | mops.read_options(o) 21 | 22 | function Set (t) 23 | local set = {} 24 | for _, v in pairs(t) do set[v] = true end 25 | return set 26 | end 27 | 28 | function SetUnion (a,b) 29 | local res = {} 30 | for k in pairs(a) do res[k] = true end 31 | for k in pairs(b) do res[k] = true end 32 | return res 33 | end 34 | 35 | function LenSet (s) 36 | local len = 0 37 | for _ in pairs(s) do len = len + 1 end 38 | return len 39 | end 40 | 41 | EXTENSIONS_VIDEO = Set { 42 | '3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov', 43 | 'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m' 44 | } 45 | 46 | EXTENSIONS_AUDIO = Set { 47 | 'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg', 48 | 'ogm', 'opus', 'wav', 'wma' 49 | } 50 | 51 | EXTENSIONS_IMAGES = Set { 52 | 'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png', 53 | 'svg', 'tga', 'tif', 'tiff', 'webp' 54 | } 55 | 56 | EXTENSIONS = SetUnion(EXTENSIONS_VIDEO, EXTENSIONS_AUDIO) 57 | EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) 58 | 59 | -- param/env/... checking proess 60 | local CheckPipeline = {} 61 | 62 | local function any(t, f) 63 | for _, v in ipairs(t) do 64 | if f(v) then return true end 65 | end 66 | return false 67 | end 68 | 69 | function CheckPipeline.is_exclude(url) 70 | local is_local_file = url:find('^file://') == 1 or not url:find('://') 71 | local contain = function(x) return url:find(x) == 1 end 72 | if is_local_file then 73 | return any(o.exclude_dir, contain) 74 | else 75 | return any(o.exclude_proto, contain) 76 | end 77 | end 78 | 79 | --- 80 | ---Detect current system. 81 | --- 82 | ---@return string The name of the system. 83 | local function get_system_name() 84 | return mp.get_property_native("platform", {}) 85 | end 86 | 87 | 88 | --- 89 | ---Wrapper for mp.commandv(). 90 | --- 91 | ---@param msg string The message to be displayed. 92 | ---@param ms number The duration of the message in milliseconds. 93 | ---@return nil 94 | local function mpv_show_text(msg, ms) 95 | mp.commandv("show-text", msg, ms) 96 | end 97 | 98 | 99 | local function levenshtein(s, t) 100 | local m, n = #s, #t 101 | local d = {} 102 | for i = 0, m do d[i] = {} end 103 | for i = 0, m do d[i][0] = i end 104 | for j = 0, n do d[0][j] = j end 105 | for i = 1, m do 106 | for j = 1, n do 107 | d[i][j] = math.min( 108 | d[i - 1][j] + 1, 109 | d[i][j - 1] + 1, 110 | d[i - 1][j - 1] + (s:sub(i, i) == t:sub(j, j) and 0 or 1) 111 | ) 112 | end 113 | end 114 | return d[m][n] 115 | end 116 | 117 | local split_string = function(str, sep) 118 | local fields = {} 119 | local pattern = string.format("([^%s]+)", sep) 120 | str:gsub(pattern, function(c) fields[#fields + 1] = c end) 121 | return fields 122 | end 123 | 124 | --- 125 | ---Get season and episode number from the file name. 126 | --- 127 | ---@param fname string The file name. 128 | ---@param ref_fname string The reference file names. 129 | ---@return number?, number? The season and episode number. 130 | local function get_episode_info(fname, ref_fname) 131 | -- Add custom patterns here. 132 | local patterns = { 133 | '[Ss](%d+)[^%d]+(%d+)', -- "S01E02", "S1 - 03" 134 | 'season.+(%d+).+episode.+(%d+)', -- "season 1 episode 2" 135 | } 136 | 137 | -- Try to match the season and episode number. 138 | for _, pattern in ipairs(patterns) do 139 | local season, ep = fname:match(pattern) 140 | if season and ep then 141 | return tonumber(season), tonumber(ep) 142 | end 143 | end 144 | 145 | -- ep numbers are usually the only different part in the file names 146 | -- so we can find the most likely ep number by comparing the file name 147 | -- with the reference file names 148 | 149 | -- try to split by space, dot and _ 150 | local fname_parts = split_string(fname, '%s%.%_') 151 | local ref_parts = split_string(ref_fname, '%s%.%_') 152 | if #fname_parts ~= #ref_parts then 153 | msg.warn(string.format('Failed to parse the episode number: %s', fname)) 154 | return nil, nil 155 | end 156 | 157 | -- zip the two lists and calculate the levenshtein distance 158 | for i, fname_part in ipairs(fname_parts) do 159 | local ref_part = ref_parts[i] 160 | local dist = levenshtein(fname_part, ref_part) 161 | if dist ~= 0 then 162 | -- grep the number from the string 163 | local nums, ref_nums = {}, {} 164 | for num in fname_part:gmatch('%d+') do table.insert(nums, tonumber(num)) end 165 | for num in ref_part:gmatch('%d+') do table.insert(ref_nums, tonumber(num)) end 166 | for j, num in ipairs(nums) do 167 | if num ~= ref_nums[j] then 168 | return nil, num 169 | end 170 | end 171 | end 172 | end 173 | 174 | return nil, nil 175 | end 176 | 177 | 178 | local function join_path(a, b) 179 | local joined_path = utils.join_path(a, b) 180 | -- fix windows path 181 | if get_system_name() == WINDOWS then 182 | joined_path = joined_path:gsub('/', '\\') 183 | end 184 | return joined_path 185 | end 186 | 187 | 188 | --- 189 | ---makedirs function for Lua. 190 | --- 191 | ---@param dir string The path to be created. 192 | ---@return boolean Whether the path is created successfully. 193 | ---@return string? The error message if failed. 194 | local function makedirs(dir) 195 | local args = {} 196 | local system = get_system_name() 197 | if system == WINDOWS then 198 | args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", dir) } 199 | elseif system == LINUX or system == MACOSX then 200 | args = { 'mkdir', '-p', dir } 201 | else 202 | return false, string.format('Unsupported system: %s', system) 203 | end 204 | 205 | local res = mp.command_native({ name = "subprocess", capture_stdout = true, playback_only = false, args = args }) 206 | if res.status ~= 0 then 207 | msg.error(string.format('Failed to create directory: %s', dir)) 208 | return false, res.error 209 | end 210 | 211 | return true, nil 212 | end 213 | 214 | local function bit_xor(a, b) 215 | local p, c = 1, 0 216 | while a > 0 or b > 0 do 217 | local ra, rb = a % 2, b % 2 218 | if ra + rb == 1 then c = c + p end 219 | a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 220 | end 221 | return c 222 | end 223 | 224 | local function bit_and(a, b) 225 | local p, c = 1, 0 226 | while a > 0 and b > 0 do 227 | local ra, rb = a % 2, b % 2 228 | if ra + rb > 1 then c = c + p end 229 | a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 230 | end 231 | return c 232 | end 233 | 234 | --- 235 | ---fnv1a hash, used for path hashing. Since there won't be too many path to be 236 | ---hashed, the performance is not a big concern. 237 | --- 238 | ---@param str string The string to be hashed. 239 | ---@return string The hash value. 240 | local function fnv1a_hash(str) 241 | local FNV_prime = 0x01000193 242 | local FNV_offset_basis = 0x811C9DC5 243 | 244 | local hash = FNV_offset_basis 245 | for i = 1, #str do 246 | hash = bit_xor(hash, str:byte(i)) 247 | hash = bit_and(hash * FNV_prime, 0xFFFFFFFF) 248 | end 249 | 250 | return string.format('%08x', hash) 251 | end 252 | 253 | 254 | -- ***************************************************************************** 255 | -- Record 256 | -- each playlist correspondings to a record 257 | -- each record is a line in the history file 258 | -- ***************************************************************************** 259 | 260 | --- 261 | ---Record class 262 | --- 263 | ---@class Record 264 | local Record = {} 265 | Record.__index = Record 266 | 267 | --- Create a new Record object. 268 | --- 269 | ---@param dir string The media directory. 270 | ---@param fname string The name of the last watched episode. 271 | ---@param season_num number? The number of the last watched episode. 272 | ---@param episode_num number The number of the last watched episode. 273 | ---@return Record 274 | function Record:new(dir, fname, season_num, episode_num) 275 | local record = {} 276 | setmetatable(record, self) 277 | record.dir = dir 278 | record.fname = fname 279 | record.season_num = season_num 280 | record.episode_num = episode_num 281 | return record 282 | end 283 | 284 | setmetatable(Record, { 285 | __call = function(cls, ...) 286 | return cls:new(...) 287 | end 288 | }) 289 | 290 | 291 | -- ***************************************************************************** 292 | -- History 293 | -- ***************************************************************************** 294 | --- 295 | ---History class 296 | --- 297 | ---@class History 298 | local History = {} 299 | History.__index = History 300 | 301 | function History:from_outer_dir(dir) 302 | local history = {} 303 | setmetatable(history, self) 304 | self.dir = join_path(dir, 'history') 305 | return history 306 | end 307 | 308 | function History:from_dir(dir) 309 | local history = {} 310 | setmetatable(history, self) 311 | self.dir = dir 312 | return history 313 | end 314 | 315 | ---Creat with History(dir) 316 | setmetatable(History, { 317 | __call = function(cls, ...) 318 | local self = setmetatable({}, cls) 319 | self:from_dir(...) 320 | return self 321 | end 322 | }) 323 | 324 | --- 325 | ---Check if the history file exists. 326 | --- 327 | ---@return boolean 328 | function History:exist() 329 | if io.open(self.dir, "r") == nil then 330 | return false 331 | end 332 | return true 333 | end 334 | 335 | --- 336 | ---Make sure the history file exists. 337 | --- 338 | ---@return nil 339 | function History:make_sure_exists() 340 | if self:exist() then 341 | msg.info(string.format('History directory exists: %s', self.dir)) 342 | return 343 | end 344 | 345 | local ok, err = makedirs(self.dir) 346 | if not ok then 347 | msg.erro(string.format('Failed to create history directory: %s', err)) 348 | else 349 | msg.info(string.format('History directory created: %s', self.dir)) 350 | end 351 | end 352 | 353 | --- 354 | ---Get records from the history file, each file only contains one record. 355 | --- 356 | ---@return Record[] 357 | function History:list() 358 | local records = {} 359 | self:make_sure_exists() 360 | 361 | local damaged_files = {} 362 | -- list all files in the directory 363 | local file_list = utils.readdir(self.dir, 'files') 364 | -- read records from each file 365 | for _, file in ipairs(file_list) do 366 | local path = join_path(self.dir, file) 367 | msg.info(string.format('Found history file: %s', path)) 368 | local file = io.open(path, "r") 369 | if file == nil then 370 | msg.warn(string.format('history:list -- Failed to open history file: %s', path)) 371 | table.insert(damaged_files, path) 372 | goto continue 373 | end 374 | local content = file:read() 375 | local status, json = pcall(utils.parse_json, content) 376 | if not status then 377 | msg.warn(string.format('Failed to decode history file: %s', path)) 378 | table.insert(damaged_files, path) 379 | else 380 | local record = Record(json.dir, json.fname, json.season_num, json.episode_num) 381 | table.insert(records, record) 382 | end 383 | file:close() 384 | ::continue:: 385 | end 386 | 387 | -- remove damaged files 388 | for _, path in ipairs(damaged_files) do 389 | msg.info(string.format('Removing damaged history file: %s', path)) 390 | os.remove(path) 391 | end 392 | 393 | return records 394 | end 395 | 396 | --- 397 | ---save a record to the history dir. 398 | --- 399 | ---@param record Record The record to be saved. 400 | ---@return boolean 401 | function History:save(record) 402 | if getmetatable(record) ~= Record then 403 | msg.warn(string.format('Invalid record: %s', record)) 404 | return false 405 | end 406 | 407 | self:make_sure_exists() 408 | 409 | local file_name = fnv1a_hash(record.dir) 410 | -- overwrite the file if it exists 411 | local path = join_path(self.dir, file_name) 412 | local file = io.open(path, "w") 413 | if file == nil then 414 | msg.warn(string.format('history:save -- Failed to open history file: %s', path)) 415 | return false 416 | end 417 | local json_str, error = utils.format_json(record) 418 | if error ~= nil then 419 | msg.error(string.format('Failed to encode history file: %s, err: %s', path, status)) 420 | return false 421 | end 422 | 423 | file:write(json_str) 424 | file:close() 425 | 426 | msg.info(string.format('History saved to: %s, vedio path: %s', path, record.dir)) 427 | return true 428 | end 429 | 430 | --- 431 | ---load a record from the history dir. 432 | --- 433 | ---@param dir string The media directory. 434 | ---@return Record? 435 | function History:load(dir) 436 | local file_name = fnv1a_hash(dir) 437 | local path = join_path(self.dir, file_name) 438 | msg.info(string.format('Loading history from file: %s of dir: %s', path, dir)) 439 | local file = io.open(path, "r") 440 | if file == nil then 441 | msg.warn(string.format('history:load -- Failed to open history file: %s', path)) 442 | return nil 443 | end 444 | local content = file:read() 445 | local status, json = pcall(utils.parse_json, content) 446 | if not status then 447 | msg.warn(string.format('Failed to decode history file: %s', path)) 448 | return nil 449 | end 450 | local record = Record(json.dir, json.fname, json.season_num, json.episode_num) 451 | return record 452 | end 453 | 454 | --- 455 | ---delete a record from the history dir. 456 | --- 457 | ---@param dir string The media directory. 458 | function History:delete(dir) 459 | local file_name = fnv1a_hash(dir) 460 | local path = join_path(self.dir, file_name) 461 | os.remove(path) 462 | end 463 | 464 | local WinHistory = {} 465 | WinHistory.__index = WinHistory 466 | 467 | function WinHistory:new(cache_dir) 468 | cache_dir = cache_dir or os.getenv('APPDATA') .. '\\mpv' 469 | local win_history = History:from_dir(cache_dir .. '\\history') 470 | setmetatable(win_history, self) 471 | return win_history 472 | end 473 | 474 | setmetatable(WinHistory, { 475 | __index = History, 476 | __call = function(cls, ...) 477 | local self = setmetatable({}, cls) 478 | return self:new(...) 479 | end 480 | }) 481 | 482 | 483 | local UnixHistory = {} 484 | UnixHistory.__index = UnixHistory 485 | 486 | function UnixHistory:new(cache_dir) 487 | cache_dir = cache_dir or os.getenv('HOME') .. '/.mpv' 488 | local unix_history = History:from_dir(cache_dir .. '/history') 489 | setmetatable(unix_history, self) 490 | return unix_history 491 | end 492 | 493 | setmetatable(UnixHistory, { 494 | __index = History, 495 | __call = function(cls, ...) 496 | local self = setmetatable({}, cls) 497 | return self:new(...) 498 | end 499 | }) 500 | 501 | local function get_history_manager() 502 | local system = get_system_name() 503 | msg.info(string.format('System: %s', system)) 504 | if system == WINDOWS then 505 | return WinHistory:new() 506 | elseif system == LINUX or system == MACOSX then 507 | return UnixHistory:new() 508 | else 509 | msg.warn(string.format('Unsupported system: %s', system)) 510 | return nil 511 | end 512 | end 513 | 514 | 515 | -- ***************************************************************************** 516 | -- Playlist logic 517 | -- ***************************************************************************** 518 | --- 519 | ---Playlist class 520 | --- 521 | ---@class Playlist 522 | local Playlist = {} 523 | Playlist.__index = Playlist 524 | 525 | --- 526 | ---Create a new Playlist object. 527 | --- 528 | ---@param dir string The media directory. 529 | ---@param exts string[]? The extensions of the media files. 530 | ---@return Playlist 531 | function Playlist:new(dir, exts) 532 | local playlist = {} 533 | setmetatable(playlist, self) 534 | playlist.dir = dir 535 | playlist.exts = exts or EXTENSIONS 536 | playlist.files = {} 537 | playlist.history = get_history_manager() 538 | 539 | msg.info('Playlist created in: ' .. dir) 540 | 541 | if playlist.history == nil then 542 | msg.error('Failed to get history manager.') 543 | os.exit(1) 544 | end 545 | 546 | playlist:scan() 547 | 548 | return playlist 549 | end 550 | 551 | --- 552 | ---Reload the playlist. 553 | --- 554 | ---@param dir string The media directory. 555 | ---@return nil 556 | function Playlist:reload(dir) 557 | self.dir = dir 558 | self.files = {} 559 | self:scan() 560 | end 561 | 562 | --- 563 | ---Scan the media files in the directory. 564 | --- 565 | ---@return nil 566 | function Playlist:scan() 567 | local file_list = utils.readdir(self.dir, 'files') 568 | table.sort(file_list) 569 | for _, file in ipairs(file_list) do 570 | -- get file extension 571 | local ftype = file:match('%.([^.]+)$') 572 | -- if file type is in the extension list 573 | if ftype and (self.exts[ftype] or LenSet(self.exts) == 0) then 574 | table.insert(self.files, file) 575 | msg.info('Playlist added: ' .. file) 576 | end 577 | end 578 | end 579 | 580 | --- 581 | ---Check if the playlist is empty. 582 | --- 583 | ---@return boolean 584 | function Playlist:empty() 585 | return #self.files == 0 586 | end 587 | 588 | --- 589 | ---Restore last watched episode. 590 | --- 591 | ---@return string? The name of the last watched episode. 592 | ---@return number? The number of the last watched season. 593 | ---@return number? The number of the last watched episode. 594 | function Playlist:restore() 595 | msg.info('Restoring from dir: ' .. self.dir) 596 | local record = self.history:load(self.dir) 597 | if not record then 598 | return nil, nil, nil 599 | end 600 | return record.fname, record.season_num, record.episode_num 601 | end 602 | 603 | --- 604 | ---Save the history. 605 | --- 606 | ---@param name string The name of the last watched episode. 607 | ---@return nil 608 | function Playlist:record(name) 609 | -- get episode number from the file name 610 | local season, ep = get_episode_info(name, self.files[1]) 611 | msg.info('Recording: ') 612 | msg.info(string.format('\tDirectory: %s', self.dir)) 613 | msg.info(string.format('\tName: %s', name)) 614 | msg.info(string.format('\tSeason: %s, Episode: %s', season, ep)) 615 | self.history:save(Record(self.dir, name, season, ep)) 616 | end 617 | 618 | -- ***************************************************************************** 619 | -- mpv event handlers 620 | -- ***************************************************************************** 621 | M.playlist = Playlist:new(utils.getcwd()) 622 | 623 | -- record process 624 | function M.record_history() 625 | local path = mp.get_property('path') 626 | local dir, fname = utils.split_path(path) 627 | M.playlist:reload(dir) 628 | if M.playlist:empty() then 629 | msg.warn('No media file found in the directory: ' .. dir) 630 | return 631 | end 632 | M.playlist:record(fname) 633 | msg.info('Recorded: ' .. fname) 634 | end 635 | 636 | local timeout = 15 637 | function M.resume_count_down() 638 | timeout = timeout - 1 639 | msg.info('Count down: ' .. timeout) 640 | -- count down only at the beginning 641 | if (timeout < 1) then 642 | M.unbind_key() 643 | return 644 | end 645 | 646 | local path = mp.get_property('path') 647 | msg.info('Resuming from dir: ' .. path) 648 | local dir, _ = utils.split_path(path) 649 | M.playlist:reload(dir) 650 | local jump_file, season, ep = M.playlist:restore() 651 | if not jump_file or jump_file == mp.get_property('filename') then 652 | M.unbind_key() 653 | return 654 | end 655 | 656 | local prompt_text = 'Last watched: ' 657 | if not ep then 658 | msg.warn('Failed to parse the episode number.') 659 | prompt_text = 'Jump to the last watched episode?' 660 | elseif not season then 661 | prompt_text = prompt_text .. 'EP ' .. ep 662 | else 663 | prompt_text = prompt_text .. 'S' .. season .. 'E' .. ep 664 | end 665 | 666 | prompt_text = prompt_text .. " -- continue? " .. timeout .. " [ENTER/n]" 667 | mpv_show_text(prompt_text, 1000) 668 | end 669 | 670 | --- 671 | ---record the file name when video is paused 672 | --- 673 | ---@param name any 674 | ---@param paused any 675 | ---@return nil 676 | function M.on_pause(name, paused) 677 | if paused then 678 | M.record_timer:stop() 679 | M.record_history() 680 | else 681 | M.record_timer:resume() 682 | end 683 | end 684 | 685 | --- 686 | ---Clean history files when mpv start up. look through each record and remove 687 | ---the record if the record's directory does not exist. 688 | --- 689 | ---@return nil 690 | function M.on_load() 691 | local records = M.playlist.history:list() 692 | for _, record in ipairs(records) do 693 | if io.open(record.dir) == nil then 694 | msg.info('Removing history file: ' .. record.dir) 695 | M.playlist.history:delete(record.dir) 696 | end 697 | end 698 | end 699 | 700 | -- ***************************************************************************** 701 | -- mpv key bindings 702 | -- ***************************************************************************** 703 | function M.bind_key() 704 | mp.add_key_binding('ENTER', 'resume_yes', function() 705 | local fname = M.playlist:restore() 706 | local dir = M.playlist.dir 707 | local path = join_path(dir, fname) 708 | msg.info('Jumping to ' .. path) 709 | mp.commandv('loadfile', path) 710 | M.unbind_key() 711 | mpv_show_text('Resume successfully', 1500) 712 | end) 713 | mp.add_key_binding('n', 'resume_not', function() 714 | M.unbind_key() 715 | msg.info('Stay at the current episode.') 716 | end) 717 | msg.info('Bound the keys: \"Enter\", \"n\".') 718 | end 719 | 720 | function M.unbind_key() 721 | mp.remove_key_binding('resume_yes') 722 | mp.remove_key_binding('resume_not') 723 | msg.info('Unbound the keys: \"Enter\", \"n\".') 724 | 725 | M.record_timer:kill() 726 | timeout = 0 727 | msg.info('Resume count down stopped.') 728 | end 729 | 730 | -- ***************************************************************************** 731 | -- register 732 | -- ***************************************************************************** 733 | mp.register_event('file-loaded', function() 734 | local fpath = mp.get_property('path') 735 | if CheckPipeline.is_exclude(fpath) then 736 | msg.info('The file is excluded.') 737 | return 738 | end 739 | local dir, _ = utils.split_path(fpath) 740 | msg.info('Loaded file: ' .. fpath) 741 | if not dir then return end 742 | 743 | M.record_timer = mp.add_periodic_timer(o.save_period, M.record_history) 744 | M.resume_timer = mp.add_periodic_timer(1, M.resume_count_down) 745 | 746 | M.bind_key() 747 | mp.observe_property("pause", "bool", M.on_pause) 748 | mp.add_hook("on_unload", 50, M.record_history) 749 | mp.add_hook("on_load", 50, M.on_load) 750 | end) 751 | -------------------------------------------------------------------------------- /res/history-bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuukidach/mpv-scripts/d19343ec9d14f3cbe925d5ec3d7f7abcb9b817ca/res/history-bookmark.png -------------------------------------------------------------------------------- /show_filename.lua: -------------------------------------------------------------------------------- 1 | -- show the name of current playing file 2 | -- press SHIFT+ENTER to call the function 3 | 4 | local mp = require 'mp' 5 | local utils = require 'mp.utils' 6 | local options = require 'mp.options' 7 | 8 | local M = {} 9 | 10 | function M.prompt_msg(msg, ms) 11 | mp.commandv("show-text", msg, ms) 12 | end 13 | 14 | 15 | function M.show_filename() 16 | local current_filename = mp.get_property("filename") 17 | M.prompt_msg(current_filename, 2000) 18 | end 19 | 20 | -- press SHIFT+ENTER to show current file name 21 | function M.bind_shift_enter() 22 | mp.add_key_binding('SHIFT+ENTER', 'check_file_name', M.show_filename) 23 | end 24 | 25 | 26 | function M.unbind_shift_enter() 27 | mp.remove_key_binding('SHIFT+ENTER') 28 | end 29 | 30 | 31 | -- main function of the file 32 | function M.main() 33 | M.bind_shift_enter() 34 | end 35 | 36 | mp.register_event("file-loaded", M.main) 37 | 38 | 39 | --------------------------------------------------------------------------------