add search douban in suggestion

This commit is contained in:
hughwan 2022-06-01 01:36:28 +08:00
parent 89d363ebcf
commit c3fb407215
22 changed files with 2854 additions and 351 deletions

37
douban/Douban.ts Normal file

@ -0,0 +1,37 @@
import { type } from "os";
interface DoubanPluginSettings {
template:string,
searchUrl:string,
searchHeaders?:string
}
export interface DoubanExtract {
id: string,
type: string;
title: string;
desc: string;
url: string;
}
export const doubanHeadrs = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36",
};
export const DEFAULT_SETTINGS:DoubanPluginSettings = {
template:
"---\n" +
"title: {{title}}" +
"cast: {{cast}}" +
"score: {{score}}" +
"---",
searchUrl: 'https://www.douban.com/search?q=',
searchHeaders: JSON.stringify(doubanHeadrs)
}
export type {DoubanPluginSettings}

@ -0,0 +1,69 @@
import { App, Editor, Modal, TextComponent } from "obsidian";
import { log } from "utils/logutil";
import DoubanPlugin from "../main";
export class DoubanSearchModal extends Modal {
searchTerm: string;
plugin: DoubanPlugin;
editor: Editor;
constructor(app: App, plugin: DoubanPlugin, editor: Editor) {
super(app);
this.plugin = plugin;
this.editor = editor;
}
onOpen() {
let { contentEl } = this;
contentEl.createEl("h2", { text: "Enter Search Term:" });
const inputs = contentEl.createDiv("inputs");
const searchInput = new TextComponent(inputs).onChange((searchTerm) => {
this.searchTerm = searchTerm;
});
searchInput.inputEl.focus();
searchInput.inputEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this.search();
}
});
const controls = contentEl.createDiv("controls");
const searchButton = controls.createEl("button", {
text: "Search",
cls: "mod-cta",
attr: {
autofocus: true,
},
});
searchButton.addEventListener("click", this.close.bind(this));
const cancelButton = controls.createEl("button", { text: "Cancel" });
cancelButton.addEventListener("click", this.close.bind(this));
}
async search() {
log.info("start search :" + this.searchTerm);
let { contentEl } = this;
contentEl.empty();
if (this.searchTerm) {
this.close();
await this.plugin.search(this.searchTerm);
// await this.plugin.pasteIntoEditor(this.editor, null);
}
}
async onClose() {
let { contentEl } = this;
contentEl.empty();
if (this.searchTerm) {
// await this.plugin.pasteIntoEditor(this.editor, this.searchTerm);
}
}
}

