Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

major changes #9

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ the same media file again.
* remove current chapter
* save chapters as a text file, either in the same directory as the media file
or in a global directory
* optionally can hash file paths in an attempt to uniquely identify media files
if chapter files are stored in a central directory
* optionally can read a specified XATTR and/or hash file paths in an attempt to
uniquely identify media files if chapter files are stored in a central
directory
* option to automatically save/load chapter files
* option to use ffmpeg to put the chapters into the media file container as a
metadata, so that other media players, like vlc, can make use of them
* should work on Unix and Windows (tested on Archlinux and Windows 10)

## Installation

* place **chapters.lua** in your **~/.config/mpv/scripts** directory
* optionally create a config file named **chapters.conf** in **~/.config/mpv/scripts-opts**, check out example config file
* place **main.lua** in the **~/.config/mpv/scripts/chapters/** directory, where
**chapters** is used as the name of this script
* optionally create a config file named **chapters.conf** (which must match the
name of the directory as used above) in **~/.config/mpv/scripts-opts**, check
out example config file
* add keybindings to your **~/.config/mpv/input.conf**, example:

```ini
Expand Down
34 changes: 24 additions & 10 deletions chapters.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ask for title or leave it empty when adding a new chapter
# if 'yes', ask for title or leave it empty when adding a new chapter
# it's relevant only if you have https://github.com/CogentRedTester/mpv-user-input
# ask_for_title=yes
# ask_for_title=no

# placeholder text that appears in the prompt when you add or edit a chapter
# it's relevant only if you have https://github.com/CogentRedTester/mpv-user-input
Expand All @@ -16,19 +16,33 @@
# when closing a media file, automatically write the chapter file to disk
# autosave=no

# if 'no', chapter files will be stored in the same directory as the corresponding
# media files, if 'yes' they will be stored in a configured global directory
# if 'yes', chapter files will be stored in the same directory as the
# corresponding media files, unless global_chapters is enabled
# local_chapters=yes

# if 'yes', chapter files will be stored in a configured global directory
# global_chapters=no

# the global dir where chapter files will be stored, if global_chapters=yes
# must be full path, no `~` or such
# chapters_dir=$HOME/.config/mpv/chapters
# must be an absolute path, no `~/`, `${HOME}` or such
# chapters_dir=/home/nonexistent/.config/mpv/scripts/chapters

# if set to a non-empty string, specifies the name of XATTRs that are read from
# media files and whose values (for example some ID or the hash value of the
# file) are used as the names (without file extension) of the respectively
# corresponding chapter files in the configured global directory
# global_chapters_by_xattr=

# affects how chapter files are stored in the global directory
# using hash prevents media files with the same name but in different directories
# from getting the same chapter files, but if you move the file to a different
# directory the hash will change
# using global_chapters_by_hash prevents media files with the same name but in
# different directories from getting the same chapter files, but if you move the
# file to a different directory the hash will change
# if set to 'no', chapter files will be named after corresponding media files,
# it might result in collisions
# hash=yes
# global_chapters_by_hash=yes

# if 'yes', determining the chapter filename in the global directory will fall
# back to using the media filename (which is however prone to filename
# collisions), in case both, global_chapters_by_xattr and
# global_chapters_by_hash, failed or are disabled
# global_chapters_by_filename=yes
208 changes: 164 additions & 44 deletions chapters.lua → main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,23 @@ local chapters_modified = false

local options = {
-- ask for title or leave it empty
ask_for_title = true,
ask_for_title = false,
-- placeholder when asking for title of a new chapter
placeholder_title = "Chapter ",
-- pause the playback when asking for chapter title
pause_on_input = false,
autoload = true,
autosave = false,
-- save all chapter files in a single global directory or next to the playback file
local_chapters = true,
global_chapters = false,
chapters_dir = mp.command_native({"expand-path", "~~home/chapters"}),
-- hash works only with global_chapters enabled
hash = true
chapters_dir = mp.command_native({"expand-path", "~~home/scripts/" .. mp.get_script_name()}),
-- global_chapters_by_xattr works only with global_chapters enabled
global_chapters_by_xattr = "",
-- global_chapters_by_hash works only with global_chapters enabled
global_chapters_by_hash = true,
-- fall back to using the media filename in case both, global_chapters_by_xattr and global_chapters_by_hash, failed or are disabled
global_chapters_by_filename = true
}
opts.read_options(options)

Expand Down Expand Up @@ -89,7 +94,9 @@ local function edit_chapter()
end

if not user_input_module then
msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input")
if options.ask_for_title then
msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input")
end
return
end
-- ask user for chapter title
Expand Down Expand Up @@ -202,7 +209,7 @@ local function command_exists(command, ...)
})

if process.status == 0 then
local command_path = process.stdout:gsub("\n", "")
local command_path = process.stdout:sub(1, -2)
msg.debug("command found:", command_path)
return {command_path, ...}
else
Expand All @@ -228,7 +235,7 @@ local function full_path()
})

if process.status == 0 then
local full_path = process.stdout:gsub("\n", "")
local full_path = process.stdout:sub(1, -2)
msg.debug("windows, full path:", full_path)
return full_path
else
Expand Down Expand Up @@ -257,7 +264,7 @@ local function full_path()
})

if process.status == 0 then
local full_path = process.stdout:gsub("\n", "")
local full_path = process.stdout:sub(1, -2)
msg.debug("unix, full path:", full_path)
return full_path
end
Expand Down Expand Up @@ -300,6 +307,44 @@ local function mkdir(path)
end


-- returns value of the XATTR named via options.global_chapters_by_xattr of the full path of the current media file
local function xattr()
if detect_os() == "windows" then
msg.warn("windows, XATTRs are not supported")
return
end

local path = full_path()
if path == nil then
msg.debug("something is wrong with the path, can't get full_path, can't get XATTR of it")
return
end

msg.debug("read XATTR", options.global_chapters_by_xattr .. ":" , path)

local process = mp.command_native({
name = "subprocess",
capture_stdout = true,
capture_stderr = true,
playback_only = false,
env = {"LC_ALL=C"},
args = {"getfattr", "--only-values", "--name", options.global_chapters_by_xattr, "--", path}
})

if process.status == 0 then
msg.debug("getfattr success:", path)
return process.stdout
else
if process.stderr:find("^.*: No such attribute\n$") then
msg.debug("XATTR unset:", path)
else
msg.error("getfattr failure:", path)
end
return
end
end


-- returns md5 hash of the full path of the current media file
local function hash()
local path = full_path()
Expand Down Expand Up @@ -405,38 +450,58 @@ local function write_chapters(...)
-- figure out the directory
local chapters_dir
if options.global_chapters then
local dir = utils.file_info(options.chapters_dir)
chapters_dir = options.chapters_dir

local dir = utils.file_info(chapters_dir)
if dir then
if dir.is_dir then
msg.debug("options.chapters_dir exists:", options.chapters_dir)
chapters_dir = options.chapters_dir
msg.debug("options.chapters_dir exists:", chapters_dir)
else
msg.error("options.chapters_dir is not a directory")
return
end
else
msg.verbose("options.chapters_dir doesn't exists:", options.chapters_dir)
if mkdir(options.chapters_dir) then
chapters_dir = options.chapters_dir
else
msg.verbose("options.chapters_dir doesn't exists:", chapters_dir)
if not mkdir(chapters_dir) then
return
end
end
else
elseif options.local_chapters then
chapters_dir = utils.split_path(mp.get_property("path"))
else
msg.error("chapters file not written, as both, options.local_chapters and options.global_chapters, are disabled")
return
end

-- and the name
local name = mp.get_property("filename")
if options.hash and options.global_chapters then
name = hash()
if name == nil then
msg.warn("hash function failed, fallback to filename")
name = mp.get_property("filename")
-- and the filename
local filename = nil
if options.global_chapters then
if options.global_chapters_by_xattr ~= "" then
filename = xattr()
if filename == nil then
if options.global_chapters_by_hash then
msg.warn("XATTR ID function failed, fallback to hash function")
else
msg.warn("XATTR ID function failed, fallback to filename")
end
end
end
if filename == nil and options.global_chapters_by_hash then
filename = hash()
if filename == nil then
msg.warn("hash function failed, fallback to filename")
end
end
if filename == nil and not options.global_chapters_by_filename then
msg.warn("chapter file is not written, as all methods for the global directory are disabled")
return
end
end
if filename == nil then
filename = mp.get_property("filename")
end

local chapters_file_path = utils.join_path(chapters_dir, name .. ".ffmetadata")
local chapters_file_path = utils.join_path(chapters_dir, filename .. ".ffmetadata")

msg.debug("opening for writing:", chapters_file_path)
local chapters_file = io.open(chapters_file_path, "w")
Expand All @@ -462,51 +527,106 @@ end

-- priority:
-- 1. chapters file in the same directory as the playing file
-- 2. hashed version of the chapters file in the global directory
-- 3. path based version of the chapters file in the global directory
-- 2. chapters file by XATTR ID in the global directory
-- 3. hashed version of the chapters file in the global directory
-- 4. path based version of the chapters file in the global directory
local function load_chapters()
local path = mp.get_property("path")
local expected_chapters_file = utils.join_path(utils.split_path(path), mp.get_property("filename") .. ".ffmetadata")
local filename = mp.get_property("filename")
local expected_chapters_file = nil
local file = nil

msg.debug("looking for:", expected_chapters_file)
-- try with a chapters file in the same directory as the playing file
if options.local_chapters then
expected_chapters_file = utils.join_path(utils.split_path(path), filename .. ".ffmetadata")

local file = utils.file_info(expected_chapters_file)
msg.debug("looking for:", expected_chapters_file)

if file then
msg.debug("found in the local directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
file = utils.file_info(expected_chapters_file)

if file then
msg.debug("found in the local directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
end
end


-- return if global_chapters is not enabled
if not options.global_chapters then
msg.debug("not in local, global chapters not enabled, aborting search")
if options.local_chapters then
msg.debug("not in local, global chapters not enabled, aborting search")
else
msg.warn("local and global chapters not enabled, aborting search")
end
return
end

msg.debug("looking in the global directory")

if options.hash then

-- try with a chapters file by XATTR ID in the global directory
if options.global_chapters_by_xattr ~= "" then
local xattr_path = xattr()
if xattr_path then
expected_chapters_file = utils.join_path(options.chapters_dir, xattr_path .. ".ffmetadata")

msg.debug("looking for:", expected_chapters_file)

file = utils.file_info(expected_chapters_file)

if file then
msg.debug("found in the global directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
end
else
if options.global_chapters_by_hash then
msg.debug("XATTR ID function failed, fallback to hash function")
else
msg.debug("XATTR ID function failed, fallback to path")
end
end
end


-- try with a hashed version of the chapters file in the global directory
if options.global_chapters_by_hash then
local hashed_path = hash()
if hashed_path then
expected_chapters_file = utils.join_path(options.chapters_dir, hashed_path .. ".ffmetadata")

msg.debug("looking for:", expected_chapters_file)

file = utils.file_info(expected_chapters_file)

if file then
msg.debug("found in the global directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
end
else
msg.debug("hash function failed, fallback to path")
expected_chapters_file = utils.join_path(options.chapters_dir, mp.get_property("filename") .. ".ffmetadata")
end
else
expected_chapters_file = utils.join_path(options.chapters_dir, mp.get_property("filename") .. ".ffmetadata")
end

msg.debug("looking for:", expected_chapters_file)

file = utils.file_info(expected_chapters_file)
-- try with path based version of the chapters file in the global directory
if options.global_chapters_by_filename then
expected_chapters_file = utils.join_path(options.chapters_dir, filename .. ".ffmetadata")

if file then
msg.debug("found in the global directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
msg.debug("looking for:", expected_chapters_file)

file = utils.file_info(expected_chapters_file)

if file then
msg.debug("found in the global directory, loading..")
mp.set_property("file-local-options/chapters-file", expected_chapters_file)
return
end
end


msg.debug("chapters file not found")
end

Expand Down