remove node, make actions extensible
This commit is contained in:
familyfriendlymikey 2023-02-05 01:17:41 -05:00
parent ee703bca50
commit 1a3f23efe4
5 changed files with 384 additions and 510 deletions

4
.gitignore vendored
View File

@ -1,6 +1,2 @@
.DS_Store
test_video*
config.lua
make_cuts
node_modules
dist/

337
README.md
View File

@ -1,33 +1,37 @@
# mpv-cut
## What Is This
This extension allows you to:
The core functionality of this script is to very
quickly cut videos both losslessly and re-encoded-ly
with the help of the fantastic media player
[mpv](https://mpv.io/installation/).
- Quickly cut videos both losslessly and re-encoded-ly.
There is also the added functionality of:
- Logging the timestamps of cuts to a text file for later use with the `make_cuts` script.
- Logging the current timestamp to a bookmark file and reloading them as chapters in mpv.
- Specify custom actions in a `config.lua` file to support your own use cases
without having to modify the script itself or write your own extension.
More details in [usage](#usage).
- Bookmark timestamps to a `.book` file and load them as chapters.
- Save cut information to a `.list` file for backup and make cuts later.
- Choose meaningful channel names to organize the aforementioned actions.
All directly in the fantastic media player [mpv](https://mpv.io/installation/).
## Requirements
Besides mpv, you must have the following in your PATH:
- ffmpeg
- node
Besides mpv, you must have `ffmpeg` in your PATH.
## Installation
#### Linux/MacOS
```
git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git" ~/.config/mpv/scripts/mpv-cut
```
#### Windows
In
`%AppData%\Roaming\mpv\scripts` or `Users\user\scoop\persist\mpv\scripts` run:
```
git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git"
```
@ -39,107 +43,112 @@ That's all you have to do, next time you run mpv the script will be automaticall
### Cutting A Video Losslessly
- Press `c` to begin a cut.
- Seek to a later time in the video.
- Press `c` again to make the cut.
The resulting cut will be placed in the same directory as the source file.
### Other Actions
### Actions
You can press `a` to cycle between the two default actions:
You can press `a` to cycle between three default actions:
- Copy (lossless cut, rounds to keyframes).
- Encode (re-encoded cut, exact).
### Cutting To Global Dir
- List (simply add the timestamps for the cut to a `.list` file).
You can press `g` to toggle saving to the
configured global directory as opposed to
the same directory as the source file.
### Cut Lists
Instead of making cuts immediately, you can choose to store all cuts
in a "cut list" text file.
Toggle this behavior with the `l` key.
When you're ready to make your cuts, press `0` in mpv.
The `make_cuts` script can also be invoked directly.
The very last argument must be rows of JSON.
In the case of a cut list, you can simply do
```
./make_cuts "$(cat cut_list_name.list)"
```
Preceding the aforementioned JSON argument may be 0 to 2 arguments:
- If there are `0` arguments,
it will use the current working directory
as the input dir to look for the filenames of the cuts passed
to it, and will also output cuts to the current directory.
- If there is `1` argument, it will use it
as both the indir and outdir.
- If there are `2` arguments, it will use the first one
as the indir and the second one as the outdir.
If any of the filenames do not exist in the indir,
the ffmpeg command for that cut will simply fail.
`mpv-cut` uses an extensible list of *actions* that you can modify in your
`config.lua`. This makes it easy to change the `ffmpeg` command (or any command
for that matter) to suit your specific situation. It should be possible to
create custom actions with limited coding knowledge. More details in
[config](#config).
### Bookmarking
Press `i` to append the current timestamp to a bookmark text file.
This automatically reloads the timestamps as chapters in mpv.
You can navigate between these chapters with the default mpv bindings,
`!` and `@`.
Press `i` to append the current timestamp to a `.book` file. This
automatically reloads the timestamps as chapters in mpv. You can navigate
between these chapters with the default mpv bindings, `!` and `@`.
### Channels
The resulting cuts and bookmark files will be prefixed a channel
number. This is to help you categorize cuts and bookmarks. You
can press `-` to decrement the channel and `=` to increment the
channel.
The resulting cuts and bookmark files will be prefixed a channel number. This
is to help you categorize cuts and bookmarks. You can press `-` to decrement
the channel and `=` to increment the channel.
You can configure a name for each channel as shown below.
### Making Cuts
If you want to make all the cuts stored in a cut list, simply press `0`.
## Config
You can configure settings by creating a `config.lua` file
in the same directory as `main.lua`.
You can configure settings by creating a `config.lua` file in the same
directory as `main.lua`.
You can include or omit any of the following:
```lua
-- Set to true if you want to use the global dir by default.
USE_GLOBAL_DIR = true
-- Configure your global directory
GLOBAL_DIR = "~/Desktop"
-- Set to true if you want to use cut lists by default.
USE_CUT_LIST = false
-- The list of actions to cycle through, in order.
ACTIONS = { "COPY", "ENCODE" }
-- Key config
KEY_CUT = "c"
KEY_CYCLE_ACTION = "a"
KEY_BOOKMARK_ADD = "i"
KEY_CHANNEL_INC = "="
KEY_CHANNEL_DEC = "-"
KEY_MAKE_CUTS = "0"
-- The list of channel names, you can choose whatever you want.
CHANNEL_NAMES = {}
CHANNEL_NAMES[1] = "FUNNY"
CHANNEL_NAMES[2] = "COOL"
-- The default channel
CHANNEL = 1
-- Key config
KEY_CUT = "c"
KEY_MAKE_CUTS = "0"
KEY_CYCLE_ACTION = "a"
KEY_TOGGLE_USE_GLOBAL_DIR = "g"
KEY_TOGGLE_USE_CUT_LIST = "l"
KEY_BOOKMARK_ADD = "i"
KEY_CHANNEL_INC = "="
KEY_CHANNEL_DEC = "-"
-- The default action
ACTION = ACTIONS.ENCODE
-- The action to use when making cuts from a cut list
MAKE_CUT = ACTIONS.COPY
-- Delete a default action
ACTIONS.LIST = nil
-- Specify custom actions
ACTIONS.ENCODE = function(d)
local args = {
"ffmpeg",
"-nostdin", "-y",
"-loglevel", "error",
"-i", d.inpath,
"-ss", d.start_time,
"-t", d.duration,
"-pix_fmt", "yuv420p",
"-crf", "16",
"-preset", "superfast",
utils.join_path(d.indir, "ENCODE_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
}
mp.command_native_async({
name = "subprocess",
args = args,
playback_only = false,
}, function() print("Done") end)
end
-- The table that gets passed to an action will have the following properties:
-- inpath, indir, infile, infile_noext, ext
-- channel
-- start_time, end_time, duration
-- start_time_hms, end_time_hms, duration_hms
```
## Optimized MPV Input Config
This is my `input.conf` file, and it is optimized
for both normal playback and quickly editing videos.
This is my `input.conf` file, and it is optimized for both normal playback and
quickly editing videos.
```
RIGHT seek 2 exact
LEFT seek -2 exact
@ -155,165 +164,137 @@ UP seek 0.01 keyframes # Seek by keyframes only.
DOWN seek -0.01 keyframes # Seek by keyframes only.
```
You may also want to change your key repeat delay and rate
by tweaking `input-ar-delay` and `input-ar-rate`
to your liking in `mpv.conf`.
You may also want to change your key repeat delay and rate by tweaking
`input-ar-delay` and `input-ar-rate` to your liking in `mpv.conf`.
## FAQ
### Why Do I Need Node?
The actual `mpv-cut` extension acts as a sort of minimal interface to an
arbitrary `make_cuts` binary. This way, users can extend the
functionality using whatever language they want, without being tied to
LUA and relying on `mpv`'s API. Most people on GitHub know how to code
in some language, but not everyone wants to learn LUA and an API to
cut their videos.
I chose to write the default `make_cuts` script in Imba, an extremely
underrated language that compiles to readable JavaScript. Python would
have been a good candidate as well, but Python's VM takes
significantly longer to start up than Node's which I didn't like.
### What Is The Point Of A Cut List?
There are plenty of reasons, but to give some examples:
- Video seems to be pretty complex, at least to me.
One video file may cause certain issues,
and another may not, which makes writing
an ffmpeg command that accounts for all scenarios difficult.
If you spend a ton of time making many cuts in a long movie
only to find that the colors look off because of some
10-bit h265 dolby mega surround whatever the fuck,
with a cut list it's trivial to edit
the ffmpeg command and re-make the cuts.
- Video seems to be pretty complex, at least to me. One video file may cause
certain issues, and another may not, which makes writing an ffmpeg command
that accounts for all scenarios difficult. If you spend a ton of time making
many cuts in a long movie only to find that the colors look off because of
some 10-bit h265 dolby mega surround whatever the fuck, with a cut list it's
trivial to edit the ffmpeg command and re-make the cuts.
- Maybe you forget that the foreign language video you're cutting has
softsubs rather than hardsubs,
and you make a bunch of encode cuts
resulting in cuts that have no subtitles.
- Maybe you forget that the foreign language video you're cutting has softsubs
rather than hardsubs, and you make a bunch of encode cuts resulting in cuts
that have no subtitles.
- You delete the source video for storage reasons,
but still want to have a back up of the cut timestamps
in the event you need to remake the cuts.
- You delete the source video for storage reasons, but still want to have a
back up of the cut timestamps in the event you need to remake the cuts.
### Why Would I Bookmark Instead Of Cutting?
Suppose you're watching a movie or show for your own enjoyment,
but you also want to compile funny moments to post online
or send to your friends.
It would ruin your viewing experience to wait for a funny
moment to be over in order to make a cut.
Instead, you can quickly make a bookmark whenever you laugh,
and once you're done watching you can go back and make actual cuts.
Suppose you're watching a movie or show for your own enjoyment, but you also
want to compile funny moments to post online or send to your friends. It would
ruin your viewing experience to wait for a funny moment to be over in order to
make a cut. Instead, you can quickly make a bookmark whenever you laugh, and
once you're done watching you can go back and make actual cuts.
### Why Is Lossless Cutting Called "Copy"?
This refers to ffmpeg's `-copy` flag which copies the input stream
instead of re-encoding it, meaning that the cut will process
extremely quickly and the resulting video will retain
100% of the original quality.
The main drawback is that the cut may have some extra
video at the beginning and end,
and as a result of that there may be some slightly wonky behavior
with video players and editors.
This refers to ffmpeg's `-copy` flag which copies the input stream instead of
re-encoding it, meaning that the cut will process extremely quickly and the
resulting video will retain 100% of the original quality. The main drawback is
that the cut may have some extra video at the beginning and end, and as a
result of that there may be some slightly wonky behavior with video players and
editors.
### Why Would I Re-Encode A Video?
- As mentioned above, copying the input stream is very fast and lossless
but the cuts are not exact. Sometimes you want a cut to be exact.
- As mentioned above, copying the input stream is very fast and lossless but
the cuts are not exact. Sometimes you want a cut to be exact.
- If you want to change the framerate.
- If you want to encode hardsubs.
- If the video's compression isn't efficient enough to upload
to a messaging platform or something, you may want to compress it more.
- If the video's compression isn't efficient enough to upload to a messaging
platform or something, you may want to compress it more.
### How Can I Merge (Concatenate) The Resulting Cuts Into One File?
To concatenate videos with ffmpeg,
you need to create a file with content like this:
To concatenate videos with ffmpeg, you need to create a file with content like
this:
```
file cut_1.mp4
file cut_2.mp4
file cut_3.mp4
file cut_4.mp4
```
You can name the file whatever you want, here I named it `concat.txt`.
Then run the command:
```
ffmpeg -f concat -safe 0 -i concat.txt -c copy out.mp4
```
That's annoying though, so you can skip
manually creating the file by using bash.
This command will concatenate all files in the current directory
that begin with "COPY_":
That's annoying though, so you can skip manually creating the file by using
bash. This command will concatenate all files in the current directory that
begin with "COPY_":
```
ffmpeg -f concat -safe 0 -i <(printf 'file %q\n' "$PWD"/COPY_*) -c copy lol.mp4
```
- You need to escape apostrophes which is why
we are using `printf %q "$string"`.
- Instead of actually creating a file we just
use process substitution `<(whatever)` to create a temporary file,
which is why we need the `$PWD` in there for the absolute path.
- You need to escape apostrophes which is why we are using `printf %q
"$string"`.
- Instead of actually creating a file we just use process substitution
`<(whatever)` to create a temporary file, which is why we need the `$PWD` in
there for the absolute path.
You can also do it in vim, among other things.
```
ls | vim -
:%s/'/\\'/g
:%norm Ifile
:wq concat.txt
```
This substitution might not cover all cases, but whatever,
if you're concatenating a file named `[{}1;']["!.mp4`
you can figure it out yourself.
This substitution might not cover all cases, but whatever, if you're
concatenating a file named `[{}1;']["!.mp4` you can figure it out yourself.
### Can I Make Seeking And Reverse Playback Faster?
Depending on the encoding of the video file being played, the following may be quite slow:
Depending on the encoding of the video file being played, the following may be
quite slow:
- The use of `exact` in `input.conf`.
- The use of the `.` and `,` keys to go frame by frame.
- The holding down of the `,` key to play the video in reverse.
Long story short, if the video uses an encoding that is difficult for mpv to decode,
exact seeking and backwards playback won't be smooth, which for normal playback is not a problem at all,
since by default mpv very quickly seeks keyframe-wise when you press `left arrow` or `right arrow`.
Long story short, if the video uses an encoding that is difficult for mpv to
decode, exact seeking and backwards playback won't be smooth, which for normal
playback is not a problem at all, since by default mpv very quickly seeks
keyframe-wise when you press `left arrow` or `right arrow`.
However if we are very intensively cutting a video,
it may be useful to be able to quickly seek to an exact time, and to quickly play in reverse.
In this case, it is useful to first make a proxy of the original video which is very easy to decode,
generate a cut list with the proxy, and then apply the cut list to the original video.
However if we are very intensively cutting a video, it may be useful to be able
to quickly seek to an exact time, and to quickly play in reverse. In this case,
it is useful to first make a proxy of the original video which is very easy to
decode, generate a cut list with the proxy, and then apply the cut list to the
original video.
To create a proxy which will be very easy to decode, you can use this ffmpeg command:
```
ffmpeg -noautorotate -i input.mp4 -pix_fmt yuv420p -g 1 -sn -an -vf colormatrix=bt601:bt709,scale=w=1280:h=1280:force_original_aspect_ratio=decrease:force_divisible_by=2 -c:v libx264 -crf 16 -preset superfast -tune fastdecode proxy.mp4
```
The important options here are the `-g 1` and the scale filter.
The other options are more or less irrelevant.
The resulting video file should seek extremely quickly
and play backwards just fine.
Once you are done generating the cut list,
simply open the `cut_list.txt` file,
substitute the proxy file name for the original file name,
and run `make_cuts` on it.
The important options here are the `-g 1` and the scale filter. The other
options are more or less irrelevant. The resulting video file should seek
extremely quickly and play backwards just fine.
## Development
Uploading releases to GitHub is a pain, so I thought why not create an orphaned branch for releases.
To set it up, all I did was:
```
git checkout --orphan release
```
Then, when I want to release a new version, until I find a better way I just do:
```
TMPDIR=`mktemp -d`
cp dist/* "$TMPDIR"
git checkout release
git rm -r '*'
cp "$TMPDIR"/* .
git add --all
git commit -m "version number"
git push
```
Once you are done generating the cut list, simply open the `cut_list.txt` file,
substitute the proxy file name for the original file name, and run `make_cuts`
on it.

400
main.lua
View File

@ -1,34 +1,158 @@
utils = require "mp.utils"
pcall(require, "config")
mp.msg.info("MPV-CUT LOADED.")
if USE_GLOBAL_DIR == nil then USE_GLOBAL_DIR = true end
if GLOBAL_DIR == nil then GLOBAL_DIR = "~/Desktop" end
if USE_CUT_LIST == nil then USE_CUT_LIST = false end
if ACTIONS == nil then ACTIONS = { "COPY", "ENCODE" } end
if CHANNEL_NAMES == nil then CHANNEL_NAMES = {} end
if CHANNEL == nil then CHANNEL = 1 end
KEY_CUT = KEY_CUT or "c"
KEY_MAKE_CUTS = KEY_MAKE_CUTS or "0"
KEY_CYCLE_ACTION = KEY_CYCLE_ACTION or "a"
KEY_TOGGLE_USE_GLOBAL_DIR = KEY_TOGGLE_USE_GLOBAL_DIR or "g"
KEY_TOGGLE_USE_CUT_LIST = KEY_TOGGLE_USE_CUT_LIST or "l"
KEY_BOOKMARK_ADD = KEY_BOOKMARK_ADD or "i"
KEY_CHANNEL_INC = KEY_CHANNEL_INC or "="
KEY_CHANNEL_DEC = KEY_CHANNEL_DEC or "-"
GLOBAL_DIR = mp.command_native({"expand-path", GLOBAL_DIR})
ACTION = ACTIONS[1]
MAKE_CUTS_SCRIPT_PATH = utils.join_path(mp.get_script_directory(), "make_cuts")
START_TIME = nil
local function print(s)
mp.msg.info(s)
mp.osd_message(s)
end
local function table_to_str(o)
if type(o) == 'table' then
local s = ''
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. table_to_str(v) .. '\n'
end
return s
else
return tostring(o)
end
end
local function to_hms(seconds)
local ms = math.floor((seconds - math.floor(seconds)) * 1000)
local secs = math.floor(seconds)
local mins = math.floor(secs / 60)
secs = secs % 60
local hours = math.floor(mins / 60)
mins = mins % 60
return string.format("%02d-%02d-%02d-%03d", hours, mins, secs, ms)
end
local function next_table_key(t, current)
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
table.sort(keys)
for i = 1, #keys do
if keys[i] == current then
return keys[(i % #keys) + 1]
end
end
return keys[1]
end
ACTIONS = {}
ACTIONS.COPY = function(d)
local args = {
"ffmpeg",
"-nostdin", "-y",
"-loglevel", "error",
"-ss", d.start_time,
"-t", d.duration,
"-i", d.inpath,
"-pix_fmt", "yuv420p",
"-c", "copy",
"-map", "0",
"-avoid_negative_ts", "make_zero",
utils.join_path(d.indir, "COPY_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
}
mp.command_native_async({
name = "subprocess",
args = args,
playback_only = false,
}, function() print("Done") end)
end
ACTIONS.ENCODE = function(d)
local args = {
"ffmpeg",
"-nostdin", "-y",
"-loglevel", "error",
"-i", d.inpath,
"-ss", d.start_time,
"-t", d.duration,
"-pix_fmt", "yuv420p",
"-crf", "16",
"-preset", "superfast",
utils.join_path(d.indir, "ENCODE_" .. d.channel .. "_" .. d.infile_noext .. "_FROM_" .. d.start_time_hms .. "_TO_" .. d.end_time_hms .. d.ext)
}
mp.command_native_async({
name = "subprocess",
args = args,
playback_only = false,
}, function() print("Done") end)
end
ACTIONS.LIST = function(d)
local inpath = mp.get_property("path")
local outpath = inpath .. ".list"
local file = io.open(outpath, "a")
if not file then print("Error writing to cut list") return end
local filesize = file:seek("end")
local s = "\n" .. d.channel
.. ":" .. d.start_time
.. ":" .. d.end_time
file:write(s)
local delta = file:seek("end") - filesize
io.close(file)
print("Δ " .. delta)
end
ACTION = "COPY"
MAKE_CUT = ACTIONS.COPY
CHANNEL = 1
CHANNEL_NAMES = {}
KEY_CUT = "c"
KEY_CYCLE_ACTION = "a"
KEY_BOOKMARK_ADD = "i"
KEY_CHANNEL_INC = "="
KEY_CHANNEL_DEC = "-"
KEY_MAKE_CUTS = "0"
pcall(require, "config")
mp.msg.info("MPV-CUT LOADED.")
for i, v in ipairs(CHANNEL_NAMES) do
CHANNEL_NAMES[i] = string.gsub(v, ":", "-")
end
if not ACTIONS[ACTION] then ACTION = next_table_key(ACTIONS, nil) end
START_TIME = nil
local function get_current_channel_name()
return CHANNEL_NAMES[CHANNEL] or tostring(CHANNEL)
end
local function get_data()
local d = {}
d.inpath = mp.get_property("path")
d.indir = utils.split_path(d.inpath)
d.infile = mp.get_property("filename")
d.infile_noext = mp.get_property("filename/no-ext")
d.ext = mp.get_property("filename"):match("^.+(%..+)$") or ".mp4"
d.channel = get_current_channel_name()
return d
end
local function get_times(start_time, end_time)
local d = {}
d.start_time = tostring(start_time)
d.end_time = tostring(end_time)
d.duration = tostring(end_time - start_time)
d.start_time_hms = tostring(to_hms(start_time))
d.end_time_hms = tostring(to_hms(end_time))
d.duration_hms = tostring(to_hms(end_time - start_time))
return d
end
text_overlay = mp.create_osd_overlay("ass-events")
text_overlay.hidden = true
text_overlay:update()
@ -40,18 +164,9 @@ local function text_overlay_off()
text_overlay:update()
end
local function get_current_channel_name()
return CHANNEL_NAMES[CHANNEL] or CHANNEL
end
local function text_overlay_on()
local channel = get_current_channel_name()
if USE_CUT_LIST then
text_overlay.data = string.format("LIST %s in %s from %s", ACTION, channel, START_TIME)
else
text_overlay.data = (USE_GLOBAL_DIR and "GLOBAL " or "")
.. string.format("%s in %s from %s", ACTION, channel, START_TIME)
end
text_overlay.data = string.format("%s in %s from %s", ACTION, channel, START_TIME)
text_overlay.hidden = false
text_overlay:update()
end
@ -60,156 +175,42 @@ local function print_or_update_text_overlay(content)
if START_TIME then text_overlay_on() else print(content) end
end
local function toggle_use_cut_list()
USE_CUT_LIST = not USE_CUT_LIST
print_or_update_text_overlay("USE CUT LIST: " .. tostring(USE_CUT_LIST))
end
local function toggle_use_global_dir()
USE_GLOBAL_DIR = not USE_GLOBAL_DIR
print_or_update_text_overlay("USE GLOBAL DIR: " .. tostring(USE_GLOBAL_DIR))
end
local function index_of(list, string)
local index = 1
while index < #list do
if list[index] == string then return index end
index = index + 1
end
return 0
end
local function cycle_action()
ACTION = ACTIONS[index_of(ACTIONS, ACTION) + 1]
ACTION = next_table_key(ACTIONS, ACTION)
print_or_update_text_overlay("ACTION: " .. ACTION)
end
local function get_file_info()
local inpath = mp.get_property("path")
local filename = mp.get_property("filename")
local channel_name = get_current_channel_name()
return inpath, filename, channel_name
end
local function get_bookmark_file_path()
local inpath, filename, channel_name = get_file_info()
local indir = utils.split_path(inpath)
local outfile = string.format("%s_%s.book", channel_name, filename)
return utils.join_path(indir, outfile)
end
local function bookmarks_load()
local inpath, filename, channel_name = get_file_info()
local inpath = get_bookmark_file_path()
local function make_cuts()
print("MAKING CUTS")
if not MAKE_CUT then print("MAKE_CUT function not found.") return end
local inpath = mp.get_property("path") .. ".list"
local file = io.open(inpath, "r")
if not file then
mp.set_property_native("chapter-list", {})
return
end
local arr = {}
if not file then print("Error reading cut list") return end
for line in file:lines() do
if tonumber(line) then
table.insert(arr, {
time = tonumber(line),
title = "chapter_" .. line
})
if line ~= "" then
local cut = {}
for token in string.gmatch(line, "[^" .. ":" .. "]+") do
table.insert(cut, token)
end
local d = get_data()
d.channel = cut[1]
local t = get_times(tonumber(cut[2]), tonumber(cut[3]))
for k, v in pairs(t) do d[k] = v end
mp.msg.info("MAKE_CUT")
mp.msg.info(table_to_str(d))
MAKE_CUT(d)
end
end
file:close()
table.sort(arr, function(a, b) return a.time < b.time end)
mp.set_property_native("chapter-list", arr)
end
local function bookmark_add()
local inpath, filename, channel_name = get_file_info()
local outpath = get_bookmark_file_path()
local file = io.open(outpath, "a")
if not file then print("Failed to open bookmark file for writing") return end
local out_string = mp.get_property_number("time-pos") .. "\n"
local filesize = file:seek("end")
file:write(out_string)
local delta = file:seek("end") - filesize
io.close(file)
bookmarks_load()
print(string.format("Δ %s, %s", delta, channel_name))
end
local function channel_inc()
CHANNEL = CHANNEL + 1
bookmarks_load()
print_or_update_text_overlay(get_current_channel_name())
end
local function channel_dec()
if CHANNEL >= 2 then CHANNEL = CHANNEL - 1 end
bookmarks_load()
print_or_update_text_overlay(get_current_channel_name())
end
local function print_async_result(success, result, error)
print("Done")
end
local function make_cuts()
local inpath, filename, channel_name = get_file_info()
local indir = utils.split_path(inpath)
local file = io.open(inpath .. ".list", "r")
local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir }
if USE_GLOBAL_DIR then table.insert(args, GLOBAL_DIR) end
if file ~= nil then
print("Making cuts")
local json_string = file:read("*all")
table.insert(args, json_string)
mp.command_native_async({
name = "subprocess",
playback_only = false,
args = args
}, print_async_result)
else
print("Failed to load cut list")
end
end
local function make_cut(json_string)
local inpath, filename, channel_name = get_file_info()
local indir = utils.split_path(inpath)
local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir }
if USE_GLOBAL_DIR then table.insert(args, GLOBAL_DIR) end
table.insert(args, json_string)
print("Making cut")
mp.command_native_async({
name = "subprocess",
playback_only = false,
args = args,
}, print_async_result)
end
local function write_cut_list(json_string)
local inpath, filename, channel_name = get_file_info()
local outpath = inpath .. ".list"
local file = io.open(outpath, "a")
if not file then print("Error writing to cut list") return end
local filesize = file:seek("end")
file:write(json_string)
local delta = file:seek("end") - filesize
io.close(file)
print("Δ " .. delta)
end
local function cut(start_time, end_time)
local inpath, filename, channel_name = get_file_info()
local json_string = "{ "
.. string.format("%q: %q", "filename", filename)
.. string.format(", %q: %q", "action", ACTION)
.. string.format(", %q: %q", "channel", channel_name)
.. string.format(", %q: %q", "start_time", start_time)
.. string.format(", %q: %q", "end_time", end_time)
.. " }\n"
if USE_CUT_LIST then
write_cut_list(json_string)
else
make_cut(json_string)
end
local d = get_data()
local t = get_times(start_time, end_time)
for k, v in pairs(t) do d[k] = v end
mp.msg.info(ACTION)
mp.msg.info(table_to_str(d))
ACTIONS[ACTION](d)
end
local function put_time()
@ -229,13 +230,62 @@ local function put_time()
end
end
mp.add_key_binding(KEY_MAKE_CUTS, "make_cuts", make_cuts)
local function get_bookmark_file_path()
local d = get_data()
mp.msg.info(table_to_str(d))
local outfile = string.format("%s_%s.book", d.channel, d.infile)
return utils.join_path(d.indir, outfile)
end
local function bookmarks_load()
local inpath = get_bookmark_file_path()
local file = io.open(inpath, "r")
if not file then return end
local arr = {}
for line in file:lines() do
if tonumber(line) then
table.insert(arr, {
time = tonumber(line),
title = "chapter_" .. line
})
end
end
file:close()
table.sort(arr, function(a, b) return a.time < b.time end)
mp.set_property_native("chapter-list", arr)
end
local function bookmark_add()
local d = get_data()
local outpath = get_bookmark_file_path()
local file = io.open(outpath, "a")
if not file then print("Failed to open bookmark file for writing") return end
local out_string = mp.get_property_number("time-pos") .. "\n"
local filesize = file:seek("end")
file:write(out_string)
local delta = file:seek("end") - filesize
io.close(file)
bookmarks_load()
print(string.format("Δ %s, %s", delta, d.channel))
end
local function channel_inc()
CHANNEL = CHANNEL + 1
bookmarks_load()
print_or_update_text_overlay(get_current_channel_name())
end
local function channel_dec()
if CHANNEL >= 2 then CHANNEL = CHANNEL - 1 end
bookmarks_load()
print_or_update_text_overlay(get_current_channel_name())
end
mp.add_key_binding(KEY_CUT, "cut", put_time)
mp.add_key_binding(KEY_BOOKMARK_ADD, "bookmark_add", bookmark_add)
mp.add_key_binding(KEY_CHANNEL_INC, "channel_inc", channel_inc)
mp.add_key_binding(KEY_CHANNEL_DEC, "channel_dec", channel_dec)
mp.add_key_binding(KEY_CYCLE_ACTION, "cycle_action", cycle_action)
mp.add_key_binding(KEY_TOGGLE_USE_GLOBAL_DIR, "toggle_use_global_dir", toggle_use_global_dir)
mp.add_key_binding(KEY_TOGGLE_USE_CUT_LIST, "toggle_use_cut_list", toggle_use_cut_list)
mp.add_key_binding(KEY_MAKE_CUTS, "make_cuts", make_cuts)
mp.register_event('file-loaded', bookmarks_load)

View File

@ -1,122 +0,0 @@
let { readFileSync, statSync } = require 'fs'
let { spawnSync } = require 'child_process'
let path = require "path"
let p = console.log
let red = "\x1b[31m"
let plain = "\x1b[0m"
let green = "\x1b[32m"
let purple = "\x1b[34m"
p "IN MAKE_CUTS"
def quit s
p "{red}{s}, quitting.{plain}\n"
process.exit!
def is_dir s
try
return yes if statSync(s).isDirectory!
catch
return no
def parse_json data
let failed = new Set!
let succeeded = new Set!
for line in data.split '\n'
line = line.trim!
continue if line.length < 1
try
JSON.parse line
succeeded.add line
catch
failed.add line
failed = [...failed]
succeeded = [...succeeded]
failed.length > 0 and p "\n{red}Failed to load JSON for lines:{plain} {failed}"
succeeded.length > 0 and p "\n{green}Cut list:{plain} {succeeded}\n"
succeeded.map! do |x| JSON.parse(x)
def to_hms secs
[
Math.floor secs / 3600
Math.floor (secs % 3600) / 60
Math.floor ((secs % 3600 % 60) * 1000) / 1000
].map(do String($1).padStart(2,0)).join("-") +
'.' + String(Math.floor secs % 1 * 1000).padEnd(3,0)
def main
let argv = process.argv.slice(2)
p "ARGS:"
p argv
let json = argv.pop!
let indir
let outdir
switch argv.length
when 0
indir = outdir = "."
when 1
indir = outdir = argv.pop!
when 2
[indir, outdir] = argv
else
quit "Invalid args: {process.argv}"
let cut_list = parse_json json
quit "No valid cuts" if cut_list.length < 1
quit "Input directory is invalid" if not is_dir indir
quit "Output directory is invalid" if not is_dir outdir
for cut, index in cut_list
let { filename, action, channel, start_time, end_time } = cut
let { name: filename_noext, ext } = path.parse(filename)
let duration = parseFloat(end_time) - parseFloat(start_time)
cut_name = "{action}_{channel}_{filename_noext}_FROM_{to_hms(start_time)}_TO_{to_hms(end_time)}{ext}"
let inpath = path.join(indir, filename)
let outpath = path.join(outdir, cut_name)
let cmd = "ffmpeg"
let args = [
"-nostdin", "-y"
"-loglevel", "error"
"-ss", start_time
"-t", duration
"-i", inpath
"-pix_fmt", "yuv420p"
]
if action is "ENCODE"
args.push(
"-crf", "16"
"-preset", "superfast"
)
else
args.push(
"-c", "copy"
"-map", "0"
"-avoid_negative_ts", "make_zero"
)
args.push(outpath)
let progress = "({index + 1}/{cut_list.length})"
let cmd_str = "{cmd} {args.join(" ")}"
p "{green}{progress}{plain} {inpath} {green}->{plain}"
p "{outpath}\n"
p "{purple}{cmd_str}{plain}\n"
spawnSync cmd, args, { stdio: 'inherit' }
p "Done.\n"
main!

View File

@ -1,31 +0,0 @@
{
"name": "mpv-cut",
"version": "1.3.0",
"description": "Quickly cut videos both losslessly and re-encoded-ly.",
"scripts": {
"build:make-dist": "mkdir -p dist",
"build:add-hashbang": "echo '#! /usr/bin/env node\n' > dist/make_cuts",
"build:compile-imba": "imbac -p make_cuts.imba >> dist/make_cuts",
"build:copy-lua": "cp main.lua dist/",
"build:copy-package.json": "cp package.json dist/",
"build": "npm exec npm-run-all build:*",
"release": "npm run build && npx gh-pages --branch release --dist dist/ --message $npm_package_version"
},
"repository": {
"type": "git",
"url": "git+https://github.com/familyfriendlymikey/mpv-cut.git"
},
"author": "Mikey Oz",
"bugs": {
"url": "https://github.com/familyfriendlymikey/mpv-cut/issues"
},
"homepage": "https://github.com/familyfriendlymikey/mpv-cut#readme",
"devDependencies": {},
"keywords": [
"video",
"cut",
"mpv",
"slice",
"editor"
]
}