@ -0,0 +1,59 @@
import DoubanPlugin from "main";
import { App, PluginSettingTab, Setting } from "obsidian";
export class DoubanSettingTab extends PluginSettingTab {
plugin: DoubanPlugin;
constructor(app: App, plugin: DoubanPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
let { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Obsidian Wikipedia" });
new Setting(containerEl)
.setName("Douban Search Url")
.setDesc(`full search url with https ahead `)
.addText((textField) => {
textField
.setValue(this.plugin.settings.searchUrl)
.onChange(async (value) => {
this.plugin.settings.searchUrl = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Douban Request Headers")
.setDesc(`full search url with https ahead `)
.addText((textField) => {
textField
.setValue(this.plugin.settings.searchHeaders)
.onChange(async (value) => {
this.plugin.settings.searchHeaders = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Content Template")
.setDesc(
`Set markdown template for extract to be inserted.\n
Available template variables are {{id}}, {{type}}, {{title}}, {{score}}, {{cast}}, {{desc}} and {{url}}.
`
)
.addTextArea((textarea) =>
textarea
.setValue(this.plugin.settings.template)
.onChange(async (value) => {
this.plugin.settings.template = value;
await this.plugin.saveSettings();
})
);
}
}

13
douban/ResponseHandle.ts Normal file

@ -0,0 +1,13 @@
import { Notice } from "obsidian";
export const ensureStatusCode = (expected:any) => {
if (!Array.isArray(expected))
expected = [expected];
return (res:any) => {
const { statusCode } = res;
if(!expected.includes(statusCode)) {
new Notice(`Request Douban failed, Status code must be "${expected}" but actually "${statusCode}"`)
}
return res;
};
};

54
douban/movie/Movie.ts Normal file

@ -0,0 +1,54 @@
import cheerio from 'cheerio';
import { DoubanExtract, doubanHeadrs } from 'douban/Douban';
import { get, readStream } from 'tiny-network';
import { ensureStatusCode } from 'douban/ResponseHandle';
interface DoubanMovieExtract extends DoubanExtract {
}
export const playing = (city:string) => {
return Promise
.resolve()
.then(() => get(`https://movie.douban.com/cinema/nowplaying/${city}/`))
.then(ensureStatusCode(200))
.then(readStream)
.then(cheerio.load)
.then(parsePlaying)
};
export const parsePlaying = (dataHtml:any) => {
return dataHtml('.list-item')
.get()
.map((i:any) => {
const item = dataHtml(i);
console.log("version 5");
const result = {
id: item.attr('id'),
title: item.attr('data-title'),
score: item.attr('data-score'),
duration: item.attr('data-duration'),
region: item.attr('data-region'),
director: item.attr('data-director'),
actors: item.attr('data-actors'),
poster: item.find('.poster img').attr('src'),
link: `https://movie.douban.com/subject/${item.attr('id')}`,
};
// console.log("content is " + JSON.stringify(result));
return result;
})
};

@ -0,0 +1,50 @@
import DoubanPlugin from "main";
import { FuzzySuggestModal,App } from "obsidian";
import { log } from "utils/logutil";
import { DoubanSearchResultExtract } from "./SearchParser";
export {DoubanFuzzySuggester}
class DoubanFuzzySuggester extends FuzzySuggestModal<DoubanSearchResultExtract> {
public app: App;
private plugin: DoubanPlugin;
private doubanSearchResultExtract:DoubanSearchResultExtract[]
constructor(app: App, plugin: DoubanPlugin) {
super(app);
this.app = app;
this.plugin = plugin;
this.setPlaceholder("Choose an item...");
}
getItems(): DoubanSearchResultExtract[] {
return this.doubanSearchResultExtract;
}
getItemText(item: DoubanSearchResultExtract): string {
let text:string = item.type + ":" + item.title + " [score]:" + item.score + ",[cast]:" + item.cast;
return text;
}
onChooseItem(item: DoubanSearchResultExtract, evt: MouseEvent | KeyboardEvent): void {
log.warn("choose item " + item.title + " id " + item.id);
}
public showSearchList(doubanSearchResultExtractList:DoubanSearchResultExtract[]) {
this.doubanSearchResultExtract = doubanSearchResultExtractList;
log.info("show search result" );
this.start();
}
public start(): void {
try {
this.open();
} catch (e) {
log.error(e);
}
}
}

24
douban/search/Search.ts Normal file

@ -0,0 +1,24 @@
import cheerio from 'cheerio';
import { doubanHeadrs, DoubanPluginSettings } from 'douban/Douban';
import { get, readStream } from 'tiny-network';
import { ensureStatusCode } from 'douban/ResponseHandle';
import { DoubanSearchResultExtract, SearchParserHandler } from './SearchParser';
import { log } from 'utils/logutil';
class Searcher {
static search(searchItem:string, doubanSettings:DoubanPluginSettings):Promise<DoubanSearchResultExtract[]> {
// getData();
// getData2();
// return Promise.resolve();
return Promise
.resolve()
.then(() => get(doubanSettings.searchUrl + searchItem, JSON.parse(doubanSettings.searchHeaders)))
.then(ensureStatusCode(200))
.then(readStream)
.then(log.info)
.then(cheerio.load)
.then(SearchParserHandler.parseSearch);
};
}
export {Searcher}

@ -0,0 +1,43 @@
import { CheerioAPI } from "cheerio";
import { DoubanExtract } from "douban/Douban";
import { type } from "os";
interface DoubanSearchResultExtract extends DoubanExtract{
cast: string;
score: string;
}
class SearchParserHandler {
static parseSearch(dataHtml:CheerioAPI):DoubanSearchResultExtract[] {
return dataHtml('.result')
.get()
.map((i:any) => {
const item = dataHtml(i);
var idPattern = /(\d){5,10}/g;
var urlPattern = /(https%3A%2F%2F)\S+(\d){5,10}/g;
var linkValue = item.find("div.content > div > h3 > a").text();
var ececResult = idPattern.exec(linkValue);
var urlResult = urlPattern.exec(linkValue);
var cast = item.find(".subject-cast").text();
const result:DoubanSearchResultExtract = {
id: ececResult?ececResult[0]:'',
title: item.find("div.content > div > h3 > a").text(),
score: item.find(".rating_nums").text(),
// duration: item.attr('data-duration'),
// region: item.attr('data-region'),
// director: item.attr('data-director'),
// actors: item.attr('data-actors'),
// poster: item.find('.poster img').attr('src'),
cast: cast,
type: item.find("div.content > div > h3 > span").text(),
desc: item.find("div.content > p").text(),
url: urlResult?decodeURIComponent(urlResult[0]):'https://www.douban.com',
};
return result;
})
};
}
export {SearchParserHandler}
export type {DoubanSearchResultExtract}

215
main.ts

@ -1,85 +1,118 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { DoubanSearchModal } from "douban/DoubanSearchModal";
import { DoubanSettingTab } from "douban/DoubanSettingTab";
import { DoubanFuzzySuggester } from "douban/search/DoubanSearchFuzzySuggestModal";
import { Editor, Notice, Plugin} from "obsidian";
import { log } from "utils/logutil";
import { DEFAULT_SETTINGS, DoubanExtract, DoubanPluginSettings } from "./douban/Douban";
import { Searcher } from "./douban/search/Search";
import { DoubanSearchResultExtract } from "./douban/search/SearchParser";
// Remember to rename these classes and interfaces!
export default class DoubanPlugin extends Plugin {
public settings: DoubanPluginSettings;
public fuzzySuggester: DoubanFuzzySuggester;
interface MyPluginSettings {
mySetting: string;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
}
formatExtractText(extract: DoubanExtract): string {
return this.settings.template ?
this.settings.template.replace("{{id}}", extract.id)
.replace("{{type}}", extract.type)
.replace("{{title}}", extract.title)
.replace("{{desc}}", extract.desc)
.replace("{{url}}", extract.url) : "";
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
handleNotFound(searchTerm: string) {
log.error(`${searchTerm} not found on Wikipedia.`);
}
handleCouldntResolveDisambiguation() {
log.error(`Could not automatically resolve disambiguation.`);
}
parseSearchList(extract: DoubanSearchResultExtract[]):DoubanSearchResultExtract[] {
// return extract.map(result => {
// return {
// id: result.id,
// type: result.type,
// title: result.title,
// desc: result.desc,
// url: result.url,
// score: result.score,
// cast: result.cast
// }
// })
return extract;
}
async getDoubanSearchList(title: string): Promise<DoubanSearchResultExtract[] | undefined> {
return Searcher.search(title, this.settings);
}
async getDoubanMovieText(title: DoubanSearchResultExtract): Promise<DoubanExtract | undefined> {
// const moviesPromise = search(title);
// const movies = await moviesPromise;
// const extract = this.parseResponse(movies);
return null;
}
async pasteIntoEditor(editor: Editor, extract: DoubanExtract) {
if (!extract) {
this.handleNotFound("Not Found Subject");
return;
}
editor.replaceSelection(this.formatExtractText(extract));
}
async search(searchTerm:string) {
log.info("plugin search :" + searchTerm);
const resultListPromise = this.getDoubanSearchList(searchTerm);
resultListPromise.then(log.info);
const resultList = await resultListPromise;
const result = this.parseSearchList(resultList);
log.info("plugin search result:" + JSON.stringify(result));
this.fuzzySuggester.showSearchList(result);
}
async getDoubanMovieTextForActiveFile(editor: Editor) {
const activeFile = await this.app.workspace.getActiveFile();
if (activeFile) {
const searchTerm = activeFile.basename;
if (searchTerm) {
await this.search(searchTerm);
}
}
}
async geDoubanMovieTextForSearchTerm(editor: Editor) {
log.info("start open search windows");
new DoubanSearchModal(this.app, this, editor).open();
}
async onload() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
id: "douban-movie-for-current-file",
name: "get dou ban movie",
editorCallback: (editor: Editor) =>
this.getDoubanMovieTextForActiveFile(editor),
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
id: "douban-movie-for-search",
name: "douban-movie-for-search",
editorCallback: (editor: Editor) =>
this.geDoubanMovieTextForSearchTerm(editor),
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
this.addSettingTab(new DoubanSettingTab(this.app, this));
this.fuzzySuggester = new DoubanFuzzySuggester(this.app, this);
}
async loadSettings() {
@ -89,49 +122,5 @@ export default class MyPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
console.log('Secret: ' + value);
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

2130
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -20,5 +20,11 @@
"obsidian": "latest",
"tslib": "2.3.1",
"typescript": "4.4.4"
},
"dependencies": {
"axios": "^0.27.2",
"cheerio": "^1.0.0-rc.11",
"douban-search-crack": "^1.0.6",
"tiny-network": "0.0.6"
}
}

@ -15,7 +15,9 @@
"ES5",
"ES6",
"ES7"
]
],
"outDir": "dist",
},
"include": [
"**/*.ts"

6
typings/tiny-network.d.ts vendored Normal file

@ -0,0 +1,6 @@
declare module 'tiny-network' {
export function get(url:string, headers:any): any;
export function get(url:string): any;
export function readStream(param:any): any;
export function ensureStatusCode(code:number): any;
}

21
utils/logutil.ts Normal file

@ -0,0 +1,21 @@
import { Notice } from "obsidian";
class Logger {
public error(e:any):any {
new Notice("Douban Plugin Error: " + e);
return e;
}
public warn(e:any):any {
new Notice("Douban Plugin Warn: " + e);
return e;
}
public info(e:any):any {
console.log("Douban Plugin Warn: " + e);
return e;
}
}
export const log:Logger = new Logger();