diff --git a/README.md b/README.md index 9077311..e9b1518 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,14 @@ video_fps=auto audio_format=opus # Opus sounds good at low bitrates 32-64k, but aac requires 128-256k. audio_bitrate=32k + +# Catbox.moe upload settings +# Whether uploads should go to litterbox instead of catbox. +# catbox files are stored permanently, while litterbox is temporary +litterbox=yes +# If using litterbox, time until video expires +# Available values: 1h, 12h, 24h, 72h +litterbox_expire=72h ``` ### Key bindings diff --git a/helpers.lua b/helpers.lua new file mode 100644 index 0000000..14e5116 --- /dev/null +++ b/helpers.lua @@ -0,0 +1,53 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +Various helper functions. +]] + +local mp = require('mp') +local this = {} + +this.is_wayland = function() + return os.getenv('WAYLAND_DISPLAY') ~= nil +end + +this.is_win = function() + return mp.get_property('options/vo-mmcss-profile') ~= nil +end + +this.is_mac = function() + return mp.get_property('options/macos-force-dedicated-gpu') ~= nil +end + +this.notify = function(message, level, duration) + level = level or 'info' + duration = duration or 1 + mp.msg[level](message) + mp.osd_message(message, duration) +end + +this.subprocess = function(args, stdin) + local command_table = { + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args, + stdin_data = (stdin or ""), + } + return mp.command_native(command_table) +end + +this.subprocess_async = function(args, on_complete) + local command_table = { + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + args = args + } + return mp.command_native_async(command_table, on_complete) +end + +return this diff --git a/platform.lua b/platform.lua new file mode 100644 index 0000000..291d286 --- /dev/null +++ b/platform.lua @@ -0,0 +1,87 @@ +--[[ +Copyright: Ren Tatsumoto and contributors +License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/gpl.html + +OS-related constants and functions. +]] + +local h = require('helpers') +local mp = require('mp') +local utils = require('mp.utils') +local this = {} + +this.Platform = { + gnu_linux = "gnu_linux", + macos = "macos", + windows = "windows", +} +this.platform = ( + h.is_win() and this.Platform.windows + or h.is_mac() and this.Platform.macos + or this.Platform.gnu_linux +) +this.default_video_folder = utils.join_path( + (os.getenv("HOME") or os.getenv("USERPROFILE")), + (this.platform == this.Platform.macos and "Movies" or "Videos") +) +this.default_audio_folder = utils.join_path( + (os.getenv("HOME") or os.getenv('USERPROFILE')), + "Music" +) +this.curl_exe = (this.platform == this.Platform.windows and 'curl.exe' or 'curl') +this.open_utility = ( + this.platform == this.Platform.windows and 'explorer.exe' + or this.platform == this.Platform.macos and 'open' + or this.platform == this.Platform.gnu_linux and 'xdg-open' +) +this.open = function(file_or_url) + return mp.commandv('run', this.open_utility, file_or_url) +end + +this.clipboard = (function() + local self = {} + if this.platform == this.Platform.windows then + self.clip_exe = "powershell.exe" + self.copy = function(text) + return h.subprocess({ self.clip_exe, '-command', 'Set-Clipboard -Value ' .. text }) + end + else + if this.platform == this.Platform.macos then + self.clip_exe = "pbcopy" + self.clip_cmd = "LANG=en_US.UTF-8 pbcopy" + elseif h.is_wayland() then + self.clip_exe = "wl-copy" + self.clip_cmd = "wl-copy" + else + self.clip_exe = "xclip" + self.clip_cmd = "xclip -i -selection clipboard" + end + self.copy = function(text) + local handle = io.popen(self.clip_cmd, 'w') + if handle then + handle:write(text) + local suc, exit, code = handle:close() + return { status = code } + else + return { status = 1 } + end + end + end + return self +end)() + +this.copy_or_open_url = function(url) + local cb = this.clipboard.copy(url) + if cb.status ~= 0 then + local msg = string.format( + "Failed to copy URL to clipboard, trying to open in browser instead. Make sure %s is installed.", + this.clipboard.clip_exe + ) + h.notify(msg, "warn", 4) + this.open(url) + else + h.notify("Done! Copied URL to clipboard.", "info", 2) + end + return cb +end +return this diff --git a/videoclip.lua b/videoclip.lua index ddb3f17..c36d779 100644 --- a/videoclip.lua +++ b/videoclip.lua @@ -22,16 +22,19 @@ local mp = require('mp') local mpopt = require('mp.options') local utils = require('mp.utils') local OSD = require('osd_styler') +local p = require('platform') +local h = require('helpers') -local is_macos = io.popen("uname"):read("*a") == "Darwin\n" +------------------------------------------------------------ +-- System-dependent variables -- Options can be changed here or in a separate config file. -- Config path: ~/.config/mpv/script-opts/videoclip.conf local config = { -- absolute paths -- relative paths (e.g. ~ for home dir) do NOT work. - video_folder_path = string.format(is_macos and '%s/Movies/' or '%s/Videos/', os.getenv("HOME") or os.getenv('USERPROFILE')), - audio_folder_path = string.format('%s/Music/', os.getenv("HOME") or os.getenv('USERPROFILE')), + video_folder_path = p.default_video_folder, + audio_folder_path = p.default_audio_folder, -- The range of the CRF scale is 0–51, where 0 is lossless, -- 23 is the default, and 51 is worst quality possible. -- Insane values like 9999 still work but produce the worst quality. @@ -48,6 +51,10 @@ local config = { audio_bitrate = '32k', -- 32k, 64k, 128k, 256k. aac requires higher bitrates. font_size = 24, clean_filename = true, + -- Whether to upload to catbox (permanent) or litterbox (temporary) + litterbox = true, + -- Determines expire time of files uploaded to litterbox + litterbox_expire = '72h', -- 1h, 12h, 24h, 72h } mpopt.read_options(config, NAME) @@ -124,17 +131,6 @@ local function construct_filename() return filename end -local function subprocess_async(args, on_complete) - local command_table = { - name = "subprocess", - playback_only = false, - capture_stdout = true, - capture_stderr = true, - args = args - } - return mp.command_native_async(command_table, on_complete) -end - local function force_resolution(width, height, clip_fn, ...) local cached_prefs = { video_width = config.video_width, @@ -184,11 +180,34 @@ local function validate_config() set_encoding_settings() end -local function notify(message, level, duration) - level = level or 'info' - duration = duration or 1 - mp.msg[level](message) - mp.osd_message(message, duration) +local function upload_to_catbox(outfile) + local endpoint = config.litterbox and 'https://litterbox.catbox.moe/resources/internals/api.php' or 'https://catbox.moe/user/api.php' + h.notify("Uploading to " .. (config.litterbox and "litterbox.catbox.moe..." or "catbox.moe..."), "info", 9999) + + -- This uses cURL to send a request to the cat-/litterbox API. + -- cURL is included on Windows 10 and up, most Linux distributions and macOS. + + local r = h.subprocess({ -- This is technically blocking, but I don't think it has any real consequences ..? + p.curl_exe, '-s', + '-F', 'reqtype=fileupload', + '-F', 'time=' .. config['litterbox_expire'], + '-F', 'fileToUpload=@"' .. outfile .. '"', + endpoint + }) + + -- Exit codes in the range [0, 99] are returned by cURL itself. + -- Any other exit code means the shell failed to execute cURL. + if r.status < 0 or r.status > 99 then + h.notify("Error: Failed to upload. Make sure cURL is installed and in your PATH.", "error", 3) + return + elseif r.status ~= 0 then + h.notify("Error: Failed to upload to " .. (config.litterbox and "litterbox.catbox.moe" or "catbox.moe"), "error", 2) + return + end + + mp.msg.info("Catbox URL: " .. r.stdout) + -- Copy to clipboard + p.copy_or_open_url(r.stdout) end ------------------------------------------------------------ @@ -282,19 +301,19 @@ encoder.mkargs_audio = function(clip_filename) } end -encoder.create_clip = function(clip_type) +encoder.create_clip = function(clip_type, on_complete) main_menu:close(); if clip_type == nil then return end if not main_menu.timings:validate() then - notify("Wrong timings. Aborting.", "warn", 2) + h.notify("Wrong timings. Aborting.", "warn", 2) return end local clip_filename = construct_filename() - notify("Please wait...", "info", 9999) + h.notify("Please wait...", "info", 9999) local args local location @@ -309,13 +328,16 @@ encoder.create_clip = function(clip_type) local process_result = function(_, ret, _) if ret.status ~= 0 or string.match(ret.stdout, "could not open") then - notify(string.format("Error: couldn't create the clip.\nDoes %s exist?", location), "error", 5) + h.notify(string.format("Error: couldn't create the clip.\nDoes %s exist?", location), "error", 5) else - notify(string.format("Clip saved to %s.", location), "info", 2) + h.notify(string.format("Clip saved to %s.", location), "info", 2) + if on_complete then + on_complete(utils.join_path(config.video_folder_path, clip_filename .. config.video_extension)) + end end end - subprocess_async(args, process_result) + h.subprocess_async(args, process_result) main_menu.timings:reset() end @@ -380,8 +402,10 @@ main_menu.keybindings = { { key = 'c', fn = function() encoder.create_clip('video') end }, { key = 'C', fn = function() force_resolution(1920, -2, encoder.create_clip, 'video') end }, { key = 'a', fn = function() encoder.create_clip('audio') end }, + { key = 'x', fn = function() main_menu:create_clip_and_upload_to_catbox() end }, + { key = 'X', fn = function() force_resolution(1920, -2, main_menu.create_clip_and_upload_to_catbox) end }, { key = 'p', fn = function() pref_menu:open() end }, - { key = 'o', fn = function() mp.commandv('run', is_macos and "open" or "xdg-open", 'https://streamable.com/') end }, + { key = 'o', fn = function() p.open('https://streamable.com/') end }, { key = 'ESC', fn = function() main_menu:close() end }, } @@ -395,7 +419,7 @@ function main_menu:set_time_sub(property) local time_pos = mp.get_property_number(string.format("sub-%s", property)) if time_pos == nil then - notify("Warning: No subtitles visible.", "warn", 2) + h.notify("Warning: No subtitles visible.", "warn", 2) return end @@ -425,6 +449,7 @@ function main_menu:update() osd:submenu('Create clip '):italics('(+shift to force fullHD preset)'):newline() osd:tab():item('c: '):append('video clip'):newline() osd:tab():item('a: '):append('audio clip'):newline() + osd:tab():item('x: '):append('video clip to ' .. (config.litterbox and 'litterbox.catbox.moe (' .. config.litterbox_expire .. ')' or 'catbox.moe')):newline() osd:submenu('Options '):newline() osd:tab():item('p: '):append('Open preferences'):newline() osd:tab():item('o: '):append('Open streamable.com'):newline() @@ -433,6 +458,10 @@ function main_menu:update() self:overlay_draw(osd:get_text()) end +function main_menu:create_clip_and_upload_to_catbox() + encoder.create_clip('video', upload_to_catbox) +end + ------------------------------------------------------------ -- Preferences @@ -445,6 +474,8 @@ pref_menu.keybindings = { { key = 'r', fn = function() pref_menu:cycle_resolutions() end }, { key = 'b', fn = function() pref_menu:cycle_audio_bitrates() end }, { key = 'e', fn = function() pref_menu:toggle_embed_subtitles() end }, + { key = 'x', fn = function() pref_menu:toggle_catbox() end }, + { key = 'z', fn = function() pref_menu:cycle_litterbox_expiration() end }, { key = 's', fn = function() pref_menu:save() end }, { key = 'c', fn = function() end }, { key = 'ESC', fn = function() pref_menu:close() end }, @@ -475,13 +506,14 @@ pref_menu.audio_bitrates = { pref_menu.vid_formats = { 'mp4', 'vp9', 'vp8', } pref_menu.aud_formats = { 'aac', 'opus', } +pref_menu.litterbox_expirations = { '1h', '12h', '24h', '72h', } function pref_menu:get_selected_resolution() - local w = config.video_width - local h = config.video_height - w = w == -2 and 'auto' or w - h = h == -2 and 'auto' or h - return string.format('%s x %s', w, h) + return string.format( + '%s x %s', + config.video_width == -2 and 'auto' or config.video_width, + config.video_height == -2 and 'auto' or config.video_height + ) end function pref_menu:cycle_resolutions() @@ -536,6 +568,28 @@ function pref_menu:toggle_embed_subtitles() self:update() end +function pref_menu:toggle_catbox() + config['litterbox'] = not config['litterbox'] + self:update() +end + +function pref_menu:cycle_litterbox_expiration() + if not config['litterbox'] then + return + end + local expirations = pref_menu.litterbox_expirations + + local selected = 1 + for i, expiration in ipairs(expirations) do + if config['litterbox_expire'] == expiration then + selected = i + break + end + end + config['litterbox_expire'] = expirations[selected + 1] or expirations[1] + self:update() +end + function pref_menu:update() local osd = OSD:new():size(config.font_size):align(4) osd:submenu('Preferences'):newline() @@ -545,6 +599,14 @@ function pref_menu:update() osd:tab():item('b: Audio bitrate: '):append(config.audio_bitrate):newline() osd:tab():item('m: Mute audio: '):append(mp.get_property("mute")):newline() osd:tab():item('e: Embed subtitles: '):append(mp.get_property("sub-visibility")):newline() + osd:submenu('Catbox'):newline() + osd:tab():item('x: Using: '):append(config.litterbox and 'Litterbox (temporary)' or 'Catbox (permanent)'):newline() + if config.litterbox then + osd:tab():item('z: Litterbox expires after: '):append(config.litterbox_expire):newline() + else + osd:tab():color("b0b0b0"):text('x: Litterbox expires after: '):append("N/A"):newline() + end + osd:submenu('Save'):newline() osd:tab():item('s: Save preferences'):newline() self:overlay_draw(osd:get_text()) end @@ -574,9 +636,9 @@ function pref_menu:save() end end handle:close() - notify("Settings saved.", "info", 2) + h.notify("Settings saved.", "info", 2) else - notify(string.format("Couldn't open %s.", config_filepath), "error", 4) + h.notify(string.format("Couldn't open %s.", config_filepath), "error", 4) end end