Compare commits

..

10 Commits

Author SHA1 Message Date
812fc17c5a build: 升级版本为2.3.2
Some checks failed
Release Draft / build (push) Has been cancelled
2026-04-02 16:33:59 +08:00
ChrsiGray
e8a0d8a00c fix: support douban image uploads with referer
- use subject page referer for douban image requests\n- keep PicGo clipboard upload flow and await clipboard writes\n- add webp clipboard fallback for image decoding
2026-04-02 11:26:16 +08:00
ChrsiGray
318aabb21b fix: upload Douban images to PicGo via temp file 2026-04-02 11:26:16 +08:00
a8094d7575
Merge pull request #177
fix: All search type 403, undefined actor crash, and lazy login on startup
2026-03-11 23:27:55 +08:00
YuBai
7ba1a7be0c feat: implement lazy login to eliminate startup network request
Replace eager login verification on startup with assumeLoggedIn(),
which sets login state from saved credentials without any network call.
Real verification is deferred until sync (which needs the actual user ID)
or when the settings page is opened.

- UserComponent: add verified flag, assumeLoggedIn(), isVerified()
- main.ts: replace onLayoutReady(login) with assumeLoggedIn(); fix
  inverted condition bug in checkLogin() (!needLogin -> needLogin)
- DoubanSyncModal: remove redundant login check before checkLogin()
- LoginSettingsHelper: handle assumed-but-unverified state in constructLoginUI()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 20:38:48 +08:00
YuBai
4e8d3f1318 Merge branch 'fix-undefined-actor-error' into fix/search-all-api-and-login-init 2026-03-04 18:25:45 +08:00
YuBai
c375512903 fix: restore All search type and initialize login state on startup
- Switch AllPageSearchPageFetcher from m.douban.com/rexxar/api/v2/search
  (returns 403 need_login) to www.douban.com/j/search (works with cookies)
- Update AllFirstPageSearchResultPageParser and OtherAllPageSearchResultPageParser
  to use SearchParserHandler.parseSearchJson() matching the j/search response format,
  replacing the old Rexxar-specific SearchParserHandlerV2 parsing logic
- Add await this.userComponent.login() in onload() so login state is initialized
  on startup, fixing false isLogin()=false causing "need login for next page" errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 18:20:58 +08:00
YuBai
99d4170626 fix: 为其他 handler 添加数组空值检查
在 parseVariable 方法中为所有数组字段的 .map() 调用添加 (field || []) 保护,

防止当字段为 undefined 时调用 .map() 报错。
2026-02-02 11:26:47 +08:00
YuBai
297ccd33cf fix: 当 JSON-LD 解析失败时添加回退机制
豆瓣现在对未登录请求返回反爬虫验证页面,导致 JSON-LD 解析返回 undefined。

添加从 OG meta 标签提取基本信息的回退机制,防止代码崩溃。

同时给所有数组字段添加默认值 || []。
2026-02-02 11:25:56 +08:00
YuBai
a0eccf7370 fix: handle() 方法 catch 块返回 undefined 而不是错误对象
当 HTTP 请求失败时,返回错误对象会导致上层代码尝试访问错误对象的属性,

造成 TypeError。改为返回 undefined 让上层代码正确处理空值情况。
2026-02-02 11:24:07 +08:00
22 changed files with 341 additions and 116 deletions

