From 1a3f23efe4342adf833e78a1f2eab4ab03a6e943 Mon Sep 17 00:00:00 2001 From: familyfriendlymikey Date: Sun, 5 Feb 2023 01:17:41 -0500 Subject: [PATCH] 2.0.0 remove node, make actions extensible --- .gitignore | 4 - README.md | 337 ++++++++++++++++++++--------------------- main.lua | 400 +++++++++++++++++++++++++++---------------------- make_cuts.imba | 122 --------------- package.json | 31 ---- 5 files changed, 384 insertions(+), 510 deletions(-) delete mode 100644 make_cuts.imba delete mode 100644 package.json diff --git a/.gitignore b/.gitignore index 55287dc..b1e16b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ .DS_Store -test_video* config.lua -make_cuts -node_modules -dist/ diff --git a/README.md b/README.md index f7dd0ea..44a92b1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main.lua b/main.lua index 85475a6..85ae649 100644 --- a/main.lua +++ b/main.lua @@ -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) diff --git a/make_cuts.imba b/make_cuts.imba deleted file mode 100644 index fe18a43..0000000 --- a/make_cuts.imba +++ /dev/null @@ -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! diff --git a/package.json b/package.json deleted file mode 100644 index bf3808f..0000000 --- a/package.json +++ /dev/null @@ -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" - ] -}