From 5ab42b2334d431a887eb814572009184520073ae Mon Sep 17 00:00:00 2001 From: ZhymabekRoman Date: Fri, 5 Apr 2024 20:48:26 +0500 Subject: [PATCH 1/3] Make use LinkPreview.net API to get page titles --- main.ts | 19 +++++++++++-------- settings.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/main.ts b/main.ts index b0a28c9..4e24506 100644 --- a/main.ts +++ b/main.ts @@ -287,16 +287,19 @@ export default class AutoLinkTitle extends Plugin { async fetchUrlTitle(url: string): Promise { try { - let title = ""; - if (this.settings.useNewScraper) { - title = await getPageTitle(url); - } else { - title = await getElectronPageTitle(url); - } + const apiEndpoint = `https://api.linkpreview.net/?q=${encodeURIComponent(url)}`; + const response = await fetch(apiEndpoint, { + headers: { + "X-Linkpreview-Api-Key": this.settings.linkPreviewApiKey, + } + }); + const data = await response.json(); + // Assuming the API returns a JSON object with a title property + const title = data.title || 'Title Unavailable'; return title.replace(/(\r\n|\n|\r)/gm, "").trim(); } catch (error) { - console.error(error) - return 'Error fetching title' + console.error(error); + return 'Error fetching title'; } } diff --git a/settings.ts b/settings.ts index f069d0c..ca228df 100644 --- a/settings.ts +++ b/settings.ts @@ -13,6 +13,7 @@ export interface AutoLinkTitleSettings { websiteBlacklist: string; maximumTitleLength: number; useNewScraper: boolean; + linkPreviewApiKey: string; } export const DEFAULT_SETTINGS: AutoLinkTitleSettings = { @@ -31,6 +32,7 @@ export const DEFAULT_SETTINGS: AutoLinkTitleSettings = { websiteBlacklist: "", maximumTitleLength: 0, useNewScraper: false, + linkPreviewApiKey: "", }; export class AutoLinkTitleSettingTab extends PluginSettingTab { @@ -135,5 +137,15 @@ export class AutoLinkTitleSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }) ); + + new Setting(containerEl) + .setName("LinkPreview API Key") + .setDesc("API key for the LinkPreview.net service. Get one at https://my.linkpreview.net/access_keys") + .addText(text => text + .setValue(this.plugin.settings.linkPreviewApiKey || "") + .onChange(async (value) => { + this.plugin.settings.linkPreviewApiKey = value; + await this.plugin.saveSettings(); + })); } } From 6ebae350d5574e106ffb6e20b4fe47e45c1b3b7d Mon Sep 17 00:00:00 2001 From: ZhymabekRoman Date: Fri, 31 May 2024 22:43:11 +0500 Subject: [PATCH 2/3] scraper: making fallback to electron parser if link-preview api fail --- electron-scraper.ts | 9 +++- main.ts | 105 ++++++++++++++++++++++++++++++-------------- scraper.ts | 2 +- 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/electron-scraper.ts b/electron-scraper.ts index a80e71a..403fe20 100644 --- a/electron-scraper.ts +++ b/electron-scraper.ts @@ -35,6 +35,11 @@ async function electronGetPageTitle(url: string): Promise { }); window.webContents.setAudioMuted(true); + window.webContents.on("will-navigate", (event: any, newUrl: any) => { + event.preventDefault(); + window.loadURL(newUrl); + }); + await load(window, url); try { @@ -52,7 +57,7 @@ async function electronGetPageTitle(url: string): Promise { } } catch (ex) { console.error(ex); - return "Site Unreachable"; + return ""; } } @@ -78,7 +83,7 @@ async function nonElectronGetPageTitle(url: string): Promise { } catch (ex) { console.error(ex); - return "Site Unreachable"; + return ""; } } diff --git a/main.ts b/main.ts index 4d61962..9b3a478 100644 --- a/main.ts +++ b/main.ts @@ -1,13 +1,13 @@ -import { CheckIf } from "checkif" -import { EditorExtensions } from "editor-enhancements" -import { Editor, Plugin } from "obsidian" -import getPageTitle from "scraper" -import getElectronPageTitle from "electron-scraper" +import { CheckIf } from "checkif"; +import { EditorExtensions } from "editor-enhancements"; +import { Editor, Plugin } from "obsidian"; +import getPageTitle from "scraper"; +import getElectronPageTitle from "electron-scraper"; import { AutoLinkTitleSettingTab, AutoLinkTitleSettings, DEFAULT_SETTINGS, -} from "./settings" +} from "./settings"; interface PasteFunction { (this: HTMLElement, ev: ClipboardEvent): void; @@ -27,7 +27,10 @@ export default class AutoLinkTitle extends Plugin { console.log("loading obsidian-auto-link-title"); await this.loadSettings(); - this.blacklist = this.settings.websiteBlacklist.split(",").map(s => s.trim()).filter(s => s.length > 0); + this.blacklist = this.settings.websiteBlacklist + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); // Listen to paste event this.pasteFunction = this.pasteUrlWithTitle.bind(this); @@ -58,9 +61,7 @@ export default class AutoLinkTitle extends Plugin { this.app.workspace.on("editor-paste", this.pasteFunction) ); - this.registerEvent( - this.app.workspace.on("editor-drop", this.dropFunction) - ); + this.registerEvent(this.app.workspace.on("editor-drop", this.dropFunction)); this.addCommand({ id: "enhance-url-with-title", @@ -89,8 +90,8 @@ export default class AutoLinkTitle extends Plugin { } // If the cursor is on the URL part of a markdown link, fetch title and replace existing link title else if (CheckIf.isLinkedUrl(selectedText)) { - const link = this.getUrlFromLink(selectedText) - this.convertUrlToTitledLink(editor, link) + const link = this.getUrlFromLink(selectedText); + this.convertUrlToTitledLink(editor, link); } } @@ -103,7 +104,7 @@ export default class AutoLinkTitle extends Plugin { // Simulate standard paste but using editor.replaceSelection with clipboard text since we can't seem to dispatch a paste event. async manualPasteUrlWithTitle(editor: Editor): Promise { - const clipboardText = await navigator.clipboard.readText() + const clipboardText = await navigator.clipboard.readText(); // Only attempt fetch if online if (!navigator.onLine) { @@ -111,7 +112,7 @@ export default class AutoLinkTitle extends Plugin { return; } - if (clipboardText == null || clipboardText == '') return + if (clipboardText == null || clipboardText == "") return; // If its not a URL, we return false to allow the default paste handler to take care of it. // Similarly, image urls don't have a meaningful attribute so downloading it @@ -129,7 +130,7 @@ export default class AutoLinkTitle extends Plugin { return; } - // If url is pasted over selected text and setting is enabled, no need to fetch title, + // If url is pasted over selected text and setting is enabled, no need to fetch title, // just insert a link let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim(); if (selectedText && this.settings.shouldPreserveSelectionAsTitle) { @@ -142,7 +143,10 @@ export default class AutoLinkTitle extends Plugin { return; } - async pasteUrlWithTitle(clipboard: ClipboardEvent, editor: Editor): Promise<void> { + async pasteUrlWithTitle( + clipboard: ClipboardEvent, + editor: Editor + ): Promise<void> { if (!this.settings.enhanceDefaultPaste) { return; } @@ -162,7 +166,6 @@ export default class AutoLinkTitle extends Plugin { return; } - // We've decided to handle the paste, stop propagation to the default handler. clipboard.stopPropagation(); clipboard.preventDefault(); @@ -175,7 +178,7 @@ export default class AutoLinkTitle extends Plugin { return; } - // If url is pasted over selected text and setting is enabled, no need to fetch title, + // If url is pasted over selected text and setting is enabled, no need to fetch title, // just insert a link let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim(); if (selectedText && this.settings.shouldPreserveSelectionAsTitle) { @@ -198,7 +201,7 @@ export default class AutoLinkTitle extends Plugin { // Only attempt fetch if online if (!navigator.onLine) return; - let dropText = dropEvent.dataTransfer.getData('text/plain'); + let dropText = dropEvent.dataTransfer.getData("text/plain"); if (dropText === null || dropText === "") return; // If its not a URL, we return false to allow the default paste handler to take care of it. @@ -220,7 +223,7 @@ export default class AutoLinkTitle extends Plugin { return; } - // If url is pasted over selected text and setting is enabled, no need to fetch title, + // If url is pasted over selected text and setting is enabled, no need to fetch title, // just insert a link let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim(); if (selectedText && this.settings.shouldPreserveSelectionAsTitle) { @@ -235,8 +238,11 @@ export default class AutoLinkTitle extends Plugin { async isBlacklisted(url: string): Promise<boolean> { await this.loadSettings(); - this.blacklist = this.settings.websiteBlacklist.split(/,|\n/).map(s => s.trim()).filter(s => s.length > 0) - return this.blacklist.some(site => url.includes(site)) + this.blacklist = this.settings.websiteBlacklist + .split(/,|\n/) + .map((s) => s.trim()) + .filter((s) => s.length > 0); + return this.blacklist.some((site) => url.includes(site)); } async convertUrlToTitledLink(editor: Editor, url: string): Promise<void> { @@ -274,9 +280,9 @@ export default class AutoLinkTitle extends Plugin { } escapeMarkdown(text: string): string { - var unescaped = text.replace(/\\(\*|_|`|~|\\|\[|\])/g, '$1') // unescape any "backslashed" character - var escaped = unescaped.replace(/(\*|_|`|<|>|~|\\|\[|\])/g, '\\$1') // escape *, _, `, ~, \, [, ], <, and > - return escaped + var unescaped = text.replace(/\\(\*|_|`|~|\\|\[|\])/g, "$1"); // unescape any "backslashed" character + var escaped = unescaped.replace(/(\*|_|`|<|>|~|\\|\[|\])/g, "\\$1"); // escape *, _, `, ~, \, [, ], <, and > + return escaped; } public shortTitle = (title: string): string => { @@ -286,25 +292,56 @@ export default class AutoLinkTitle extends Plugin { if (title.length < this.settings.maximumTitleLength + 3) { return title; } - const shortenedTitle = `${title.slice(0, this.settings.maximumTitleLength)}...`; + const shortenedTitle = `${title.slice( + 0, + this.settings.maximumTitleLength + )}...`; return shortenedTitle; - } + }; - async fetchUrlTitle(url: string): Promise<string> { + public async fetchUrlTitleViaLinkPreview(url: string): Promise<string> { try { - const apiEndpoint = `https://api.linkpreview.net/?q=${encodeURIComponent(url)}`; + const apiEndpoint = `https://api.linkpreview.net/?q=${encodeURIComponent( + url + )}`; const response = await fetch(apiEndpoint, { headers: { "X-Linkpreview-Api-Key": this.settings.linkPreviewApiKey, - } + }, }); const data = await response.json(); - // Assuming the API returns a JSON object with a title property - const title = data.title || 'Title Unavailable'; - return title.replace(/(\r\n|\n|\r)/gm, "").trim(); + return data.title; + } catch (error) { + console.error(error); + return ""; + } + } + + async fetchUrlTitle(url: string): Promise<string> { + try { + let title = ""; + title = await this.fetchUrlTitleViaLinkPreview(url); + console.log(`Title via Link Preview: ${title}`); + + if (title === "") { + console.log("Title via Link Preview failed, falling back to scraper"); + if (this.settings.useNewScraper) { + console.log("Using new scraper"); + title = await getPageTitle(url); + } else { + console.log("Using old scraper"); + title = await getElectronPageTitle(url); + } + } + + console.log(`Title: ${title}`); + title = + title.replace(/(\r\n|\n|\r)/gm, "").trim() || + "Title Unavailable | Site Unreachable"; + return title; } catch (error) { console.error(error); - return 'Error fetching title'; + return "Error fetching title"; } } diff --git a/scraper.ts b/scraper.ts index 9542052..9bc2b4b 100644 --- a/scraper.ts +++ b/scraper.ts @@ -31,7 +31,7 @@ async function scrape(url: string): Promise<string> { return title.innerText } catch (ex) { console.error(ex) - return 'Site Unreachable' + return '' } } From cc1ea1c68aae6006f40216db0242ac4be8cd3672 Mon Sep 17 00:00:00 2001 From: ZhymabekRoman <robanokssamit@yandex.ru> Date: Wed, 4 Dec 2024 10:31:28 +0500 Subject: [PATCH 3/3] feat: Enhance LinkPreview API key validation and improve settings UI --- main.ts | 7 +++++++ settings.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/main.ts b/main.ts index 9b3a478..3f4d2ec 100644 --- a/main.ts +++ b/main.ts @@ -300,6 +300,13 @@ export default class AutoLinkTitle extends Plugin { }; public async fetchUrlTitleViaLinkPreview(url: string): Promise<string> { + if (this.settings.linkPreviewApiKey.length !== 32) { + console.error( + "LinkPreview API key is not 32 characters long, please check your settings" + ); + return ""; + } + try { const apiEndpoint = `https://api.linkpreview.net/?q=${encodeURIComponent( url diff --git a/settings.ts b/settings.ts index 2eb4365..53be3f6 100644 --- a/settings.ts +++ b/settings.ts @@ -1,5 +1,6 @@ import AutoLinkTitle from "main"; import { App, PluginSettingTab, Setting } from "obsidian"; +import { Notice } from "obsidian"; export interface AutoLinkTitleSettings { regex: RegExp; @@ -80,18 +81,17 @@ export class AutoLinkTitleSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Maximum title length") - .setDesc( - "Set the maximum length of the title. Set to 0 to disable." - ) + .setDesc("Set the maximum length of the title. Set to 0 to disable.") .addText((val) => val .setValue(this.plugin.settings.maximumTitleLength.toString(10)) .onChange(async (value) => { - const titleLength = (Number(value)) - this.plugin.settings.maximumTitleLength = isNaN(titleLength) || titleLength < 0 ? 0 : titleLength; + const titleLength = Number(value); + this.plugin.settings.maximumTitleLength = + isNaN(titleLength) || titleLength < 0 ? 0 : titleLength; await this.plugin.saveSettings(); }) - ) + ); new Setting(containerEl) .setName("Preserve selection as title") @@ -140,12 +140,22 @@ export class AutoLinkTitleSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("LinkPreview API Key") - .setDesc("API key for the LinkPreview.net service. Get one at https://my.linkpreview.net/access_keys") - .addText(text => text - .setValue(this.plugin.settings.linkPreviewApiKey || "") - .onChange(async (value) => { - this.plugin.settings.linkPreviewApiKey = value; - await this.plugin.saveSettings(); - })); + .setDesc( + "API key for the LinkPreview.net service. Get one at https://my.linkpreview.net/access_keys" + ) + .addText((text) => + text + .setValue(this.plugin.settings.linkPreviewApiKey || "") + .onChange(async (value) => { + const trimmedValue = value.trim(); + if (trimmedValue.length > 0 && trimmedValue.length !== 32) { + new Notice("LinkPreview API key must be 32 characters long"); + this.plugin.settings.linkPreviewApiKey = ""; + } else { + this.plugin.settings.linkPreviewApiKey = trimmedValue; + } + await this.plugin.saveSettings(); + }) + ); } }