mpv2oboeru

mpv helpers to create flashcards from movies and TV shows
git clone anongit@rnpnr.xyz:mpv2oboeru.git
Log | Files | Refs | Feed | README | LICENSE

Commit: 92c8968437f1f902284a7dad069f8a8973ba2274
Parent: c46ccdb174b3577fac410afcfb066e95743fac99
Author: Randy Palamar
Date:   Tue, 24 May 2022 20:59:19 -0600

remove ankiconnect and related functions

for now files will be always stored in tmpdir

Diffstat:
Mencoder.lua | 11++++-------
Msubs2srs.lua | 497++-----------------------------------------------------------------------------
2 files changed, 16 insertions(+), 492 deletions(-)

diff --git a/encoder.lua b/encoder.lua @@ -150,13 +150,12 @@ end -- main interface local create_snapshot = function(timestamp, filename) - if not helpers.is_empty(_config.image_field) then + if _config.create_image == true then local source_path = mp.get_property("path") local output_path = utils.join_path(_os_temp_dir(), filename) local args = encoder.make_snapshot_args(source_path, output_path, timestamp) local on_finish = function() - _store_fn(filename, output_path) - os.remove(output_path) + helpers.notify(string.format("File stored: '%s'.", filename)) end _subprocess(args, on_finish) else @@ -179,8 +178,7 @@ local create_audio = function(start_timestamp, end_timestamp, filename, padding) table.insert(args, #args, arg) end local on_finish = function() - _store_fn(filename, output_path) - os.remove(output_path) + helpers.notify(string.format("File stored: '%s'.", filename)) end _subprocess(args, on_finish) else @@ -188,9 +186,8 @@ local create_audio = function(start_timestamp, end_timestamp, filename, padding) end end -local init = function(config, store_fn, os_temp_dir, subprocess) +local init = function(config, os_temp_dir, subprocess) _config = config - _store_fn = store_fn _os_temp_dir = os_temp_dir _subprocess = subprocess encoder = config.use_ffmpeg and ffmpeg or mpv diff --git a/subs2srs.lua b/subs2srs.lua @@ -1,5 +1,6 @@ --[[ Copyright (C) 2020-2022 Ren Tatsumoto and contributors +Copyright (C) 2022 Randy Palamar This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,8 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. Requirements: * mpv >= 0.32.0 -* AnkiConnect -* curl +* curl (for forvo) * xclip (when running X11) * wl-copy (when running Wayland) @@ -56,35 +56,9 @@ local config = { mpv_audio_args = '--af-append=silenceremove=1:0:-50dB', -- Anki - create_deck = false, -- automatically create a deck for new cards - allow_duplicates = false, -- allow making notes with the same sentence field - deck_name = "Learning", -- name of the deck for new cards - model_name = "Japanese sentences", -- Tools -> Manage note types sentence_field = "SentKanji", audio_field = "SentAudio", - image_field = "Image", - append_media = true, -- True to append video media after existing data, false to insert media before - disable_gui_browse = false, -- Lets you disable anki browser manipulation by mpvacious. - - -- Note tagging - -- The tag(s) added to new notes. Spaces separate multiple tags. - -- Change to "" to disable tagging completely. - -- The following substitutions are supported: - -- %n - the name of the video - -- %t - timestamp - -- %d - episode number (if none found, returns nothing) - -- %e - SUBS2SRS_TAGS environment variable - note_tag = "subs2srs %n", - tag_nuke_brackets = true, -- delete all text inside brackets before substituting filename into tag - tag_nuke_parentheses = false, -- delete all text inside parentheses before substituting filename into tag - tag_del_episode_num = true, -- delete the episode number if found - tag_del_after_episode_num = true, -- delete everything after the found episode number (does nothing if tag_del_episode_num is disabled) - tag_filename_lowercase = false, -- convert filename to lowercase for tagging. - - -- Misc info - miscinfo_enable = true, - miscinfo_field = "Notes", -- misc notes and source information field - miscinfo_format = "%n EP%d (%t)", -- format string to use for the miscinfo_field, accepts note_tag-style format strings + create_image = true, -- Forvo support use_forvo = "yes", -- 'yes', 'no', 'always' @@ -162,14 +136,6 @@ function table.get(table, key, default) end end -local function is_running_windows() - return mp.get_property('options/vo-mmcss-profile') ~= nil -end - -local function is_running_macOS() - return mp.get_property('options/cocoa-force-dedicated-gpu') ~= nil -end - local function is_running_wayland() return os.getenv('WAYLAND_DISPLAY') ~= nil end @@ -340,135 +306,15 @@ local function warn_formats(osd) end end -local function ensure_deck() - if config.create_deck == true then - ankiconnect.create_deck(config.deck_name) - end -end - local function load_next_profile() config_manager.next_profile() - ensure_deck() helpers.notify("Loaded profile " .. profiles.active) end -local function tag_format(filename) - filename = remove_extension(filename) - filename = remove_common_resolutions(filename) - - local s, e, episode_num = helpers.get_episode_number(filename) - - if config.tag_del_episode_num == true and not helpers.is_empty(s) then - if config.tag_del_after_episode_num == true then - -- Removing everything (e.g. episode name) after the episode number including itself. - filename = filename:sub(1, s) - else - -- Removing the first found instance of the episode number. - filename = filename:sub(1, s) .. filename:sub(e + 1, -1) - end - end - - if config.tag_nuke_brackets == true then - filename = remove_text_in_brackets(filename) - end - if config.tag_nuke_parentheses == true then - filename = remove_filename_text_in_parentheses(filename) - end - - if config.tag_filename_lowercase == true then - filename = filename:lower() - end - - filename = remove_leading_trailing_spaces(filename) - filename = filename:gsub(" ", "_") - filename = filename:gsub("_%-_", "_") -- Replaces garbage _-_ substrings with a underscore - filename = remove_leading_trailing_dashes(filename) - return filename, episode_num or '' -end - -local function substitute_fmt(tag) - local filename, episode = tag_format(mp.get_property("filename")) - - local function substitute_filename(_tag) - return _tag:gsub("%%n", filename) - end - - local function substitute_episode_number(_tag) - return _tag:gsub("%%d", episode) - end - - local function substitute_time_pos(_tag) - local time_pos = human_readable_time(mp.get_property_number('time-pos')) - return _tag:gsub("%%t", time_pos) - end - - local function substitute_envvar(_tag) - local env_tags = os.getenv('SUBS2SRS_TAGS') or '' - return _tag:gsub("%%e", env_tags) - end - - tag = substitute_filename(tag) - tag = substitute_episode_number(tag) - tag = substitute_time_pos(tag) - tag = substitute_envvar(tag) - tag = remove_leading_trailing_spaces(tag) - - return tag -end - -local function construct_note_fields(sub_text, snapshot_filename, audio_filename) - local ret = { - [config.sentence_field] = sub_text, - } - if not helpers.is_empty(config.image_field) then - ret[config.image_field] = string.format('<img alt="snapshot" src="%s">', snapshot_filename) - end - if not helpers.is_empty(config.audio_field) then - ret[config.audio_field] = string.format('[sound:%s]', audio_filename) - end - if config.miscinfo_enable == true then - ret[config.miscinfo_field] = substitute_fmt(config.miscinfo_format) - end - return ret -end - local function minutes_ago(m) return (os.time() - 60 * m) * 1000 end -local function join_media_fields(new_data, stored_data) - for _, field in pairs { config.audio_field, config.image_field, config.miscinfo_field } do - if not helpers.is_empty(field) then - new_data[field] = table.get(stored_data, field, "") .. table.get(new_data, field, "") - end - end - return new_data -end - -local function update_sentence(new_data, stored_data) - -- adds support for TSCs - -- https://tatsumoto-ren.github.io/blog/discussing-various-card-templates.html#targeted-sentence-cards-or-mpvacious-cards - -- if the target word was marked by yomichan, this function makes sure that the highlighting doesn't get erased. - - if helpers.is_empty(stored_data[config.sentence_field]) then - -- sentence field is empty. can't continue. - return new_data - elseif helpers.is_empty(new_data[config.sentence_field]) then - -- *new* sentence field is empty, but old one contains data. don't delete the existing sentence. - new_data[config.sentence_field] = stored_data[config.sentence_field] - return new_data - end - - local _, opentag, target, closetag, _ = stored_data[config.sentence_field]:match('^(.-)(<[^>]+>)(.-)(</[^>]+>)(.-)$') - if target then - local prefix, _, suffix = new_data[config.sentence_field]:match(table.concat { '^(.-)(', target, ')(.-)$' }) - if prefix and suffix then - new_data[config.sentence_field] = table.concat { prefix, opentag, target, closetag, suffix } - end - end - return new_data -end - local function audio_padding() local video_duration = mp.get_property_number('duration') if config.audio_padding == 0.0 or not video_duration then @@ -637,79 +483,20 @@ end)() ------------------------------------------------------------ -- front for adding and updating notes -local function export_to_anki(gui) +local function export_data() local sub = subs.get() if sub == nil then helpers.notify("Nothing to export.", "warn", 1) return end - if not gui and helpers.is_empty(sub['text']) then - sub['text'] = string.format("mpvacious wasn't able to grab subtitles (%s)", os.time()) - end - local snapshot_timestamp = mp.get_property_number("time-pos", 0) local snapshot_filename = filename_factory.make_snapshot_filename(snapshot_timestamp) local audio_filename = filename_factory.make_audio_filename(sub['start'], sub['end']) encoder.create_snapshot(snapshot_timestamp, snapshot_filename) encoder.create_audio(sub['start'], sub['end'], audio_filename, audio_padding()) - - local note_fields = construct_note_fields(sub['text'], snapshot_filename, audio_filename) - ankiconnect.add_note(note_fields, gui) - subs.clear() -end - -local function update_last_note(overwrite) - local sub = subs.get() - local last_note_id = ankiconnect.get_last_note_id() - - if sub == nil then - helpers.notify("Nothing to export. Have you set the timings?", "warn", 2) - return - elseif helpers.is_empty(sub['text']) then - -- In this case, don't modify whatever existing text there is and just - -- modify the other fields we can. The user might be trying to add - -- audio to a card which they've manually transcribed (either the video - -- has no subtitles or it has image subtitles). - sub['text'] = nil - end - - if last_note_id < minutes_ago(10) then - helpers.notify("Couldn't find the target note.", "warn", 2) - return - end - - local snapshot_timestamp = mp.get_property_number("time-pos", 0) - local snapshot_filename = filename_factory.make_snapshot_filename(snapshot_timestamp) - local audio_filename = filename_factory.make_audio_filename(sub['start'], sub['end']) - - local create_media = function() - encoder.create_snapshot(snapshot_timestamp, snapshot_filename) - encoder.create_audio(sub['start'], sub['end'], audio_filename, audio_padding()) - end - - local new_data = construct_note_fields(sub['text'], snapshot_filename, audio_filename) - local stored_data = ankiconnect.get_note_fields(last_note_id) - if stored_data then - new_data = append_forvo_pronunciation(new_data, stored_data) - new_data = update_sentence(new_data, stored_data) - if not overwrite then - if config.append_media then - new_data = join_media_fields(new_data, stored_data) - else - new_data = join_media_fields(stored_data, new_data) - end - end - end - - -- If the text is still empty, put some dummy text to let the user know why - -- there's no text in the sentence field. - if helpers.is_empty(new_data[config.sentence_field]) then - new_data[config.sentence_field] = string.format("mpvacious wasn't able to grab subtitles (%s)", os.time()) - end - - ankiconnect.append_media(last_note_id, new_data, create_media) + -- FIXME: export to correct folder subs.clear() end @@ -801,48 +588,9 @@ end)() ------------------------------------------------------------ -- platform specific -local function init_platform_windows() - local self = {} - local curl_tmpfile_path = utils.join_path(os.getenv('TEMP'), 'curl_tmp.txt') - mp.register_event('shutdown', function() - os.remove(curl_tmpfile_path) - end) - - self.tmp_dir = function() - return os.getenv('TEMP') - end - - self.copy_to_clipboard = function(text) - text = text:gsub("&", "^^^&"):gsub("[<>|]", "") - mp.commandv("run", "cmd.exe", "/d", "/c", string.format("@echo off & chcp 65001 >nul & echo %s|clip", text)) - end - - self.curl_request = function(request_json, completion_fn) - local handle = io.open(curl_tmpfile_path, "w") - handle:write(request_json) - handle:close() - local args = { - 'curl', - '-s', - 'localhost:8765', - '-H', - 'Content-Type: application/json; charset=UTF-8', - '-X', - 'POST', - '--data-binary', - table.concat { '@', curl_tmpfile_path } - } - return subprocess(args, completion_fn) - end - - self.windows = true - - return self -end - local function init_platform_nix() local self = {} - local clip = is_running_macOS() and 'LANG=en_US.UTF-8 pbcopy' or is_running_wayland() and 'wl-copy' or 'xclip -i -selection clipboard' + local clip = is_running_wayland() and 'wl-copy' or 'xclip -i -selection clipboard' self.tmp_dir = function() return '/tmp' @@ -862,7 +610,7 @@ local function init_platform_nix() return self end -platform = is_running_windows() and init_platform_windows() or init_platform_nix() +platform = init_platform_nix() ------------------------------------------------------------ -- utils for downloading pronunciations from Forvo @@ -924,8 +672,7 @@ do local function reencode_and_store(source_path, filename) local reencoded_path = utils.join_path(platform.tmp_dir(), 'reencoded_' .. filename) reencode(source_path, reencoded_path) - local result = ankiconnect.store_file(filename, reencoded_path) - os.remove(reencoded_path) + helpers.notify(string.format("Reencoded: '%s'", reencoded_path)) return result end @@ -1005,218 +752,6 @@ do end ------------------------------------------------------------ --- AnkiConnect requests - -ankiconnect = {} - -ankiconnect.execute = function(request, completion_fn) - -- utils.format_json returns a string - -- On error, request_json will contain "null", not nil. - local request_json, error = utils.format_json(request) - - if error ~= nil or request_json == "null" then - return completion_fn and completion_fn() - else - return platform.curl_request(request_json, completion_fn) - end -end - -ankiconnect.parse_result = function(curl_output) - -- there are two values that we actually care about: result and error - -- but we need to crawl inside to get them. - - if curl_output == nil then - return nil, "Failed to format json or no args passed" - end - - if curl_output.status ~= 0 then - return nil, "Ankiconnect isn't running" - end - - local stdout_json = utils.parse_json(curl_output.stdout) - - if stdout_json == nil then - return nil, "Fatal error from Ankiconnect" - end - - if stdout_json.error ~= nil then - return nil, tostring(stdout_json.error) - end - - return stdout_json.result, nil -end - -ankiconnect.store_file = function(filename, file_path) - local args = { - action = "storeMediaFile", - version = 6, - params = { - filename = filename, - path = file_path - } - } - - local ret = ankiconnect.execute(args) - local _, error = ankiconnect.parse_result(ret) - if not error then - msg.info(string.format("File stored: '%s'.", filename)) - return true - else - msg.error(string.format("Couldn't store file '%s': %s", filename, error)) - return false - end -end - -ankiconnect.create_deck = function(deck_name) - local args = { - action = "changeDeck", - version = 6, - params = { - cards = {}, - deck = deck_name - } - } - local result_notify = function(_, result, _) - local _, error = ankiconnect.parse_result(result) - if not error then - msg.info(string.format("Deck %s: check completed.", deck_name)) - else - msg.warn(string.format("Deck %s: check failed. Reason: %s.", deck_name, error)) - end - end - ankiconnect.execute(args, result_notify) -end - -ankiconnect.add_note = function(note_fields, gui) - local action = gui and 'guiAddCards' or 'addNote' - local tags = helpers.is_empty(config.note_tag) and {} or { substitute_fmt(config.note_tag) } - local args = { - action = action, - version = 6, - params = { - note = { - deckName = config.deck_name, - modelName = config.model_name, - fields = note_fields, - options = { - allowDuplicate = config.allow_duplicates, - duplicateScope = "deck", - }, - tags = tags, - } - } - } - local result_notify = function(_, result, _) - local note_id, error = ankiconnect.parse_result(result) - if not error then - helpers.notify(string.format("Note added. ID = %s.", note_id)) - else - helpers.notify(string.format("Error: %s.", error), "error", 2) - end - end - ankiconnect.execute(args, result_notify) -end - -ankiconnect.get_last_note_id = function() - local ret = ankiconnect.execute { - action = "findNotes", - version = 6, - params = { - query = "added:1" -- find all notes added today - } - } - - local note_ids, _ = ankiconnect.parse_result(ret) - - if not helpers.is_empty(note_ids) then - return table.max_num(note_ids) - else - return -1 - end -end - -ankiconnect.get_note_fields = function(note_id) - local ret = ankiconnect.execute { - action = "notesInfo", - version = 6, - params = { - notes = { note_id } - } - } - - local result, error = ankiconnect.parse_result(ret) - - if error == nil then - result = result[1].fields - for key, value in pairs(result) do - result[key] = value.value - end - return result - else - return nil - end -end - -ankiconnect.gui_browse = function(query) - if config.disable_gui_browse then - return - end - ankiconnect.execute { - action = 'guiBrowse', - version = 6, - params = { - query = query - } - } -end - -ankiconnect.add_tag = function(note_id, tag) - if not helpers.is_empty(tag) then - tag = substitute_fmt(tag) - ankiconnect.execute { - action = 'addTags', - version = 6, - params = { - notes = { note_id }, - tags = tag - } - } - end -end - -ankiconnect.append_media = function(note_id, fields, create_media_fn) - -- AnkiConnect will fail to update the note if it's selected in the Anki Browser. - -- https://github.com/FooSoft/anki-connect/issues/82 - -- Switch focus from the current note to avoid it. - ankiconnect.gui_browse("nid:1") -- impossible nid - - local args = { - action = "updateNoteFields", - version = 6, - params = { - note = { - id = note_id, - fields = fields, - } - } - } - - local on_finish = function(_, result, _) - local _, error = ankiconnect.parse_result(result) - if not error then - create_media_fn() - ankiconnect.add_tag(note_id, config.note_tag) - ankiconnect.gui_browse(string.format("nid:%s", note_id)) -- select the updated note in the card browser - helpers.notify(string.format("Note #%s updated.", note_id)) - else - helpers.notify(string.format("Error: %s.", error), "error", 2) - end - end - - ankiconnect.execute(args, on_finish) -end - ------------------------------------------------------------- -- subtitles and timings subs = { @@ -1410,10 +945,8 @@ menu.keybindings = { { key = 'e', fn = menu:with_update { subs.set_timing, 'end' } }, { key = 'c', fn = menu:with_update { subs.set_starting_line } }, { key = 'r', fn = menu:with_update { subs.clear_and_notify } }, - { key = 'g', fn = menu:with_update { export_to_anki, true } }, - { key = 'n', fn = menu:with_update { export_to_anki, false } }, - { key = 'm', fn = menu:with_update { update_last_note, false } }, - { key = 'M', fn = menu:with_update { update_last_note, true } }, + { key = 'g', fn = menu:with_update { export_data } }, + { key = 'n', fn = menu:with_update { export_data } }, { key = 't', fn = menu:with_update { clip_autocopy.toggle } }, { key = 'i', fn = menu:with_update { menu.hints_state.bump } }, { key = 'p', fn = menu:with_update { load_next_profile } }, @@ -1429,7 +962,6 @@ function menu:make_osd() osd:item(' to '):text(human_readable_time(subs.get_timing('end'))):newline() osd:item('Clipboard autocopy: '):text(clip_autocopy.is_enabled()):newline() osd:item('Active profile: '):text(profiles.active):newline() - osd:item('Deck: '):text(config.deck_name):newline() if self.hints_state.get() == 'global' then osd:submenu('Global bindings'):newline() @@ -1474,22 +1006,17 @@ local main = (function() end config_manager.init(config, profiles) - encoder.init(config, ankiconnect.store_file, platform.tmp_dir, subprocess) + encoder.init(config, platform.tmp_dir, subprocess) clip_autocopy.init() - ensure_deck() -- Key bindings - mp.add_forced_key_binding("Ctrl+n", "mpvacious-export-note", export_to_anki) + mp.add_forced_key_binding("Ctrl+n", "mpvacious-export-note", export_data) mp.add_forced_key_binding("Ctrl+c", "mpvacious-copy-sub-to-clipboard", copy_sub_to_clipboard) mp.add_key_binding("Ctrl+t", "mpvacious-autocopy-toggle", clip_autocopy.toggle) -- Open advanced menu mp.add_key_binding("a", "mpvacious-menu-open", function() menu:open() end) - -- Note updating - mp.add_key_binding("Ctrl+m", "mpvacious-update-last-note", _ { update_last_note, false }) - mp.add_key_binding("Ctrl+M", "mpvacious-overwrite-last-note", _ { update_last_note, true }) - -- Vim-like seeking between subtitle lines mp.add_key_binding("H", "mpvacious-sub-seek-back", _ { play_control.sub_seek, 'backward' }) mp.add_key_binding("L", "mpvacious-sub-seek-forward", _ { play_control.sub_seek, 'forward' })