@ -1,7 +1,7 @@
{ {
"id": "obsidian-douban-plugin", "id": "obsidian-douban-plugin",
"name": "Douban", "name": "Douban",
"version": "2.3.1", "version": "2.3.2",
"minAppVersion": "0.12.0", "minAppVersion": "0.12.0",
"description": "This is a plugin that can import movies/books/musics/notes/games info data from Douban for Obsidian .", "description": "This is a plugin that can import movies/books/musics/notes/games info data from Douban for Obsidian .",
"author": "Wanxp", "author": "Wanxp",

@ -1,6 +1,6 @@
{ {
"name": "obsidian-douban-plugin", "name": "obsidian-douban-plugin",
"version": "2.3.1", "version": "2.3.2",
"description": "This is a plugin for Obsidian (https://obsidian.md) that can import data from Douban (https://www.douban.com/).", "description": "This is a plugin for Obsidian (https://obsidian.md) that can import data from Douban (https://www.douban.com/).",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

@ -33,24 +33,24 @@ export default class DoubanTheaterAiLoadHandler extends DoubanAbstractLoadHandle
"director", "director",
DataValueType.array, DataValueType.array,
extract.director, extract.director,
extract.director.map(SchemaOrg.getPersonName).filter(c => c) (extract.director || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("actor", new DataField( variableMap.set("actor", new DataField(
"actor", "actor",
DataValueType.array, DataValueType.array,
extract.actor, extract.actor,
extract.actor.map(SchemaOrg.getPersonName).filter(c => c) (extract.actor || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("author", new DataField( variableMap.set("author", new DataField(
"author", "author",
DataValueType.array, DataValueType.array,
extract.author, extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c) (extract.author || []).map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
)); ));
variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases, variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases,
extract.aliases.map(a=>a (extract.aliases || []).map(a=>a
.trim() .trim()
// .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_') // .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
// //replase multiple _ to single _ // //replase multiple _ to single _

@ -169,9 +169,6 @@ ${syncStatus.getHandle() == 0? '...' : i18nHelper.getMessage('110042') + ':' + T
const syncButton = new ButtonComponent(controls) const syncButton = new ButtonComponent(controls)
.setButtonText(i18nHelper.getMessage('110007')) .setButtonText(i18nHelper.getMessage('110007'))
.onClick(async () => { .onClick(async () => {
if (!this.plugin.userComponent.isLogin()) {
await this.plugin.userComponent.login();
}
if(!await this.plugin.checkLogin(this.context)) { if(!await this.plugin.checkLogin(this.context)) {
return; return;
} }

@ -142,7 +142,7 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
}else { }else {
context.syncStatusHolder?context.syncStatusHolder.syncStatus.handled(1):null; context.syncStatusHolder?context.syncStatusHolder.syncStatus.handled(1):null;
} }
return e; return undefined;
}); });
@ -543,15 +543,20 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
} }
fileName = this.parsePartPath(fileName, extract, context, variableMap) fileName = this.parsePartPath(fileName, extract, context, variableMap)
fileName = fileName + fileNameSuffix; fileName = fileName + fileNameSuffix;
// const referHeaders = {'referer': image}; const imageReferer = (extract.id ? this.getSubjectUrl(extract.id) : '') || extract.url;
const referHeaders = context.settings.loginHeadersContent ? JSON.parse(context.settings.loginHeadersContent) : {}; const referHeaders = HttpUtil.buildImageRequestHeaders(
context.plugin.settingsManager.getHeaders() as Record<string, any>,
imageReferer
);
if ((syncConfig ? syncConfig.cacheHighQuantityImage : context.settings.cacheHighQuantityImage) && context.userComponent.isLogin()) { if ((syncConfig ? syncConfig.cacheHighQuantityImage : context.settings.cacheHighQuantityImage) && context.userComponent.isLogin()) {
try { try {
const fileNameSpilt = fileName.split('.'); const highImageFilename = this.getImageFilename(image);
const highFilename = fileNameSpilt.first() + '.jpg'; const highImage = this.getHighQuantityImageUrl(highImageFilename);
const highImageHeaders = HttpUtil.buildImageRequestHeaders(
const highImage = this.getHighQuantityImageUrl(highFilename); context.plugin.settingsManager.getHeaders() as Record<string, any>,
const resultValue = await this.handleImage(highImage, folder, highFilename, context, false, referHeaders); imageReferer
);
const resultValue = await this.handleImage(highImage, folder, fileName, context, false, highImageHeaders);
if (resultValue && resultValue.success) { if (resultValue && resultValue.success) {
extract.image = resultValue.filepath; extract.image = resultValue.filepath;
this.initImageVariableMap(extract, context, variableMap); this.initImageVariableMap(extract, context, variableMap);
@ -569,6 +574,14 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
} }
} }
private getImageFilename(image: string): string {
if (!image) {
return '';
}
const imageUrl = image.split('?').first() || image;
return imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
}
private initImageVariableMap(extract: T, context: HandleContext, variableMap : Map<string, DataField>) { private initImageVariableMap(extract: T, context: HandleContext, variableMap : Map<string, DataField>) {
variableMap.set(DoubanParameterName.IMAGE_URL, new DataField( variableMap.set(DoubanParameterName.IMAGE_URL, new DataField(
DoubanParameterName.IMAGE_URL, DoubanParameterName.IMAGE_URL,
@ -608,12 +621,20 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
handlePersonNameByMeta(html: CheerioAPI, movie: DoubanSubject, context: HandleContext, handlePersonNameByMeta(html: CheerioAPI, movie: DoubanSubject, context: HandleContext,
metaProperty:string, objectProperty:string) { metaProperty:string, objectProperty:string) {
if (!movie) {
return;
}
const metaProperties: string[] = html(`head > meta[property='${metaProperty}']`).get() const metaProperties: string[] = html(`head > meta[property='${metaProperty}']`).get()
.map((e) => { .map((e) => {
return html(e).attr('content'); return html(e).attr('content');
}); });
// @ts-ignore // @ts-ignore
movie[objectProperty] const currentArray = movie[objectProperty];
if (!Array.isArray(currentArray)) {
return;
}
// @ts-ignore
currentArray
// @ts-ignore // @ts-ignore
.filter((p:Person) => p.name) .filter((p:Person) => p.name)
// @ts-ignore // @ts-ignore

@ -30,9 +30,9 @@ export default class DoubanBookLoadHandler extends DoubanAbstractLoadHandler<Dou
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanBookSubject, context: HandleContext): void { parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanBookSubject, context: HandleContext): void {
variableMap.set(DoubanBookParameter.author, new DataField(DoubanBookParameter.author, variableMap.set(DoubanBookParameter.author, new DataField(DoubanBookParameter.author,
DataValueType.array, extract.author, extract.author.map(this.handleSpecialAuthorName))); DataValueType.array, extract.author, (extract.author || []).map(this.handleSpecialAuthorName)));
variableMap.set(DoubanBookParameter.translator, new DataField(DoubanBookParameter.translator, variableMap.set(DoubanBookParameter.translator, new DataField(DoubanBookParameter.translator,
DataValueType.array, extract.translator, extract.translator.map(this.handleSpecialAuthorName))); DataValueType.array, extract.translator, (extract.translator || []).map(this.handleSpecialAuthorName)));
} }
support(extract: DoubanSubject): boolean { support(extract: DoubanSubject): boolean {

@ -31,7 +31,7 @@ export default class DoubanGameLoadHandler extends DoubanAbstractLoadHandler<Dou
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanGameSubject, context: HandleContext): void { parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanGameSubject, context: HandleContext): void {
// super.parseAliases(beforeContent, variableMap, extract, context); // super.parseAliases(beforeContent, variableMap, extract, context);
variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases, variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases,
extract.aliases.map(a=>a (extract.aliases || []).map(a=>a
.trim() .trim()
// .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_') // .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
// //replase multiple _ to single _ // //replase multiple _ to single _

@ -35,24 +35,24 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
"director", "director",
DataValueType.array, DataValueType.array,
extract.director, extract.director,
extract.director.map(SchemaOrg.getPersonName).filter(c => c) (extract.director || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("actor", new DataField( variableMap.set("actor", new DataField(
"actor", "actor",
DataValueType.array, DataValueType.array,
extract.actor, extract.actor,
extract.actor.map(SchemaOrg.getPersonName).filter(c => c) (extract.actor || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("author", new DataField( variableMap.set("author", new DataField(
"author", "author",
DataValueType.array, DataValueType.array,
extract.author, extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c) (extract.author || []).map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
)); ));
variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases, variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases,
extract.aliases.map(a=>a (extract.aliases || []).map(a=>a
.trim() .trim()
// .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_') // .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
// //replase multiple _ to single _ // //replase multiple _ to single _
@ -98,7 +98,7 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
} }
parseSubjectFromHtml(html: CheerioAPI, context: HandleContext): DoubanMovieSubject { parseSubjectFromHtml(html: CheerioAPI, context: HandleContext): DoubanMovieSubject {
const movie:DoubanMovieSubject = html('script') let movie: DoubanMovieSubject | undefined = html('script')
.get() .get()
.filter(scd => "application/ld+json" == html(scd).attr("type")) .filter(scd => "application/ld+json" == html(scd).attr("type"))
.map(i => { .map(i => {
@ -108,8 +108,8 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
const idPattern = /(\d){5,10}/g; const idPattern = /(\d){5,10}/g;
const id = idPattern.exec(obj.url); const id = idPattern.exec(obj.url);
const name = obj.name; const name = obj.name;
const title = super.getTitleNameByMode(name, PersonNameMode.CH_NAME, context)??name; const title = super.getTitleNameByMode(name, PersonNameMode.CH_NAME, context) ?? name;
const originalTitle = super.getTitleNameByMode(name, PersonNameMode.EN_NAME, context) ?? name; const originalTitle = super.getTitleNameByMode(name, PersonNameMode.EN_NAME, context) ?? name;
const result: DoubanMovieSubject = { const result: DoubanMovieSubject = {
id: id ? id[0] : '', id: id ? id[0] : '',
@ -119,14 +119,14 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
originalTitle: originalTitle, originalTitle: originalTitle,
desc: obj.description, desc: obj.description,
url: "https://movie.douban.com" + obj.url, url: "https://movie.douban.com" + obj.url,
director: obj.director, director: obj.director || [],
author: obj.author, author: obj.author || [],
actor: obj.actor, actor: obj.actor || [],
aggregateRating: obj.aggregateRating, aggregateRating: obj.aggregateRating,
datePublished: obj.datePublished ? new Date(obj.datePublished) : undefined, datePublished: obj.datePublished ? new Date(obj.datePublished) : undefined,
image: obj.image, image: obj.image,
imageUrl: obj.image, imageUrl: obj.image,
genre: obj.genre, genre: obj.genre || [],
publisher: '', publisher: '',
aliases: [""], aliases: [""],
language: [""], language: [""],
@ -136,10 +136,52 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
} }
return result; return result;
})[0]; })[0];
this.handlePersonNameByMeta(html, movie, context, 'video:actor', 'actor');
this.handlePersonNameByMeta(html, movie, context, 'video:director', 'director');
const desc:string = html("span[property='v:summary']").text(); // Fallback: if JSON-LD parsing failed (e.g., anti-bot page), extract from meta tags
if (!movie) {
const title = html(html("head > meta[property='og:title']").get(0)).attr("content") || '';
const image = html(html("head > meta[property='og:image']").get(0)).attr("content") || '';
const urlMeta = html(html("head > meta[property='og:url']").get(0)).attr("content") || '';
const desc = html(html("head > meta[property='og:description']").get(0)).attr("content") || '';
// Extract ID from URL
const idPattern = /(\d){5,10}/g;
const idMatch = idPattern.exec(urlMeta);
const id = idMatch ? idMatch[0] : '';
// Extract score from HTML
const scoreText = html("#interest_sectl strong[property='v:average']").text();
const score = scoreText ? parseFloat(scoreText) : undefined;
movie = {
id,
title,
type: this.getSupportType(),
score,
originalTitle: title,
desc,
url: urlMeta || (id ? `https://movie.douban.com/subject/${id}/` : ''),
director: [],
author: [],
actor: [],
aggregateRating: undefined,
datePublished: undefined,
image,
imageUrl: image,
genre: [],
publisher: '',
aliases: [],
language: [],
country: [],
time: null,
IMDb: null,
};
}
this.handlePersonNameByMeta(html, movie, context, 'video:actor', 'actor');
this.handlePersonNameByMeta(html, movie, context, 'video:director', 'director');
const desc: string = html("span[property='v:summary']").text();
if (desc) { if (desc) {
movie.desc = desc; movie.desc = desc;
} }
@ -156,7 +198,7 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
// value = html(info.next.next).text().trim(); // value = html(info.next.next).text().trim();
const vas = html(info.next).text().trim(); const vas = html(info.next).text().trim();
value = vas.split("/").map((v) => v.trim()); value = vas.split("/").map((v) => v.trim());
} else if(key.indexOf('片长') >= 0) { } else if (key.indexOf('片长') >= 0) {
value = html(info.next.next).text().trim() value = html(info.next.next).text().trim()
} else { } else {
value = html(info.next).text().trim(); value = html(info.next).text().trim();
@ -164,11 +206,11 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
valueMap.set(MovieKeyValueMap.get(key), value); valueMap.set(MovieKeyValueMap.get(key), value);
}) })
movie.country = valueMap.has('country') ? valueMap.get('country') : []; movie.country = valueMap.has('country') ? valueMap.get('country') : [];
movie.language = valueMap.has('language') ? valueMap.get('language') : []; movie.language = valueMap.has('language') ? valueMap.get('language') : [];
movie.time = valueMap.has('time') ? valueMap.get('time') : ""; movie.time = valueMap.has('time') ? valueMap.get('time') : "";
movie.aliases = valueMap.has('aliases') ? valueMap.get('aliases') : []; movie.aliases = valueMap.has('aliases') ? valueMap.get('aliases') : [];
movie.IMDb = valueMap.has('IMDb') ? valueMap.get('IMDb') : ""; movie.IMDb = valueMap.has('IMDb') ? valueMap.get('IMDb') : "";
return movie; return movie;
} }

@ -25,22 +25,22 @@ export class DoubanTeleplayLoadHandler extends DoubanAbstractLoadHandler<DoubanT
} }
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanTeleplaySubject, context: HandleContext): void { parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanTeleplaySubject, context: HandleContext): void {
variableMap.set("director", new DataField("director", DataValueType.array, extract.director,extract.director.map(SchemaOrg.getPersonName).filter(c => c))); variableMap.set("director", new DataField("director", DataValueType.array, extract.director,(extract.director || []).map(SchemaOrg.getPersonName).filter(c => c)));
variableMap.set("actor", new DataField( variableMap.set("actor", new DataField(
"actor", "actor",
DataValueType.array, DataValueType.array,
extract.actor, extract.actor,
extract.actor.map(SchemaOrg.getPersonName).filter(c => c) (extract.actor || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("author", new DataField( variableMap.set("author", new DataField(
"author", "author",
DataValueType.array, DataValueType.array,
extract.author, extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c) (extract.author || []).map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
)); ));
variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases, variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases,
extract.aliases.map(a=>a (extract.aliases || []).map(a=>a
.trim() .trim()
// .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_') // .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
// //replase multiple _ to single _ // //replase multiple _ to single _
@ -84,7 +84,7 @@ export class DoubanTeleplayLoadHandler extends DoubanAbstractLoadHandler<DoubanT
} }
parseSubjectFromHtml(html: CheerioAPI, context: HandleContext): DoubanTeleplaySubject { parseSubjectFromHtml(html: CheerioAPI, context: HandleContext): DoubanTeleplaySubject {
const teleplay:DoubanTeleplaySubject = html('script') let teleplay: DoubanTeleplaySubject | undefined = html('script')
.get() .get()
.filter(scd => "application/ld+json" == html(scd).attr("type")) .filter(scd => "application/ld+json" == html(scd).attr("type"))
.map(i => { .map(i => {
@ -104,14 +104,14 @@ export class DoubanTeleplayLoadHandler extends DoubanAbstractLoadHandler<DoubanT
originalTitle: originalTitle, originalTitle: originalTitle,
desc: obj.description, desc: obj.description,
url: "https://movie.douban.com" + obj.url, url: "https://movie.douban.com" + obj.url,
director: obj.director, director: obj.director || [],
author: obj.author, author: obj.author || [],
actor: obj.actor, actor: obj.actor || [],
aggregateRating: obj.aggregateRating, aggregateRating: obj.aggregateRating,
datePublished: obj.datePublished ? new Date(obj.datePublished) : undefined, datePublished: obj.datePublished ? new Date(obj.datePublished) : undefined,
image: obj.image, image: obj.image,
imageUrl: obj.image, imageUrl: obj.image,
genre: obj.genre, genre: obj.genre || [],
score: obj.aggregateRating ? obj.aggregateRating.ratingValue : undefined, score: obj.aggregateRating ? obj.aggregateRating.ratingValue : undefined,
publisher: "", publisher: "",
aliases: [""], aliases: [""],
@ -124,6 +124,46 @@ export class DoubanTeleplayLoadHandler extends DoubanAbstractLoadHandler<DoubanT
return result; return result;
})[0]; })[0];
// Fallback: if JSON-LD parsing failed, extract from meta tags
if (!teleplay) {
const title = html(html("head > meta[property='og:title']").get(0)).attr("content") || '';
const image = html(html("head > meta[property='og:image']").get(0)).attr("content") || '';
const urlMeta = html(html("head > meta[property='og:url']").get(0)).attr("content") || '';
const desc = html(html("head > meta[property='og:description']").get(0)).attr("content") || '';
const idPattern = /(\d){5,10}/g;
const idMatch = idPattern.exec(urlMeta);
const id = idMatch ? idMatch[0] : '';
const scoreText = html("#interest_sectl strong[property='v:average']").text();
const score = scoreText ? parseFloat(scoreText) : undefined;
teleplay = {
id,
title,
type: this.getSupportType(),
score,
originalTitle: title,
desc,
url: urlMeta || (id ? `https://movie.douban.com/subject/${id}/` : ''),
director: [],
author: [],
actor: [],
aggregateRating: undefined,
datePublished: undefined,
image,
imageUrl: image,
genre: [],
publisher: '',
aliases: [],
language: [],
country: [],
episode: null,
time: null,
IMDb: null,
};
}
this.handlePersonNameByMeta(html, teleplay, context, 'video:actor', 'actor'); this.handlePersonNameByMeta(html, teleplay, context, 'video:actor', 'actor');
this.handlePersonNameByMeta(html, teleplay, context, 'video:director', 'director'); this.handlePersonNameByMeta(html, teleplay, context, 'video:director', 'director');
const desc:string = html("span[property='v:summary']").text(); const desc:string = html("span[property='v:summary']").text();

@ -33,28 +33,28 @@ export default class DoubanTheaterLoadHandler extends DoubanAbstractLoadHandler<
"director", "director",
DataValueType.array, DataValueType.array,
extract.director, extract.director,
extract.director.map(SchemaOrg.getPersonName).filter(c => c) (extract.director || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("actor", new DataField( variableMap.set("actor", new DataField(
"actor", "actor",
DataValueType.array, DataValueType.array,
extract.actor, extract.actor,
extract.actor.map(SchemaOrg.getPersonName).filter(c => c) (extract.actor || []).map(SchemaOrg.getPersonName).filter(c => c)
)); ));
variableMap.set("author", new DataField( variableMap.set("author", new DataField(
"author", "author",
DataValueType.array, DataValueType.array,
extract.author, extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c) (extract.author || []).map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
)); ));
variableMap.set("aliases", new DataField( variableMap.set("aliases", new DataField(
"aliases", "aliases",
DataValueType.array, DataValueType.array,
extract.aliases, extract.aliases,
extract.aliases.map(a => a (extract.aliases || []).map(a => a
.trim() .trim()
.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_') .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
//replace multiple _ to single _ //replace multiple _ to single _

@ -3,8 +3,7 @@ import {
} from "../../../../constant/Constsant"; } from "../../../../constant/Constsant";
import {SearchResultPageParserInterface} from "./SearchResultPageParserInterface"; import {SearchResultPageParserInterface} from "./SearchResultPageParserInterface";
import {SearchPage} from "../../model/SearchPage"; import {SearchPage} from "../../model/SearchPage";
import SearchParserHandlerV2 from "../SearchParserV2"; import SearchParserHandler from "../SearchParser";
import StringUtil from "../../../../utils/StringUtil";
import {log} from "../../../../utils/Logutil"; import {log} from "../../../../utils/Logutil";
export class AllFirstPageSearchResultPageParser implements SearchResultPageParserInterface { export class AllFirstPageSearchResultPageParser implements SearchResultPageParserInterface {
@ -12,22 +11,11 @@ export class AllFirstPageSearchResultPageParser implements SearchResultPageParse
return pageNum == 1 && type == SupportType.all; return pageNum == 1 && type == SupportType.all;
} }
parse(source:string, type:SupportType, pageNum:number, pageSize:number):SearchPage { parse(source:string, type:SupportType, pageNum:number, pageSize:number):SearchPage {
if (!source || StringUtil.notJsonString(source)) { log.debug("解析给多页面结果");
//TODO 国际化 if (!source) {
log.notice("Obsidian-Douban:查询结果为空,无匹配结果,请尝试登录获取获取更多数据(已登录则忽略)"); return new SearchPage(0, 0, 0, type, []);
return SearchPage.empty(type);
} }
return SearchParserHandler.parseSearchJson(source, type, pageNum);
const {subjects} = JSON.parse(source);
if (!subjects) {
return SearchPage.empty(type);
}
const {items} = subjects;
if (!items ||items.length == 0) {
return SearchPage.empty(type);
}
const doubanSearchResultSubjects = SearchParserHandlerV2.itemMapToSearchResult(items);
return new SearchPage(2000, pageNum, pageSize, type, doubanSearchResultSubjects);
} }

@ -2,7 +2,7 @@ import {SupportType} from "../../../../constant/Constsant";
import {SearchResultPageParserInterface} from "./SearchResultPageParserInterface"; import {SearchResultPageParserInterface} from "./SearchResultPageParserInterface";
import {log} from "../../../../utils/Logutil"; import {log} from "../../../../utils/Logutil";
import {SearchPage} from "../../model/SearchPage"; import {SearchPage} from "../../model/SearchPage";
import SearchParserHandlerV2 from "../SearchParserV2"; import SearchParserHandler from "../SearchParser";
export class OtherAllPageSearchResultPageParser implements SearchResultPageParserInterface { export class OtherAllPageSearchResultPageParser implements SearchResultPageParserInterface {
support(type:SupportType, pageNum:number):boolean { support(type:SupportType, pageNum:number):boolean {
@ -10,13 +10,10 @@ export class OtherAllPageSearchResultPageParser implements SearchResultPageParse
} }
parse(source:string, type:SupportType, pageNum:number, pageSize:number):SearchPage { parse(source:string, type:SupportType, pageNum:number, pageSize:number):SearchPage {
log.debug("解析给多页面结果"); log.debug("解析给多页面结果");
const {contents} = JSON.parse(source); if (!source) {
if (!contents) {
return new SearchPage(0, 0, 0, type, []); return new SearchPage(0, 0, 0, type, []);
} }
const data:{total:number, start:number, count:number, items:any[]} = contents; return SearchParserHandler.parseSearchJson(source, type, pageNum);
const doubanSearchResultSubjects = SearchParserHandlerV2.itemMapToSearchResult(data.items);
return new SearchPage(data.total, pageNum, pageSize, type, doubanSearchResultSubjects);
} }
} }

@ -2,8 +2,8 @@ import {AbstractSearchPageFetcher} from "./AbstractSearchPageFetcher";
import { SupportType } from "src/org/wanxp/constant/Constsant"; import { SupportType } from "src/org/wanxp/constant/Constsant";
export class AllPageSearchPageFetcher extends AbstractSearchPageFetcher { export class AllPageSearchPageFetcher extends AbstractSearchPageFetcher {
getUrl(keyword: string, pageNum: number, pageSize: number): string { getUrl(keyword: string, start: number, pageSize: number): string {
return `https://m.douban.com/rexxar/api/v2/search?q=${keyword}&start=${pageNum}&count=${pageSize}`; return `https://www.douban.com/j/search?q=${keyword}&start=${start}`;
} }
support(type: SupportType): boolean { support(type: SupportType): boolean {
return type == SupportType.all; return type == SupportType.all;

@ -10,17 +10,17 @@ export function constructLoginUI(containerEl: HTMLElement, manager: SettingsMana
// containerEl.createEl('h3', { text: i18nHelper.getMessage('1210') }); // containerEl.createEl('h3', { text: i18nHelper.getMessage('1210') });
const userComponent = manager.plugin.userComponent; const userComponent = manager.plugin.userComponent;
if (userComponent.needLogin()) { if (userComponent.isLogin() && !userComponent.isVerified()) {
try { // Assumed login — verify to get user ID/name for settings display
userComponent.login() userComponent.login()
.then(() => { .then(() => constructDoubanLoginSettingsUI(containerEl, manager))
constructDoubanLoginSettingsUI(containerEl, manager); .catch(() => constructDoubanLoginSettingsUI(containerEl, manager));
}); } else if (userComponent.needLogin()) {
}catch (e) { // Has credentials but not yet logged in
log.debug(i18nHelper.getMessage('100101')); userComponent.login()
constructDoubanLoginSettingsUI(containerEl, manager); .then(() => constructDoubanLoginSettingsUI(containerEl, manager))
} .catch(() => constructDoubanLoginSettingsUI(containerEl, manager));
}else { } else {
constructDoubanLoginSettingsUI(containerEl, manager); constructDoubanLoginSettingsUI(containerEl, manager);
} }

@ -15,6 +15,7 @@ import {DoubanHttpUtil} from "../../utils/DoubanHttpUtil";
export default class UserComponent { export default class UserComponent {
private settingsManager: SettingsManager; private settingsManager: SettingsManager;
private user: User; private user: User;
private verified: boolean = false;
constructor(settingsManager: SettingsManager) { constructor(settingsManager: SettingsManager) {
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
@ -39,11 +40,26 @@ export default class UserComponent {
this.user.login = false; this.user.login = false;
} }
this.user = null; this.user = null;
this.verified = false;
this.settingsManager.updateSetting('loginCookiesContent', ''); this.settingsManager.updateSetting('loginCookiesContent', '');
this.settingsManager.updateSetting('loginHeadersContent', ''); this.settingsManager.updateSetting('loginHeadersContent', '');
} }
assumeLoggedIn(): void {
const headers: any = this.settingsManager.getSetting('loginHeadersContent');
const cookies: any = this.settingsManager.getSetting('loginCookiesContent');
if (headers || cookies) {
this.user = new User();
this.user.login = true;
this.verified = false;
}
}
isVerified(): boolean {
return this.verified;
}
needLogin() { needLogin() {
@ -68,6 +84,7 @@ export default class UserComponent {
this.settingsManager.debug(`配置界面:loginCookie:豆瓣headers信息正常${user&&user.id?'获取用户信息成功id:'+ StringUtil.confuse(user.id) + ',用户名:'+ StringUtil.confuse(user.name) :'获取用户信息失败'}`); this.settingsManager.debug(`配置界面:loginCookie:豆瓣headers信息正常${user&&user.id?'获取用户信息成功id:'+ StringUtil.confuse(user.id) + ',用户名:'+ StringUtil.confuse(user.name) :'获取用户信息失败'}`);
}); });
if(this.user) { if(this.user) {
this.verified = true;
this.settingsManager.updateSetting('loginHeadersContent', JSON.stringify(headers)); this.settingsManager.updateSetting('loginHeadersContent', JSON.stringify(headers));
} }
return this.user; return this.user;
@ -139,6 +156,9 @@ export default class UserComponent {
this.user = user; this.user = user;
this.settingsManager.debug(`主界面:loginByCookie:豆瓣cookies信息正常${user&&user.id?'获取用户信息成功id:'+ StringUtil.confuse(user.id) + ',用户名:'+ StringUtil.confuse(user.name) :'获取用户信息失败'}`); this.settingsManager.debug(`主界面:loginByCookie:豆瓣cookies信息正常${user&&user.id?'获取用户信息成功id:'+ StringUtil.confuse(user.id) + ',用户名:'+ StringUtil.confuse(user.name) :'获取用户信息失败'}`);
}); });
if (this.user && this.user.id) {
this.verified = true;
}
return this.user; return this.user;
} }
} }

@ -335,6 +335,8 @@ PS: This file could be delete if you want to.
'130105': `Can not use Douban this time, Please try again after 12 hour or 24 hour. Or you can reset your connection `, '130105': `Can not use Douban this time, Please try again after 12 hour or 24 hour. Or you can reset your connection `,
'130106': `Can not use Douban this time, Please try Login In Douban Plugin. If not working please again after 12 hour or 24 hour. Or you can reset your connection `, '130106': `Can not use Douban this time, Please try Login In Douban Plugin. If not working please again after 12 hour or 24 hour. Or you can reset your connection `,
'130404': `404 Url Not Found`, '130404': `404 Url Not Found`,
'130109': `Downloaded image is empty`,
'130110': `Clipboard write failed`,
'130107': `Can not find array setting for {1} in {0} , Please add it in array settings`, '130107': `Can not find array setting for {1} in {0} , Please add it in array settings`,

@ -350,6 +350,8 @@ export default {
'130105': `由于多次频繁请求数据,豆瓣当前暂时不可用. 请于12小时或24小时后再重试或重置你的网络(如重新拨号或更换网络) `, '130105': `由于多次频繁请求数据,豆瓣当前暂时不可用. 请于12小时或24小时后再重试或重置你的网络(如重新拨号或更换网络) `,
'130106': `请尝试在Douban插件中登录后操作. 若还是无效果则尝试于12小时或24小时后再重试或重置你的网络(如重新拨号或更换网络) `, '130106': `请尝试在Douban插件中登录后操作. 若还是无效果则尝试于12小时或24小时后再重试或重置你的网络(如重新拨号或更换网络) `,
'130107': `参数{0}中指定的数组输出类型{1}不存在,请前往配置进行设置`, '130107': `参数{0}中指定的数组输出类型{1}不存在,请前往配置进行设置`,
'130109': `下载图片内容为空`,
'130110': `写入剪贴板失败`,
'130120': `同步时发生错误,但同步将会继续。错误项目是 {}。`, '130120': `同步时发生错误,但同步将会继续。错误项目是 {}。`,
'130121': `总数只有{0}, 选择的开始比总数还要大,将不会同步。`, '130121': `总数只有{0}, 选择的开始比总数还要大,将不会同步。`,

@ -286,6 +286,7 @@ export default class DoubanPlugin extends Plugin {
// this.fetchOnlineData(this.settingsManager); // this.fetchOnlineData(this.settingsManager);
this.userComponent = new UserComponent(this.settingsManager); this.userComponent = new UserComponent(this.settingsManager);
this.netFileHandler = new NetFileHandler(this.fileHandler); this.netFileHandler = new NetFileHandler(this.fileHandler);
this.userComponent.assumeLoggedIn();
this.settingTab = new DoubanSettingTab(this.app, this); this.settingTab = new DoubanSettingTab(this.app, this);
this.addSettingTab(this.settingTab); this.addSettingTab(this.settingTab);
@ -355,11 +356,16 @@ export default class DoubanPlugin extends Plugin {
async checkLogin(context: HandleContext):Promise<boolean> { async checkLogin(context: HandleContext):Promise<boolean> {
this.settingsManager.debug('主界面:同步时的登录状态检测'); this.settingsManager.debug('主界面:同步时的登录状态检测');
if (!context.userComponent.needLogin()) { const uc = context.userComponent;
this.settingsManager.debug('主界面:同步时的登录状态检测完成: 无用户信息, 尝试获取用户信息'); // If assumed-logged-in but not verified, verify now (sync needs real user ID)
await context.userComponent.login(); if (uc.isLogin() && !uc.isVerified()) {
await uc.login();
} }
if (!context.userComponent.isLogin()) { // If has saved credentials but not logged in, try login
if (uc.needLogin()) {
await uc.login();
}
if (!uc.isLogin()) {
this.settingsManager.debug('主界面:同步时的登录状态检测完成: 尝试获取用户信息失败'); this.settingsManager.debug('主界面:同步时的登录状态检测完成: 尝试获取用户信息失败');
new Notice(i18nHelper.getMessage('140303')); new Notice(i18nHelper.getMessage('140303'));
return false; return false;

@ -17,16 +17,19 @@ export default class NetFileHandler {
async downloadDBFile(url: string, folder:string, filename: string, context:HandleContext, showError:boolean, headers?:any): Promise<{ success: boolean, error:string, filepath: string }> { async downloadDBFile(url: string, folder:string, filename: string, context:HandleContext, showError:boolean, headers?:any): Promise<{ success: boolean, error:string, filepath: string }> {
const filePath:string = FileUtil.join(folder, filename); const filePath:string = FileUtil.join(folder, filename);
return HttpUtil.httpRequestBuffer(url, headers, context.plugin.settingsManager) return HttpUtil.httpRequestBuffer(url, headers, context.plugin.settingsManager)
.then((response) => { .then((response) => {
if (response.status == 404) { if (response.status == 404) {
throw new Error(i18nHelper.getMessage('130404')); throw new Error(i18nHelper.getMessage('130404'));
} }
if (response.status == 403) { if (response.status == 403) {
throw new Error(i18nHelper.getMessage('130106')); throw new Error(i18nHelper.getMessage('130106'));
} }
return response.textArrayBuffer; if (response.status == 418) {
}) throw new Error(i18nHelper.getMessage('130105'));
}
return response.textArrayBuffer;
})
.then((buffer) => { .then((buffer) => {
if (!buffer || buffer.byteLength == 0) { if (!buffer || buffer.byteLength == 0) {
return 0; return 0;
@ -56,15 +59,23 @@ export default class NetFileHandler {
async downloadDBUploadPicGoByClipboard(url: string, filename: string, context:HandleContext, showError:boolean, headers?:any): Promise<{ success: boolean, error:string, filepath: string }> { async downloadDBUploadPicGoByClipboard(url: string, filename: string, context:HandleContext, showError:boolean, headers?:any): Promise<{ success: boolean, error:string, filepath: string }> {
return HttpUtil.httpRequestBuffer(url, headers, context.plugin.settingsManager) return HttpUtil.httpRequestBuffer(url, headers, context.plugin.settingsManager)
.then((response) => { .then((response) => {
if (response.status == 404) {
throw new Error(i18nHelper.getMessage('130404'));
}
if (response.status == 403) { if (response.status == 403) {
throw new Error(i18nHelper.getMessage('130106')); throw new Error(i18nHelper.getMessage('130106'));
} }
if (response.status == 418) {
throw new Error(i18nHelper.getMessage('130105'));
}
return response.textArrayBuffer; return response.textArrayBuffer;
}) })
.then((buffer) => { .then(async (buffer) => {
ClipboardUtil.writeImage(buffer); if (!buffer || buffer.byteLength == 0) {
}).then(() => { throw new Error(i18nHelper.getMessage('130109'));
return this.uploadClipboardFile(context); }
await ClipboardUtil.writeImage(buffer);
return await this.uploadClipboardFile(context);
}).then((data) => { }).then((data) => {
if (data.success) { if (data.success) {
return {success: true, error: '', filepath: HttpUtil.extractURLFromString(data.result[0])}; return {success: true, error: '', filepath: HttpUtil.extractURLFromString(data.result[0])};
@ -108,4 +119,3 @@ export default class NetFileHandler {
} }
} }

@ -1,12 +1,78 @@
export class ClipboardUtil { import {i18nHelper} from "../lang/helper";
public static async writeImage(data:ArrayBuffer, options: ClipboardOptions = defaultClipboardOptions) { export class ClipboardUtil {
const { clipboard, nativeImage } = require('electron');
await clipboard.writeImage(nativeImage.createFromBuffer(data)); public static async writeImage(data:ArrayBuffer, options: ClipboardOptions = defaultClipboardOptions) {
console.log(`Copied to clipboard as HTML`); if (!data || data.byteLength == 0) {
throw new Error(i18nHelper.getMessage('130110'));
}
const { clipboard, nativeImage } = require('electron');
const isWebp = this.isWebp(data);
let image = nativeImage.createFromBuffer(Buffer.from(data));
if ((!image || image.isEmpty()) && isWebp) {
image = await this.createNativeImageFromWebp(data);
}
if (!image || image.isEmpty()) {
throw new Error(i18nHelper.getMessage('130110'));
}
clipboard.clear();
clipboard.writeImage(image);
await new Promise(resolve => setTimeout(resolve, 100));
const clipboardImage = clipboard.readImage();
if (!clipboardImage || clipboardImage.isEmpty()) {
throw new Error(i18nHelper.getMessage('130110'));
}
console.log(`Copied to clipboard as HTML`);
}
private static isWebp(data: ArrayBuffer): boolean {
const bytes = new Uint8Array(data);
if (bytes.length < 12) {
return false;
}
return bytes[0] === 0x52
&& bytes[1] === 0x49
&& bytes[2] === 0x46
&& bytes[3] === 0x46
&& bytes[8] === 0x57
&& bytes[9] === 0x45
&& bytes[10] === 0x42
&& bytes[11] === 0x50;
}
private static async createNativeImageFromWebp(data: ArrayBuffer) {
const { nativeImage } = require('electron');
const imageElement = await this.loadImage(URL.createObjectURL(new Blob([data], {type: 'image/webp'})));
try {
const canvas = document.createElement('canvas');
canvas.width = imageElement.naturalWidth || imageElement.width;
canvas.height = imageElement.naturalHeight || imageElement.height;
const context = canvas.getContext('2d');
if (!context) {
throw new Error(i18nHelper.getMessage('130110'));
}
context.drawImage(imageElement, 0, 0);
return nativeImage.createFromDataURL(canvas.toDataURL('image/png'));
} finally {
imageElement.src = '';
}
}
private static loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
URL.revokeObjectURL(url);
resolve(image);
};
image.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error(i18nHelper.getMessage('130110')));
};
image.src = url;
});
} }
} }
@ -20,4 +86,3 @@ interface ClipboardOptions {
const defaultClipboardOptions: ClipboardOptions = { const defaultClipboardOptions: ClipboardOptions = {
contentType: 'text/plain', contentType: 'text/plain',
} }

@ -7,6 +7,19 @@ import {HttpResponse} from "./model/HttpResponse";
export default class HttpUtil { export default class HttpUtil {
private static readonly IMAGE_REQUEST_HEADERS_TO_DROP = new Set([
'authority',
'content-length',
'content-type',
'host',
'origin',
'referer',
'sec-fetch-dest',
'sec-fetch-mode',
'sec-fetch-site',
'sec-fetch-user',
]);
/** /**
@ -60,6 +73,27 @@ export default class HttpUtil {
} }
} }
public static buildImageRequestHeaders(headers: Record<string, any> = {}, referer?: string): Record<string, string> {
const nextHeaders: Record<string, string> = {};
Object.entries(headers || {}).forEach(([key, value]) => {
if (value == null || value === '') {
return;
}
const lowerKey = key.toLowerCase();
if (lowerKey === 'referer') {
return;
}
if (this.IMAGE_REQUEST_HEADERS_TO_DROP.has(lowerKey) || lowerKey.startsWith('sec-ch-ua')) {
return;
}
nextHeaders[key] = String(value);
});
if (referer) {
nextHeaders.Referer = referer;
}
return nextHeaders;
}
public static parse(url: string): { protocol: string, host: string, port: string, path: string } { public static parse(url: string): { protocol: string, host: string, port: string, path: string } {
const regex = /^(.*?):\/\/([^\/:]+)(?::(\d+))?([^?]*)$/; const regex = /^(.*?):\/\/([^\/:]+)(?::(\d+))?([^?]*)$/;

@ -61,6 +61,7 @@
"2.2.3": "0.12.0", "2.2.3": "0.12.0",
"2.2.4": "0.12.0", "2.2.4": "0.12.0",
"2.3.0": "0.12.0", "2.3.0": "0.12.0",
"2.3.1": "0.12.0" "2.3.1": "0.12.0",
"2.3.2": "0.12.0"
} }