parent
ee703bca50
commit
1a3f23efe4
|
@ -1,6 +1,2 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
test_video*
|
|
||||||
config.lua
|
config.lua
|
||||||
make_cuts
|
|
||||||
node_modules
|
|
||||||
dist/
|
|
||||||
|
|
337
README.md
337
README.md
|
@ -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
400
main.lua
|
@ -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)
|
||||||
|
|
122
make_cuts.imba
122
make_cuts.imba
|
@ -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!
|
|
31
package.json
31
package.json
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
Reference in New Issue