From d52981651d72ccdf81090cd11a1b612d889b1f78 Mon Sep 17 00:00:00 2001 From: zuluwi <111116092+zuluwi@users.noreply.github.com> Date: Sat, 27 Jan 2024 06:38:09 +0300 Subject: [PATCH] add subtitle path to timestamp link --- src/language/locale/en.ts | 3 + src/language/locale/tr.ts | 3 + src/main.ts | 163 +++++++++++++++++++++++++++++++------- src/settings.ts | 4 +- src/vlcHelper.ts | 125 ++++++++++++++++++++++++++--- 5 files changed, 257 insertions(+), 41 deletions(-) diff --git a/src/language/locale/en.ts b/src/language/locale/en.ts index 458f4b4..deb3086 100644 --- a/src/language/locale/en.ts +++ b/src/language/locale/en.ts @@ -6,6 +6,9 @@ export default { "No video information available": "No video information available", // "Select a file to open with VLC Player": "Select a file to open with VLC Player", + "Add subtitles (if you want subtitle path in the timestamp link, you need to add them with this command)": + "Add subtitles (if you want subtitle path in the timestamp link, you need to add them with this command)", + "A video must be open to add subtitles": "A video must be open to add subtitles", "Seek forward": "Seek forward", "Seek backward": "Seek backward", "Long seek forward": "Long seek forward", diff --git a/src/language/locale/tr.ts b/src/language/locale/tr.ts index cb1b480..d3da05b 100644 --- a/src/language/locale/tr.ts +++ b/src/language/locale/tr.ts @@ -6,6 +6,9 @@ export default { "No video information available": "Mevcut video bilgisine ulaşılamadı", // "Select a file to open with VLC Player": "VLC Player ile açmak için bir dosya seçin", + "Add subtitles (if you want subtitle path in the timestamp link, you need to add them with this command)": + "Altyazı ekle (zaman damgalı linkte altyazı adresinin bulunmasını istiyorsanız bu komutla eklemeniz gerekmekte)", + "A video must be open to add subtitles": "Altyazı ekleyebilmek için bir video açık olmalı", "Seek forward": "İleri sar", "Seek backward": "Geri sar", "Long seek forward": "İleri uzun sar", diff --git a/src/main.ts b/src/main.ts index fecfbf5..9c7f97a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,21 +1,28 @@ import { App, Editor, MarkdownView, Modal, Notice, ObsidianProtocolData, Plugin, PluginSettingTab, RequestUrlResponse, RequestUrlResponsePromise, Setting } from "obsidian"; import { DEFAULT_SETTINGS, VBPluginSettingsTab, VBPluginSettings } from "./settings"; -import { passPlugin, currentConfig } from "./vlcHelper"; +import { passPlugin, currentConfig, currentMedia, vlcStatusResponse } from "./vlcHelper"; import { t } from "./language/helpers"; -// Remember to rename these classes and interfaces! +declare global { + interface File { + readonly path: string; + } +} export default class VLCBridgePlugin extends Plugin { settings: VBPluginSettings; - openVideo: (filePath: string, time?: number) => void; + openVideo: ({ filePath, subPath, subDelay, time }: { filePath: string; subPath?: string; subDelay?: number; time?: number }) => void; + addSubtitle: (filePath: string, subDelay?: number) => void; sendVlcRequest: (command: string) => Promise; getStatus: () => Promise; getCurrentVideo: () => Promise; vlcExecOptions: () => string[]; + async onload() { await this.loadSettings(); - var { getStatus, getCurrentVideo, checkPort, sendVlcRequest, openVideo, launchVLC, vlcExecOptions } = passPlugin(this); + var { getStatus, getCurrentVideo, checkPort, sendVlcRequest, openVideo, launchVLC, vlcExecOptions, addSubtitle } = passPlugin(this); this.openVideo = openVideo; + this.addSubtitle = addSubtitle; this.sendVlcRequest = sendVlcRequest; this.getStatus = getStatus; this.getCurrentVideo = getCurrentVideo; @@ -27,13 +34,23 @@ export default class VLCBridgePlugin extends Plugin { }); this.registerObsidianProtocolHandler("vlcBridge", (params: ObsidianProtocolData) => { - var { mediaPath, timestamp } = params; + var { mediaPath, subPath, subDelay, timestamp } = params; if (!mediaPath) { return new Notice(t("The link does not have a 'mediaPath' parameter to play")); } mediaPath = decodeURIComponent(mediaPath); - var time = Number(timestamp); - this.openVideo(mediaPath, time); + var openParams: { filePath: string; subPath?: string; subDelay?: number; time?: number } = { filePath: mediaPath }; + if (timestamp) { + openParams.time = Number(timestamp); + } + if (subPath) { + openParams.subPath = decodeURIComponent(subPath); + } + if (subDelay) { + openParams.subDelay = Number(subDelay); + } + + this.openVideo(openParams); }); this.addCommand({ @@ -43,29 +60,36 @@ export default class VLCBridgePlugin extends Plugin { if (this.settings.pauseOnPasteLink) { this.sendVlcRequest("pl_forcepause"); } - var currentStats = await this.getStatus(); - if (!currentStats) { + try { + var status = await this.getStatus(); + } catch (error) { + // if (!currentStatusResponse) { + console.log(error); return new Notice(t("VLC Player must be open to use this command")); + // } } - var currentFile = await this.getCurrentVideo(); - if (!currentFile) { - return new Notice(t("No video information available")); - } - var currentTime = currentStats.json.time; - var timestamp = this.secondsToTimestamp(currentTime); - editor.replaceSelection(`[${timestamp}](obsidian://vlcBridge?mediaPath=${encodeURIComponent(currentFile)}×tamp=${currentTime}) `); + editor.replaceSelection(`${await this.getTimestampLink(status)} `); }, }); this.addCommand({ id: "open-video-with-vlc", + icon: "lucide-video", name: t("Select a file to open with VLC Player"), callback: async () => { this.fileOpen(); }, }); - // This adds an editor command that can perform some operation on the current editor instance + this.addCommand({ + id: "add-subtitle", + icon: "lucide-subtitles", + name: t("Add subtitles (if you want subtitle path in the timestamp link, you need to add them with this command)"), + callback: async () => { + this.subtitleOpen(); + }, + }); + this.addCommand({ id: "vlc-normal-seek-forward", name: t("Seek forward"), @@ -120,7 +144,13 @@ export default class VLCBridgePlugin extends Plugin { if (currentConfig.snapshotFolder && currentConfig.snapshotFolder !== this.settings.snapshotFolder) { new Notice(t("You must restart VLC for the snapshots to be saved in the folder you set.")); } - if ((await this.getStatus()).json.state == "stopped") { + try { + var status = await this.getStatus(); + } catch (error) { + console.log(error); + return new Notice(t("VLC Player must be open to use this command")); + } + if (status.json.state == "stopped") { return new Notice(t("No video is currently playing")); } if (this.settings.pauseOnPasteSnapshot) { @@ -142,13 +172,7 @@ export default class VLCBridgePlugin extends Plugin { .filter((f) => f.path.startsWith(`${currentConfig.snapshotFolder || this.settings.snapshotFolder}/`) && f.stat.mtime > beforeReq && f.stat.mtime < afterReq) ?.first(); if (snapshot) { - editor.replaceSelection( - `${ - currentFile - ? `[${this.secondsToTimestamp(response.json.time)}](obsidian://vlcBridge?mediaPath=${encodeURIComponent(currentFile)}×tamp=${response.json.time})` - : `${this.secondsToTimestamp(response.json.time)}` - } ![](${snapshot.path})\n` - ); + editor.replaceSelection(`${currentFile ? `${await this.getTimestampLink(response)}` : `${this.secondsToTimestamp(response.json.time)}`} ![](${snapshot.path})\n`); } else { new Notice(t("Snapshot not found, if you made a change to the snapshot folder name, try restarting VLC.")); } @@ -170,6 +194,40 @@ export default class VLCBridgePlugin extends Plugin { secondsToTimestamp(seconds: number) { return new Date(seconds * 1000).toISOString().slice(seconds < 3600 ? 14 : 11, 19); } + + getTimestampLink = async (response: RequestUrlResponse) => { + return new Promise(async (resolve, reject) => { + var currentStats: vlcStatusResponse = response?.json; + if (!currentStats) { + reject(); + return new Notice(t("VLC Player must be open to use this command")); + } + var currentFile = await this.getCurrentVideo(); + if (!currentFile) { + return new Notice(t("No video information available")); + } + var currentTime: number = currentStats.time; + var timestamp = this.secondsToTimestamp(currentTime); + var params: { + mediaPath: string; + timestamp: string; + subPath?: string; + subDelay?: string; + } = { + mediaPath: encodeURIComponent(currentFile), + timestamp: currentTime.toString(), + }; + + if (currentMedia.subtitlePath && currentMedia.mediaPath == currentFile) { + params.subPath = encodeURIComponent(currentMedia.subtitlePath); + } + if (typeof currentStats.subtitledelay == "number" && currentStats.subtitledelay !== 0) { + params.subDelay = currentStats.subtitledelay.toString(); + } + var paramStr = new URLSearchParams(params).toString(); + resolve(`[${timestamp}](obsidian://vlcBridge?${paramStr})`); + }); + }; async fileOpen() { if (!this.settings.vlcPath) { return new Notice(t("Before you can use the plugin, you need to select 'vlc.exe' in the plugin settings")); @@ -182,10 +240,61 @@ export default class VLCBridgePlugin extends Plugin { var files = (e.target as HTMLInputElement)?.files as FileList; for (let i = 0; i < files.length; i++) { var file = files[i]; - // @ts-ignore + var fileURI = new URL(file.path).href; // console.log(fileURI); - this.openVideo(fileURI); + this.openVideo({ filePath: fileURI }); + + input.remove(); + } + }; + + input.click(); + } + async subtitleOpen() { + if (!this.settings.vlcPath) { + return new Notice(t("Before you can use the plugin, you need to select 'vlc.exe' in the plugin settings")); + } + var mevcutVideo = await this.getCurrentVideo(); + if (!mevcutVideo) { + return new Notice(t("A video must be open to add subtitles")); + } + const input = document.createElement("input"); + input.setAttribute("type", "file"); + // https://wiki.videolan.org/subtitles#Subtitles_support_in_VLC + let supportedSubtitleFormats = [ + ".aqt", + ".usf", + ".txt", + ".svcd", + ".sub", + ".idx", + ".sub", + ".sub", + ".sub", + ".ssa", + ".ass", + ".srt", + ".smi", + ".rt", + ".pjs", + ".mpl", + ".jss", + ".dks", + ".cvd", + ".aqt", + ".ttxt", + ".ssf", + ".psb", + ]; + input.accept = supportedSubtitleFormats.join(","); + input.onchange = (e: Event) => { + var files = (e.target as HTMLInputElement)?.files as FileList; + for (let i = 0; i < files.length; i++) { + var file = files[i]; + // var fileURI = new URL(file.path).href; + // console.log(file, file.path, fileURI); + this.addSubtitle(file.path); input.remove(); } diff --git a/src/settings.ts b/src/settings.ts index 4c8dd8b..6a683cd 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -97,7 +97,6 @@ export class VBPluginSettingsTab extends PluginSettingTab { copyArgEl.setDesc(`${this.plugin.vlcExecOptions().join(" ").replace(/["]/g, "")}`); if (/\s/.test(this.plugin.app.vault.adapter.getFullRealPath(this.plugin.settings.snapshotFolder))) { - console.log("boşluk var"); MarkdownRenderer.render( this.app, @@ -269,7 +268,8 @@ export class VBPluginSettingsTab extends PluginSettingTab { ); copyArgEl = new Setting(containerEl).setName(t("Copy arguments for starting VLC (for Syncplay)")).addButton((btn) => btn.setButtonText(t("Copy to clipboard")).onClick(async () => { - await navigator.clipboard.writeText(`${this.plugin.vlcExecOptions().join(" ").trim().replace(/["]/g, "")}`); + // await navigator.clipboard.writeText(`${this.plugin.vlcExecOptions().join(" ").trim().replace(/["]/g, "")}`); + await navigator.clipboard.writeText(`${this.plugin.vlcExecOptions().join(" ").trim()}`); new Notice(t("Copied to clipboard")); }) ); diff --git a/src/vlcHelper.ts b/src/vlcHelper.ts index 38e0cc4..8f84a58 100644 --- a/src/vlcHelper.ts +++ b/src/vlcHelper.ts @@ -4,6 +4,7 @@ const exec = util.promisify(require("child_process").exec); import { Notice, RequestUrlResponse, request, requestUrl } from "obsidian"; import VLCBridgePlugin from "./main"; import { t } from "./language/helpers"; +import { fileURLToPath } from "url"; interface config { port: number | null; @@ -24,6 +25,46 @@ export const currentConfig: config = { lang: "en", }; +export const currentMedia: { + mediaPath: string | null; + subtitlePath: string | null; +} = { + mediaPath: null, + subtitlePath: null, +}; + +// https://transform.tools/json-to-typescript +export interface vlcStatusResponse { + fullscreen: boolean; + // stats: Stats + aspectratio: string; + seek_sec: number; + apiversion: number; + currentplid: number; + time: number; + volume: number; + length: number; + random: boolean; + // audiofilters: Audiofilters + information: { + chapter: number; + chapters: number[]; + title: number; + category: {}; + titles: number[]; + }; + rate: number; + // videoeffects: Videoeffects + state: string; + loop: boolean; + version: string; + position: number; + audiodelay: number; + repeat: boolean; + subtitledelay: number; + equalizer: any[]; +} + // export var isVlcOpen: boolean | null = null; var checkTimeout: ReturnType; var checkInterval: ReturnType; @@ -127,7 +168,7 @@ export function passPlugin(plugin: VLCBridgePlugin) { .catch((err) => { console.log("vlc request error:", err); new Notice(t("Could not connect to VLC Player.")); - reject(err); + resolve(null); }); }); }; @@ -159,7 +200,7 @@ export function passPlugin(plugin: VLCBridgePlugin) { } }; - const openVideo = async (filePath: string, time?: number) => { + const openVideo = async ({ filePath, subPath, subDelay, time }: { filePath: string; subPath?: string; subDelay?: number; time?: number }) => { var port_ = currentConfig.port || plugin.settings.port; var password_ = currentConfig.password || plugin.settings.password; @@ -177,36 +218,96 @@ export function passPlugin(plugin: VLCBridgePlugin) { // console.log(filePath, currentConfig.currentFile, filePath == currentConfig.currentFile); if (fileCheck) { - if (fileCheck.current && time) { - requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); + if (fileCheck.current) { + if (time) { + requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); + } + if (subPath && subPath !== currentMedia.subtitlePath) { + addSubtitle(subPath, subDelay); + } } else { - await requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=pl_play&id=${fileCheck.id}`).then((response) => { - if (response.status == 200) { + await requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=pl_play&id=${fileCheck.id}`).then(async (response) => { + if (response.status == 200 && (await waitStreams())) { // currentConfig.currentFile = filePath; if (time) { requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); } + if (subPath) { + addSubtitle(subPath, subDelay); + } } }); } } else { - await requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=in_play&input=${filePath}`).then((response) => { - if (response.status == 200) { + await requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=in_play&input=${encodeURIComponent(filePath)}`).then(async (response) => { + if (response.status == 200 && (await waitStreams())) { // currentConfig.currentFile = filePath; if (time) { requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); } + if (subPath) { + addSubtitle(subPath, subDelay); + } } }); - if (time) { - requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); - } + // if (time) { + // requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=seek&val=${time}`); + // } } } else { new Notice(t("Could not connect to VLC Player.")); } }; + const waitStreams = () => { + var port_ = currentConfig.port || plugin.settings.port; + var password_ = currentConfig.password || plugin.settings.password; + return new Promise((resolve, reject) => { + let streamTimeout: ReturnType; + let streamInterval: ReturnType; + streamInterval = setInterval(() => { + requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json`).then(async (response) => { + if (response.status == 200) { + let streams = Object.keys(response.json.information.category); + if (streams.length > 1) { + resolve(streams); + clearInterval(streamInterval); + clearTimeout(streamTimeout); + } + } + }); + }, 500); + streamTimeout = setTimeout(() => { + if (!(streamInterval as any)._destroyed) { + clearInterval(streamInterval); + reject(); + } + }, 10000); + }); + }; + + const addSubtitle = async (filePath: string, subDelay?: number | undefined) => { + var port_ = currentConfig.port || plugin.settings.port; + var password_ = currentConfig.password || plugin.settings.password; + + if (filePath.startsWith("file:///")) { + filePath = fileURLToPath(filePath); + // filePath = filePath.substring(7).replace(/\//g, "\\"); + } + + requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=addsubtitle&val=${encodeURIComponent(filePath)}`).then(async (response) => { + if (response.status == 200) { + currentMedia.mediaPath = await getCurrentVideo(); + currentMedia.subtitlePath = filePath; + let subIndex = (await waitStreams()).filter((e) => e !== "meta").length - 1; + requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=subtitle_track&val=${subIndex}`); + if (subDelay) { + requestUrl(`http://:${password_}@localhost:${port_}/requests/status.json?command=subdelay&val=${subDelay}`); + } + } + }); + }; + const vlcExecOptions = () => [ `--extraintf=luaintf:http`, `--http-port=${plugin.settings.port}`, @@ -249,5 +350,5 @@ export function passPlugin(plugin: VLCBridgePlugin) { currentConfig.snapshotExt = plugin.settings.snapshotExt; }; - return { getCurrentVideo, getStatus, checkPort, sendVlcRequest, openVideo, vlcExecOptions, launchVLC }; + return { getCurrentVideo, getStatus, checkPort, sendVlcRequest, openVideo, vlcExecOptions, launchVLC, addSubtitle }; }