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 .DS_Store
test_video*
config.lua config.lua
make_cuts
node_modules
dist/

337
README.md
View File

@ -1,33 +1,37 @@
# mpv-cut # 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.
quickly cut videos both losslessly and re-encoded-ly
with the help of the fantastic media player
[mpv](https://mpv.io/installation/).
There is also the added functionality of: - Specify custom actions in a `config.lua` file to support your own use cases
- Logging the timestamps of cuts to a text file for later use with the `make_cuts` script. without having to modify the script itself or write your own extension.
- Logging the current timestamp to a bookmark file and reloading them as chapters in mpv.
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 ## Requirements
Besides mpv, you must have the following in your PATH:
- ffmpeg Besides mpv, you must have `ffmpeg` in your PATH.
- node
## Installation ## Installation
#### Linux/MacOS #### Linux/MacOS
``` ```
git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git" ~/.config/mpv/scripts/mpv-cut git clone -b release --single-branch "https://github.com/familyfriendlymikey/mpv-cut.git" ~/.config/mpv/scripts/mpv-cut
``` ```
#### Windows #### Windows
In In
`%AppData%\Roaming\mpv\scripts` or `Users\user\scoop\persist\mpv\scripts` run: `%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" 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 ### Cutting A Video Losslessly
- Press `c` to begin a cut. - Press `c` to begin a cut.
- Seek to a later time in the video. - Seek to a later time in the video.
- Press `c` again to make the cut. - Press `c` again to make the cut.
The resulting cut will be placed in the same directory as the source file. 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). - Copy (lossless cut, rounds to keyframes).
- Encode (re-encoded cut, exact). - 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 `mpv-cut` uses an extensible list of *actions* that you can modify in your
configured global directory as opposed to `config.lua`. This makes it easy to change the `ffmpeg` command (or any command
the same directory as the source file. for that matter) to suit your specific situation. It should be possible to
create custom actions with limited coding knowledge. More details in
### Cut Lists [config](#config).
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.
### Bookmarking ### Bookmarking
Press `i` to append the current timestamp to a bookmark text file. Press `i` to append the current timestamp to a `.book` file. This
This automatically reloads the timestamps as chapters in mpv. automatically reloads the timestamps as chapters in mpv. You can navigate
You can navigate between these chapters with the default mpv bindings, between these chapters with the default mpv bindings, `!` and `@`.
`!` and `@`.
### Channels ### Channels
The resulting cuts and bookmark files will be prefixed a channel The resulting cuts and bookmark files will be prefixed a channel number. This
number. This is to help you categorize cuts and bookmarks. You is to help you categorize cuts and bookmarks. You can press `-` to decrement
can press `-` to decrement the channel and `=` to increment the the channel and `=` to increment the channel.
channel.
You can configure a name for each channel as shown below. 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 ## Config
You can configure settings by creating a `config.lua` file You can configure settings by creating a `config.lua` file in the same
in the same directory as `main.lua`. directory as `main.lua`.
You can include or omit any of the following: You can include or omit any of the following:
```lua ```lua
-- Set to true if you want to use the global dir by default. -- Key config
USE_GLOBAL_DIR = true KEY_CUT = "c"
KEY_CYCLE_ACTION = "a"
-- Configure your global directory KEY_BOOKMARK_ADD = "i"
GLOBAL_DIR = "~/Desktop" KEY_CHANNEL_INC = "="
KEY_CHANNEL_DEC = "-"
-- Set to true if you want to use cut lists by default. KEY_MAKE_CUTS = "0"
USE_CUT_LIST = false
-- The list of actions to cycle through, in order.
ACTIONS = { "COPY", "ENCODE" }
-- The list of channel names, you can choose whatever you want. -- The list of channel names, you can choose whatever you want.
CHANNEL_NAMES = {}
CHANNEL_NAMES[1] = "FUNNY" CHANNEL_NAMES[1] = "FUNNY"
CHANNEL_NAMES[2] = "COOL" CHANNEL_NAMES[2] = "COOL"
-- The default channel -- The default channel
CHANNEL = 1 CHANNEL = 1
-- Key config -- The default action
KEY_CUT = "c" ACTION = ACTIONS.ENCODE
KEY_MAKE_CUTS = "0"
KEY_CYCLE_ACTION = "a" -- The action to use when making cuts from a cut list
KEY_TOGGLE_USE_GLOBAL_DIR = "g" MAKE_CUT = ACTIONS.COPY
KEY_TOGGLE_USE_CUT_LIST = "l"
KEY_BOOKMARK_ADD = "i" -- Delete a default action
KEY_CHANNEL_INC = "=" ACTIONS.LIST = nil
KEY_CHANNEL_DEC = "-"
-- 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 ## Optimized MPV Input Config
This is my `input.conf` file, and it is optimized This is my `input.conf` file, and it is optimized for both normal playback and
for both normal playback and quickly editing videos. quickly editing videos.
``` ```
RIGHT seek 2 exact RIGHT seek 2 exact
LEFT 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. DOWN seek -0.01 keyframes # Seek by keyframes only.
``` ```
You may also want to change your key repeat delay and rate You may also want to change your key repeat delay and rate by tweaking
by tweaking `input-ar-delay` and `input-ar-rate` `input-ar-delay` and `input-ar-rate` to your liking in `mpv.conf`.
to your liking in `mpv.conf`.
## FAQ ## 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? ### What Is The Point Of A Cut List?
There are plenty of reasons, but to give some examples: There are plenty of reasons, but to give some examples:
- Video seems to be pretty complex, at least to me. - Video seems to be pretty complex, at least to me. One video file may cause
One video file may cause certain issues, certain issues, and another may not, which makes writing an ffmpeg command
and another may not, which makes writing that accounts for all scenarios difficult. If you spend a ton of time making
an ffmpeg command that accounts for all scenarios difficult. many cuts in a long movie only to find that the colors look off because of
If you spend a ton of time making many cuts in a long movie some 10-bit h265 dolby mega surround whatever the fuck, with a cut list it's
only to find that the colors look off because of some trivial to edit the ffmpeg command and re-make the cuts.
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 - Maybe you forget that the foreign language video you're cutting has softsubs
softsubs rather than hardsubs, rather than hardsubs, and you make a bunch of encode cuts resulting in cuts
and you make a bunch of encode cuts that have no subtitles.
resulting in cuts that have no subtitles.
- You delete the source video for storage reasons, - You delete the source video for storage reasons, but still want to have a
but still want to have a back up of the cut timestamps back up of the cut timestamps in the event you need to remake the cuts.
in the event you need to remake the cuts.
### Why Would I Bookmark Instead Of Cutting? ### Why Would I Bookmark Instead Of Cutting?
Suppose you're watching a movie or show for your own enjoyment, Suppose you're watching a movie or show for your own enjoyment, but you also
but you also want to compile funny moments to post online want to compile funny moments to post online or send to your friends. It would
or send to your friends. ruin your viewing experience to wait for a funny moment to be over in order to
It would ruin your viewing experience to wait for a funny make a cut. Instead, you can quickly make a bookmark whenever you laugh, and
moment to be over in order to make a cut. once you're done watching you can go back and make actual cuts.
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"? ### 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 This refers to ffmpeg's `-copy` flag which copies the input stream instead of
extremely quickly and the resulting video will retain re-encoding it, meaning that the cut will process extremely quickly and the
100% of the original quality. resulting video will retain 100% of the original quality. The main drawback is
The main drawback is that the cut may have some extra that the cut may have some extra video at the beginning and end, and as a
video at the beginning and end, result of that there may be some slightly wonky behavior with video players and
and as a result of that there may be some slightly wonky behavior editors.
with video players and editors.
### Why Would I Re-Encode A Video? ### 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 change the framerate.
- If you want to encode hardsubs. - 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? ### How Can I Merge (Concatenate) The Resulting Cuts Into One File?
To concatenate videos with ffmpeg, To concatenate videos with ffmpeg, you need to create a file with content like
you need to create a file with content like this: this:
``` ```
file cut_1.mp4 file cut_1.mp4
file cut_2.mp4 file cut_2.mp4
file cut_3.mp4 file cut_3.mp4
file cut_4.mp4 file cut_4.mp4
``` ```
You can name the file whatever you want, here I named it `concat.txt`. You can name the file whatever you want, here I named it `concat.txt`.
Then run the command: Then run the command:
``` ```
ffmpeg -f concat -safe 0 -i concat.txt -c copy out.mp4 ffmpeg -f concat -safe 0 -i concat.txt -c copy out.mp4
``` ```
That's annoying though, so you can skip That's annoying though, so you can skip manually creating the file by using
manually creating the file by using bash. bash. This command will concatenate all files in the current directory that
This command will concatenate all files in the current directory begin with "COPY_":
that begin with "COPY_":
``` ```
ffmpeg -f concat -safe 0 -i <(printf 'file %q\n' "$PWD"/COPY_*) -c copy lol.mp4 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"`. - You need to escape apostrophes which is why we are using `printf %q
- Instead of actually creating a file we just "$string"`.
use process substitution `<(whatever)` to create a temporary file,
which is why we need the `$PWD` in there for the absolute path. - 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. You can also do it in vim, among other things.
``` ```
ls | vim - ls | vim -
:%s/'/\\'/g :%s/'/\\'/g
:%norm Ifile :%norm Ifile
:wq concat.txt :wq concat.txt
``` ```
This substitution might not cover all cases, but whatever,
if you're concatenating a file named `[{}1;']["!.mp4` This substitution might not cover all cases, but whatever, if you're
you can figure it out yourself. concatenating a file named `[{}1;']["!.mp4` you can figure it out yourself.
### Can I Make Seeking And Reverse Playback Faster? ### 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 `exact` in `input.conf`.
- The use of the `.` and `,` keys to go frame by frame. - The use of the `.` and `,` keys to go frame by frame.
- The holding down of the `,` key to play the video in reverse. - 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, Long story short, if the video uses an encoding that is difficult for mpv to
exact seeking and backwards playback won't be smooth, which for normal playback is not a problem at all, decode, exact seeking and backwards playback won't be smooth, which for normal
since by default mpv very quickly seeks keyframe-wise when you press `left arrow` or `right arrow`. 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, However if we are very intensively cutting a video, it may be useful to be able
it may be useful to be able to quickly seek to an exact time, and to quickly play in reverse. to quickly seek to an exact time, and to quickly play in reverse. In this case,
In this case, it is useful to first make a proxy of the original video which is very easy to decode, it is useful to first make a proxy of the original video which is very easy to
generate a cut list with the proxy, and then apply the cut list to the original video. 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: 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 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, The important options here are the `-g 1` and the scale filter. The other
simply open the `cut_list.txt` file, options are more or less irrelevant. The resulting video file should seek
substitute the proxy file name for the original file name, extremely quickly and play backwards just fine.
and run `make_cuts` on it.
## Development 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`
Uploading releases to GitHub is a pain, so I thought why not create an orphaned branch for releases. on it.
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
```

400
main.lua
View File

@ -1,34 +1,158 @@
utils = require "mp.utils" 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) local function print(s)
mp.msg.info(s) mp.msg.info(s)
mp.osd_message(s) mp.osd_message(s)
end 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 = mp.create_osd_overlay("ass-events")
text_overlay.hidden = true text_overlay.hidden = true
text_overlay:update() text_overlay:update()
@ -40,18 +164,9 @@ local function text_overlay_off()
text_overlay:update() text_overlay:update()
end end
local function get_current_channel_name()
return CHANNEL_NAMES[CHANNEL] or CHANNEL
end
local function text_overlay_on() local function text_overlay_on()
local channel = get_current_channel_name() local channel = get_current_channel_name()
if USE_CUT_LIST then text_overlay.data = string.format("%s in %s from %s", ACTION, channel, START_TIME)
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.hidden = false text_overlay.hidden = false
text_overlay:update() text_overlay:update()
end end
@ -60,156 +175,42 @@ local function print_or_update_text_overlay(content)
if START_TIME then text_overlay_on() else print(content) end if START_TIME then text_overlay_on() else print(content) end
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() local function cycle_action()
ACTION = ACTIONS[index_of(ACTIONS, ACTION) + 1] ACTION = next_table_key(ACTIONS, ACTION)
print_or_update_text_overlay("ACTION: " .. ACTION) print_or_update_text_overlay("ACTION: " .. ACTION)
end end
local function get_file_info() local function make_cuts()
local inpath = mp.get_property("path") print("MAKING CUTS")
local filename = mp.get_property("filename") if not MAKE_CUT then print("MAKE_CUT function not found.") return end
local channel_name = get_current_channel_name() local inpath = mp.get_property("path") .. ".list"
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 file = io.open(inpath, "r") local file = io.open(inpath, "r")
if not file then if not file then print("Error reading cut list") return end
mp.set_property_native("chapter-list", {})
return
end
local arr = {}
for line in file:lines() do for line in file:lines() do
if tonumber(line) then if line ~= "" then
table.insert(arr, { local cut = {}
time = tonumber(line), for token in string.gmatch(line, "[^" .. ":" .. "]+") do
title = "chapter_" .. line 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
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) 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 end
local function cut(start_time, end_time) local function cut(start_time, end_time)
local inpath, filename, channel_name = get_file_info() local d = get_data()
local json_string = "{ " local t = get_times(start_time, end_time)
.. string.format("%q: %q", "filename", filename) for k, v in pairs(t) do d[k] = v end
.. string.format(", %q: %q", "action", ACTION) mp.msg.info(ACTION)
.. string.format(", %q: %q", "channel", channel_name) mp.msg.info(table_to_str(d))
.. string.format(", %q: %q", "start_time", start_time) ACTIONS[ACTION](d)
.. 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
end end
local function put_time() local function put_time()
@ -229,13 +230,62 @@ local function put_time()
end end
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_CUT, "cut", put_time)
mp.add_key_binding(KEY_BOOKMARK_ADD, "bookmark_add", bookmark_add) 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_INC, "channel_inc", channel_inc)
mp.add_key_binding(KEY_CHANNEL_DEC, "channel_dec", channel_dec) 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_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_MAKE_CUTS, "make_cuts", make_cuts)
mp.add_key_binding(KEY_TOGGLE_USE_CUT_LIST, "toggle_use_cut_list", toggle_use_cut_list)
mp.register_event('file-loaded', bookmarks_load) 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"
]
}