feat: 附件(图片)文件保存本地支持自定义

fix: 修复有些图片无法下问题
fix: 修复同步汇总文件中文件名和真实保存不一致问题
feat: 调整别名的转义
This commit is contained in:
wanxp 2025-11-09 22:43:17 +08:00
parent 266cfb3901
commit 08749116f3
20 changed files with 144 additions and 66 deletions

@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: DoubanPluginSetting = {
cacheImage: true,
cacheHighQuantityImage: true,
attachmentPath: 'assets',
attachmentFileName: "{{title}}",
syncHandledDataArray: [],
// syncLastUpdateTime: new Map<string, string>(),
scoreSetting: {

@ -49,13 +49,7 @@ export default class DoubanTheaterAiLoadHandler extends DoubanAbstractLoadHandle
extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
));
variableMap.set("aliases", new DataField(
"aliases",
DataValueType.array,
extract.aliases,
extract.aliases.map(a => a.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_'))
));
super.parseAliases(beforeContent, variableMap, extract, context);
}
support(extract: DoubanSubject): boolean {

@ -150,6 +150,7 @@ ${syncStatus.getHandle() == 0? '...' : i18nHelper.getMessage('110042') + ':' + T
cacheImage: ( settings.cacheImage == null) ? DEFAULT_SETTINGS.cacheImage : settings.cacheImage,
cacheHighQuantityImage: ( settings.cacheHighQuantityImage == null) ? DEFAULT_SETTINGS.cacheHighQuantityImage : settings.cacheHighQuantityImage,
attachmentPath: (settings.attachmentPath == '' || settings.attachmentPath == null) ? DEFAULT_SETTINGS.attachmentPath : settings.attachmentPath,
attachmentFileName: (settings.attachmentFileName == '' || settings.attachmentFileName == null) ? DEFAULT_SETTINGS.attachmentFileName : settings.attachmentFileName,
templateFile: (settings.movieTemplateFile == '' || settings.movieTemplateFile == null) ? DEFAULT_SETTINGS.movieTemplateFile : settings.movieTemplateFile,
incrementalUpdate: true,
syncConditionType: SyncConditionType.ALL,
@ -427,6 +428,20 @@ ${syncStatus.getHandle() == 0? '...' : i18nHelper.getMessage('110042') + ':' + T
});
})
.setDisabled(disable);
new Setting(containerEl)
.setName( i18nHelper.getMessage('121452'))
.setDesc( i18nHelper.getMessage('121453'))
.addSearch(async (search: SearchComponent) => {
new FolderSuggest(this.plugin.app, search.inputEl);
// @ts-ignore
search.setValue(config.attachmentFileName)
// @ts-ignore
.setPlaceholder(i18nHelper.getMessage('121454'))
.onChange(async (value: string) => {
config.attachmentFileName = value;
});
})
.setDisabled(disable);
new Setting(containerEl)
.setName(i18nHelper.getMessage('121435'))

