// Go to https://jimaku.cc/login and create a new account. // Then go to https://jimaku.cc/account and click the `Generate` button to create a new API key // Click the `Copy` button and paste it below var API_KEY = ""; // Configuration options var CONFIG = { // Filter the response to only have the specified episode prompt_episode: true, // Subtitle suffix (e.g., ".JA" for Japanese subtitles) subtitle_suffix: ".JA", // Preferred subtitle format (order matters, first is most preferred) preferred_formats: ["ass", "srt", "vtt"], // Automatically load the subtitle after download auto_load: true, // Default subtitle delay in seconds (can be positive or negative) default_delay: 0, // Default subtitle font size default_font_size: 16, // Automatically rename the subtitle file after download auto_rename: true, // Automatically run autosubsync-mpv after downloading the subtitle run_auto_subsync: true }; // Keybindings // var MANUAL_SEARCH_KEY = "g"; var FILENAME_AUTO_SEARCH_KEY = "ctrl+J"; var PARENT_FOLDER_AUTO_SEARCH_KEY = "n"; function api(url, extraArgs) { var baseArgs = [ "curl", "-s", "--url", url, "--header", "Authorization: " + API_KEY ]; var args = Array.prototype.concat.apply(baseArgs, extraArgs); var res = mp.command_native({ name: "subprocess", playback_only: false, capture_stdout: true, capture_stderr: true, args: args }); if (res.stdout) return JSON.parse(res.stdout); } function downloadSub(sub) { return api(sub.url, ["--output", sub.name]); } function showMessage(message, persist) { var ass_start = mp.get_property_osd("osd-ass-cc/0"); var ass_stop = mp.get_property_osd("osd-ass-cc/1"); mp.osd_message( ass_start + "{\\fs16}" + message + ass_stop, persist ? 999 : 2 ); } // The timeout is neccessary due to a weird bug in mpv function inputGet(args) { mp.input.terminate(); setTimeout(function () { mp.input.get(args); }, 1); } // The timeout is neccessary due to a weird bug in mpv function inputSelect(args) { mp.input.terminate(); setTimeout(function () { mp.input.select(args); }, 1); } // Taken from mpv-subversive // https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L146 function sanitize(text) { var subPatterns = [ /\.[a-zA-Z]+$/, // extension /\./g, /-/g, /_/g, /\[[^\]]+\]/g, // [] bracket /\([^\)]+\)/g, // () bracket /720[pP]/g, /480[pP]/g, /1080[pP]/g, /[xX]26[45]/g, /[bB]lu[-]?[rR]ay/g, /^[\s]*/, /[\s]*$/, /1920x1080/g, /1920X1080/g, /Hi10P/g, /FLAC/g, /AAC/g ]; var result = text; subPatterns.forEach(function (subPattern) { var newResult = result.replace(subPattern, " "); if (newResult.length > 0) { result = newResult; } }); return result; } // Adapted from mpv-subversive // https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L164 function extractTitle(text) { var matchers = [ { regex: /^([\w\s\d]+)[Ss]\d+[Ee]?\d+/, group: 1 }, { regex: /^([\w\s\d]+)-[\s]*\d+[\s]*[^\w]*$/, group: 1 }, { regex: /^([\w\s\d]+)[Ee]?[Pp]?[\s]+\d+$/, group: 1 }, { regex: /^([\w\s\d]+)[\s]\d+.*$/, group: 1 }, { regex: /^\d+[\s]*(.+)$/, group: 1 } ]; for (var i = 0; i < matchers.length; i++) { var matcher = matchers[i]; var match = text.match(matcher.regex); if (match) { return match[matcher.group].trim(); } } return text; } function getNames(results) { return results.map(function (item) { return item.name; }); } function runAutoSubSyncMPV() { try { mp.command_native(["script-binding", "autosubsync-menu"]); } catch (e) { showMessage("autosubsync-mpv not installed"); return; } } function selectSub(selectedSub) { showMessage("Downloading: " + selectedSub.name); try { downloadSub(selectedSub); // Get current video filename without extension var videoPath = mp.get_property("path"); if (!videoPath) { throw new Error("No video file is currently playing"); } var videoName = videoPath.substring(0, videoPath.lastIndexOf(".")); // Get subtitle extension var subExt = selectedSub.name.substring(selectedSub.name.lastIndexOf(".")); var newSubName = selectedSub.name; if (CONFIG.auto_rename) { // Create new subtitle filename newSubName = videoName + CONFIG.subtitle_suffix + subExt; // Rename the downloaded subtitle file var renameResult = mp.command_native({ name: "subprocess", playback_only: false, args: ["mv", selectedSub.name, newSubName] }); if (renameResult.error) { throw new Error( "Failed to rename subtitle file: " + renameResult.error ); } showMessage(newSubName + " downloaded and renamed"); } else { showMessage(newSubName + " downloaded"); } if (CONFIG.auto_load) { mp.commandv("sub_add", newSubName); showMessage(newSubName + " added"); // Apply subtitle settings if configured if (CONFIG.default_delay !== 0) { mp.commandv("sub_delay", CONFIG.default_delay); } if (CONFIG.default_font_size !== 16) { mp.commandv("sub_font_size", CONFIG.default_font_size); } } if (CONFIG.run_auto_subsync) { runAutoSubSyncMPV(); } mp.set_property("pause", "no"); } catch (error) { showMessage("Error: " + error.message, true); mp.set_property("pause", "no"); } } function sortByPreferredFormat(files) { return files.sort(function (a, b) { var extA = a.name.substring(a.name.lastIndexOf(".") + 1).toLowerCase(); var extB = b.name.substring(b.name.lastIndexOf(".") + 1).toLowerCase(); var indexA = CONFIG.preferred_formats.indexOf(extA); var indexB = CONFIG.preferred_formats.indexOf(extB); if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); } function selectEpisode(anime, episode) { mp.input.terminate(); var episodeResults; if (episode) { showMessage("Fetching subs for: " + anime.name + " episode " + episode); episodeResults = api( "https://jimaku.cc/api/entries/" + anime.id + "/files?episode=" + episode ); } else { showMessage("Fetching all subs for: " + anime.name); episodeResults = api( "https://jimaku.cc/api/entries/" + anime.id + "/files" ); } if (episodeResults.error) { showMessage("Error: " + animeResults.error); return; } if (episodeResults.length === 0) { showMessage("No results found"); return; } // Sort results by preferred format episodeResults = sortByPreferredFormat(episodeResults); if (episodeResults.length === 1) { var selectedEpisode = episodeResults[0]; selectSub(selectedEpisode); return; } var items = getNames(episodeResults); inputSelect({ prompt: "Select episode: ", items: items, submit: function (id) { var selectedEpisode = episodeResults[id - 1]; selectSub(selectedEpisode); } }); } function onAnimeSelected(anime) { if (CONFIG.prompt_episode) { inputGet({ prompt: "Episode (leave blank for all): ", submit: function (episode) { selectEpisode(anime, episode); } }); } else { selectEpisode(anime); } } function search(searchTerm, isAuto) { mp.input.terminate(); showMessage('Searching for: "' + searchTerm + '"'); var animeResults = api( encodeURI( "https://jimaku.cc/api/entries/search?anime=true&query=" + searchTerm ) ); if (animeResults.error) { showMessage("Error: " + animeResults.error); return; } if (animeResults.length === 0) { showMessage("No results found"); if (isAuto) { manualSearch(searchTerm); } return; } if (animeResults.length === 1) { var selectedAnime = animeResults[0]; onAnimeSelected(selectedAnime); return; } var items = getNames(animeResults); inputSelect({ prompt: "Select anime: ", items: items, submit: function (id) { var selectedAnime = animeResults[id - 1]; showMessage(selectedAnime.name, true); onAnimeSelected(selectedAnime); } }); } function manualSearch(defaultText) { inputGet({ prompt: "Search term: ", submit: search, default_text: defaultText }); mp.set_property("pause", "yes"); showMessage("Manual Jimaku Search", true); } function autoSearch() { var filename = mp.get_property("filename"); var sanitizedFilename = sanitize(filename); var currentAnime = extractTitle(sanitizedFilename); mp.set_property("pause", "yes"); search(currentAnime, true); } function autoSearchParentFolder() { var path = mp.get_property("stream-open-filename"); var pathSplit = path.split(path.indexOf("/") >= 0 ? "/" : "\\"); var filename = pathSplit.length === 1 ? pathSplit[0] : pathSplit[pathSplit.length - 2]; var sanitizedFilename = sanitize(filename); var currentAnime = extractTitle(sanitizedFilename); mp.set_property("pause", "yes"); search(currentAnime, true); } // mp.add_key_binding(MANUAL_SEARCH_KEY, "jimaku-manual-search", manualSearch); mp.add_key_binding( FILENAME_AUTO_SEARCH_KEY, "jimaku-filename-auto-search", autoSearch ); mp.add_key_binding( PARENT_FOLDER_AUTO_SEARCH_KEY, "jimaku-parent-folder-auto-search", autoSearchParentFolder );