@ -5,7 +5,7 @@ import {moment, Platform, TFile} from "obsidian";
import {i18nHelper} from 'src/org/wanxp/lang/helper';
import {log} from "src/org/wanxp/utils/Logutil";
import {CheerioAPI, load} from "cheerio";
import YamlUtil from "../../../utils/YamlUtil";
import YamlUtil, {TITLE_ALIASES_SPECIAL_CHAR_REG_G} from "../../../utils/YamlUtil";
import {
BasicConst,
DataValueType,
@ -47,7 +47,11 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
async parse(extract: T, context: HandleContext): Promise<HandleResult> {
const template: string = await this.getTemplate(extract, context);
await this.saveImage(extract, context);
const variableMap = this.buildVariableMap(extract, context);
this.parseUserInfo(template, variableMap, extract, context);
this.parseVariable(template, variableMap, extract, context);
await this.saveImage(extract, context, variableMap);
const frontMatterStart: number = template.indexOf(BasicConst.YAML_FRONT_MATTER_SYMBOL, 0);
const frontMatterEnd: number = template.indexOf(BasicConst.YAML_FRONT_MATTER_SYMBOL, frontMatterStart + 1);
let frontMatter = '';
@ -55,10 +59,6 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
let frontMatterAfter = '';
let result = '';
const variableMap = this.buildVariableMap(extract, context);
this.parseUserInfo(template, variableMap, extract, context);
this.parseVariable(template, variableMap, extract, context);
if (frontMatterStart > -1 && frontMatterEnd > -1) {
frontMatterBefore = template.substring(0, frontMatterStart);
frontMatter = template.substring(frontMatterStart, frontMatterEnd + 3);
@ -111,7 +111,19 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
abstract getSupportType(): SupportType;
abstract parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: T, context: HandleContext): void;
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: T, context: HandleContext): void;
parseAliases(beforeContent: string, variableMap:Map<string, DataField>, extract: T, context: HandleContext): string[] {
// variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases,
// extract.aliases.map(a=>a
// .trim()
// .replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
// //replase multiple _ to single _
// .replace(/_+/g, '_')
// .replace(/^_/, '')
// .replace(/_$/, '')
// )));
}
abstract support(extract: DoubanSubject): boolean;
@ -522,43 +534,69 @@ export default abstract class DoubanAbstractLoadHandler<T extends DoubanSubject>
}
}
private async saveImage(extract: T, context: HandleContext) {
private async saveImage(extract: T, context: HandleContext, variableMap : Map<string, DataField>) {
const {syncConfig} = context;
if (!extract.image || (syncConfig && !syncConfig.cacheImage) || !context.settings.cacheImage) {
return;
}
const image = extract.image;
const filename = image.split('/').pop();
let folder = syncConfig? syncConfig.attachmentPath : context.settings.attachmentPath;
if (!folder) {
folder = DEFAULT_SETTINGS.attachmentPath;
}
folder = this.parsePartText(folder, extract, context)
const referHeaders = {'referer': image};
folder = this.parsePartPath(folder, extract, context, variableMap)
let fileName = syncConfig? syncConfig.attachmentFileName : context.settings.attachmentFileName;
if (!fileName) {
fileName = DEFAULT_SETTINGS.attachmentFileName;
}
let fileNameSuffix = image ? image.substring(image.lastIndexOf('.')) : '.jpg';
if (fileNameSuffix && fileNameSuffix.length > 10) {
fileNameSuffix = '.jpg';
}
fileName = this.parsePartPath(fileName, extract, context, variableMap)
fileName = fileName + fileNameSuffix;
// const referHeaders = {'referer': image};
const referHeaders = context.settings.loginHeadersContent ? JSON.parse(context.settings.loginHeadersContent) : {};
if ((syncConfig ? syncConfig.cacheHighQuantityImage : context.settings.cacheHighQuantityImage) && context.userComponent.isLogin()) {
try {
const fileNameSpilt = filename.split('.');
const fileNameSpilt = fileName.split('.');
const highFilename = fileNameSpilt.first() + '.jpg';
const highImage = this.getHighQuantityImageUrl(highFilename);
const resultValue = await this.handleImage(highImage, folder, highFilename, context, false, referHeaders);
if (resultValue && resultValue.success) {
extract.image = resultValue.filepath;
this.initImageVariableMap(extract, context, variableMap);
return;
}
}catch (e) {
console.error(e);
console.error('下载高清封面失败,将会使用普通封面')
}
}
const resultValue = await this.handleImage(image, folder, filename, context, true, referHeaders);
const resultValue = await this.handleImage(image, folder, fileName, context, true, referHeaders);
if (resultValue && resultValue.success) {
extract.image = resultValue.filepath;
this.initImageVariableMap(extract, context, variableMap);
}
}
private initImageVariableMap(extract: T, context: HandleContext, variableMap : Map<string, DataField>) {
variableMap.set(DoubanParameterName.IMAGE_URL, new DataField(
DoubanParameterName.IMAGE_URL,
DataValueType.url,
extract.imageUrl,
extract.imageUrl
));
variableMap.set(DoubanParameterName.IMAGE, new DataField(
DoubanParameterName.IMAGE,
DataValueType.path,
extract.image,
extract.image
));
}
private async handleImage(image: string, folder: string, filename: string, context: HandleContext, showError: boolean, headers?: any) {
//只有在桌面版且开启了图片上传才会使用PicGo并且开启图床功能
if (context.settings.pictureBedFlag && Platform.isDesktopApp) {

@ -28,7 +28,7 @@ export default class DoubanBookLoadHandler extends DoubanAbstractLoadHandler<Dou
return `https://book.douban.com/subject/${id}/`;
}
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanBookSubject, context: HandleContext, textMode: TemplateTextMode): void {
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanBookSubject, context: HandleContext): void {
variableMap.set(DoubanBookParameter.author, new DataField(DoubanBookParameter.author,
DataValueType.array, extract.author, extract.author.map(this.handleSpecialAuthorName)));
variableMap.set(DoubanBookParameter.translator, new DataField(DoubanBookParameter.translator,

@ -9,6 +9,7 @@ import {UserStateSubject} from "../model/UserStateSubject";
import {moment} from "obsidian";
import {TITLE_ALIASES_SPECIAL_CHAR_REG_G} from "../../../utils/YamlUtil";
import {DataField} from "../../../utils/model/DataField";
import {b} from "@shikijs/engine-javascript/dist/shared/engine-javascript.BnuFKbIS";
export default class DoubanGameLoadHandler extends DoubanAbstractLoadHandler<DoubanGameSubject> {
@ -28,7 +29,7 @@ export default class DoubanGameLoadHandler extends DoubanAbstractLoadHandler<Dou
}
parseVariable(beforeContent: string, variableMap:Map<string, DataField>, extract: DoubanGameSubject, context: HandleContext): void {
variableMap.set("aliases", new DataField("aliases", DataValueType.array, extract.aliases, extract.aliases.map(a=>a.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_'))));
super.parseAliases(beforeContent, variableMap, extract, context);
}
support(extract: DoubanSubject): boolean {

@ -51,13 +51,7 @@ export default class DoubanMovieLoadHandler extends DoubanAbstractLoadHandler<Do
extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
));
variableMap.set("aliases", new DataField(
"aliases",
DataValueType.array,
extract.aliases,
extract.aliases.map(a => a.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_'))
));
super.parseAliases(beforeContent, variableMap, extract, context);
}
support(extract: DoubanSubject): boolean {

@ -39,13 +39,7 @@ export class DoubanTeleplayLoadHandler extends DoubanAbstractLoadHandler<DoubanT
extract.author,
extract.author.map(SchemaOrg.getPersonName).map(name => super.getPersonName(name, context)).filter(c => c)
));
variableMap.set("aliases", new DataField(
"aliases",
DataValueType.array,
extract.aliases,
extract.aliases.map(a => a.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_'))
));
super.parseAliases(beforeContent, variableMap, extract, context);
}
support(extract: DoubanSubject): boolean {

@ -54,7 +54,15 @@ export default class DoubanTheaterLoadHandler extends DoubanAbstractLoadHandler<
"aliases",
DataValueType.array,
extract.aliases,
extract.aliases.map(a => a.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_'))
extract.aliases.map(a => a
.trim()
.replace(TITLE_ALIASES_SPECIAL_CHAR_REG_G, '_')
//replace multiple _ to single _
.replace(/_+/g, '_')
.replace(/^_/, '')
.replace(/_$/, '')
)
));
}

@ -31,7 +31,7 @@ class DoubanFuzzySuggester extends FuzzySuggestModal<DoubanSearchResultSubject>
private searchItem:string;
constructor(plugin: DoubanPlugin, context: HandleContext, searchItem:string) {
super(app);
super(plugin.app);
this.plugin = plugin;
this.context = context;
this.searchItem = searchItem;

@ -223,7 +223,8 @@ export function constructAttachmentFileSettingsUI(containerEl: HTMLElement, mana
}else {
new Setting(containerEl).then(createFolderSelectionSetting({containerEl: containerEl, name: '121432', desc: '121433', placeholder: null, key: null, manager: manager}));
new Setting(containerEl).then(createFolderSelectionSettingInput({containerEl: containerEl, name: null, desc: null, placeholder: '121434', key: 'attachmentPath', manager: manager}));
new Setting(containerEl).then(createFolderSelectionSetting({containerEl: containerEl, name: '121452', desc: '121453', placeholder: null, key: null, manager: manager}));
new Setting(containerEl).then(createFolderSelectionSettingInput({containerEl: containerEl, name: null, desc: null, placeholder: '121454', key: 'attachmentFileName', manager: manager}));
;
}

@ -33,6 +33,7 @@ export interface DoubanPluginSetting {
cacheImage: boolean,
cacheHighQuantityImage: boolean,
attachmentPath: string,
attachmentFileName: string,
pictureBedFlag: boolean
pictureBedType: string;
pictureBedSetting: PictureBedSetting;

@ -11,6 +11,9 @@ import {i18nHelper} from "../../../lang/helper";
import {DoubanTeleplaySyncHandler} from "./DoubanTeleplaySyncHandler";
import {SyncConditionType} from "../../../constant/Constsant";
import {DoubanGameSyncHandler} from "./DoubanGameSyncHandler";
import {DataField} from "../../../utils/model/DataField";
import {VariableUtil} from "../../../utils/VariableUtil";
import {FileUtil} from "../../../utils/FileUtil";
export default class SyncHandler {
private app: App;
@ -101,7 +104,7 @@ export default class SyncHandler {
`;
}else {
// @ts-ignore
details+= `${value.id}-[[${value.title}]]: ${i18nHelper.getMessage(value.status)}
details+= `${value.id}-[[${FileUtil.replaceSpecialCharactersForFileName(value.title)}]]: ${i18nHelper.getMessage(value.status)}
`;
}

@ -13,6 +13,7 @@ export interface SyncConfig {
cacheHighQuantityImage:boolean;
attachmentPath: string;
attachmentFileName: string;
templateFile: string;
incrementalUpdate: boolean;
}

@ -248,12 +248,16 @@ PS: This file could be delete if you want to.
'121410': `Search Default Type`,
'121411': `Search defuault type when open command palette 'search douban and create file'`,
'121430': `Save Attachment File`,
'121430': `Save Attachment(Picture) File`,
'121431': `Save attachment file to local disk, such as image ? If you do not enable this feature, it will not show cover image in note`,
'121432': `Attachment folder`,
'121432': `Attachment(Picture) folder`,
'121433': `Attachment file created from Obsidian-Douban will be placed in this folder,
If blank, they will be created by default name. support all basic template variables. example: {{type}}/assets`,
'121434': `assets`,
'121452': `Attachment(Picture) File Name`,
'121453': `Attachment file name, If blank, they will be created by default name '{{title}}'. support all basic template variables. example: {{type}}-{{title}}`,
'121454': `{{title}}`,
'121435': `Save High Definition Cover`,
'121436': `High Definition Cover looks better but it will take more space, and you must login douban in this plugin`,
'121437': `Please login first, Then this function could be enable`,
@ -330,6 +334,8 @@ PS: This file could be delete if you want to.
'140102': `subject type is different, will not sync this, chosen sync type is {0} but this {1} subject type is {2}`,
'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 `,
'130404': `404 Url Not Found`,
'130107': `Can not find array setting for {1} in {0} , Please add it in array settings`,
'130108': `Redirect times too much, please check your network or proxy`,

@ -1,4 +1,5 @@
//简体中文
//简体中文
import {SyncItemStatus} from "../../constant/Constsant";
@ -302,9 +303,12 @@ export default {
'121430': `保存图片附件`,
'121431': `导入数据会同步保存图片附件到本地文件夹, 如电影封面,书籍封面。如果需要显示封面,请保持开启该功能。`,
'121432': `附件存放位置`,
'121432': `附件(图片)存放位置`,
'121433': `保存的附件将会存放至该文件夹中. 如果为空, 笔记将会存放到默认位置(assets), 且支持所有'通用'的参数。如:{{myType}}/attachments`,
'121452': `附件(图片)文件名`,
'121453': `附件的文件名模板, 支持所有'通用'的参数作为名称(如:{{type}}-{{title}}),且支持路径, 比如: '{{myType}}/附件-{{title}}'。如果为空, 则使用默认名称{{title}}`,
'121434': `assets`,
'121454': `{{title}}`,
'121435': `保存高清封面`,
'121436': `高清封面图片质量更高清晰度更好, 需要您在此插件 登录豆瓣 才能生效, 若未登录则默认使用低精度版本封面`,
'121437': `登录后此功能才会生效`,

@ -282,7 +282,7 @@ export default class DoubanPlugin extends Plugin {
this.settingsManager = new SettingsManager(app, this);
this.settingsManager = new SettingsManager(this.app, this);
// this.fetchOnlineData(this.settingsManager);
this.userComponent = new UserComponent(this.settingsManager);
this.netFileHandler = new NetFileHandler(this.fileHandler);

@ -19,15 +19,25 @@ export default class NetFileHandler {
const filePath:string = FileUtil.join(folder, filename);
return HttpUtil.httpRequestBuffer(url, headers, context.plugin.settingsManager)
.then((response) => {
if (response.status == 404) {
throw new Error(i18nHelper.getMessage('130404'));
}
if (response.status == 403) {
throw new Error(i18nHelper.getMessage('130106'));
}
return response.textArrayBuffer;
})
.then((buffer) => {
if (!buffer || buffer.byteLength == 0) {
return 0;
}
this.fileHandler.creatAttachmentWithData(filePath, buffer);
}).then(() => {
return {success: true, error: '', filepath: filePath};
return buffer.byteLength;
}).then((size) => {
if (size == 0) {
return {success: false, size: size, error: '文件唯恐', filepath: null};
}
return {success: true, size: size, error: '', filepath: filePath};
})
.catch(e => {
if (showError) {

@ -96,7 +96,7 @@ export class VariableUtil {
} else if(value instanceof DataField) {
content = this.replaceDataField(variable, value, content, settingManager, targetType);
} else {
content = this.replaceString(variable, value, content, settingManager, targetType);
content = this.replaceString(variable, value, value, content, settingManager, targetType);
}
return content;
}
@ -135,12 +135,12 @@ export class VariableUtil {
return `{{${key}}}`;
}
static replaceString(variable: FieldVariable, value: any, content: string, settingManager:SettingsManager, targetType: TargetType): string {
static replaceString(variable: FieldVariable, valueField: DataField, value: any, content: string, settingManager:SettingsManager, targetType: TargetType): string {
if (!content) {
return content;
}
let strValue = value? value.toString() : "";
return content.replaceAll(variable.variable, this.handleText(strValue, targetType));
return content.replaceAll(variable.variable, this.handleText(strValue, targetType, valueField));
}
/**
@ -220,19 +220,19 @@ export class VariableUtil {
}
switch (value.type) {
case DataValueType.string:
content = this.replaceString(variable, value.value, content, settingManager, targetType);
content = this.replaceString(variable, value, value.value, content, settingManager, targetType);
break;
case DataValueType.number:
content = content.replaceAll(variableStr, this.handleText(value.value.toString(), targetType));
content = content.replaceAll(variableStr, this.handleText(value.value.toString(), targetType, value));
break;
case DataValueType.date:
content = content.replaceAll(variableStr, this.handleText(value.value, targetType));
content = content.replaceAll(variableStr, this.handleText(value.value, targetType, value));
break;
case DataValueType.array:
content = this.replaceArray(variable, value.value, content, settingManager, targetType);
break;
default:
content = content.replaceAll(variableStr, this.handleText(value.value, targetType));
content = content.replaceAll(variableStr, this.handleText(value.value, targetType, value));
break;
}
@ -277,9 +277,9 @@ export class VariableUtil {
return map;
}
private static handleText(v: string, targetType: TargetType) {
private static handleText(v: string, targetType: TargetType, dataField: DataField = null): string {
if (targetType === 'yml_text') {
return YamlUtil.handleText(v);
return YamlUtil.handleText(v, dataField);
}
if (targetType === 'text') {
return v;

@ -1,5 +1,7 @@
export default class YamlUtil {
import {DataField} from "./model/DataField";
import {DataValueType} from "../constant/Constsant";
export default class YamlUtil {
public static hasSpecialChar(str: string): boolean {
return SPECIAL_CHAR_REG.test(str);
@ -14,15 +16,20 @@ export default class YamlUtil {
return '"' + text + '"';
}
public static handleText(text: string) {
return YamlUtil.hasSpecialChar(text)
? YamlUtil.handleSpecialChar(text.replaceAll('"', '\\"'))
public static handleText(text: string, dataField: DataField = null): string {
if (YamlUtil.hasSpecialChar(text)) {
text = text.replaceAll('"', '\\"')
.replaceAll(/\s+/g, ' ')
.replaceAll('\n', '。')
.replaceAll('。。', '。')
.replace(/^" /, '"') // Remove leading "
.replace(/ "$/, '"') // Remove trailing "
: text;
if (dataField && dataField.type === DataValueType.date) {
return text;
}
YamlUtil.handleSpecialChar(text);
}
return text;
}
}