README

Note
Web Clipper
- EXHentai Web Clipper for Obsidian | https://github.com/abc202306/exhentai-web-clipper-for-obsidian
- NHentai Web Clipper for Obsidian | https://github.com/abc202306/nhentai-web-clipper-for-obsidian
Folder Struct
DFC stands for the total number of descendant files
Views of gallery-base.base
Note
artist
seealso: artist
- kiira | 5 | kiira
- henreader | 5 | henreader
- utatane | 4 | utatane
- wancho | 5 | wancho
- custom-udon | 3 | custom-udon
- komugi | 3 | komugi
- hikami-izuto | 2 | hikami-izuto
- murai-renji | 1 | murai-renji
- yoyomax | 1 | yoyomax
- kani-biimu | 1 | kani-biimu
- baku-p | 1 | baku-p
categories
seealso: categories
- doujinshi | 463 | doujinshi
- manga | 114 | manga
- image-set | 37 | image-set
- misc | 23 | misc
- artist-cg | 33 | artist-cg
- game-cg | 8 | game-cg
- non-h | 4 | non-h
- western | 1 | western
parody
seealso: parody
- original | 193 | original
- blue-archive | 93 | blue-archive
- touhou-project | 25 | touhou-project
- mahoujin-guru-guru | 20 | mahoujin-guru-guru
female
male
mixed
- kodomo-doushi | 27 | kodomo-doushi
character
Script
Build Index Content
const config = {
path: {
folder: {
tag: "tag/",
gallery: "galleries/",
property: "property/",
uploader: "uploader/",
docsTag: "docs/tag/",
docsYear: "docs/year/",
},
file: {
readme: "README.md",
tag: "docs/docs/tag.md",
uploader: "docs/docs/uploader.md",
notes: "docs/collection/notes.md",
gallery: "docs/collection/gallery.md",
exhentai: "docs/galleries/exhentai.md",
nhentai: "docs/galleries/nhentai.md",
},
},
};
function getLocalISOStringWithTimezone() {
const date = new Date();
const pad = (n) => String(n).padStart(2, "0");
const offset = -date.getTimezoneOffset();
const sign = offset >= 0 ? "+" : "-";
const hours = pad(Math.floor(Math.abs(offset) / 60));
const minutes = pad(Math.abs(offset) % 60);
return (
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
`${sign}${hours}:${minutes}`
);
}
// Utility helpers to avoid relying on environment-specific prototype extensions
function uniqueArray(arr) {
return Array.from(new Set(arr));
}
function groupBy(array, keyFn) {
const map = new Map();
for (const item of array) {
const key = keyFn(item);
const list = map.get(key) || [];
list.push(item);
map.set(key, list);
}
return Array.from(map.entries());
}
function safeArray(v) {
if (!v) return [];
return Array.isArray(v) ? v : [v];
}
function compareGalleryPathWithPropertyUploaded(path1, path2) {
const f1 = app.vault.getAbstractFileByPath(path1);
const f2 = app.vault.getAbstractFileByPath(path2);
const fc1 = app.metadataCache.getFileCache(f1);
const fc2 = app.metadataCache.getFileCache(f2);
const v1 = String(fc1?.frontmatter?.uploaded || "_");
const v2 = String(fc2?.frontmatter?.uploaded || "_");
// sort descending
return v2.localeCompare(v1);
}
function getGalleryPathRepresentationStr(path) {
const f2 = app.vault.getAbstractFileByPath(path);
const linktext2 = app.metadataCache.fileToLinktext(f2);
const fc2 = app.metadataCache.getFileCache(f2) || {};
const display2 = fc2.frontmatter?.japanese || fc2.frontmatter?.english || linktext2;
const link2 =
display2 === linktext2
? `| [[${linktext2}]]`
: `\u001C${display2}\u001C | [[${linktext2}]]`.replace(/\u001C/g, "`");
const coverField = fc2.frontmatter?.cover;
let coverEmbed = "";
if (coverField) {
const res = /^\[\[(?<linktext3>[^\|]*)\|?.*\]\]$/.exec(coverField);
coverEmbed = res
? `\n\t- ![[${res.groups.linktext3}|200]]`
: `\n\t- `;
}
return `1. ${link2}${coverEmbed}`;
}
function getNGStr(nonGalleryNotePaths) {
const ngls = [...nonGalleryNotePaths].sort();
return ngls
.map((path) => `[[${app.metadataCache.fileToLinktext(app.vault.getAbstractFileByPath(path))}]]`)
.join(", ");
}
function getGStrASList(galleryNotePaths) {
const gls = [...galleryNotePaths].sort(compareGalleryPathWithPropertyUploaded);
return gls.map(getGalleryPathRepresentationStr).join("\n");
}
function getGStrASGroupedList(galleryNotePaths) {
const gls = [...galleryNotePaths].sort(compareGalleryPathWithPropertyUploaded);
const grouped = groupBy(gls, (gnPath) => getYear(app.vault.getAbstractFileByPath(gnPath)));
const parts = grouped
.sort((a, b) => b[0].localeCompare(a[0]))
.flatMap(([key, group]) => {
const grouped02 = groupBy(group, (gnPath) => getMonth(app.vault.getAbstractFileByPath(gnPath)));
const parts02 = grouped02
.sort((a, b) => b[0].localeCompare(a[0]))
.flatMap(([key02, group02]) => [
`#### ${key02}`,
group02.map(getGalleryPathRepresentationStr).join("\n")
])
return [
`### ${key}`,
...parts02
]
});
return parts.join("\n\n");
}
function getGStr(galleryNotePaths) {
return getGStrASGroupedList(galleryNotePaths);
}
function getTagFileContent(title, ctime, mtime) {
const f = app.metadataCache.getFirstLinkpathDest(title);
const backlinks = app.metadataCache.getBacklinksForFile(f)?.data;
const paths = backlinks ? [...backlinks.keys()] : [];
const ngstr = getNGStr(
paths.filter((i) => !i.startsWith(config.path.folder.gallery)).filter((i) => i !== config.path.file.readme)
);
const gstr = getGStr(paths.filter((i) => i.startsWith(config.path.folder.gallery)));
return `---\nctime: ${ctime}\nmtime: ${mtime}\n---\n\n# ${title}\n\n> seealso: ${ngstr}\n\n![[gallery-dynamic-base.base]]\n\n## gallery-notes\n\n${gstr}\n`;
}
function getYearFileContent(title, ctime, mtime) {
const f = app.metadataCache.getFirstLinkpathDest(title);
const backlinks = app.metadataCache.getBacklinksForFile(f)?.data;
const paths = backlinks ? [...backlinks.keys()] : [];
const ngstr = getNGStr(
paths.filter((i) => !i.startsWith(config.path.folder.gallery)).filter((i) => i !== config.path.file.readme)
);
const galleryNotePaths = app
.vault
.getMarkdownFiles()
.filter((f) => f.path.startsWith(config.path.folder.gallery))
.filter((f) => getYear(f) === title)
.map((f) => f.path);
const gstr = getGStr(galleryNotePaths);
return `---\nctime: ${ctime}\nmtime: ${mtime}\n---\n\n# ${title}\n\n> seealso: ${ngstr}\n\n## gallery-notes\n\n${gstr}\n`;
}
function removeWikiLinkMark(str) {
return String(str).replace(/^\[\[/, "").replace(/\]\]$/, "");
}
function getTagGroupMOC(title) {
const property = title.replace(/-ns$/, "");
const galleryMDFileCaches = app
.vault
.getMarkdownFiles()
.filter((f) => f.path.startsWith(config.path.folder.gallery))
.map((f) => app.metadataCache.getFileCache(f) || {});
const allValues = galleryMDFileCaches.flatMap((fc) => safeArray((fc.frontmatter || {})[property]));
const uniqueValues = uniqueArray(allValues).filter((v) => v);
return uniqueValues
.sort((a, b) => removeWikiLinkMark(a).localeCompare(removeWikiLinkMark(b)))
.map((v) =>
`1. ${v} | ${galleryMDFileCaches.filter((fc) => safeArray((fc.frontmatter || {})[property]).includes(v)).length}`
)
.join("\n");
}
function getGroupFileContent(title, ctime, mtime, seealso) {
return `---\nctime: ${ctime}\nmtime: ${mtime}\n---\n\n# ${title}\n\n> seealso: ${seealso}\n\n${getTagGroupMOC(title)}\n`;
}
function getTagCount(tagNameSpaceStr) {
const property = tagNameSpaceStr.replace(/-ns$/, "");
const galleryMDFileCaches = app
.vault
.getMarkdownFiles()
.filter((f) => f.path.startsWith(config.path.folder.gallery))
.map((f) => app.metadataCache.getFileCache(f) || {});
return uniqueArray(galleryMDFileCaches.flatMap((fc) => safeArray((fc.frontmatter || {})[property]))).filter((v) => v)
.length;
}
function getTagMetaFileContent(_title, ctime, mtime) {
return `---\nctime: ${ctime}\nmtime: ${mtime}\n---\n\n# tag\n\n> seealso: [[docs]]\n\n1. [[artist]] | ${getTagCount("artist")}\n1. [[categories]] | ${getTagCount("categories")}\n1. [[character]] | ${getTagCount("character")}\n1. [[cosplayer]] | ${getTagCount("cosplayer")}\n1. [[female]] | ${getTagCount("female")}\n1. [[group-ns]] | ${getTagCount("group-ns")}\n1. [[keywords]] | ${getTagCount("keywords")}\n1. [[language]] | ${getTagCount("language")}\n1. [[location]] | ${getTagCount("location")}\n1. [[male]] | ${getTagCount("male")}\n1. [[mixed]] | ${getTagCount("mixed")}\n1. [[other]] | ${getTagCount("other")}\n1. [[parody]] | ${getTagCount("parody")}\n1. [[temp]] | ${getTagCount("temp")}\n`;
}
function getTagGroupFileContent(title, ctime, mtime) {
return getGroupFileContent(title, ctime, mtime, "[[tag]]");
}
function getUploaderGroupFileContent(title, ctime, mtime) {
return getGroupFileContent(title, ctime, mtime, "[[docs]]");
}
function getPropertyFileContent(title, ctime, mtime) {
const f = app.metadataCache.getFirstLinkpathDest(title);
const backlinks = app.metadataCache.getBacklinksForFile(f)?.data;
const paths = backlinks ? [...backlinks.keys()] : [];
const ngstr = getNGStr(
paths.filter((i) => !i.startsWith(config.path.folder.gallery)).filter((i) => i !== config.path.file.readme)
);
return `---\nctime: ${ctime}\nmtime: ${mtime}\n---\n\n# ${title}\n\n> seealso: ${ngstr}\n\n![[property-dynamic-base.base]]\n`;
}
function getRenderedFolderPath(folder) {
return folder.path.split("/").map((part) => `[[${part}]]`).join("/");
}
function getDecendantFilesCount(folder, files, extension=/.*/) {
return files.filter((f) => f.path.startsWith(folder.path + "/") && extension.exec(f.extension)).length;
}
function replaceFrontMatter(fileContent, ctime, mtime, preFMBlock = "") {
return `---${preFMBlock}\nctime: ${ctime}\nmtime: ${mtime}\n---\n` + fileContent.replace(/^---\r?\n[^]*?(?<=\n)---\r?\n/, "");
}
async function getReadmeFileContent(_title, ctime, mtime) {
const file = app.vault.getAbstractFileByPath(config.path.file.readme);
const fileContent = await app.vault.read(file);
const files = app.vault.getFiles();
const folders = app.vault.getAllFolders().sort((a, b) => a.path.localeCompare(b.path));
const tableStr = `| Folder Path | DFC | DFMC | DFOC |\n| :--- | ---: | ---: | ---: |\n${folders
.map((folder) => `| ${getRenderedFolderPath(folder)} | ${getDecendantFilesCount(folder, files, /.*/)} | ${getDecendantFilesCount(folder, files, /^md$/)} | ${getDecendantFilesCount(folder, files, /^(?!md$)/)} |`)
.join("\n")}`;
const newData = replaceFrontMatter(fileContent, ctime, mtime).replace(
/(?<=\n)## Folder Struct\n[^#]*(?=\n##\s)/,
"## Folder Struct\n\n> DFC stands for the total number of descendant files\n\n" + tableStr + "\n"
);
return newData;
}
async function getNoteMetaFileContent(_title, ctime, mtime) {
const metaFilePath = config.path.file.notes;
const noteFiles = app
.vault
.getMarkdownFiles()
.filter((f) => safeArray(app.metadataCache.getFileCache(f)?.frontmatter?.up).includes("[[notes]]"));
const file = app.vault.getAbstractFileByPath(metaFilePath);
const fileContent = await app.vault.read(file);
const gls = noteFiles
.sort((f1, f2) => {
const fc1 = app.metadataCache.getFileCache(f1) || {};
const fc2 = app.metadataCache.getFileCache(f2) || {};
const v1 = fc1.frontmatter?.ctime || "_";
const v2 = fc2.frontmatter?.ctime || "_";
return v2.localeCompare(v1);
})
.map((f) => f.path);
const gstr = gls.map(getGalleryPathRepresentationStr).join("\n");
const preFMBlock = `\nup:\n - "[[collection]]"`;
const newData = replaceFrontMatter(fileContent, ctime, mtime, preFMBlock).replace(/(?<=\n)## note-list\n[^]*/,
"## note-list\n\n" + gstr + "\n"
);
return newData;
}
async function getGalleryMetaFileContentWithSpecPath(_title, ctime, mtime, metaFilePath, galleryNoteFiles, preFMBlock = "") {
const file = app.vault.getAbstractFileByPath(metaFilePath);
const fileContent = await app.vault.read(file);
const gstr = getGStr(galleryNoteFiles.map((f) => f.path));
const newData = replaceFrontMatter(fileContent, ctime, mtime, preFMBlock).replace(/(?<=\n)## gallery-notes\n[^]*/,
"## gallery-notes\n\n" + gstr + "\n"
);
return newData;
}
async function getSpecGalleryMetaFileContent(_title, ctime, mtime) {
const metaFilePath = config.path.file.gallery;
const galleryNoteFiles = app
.vault
.getMarkdownFiles()
.filter((f) => safeArray(app.metadataCache.getFileCache(f)?.frontmatter?.up).includes("[[gallery]]"));
const preFMBlock = `\nup:\n - "[[collection]]"\nbases:\n - "[[gallery-base.base]]"`;
return await getGalleryMetaFileContentWithSpecPath(_title, ctime, mtime, metaFilePath, galleryNoteFiles, preFMBlock);
}
async function getSpecEXHentaiGalleryMetaFileContent(_title, ctime, mtime) {
const metaFilePath = config.path.file.exhentai;
const galleryNoteFiles = app
.vault
.getMarkdownFiles()
.filter((f) => safeArray(app.metadataCache.getFileCache(f)?.frontmatter?.up).includes("[[gallery]]"))
.filter((f) => (app.metadataCache.getFileCache(f)?.frontmatter?.url || "").includes("exhentai"));
return await getGalleryMetaFileContentWithSpecPath(_title, ctime, mtime, metaFilePath, galleryNoteFiles);
}
async function getSpecNHentaiGalleryMetaFileContent(_title, ctime, mtime) {
const metaFilePath = config.path.file.nhentai;
const galleryNoteFiles = app
.vault
.getMarkdownFiles()
.filter((f) => safeArray(app.metadataCache.getFileCache(f)?.frontmatter?.up).includes("[[gallery]]"))
.filter((f) => (app.metadataCache.getFileCache(f)?.frontmatter?.url || "").includes("nhentai"));
return await getGalleryMetaFileContentWithSpecPath(_title, ctime, mtime, metaFilePath, galleryNoteFiles);
}
async function getFileContent(file, data, getSpecTypeFileContent) {
const title = file.basename;
const fileCache = app.metadataCache.getFileCache(file) || {};
const ctimeInFrontMatter = fileCache.frontmatter?.ctime;
const mtimeInFrontMatter = fileCache.frontmatter?.mtime;
const mtime = getLocalISOStringWithTimezone();
const ctime = ctimeInFrontMatter || mtime;
const formattedData = data.replace(/\r/g, "");
const newData1 = await getSpecTypeFileContent(title, ctimeInFrontMatter, mtimeInFrontMatter);
if (formattedData === newData1) return data;
const newData2 = await getSpecTypeFileContent(title, ctime, mtime);
return newData2;
}
function processFileWith(getSpecTypeFileContent) {
return async function processFileWrapper(file) {
const originalData = await app.vault.read(file);
const newData = await getFileContent(file, originalData, getSpecTypeFileContent);
if (newData !== originalData) {
await app.vault.process(file, () => newData);
}
};
}
function removeDuplicatedValueInArrayPropertyInFrontmatterForAllMarkdownFiles() {
app.vault.getMarkdownFiles().forEach((f) => {
const fc = app.metadataCache.getFileCache(f) || {};
if (!fc.frontmatter) return;
for (const k of Object.keys(fc.frontmatter)) {
const v1 = fc.frontmatter[k];
if (!Array.isArray(v1)) continue;
const v2 = uniqueArray(v1);
if (v2.length === v1.length) continue;
app.fileManager.processFrontMatter(f, (fm) => {
fm[k] = v2;
});
}
});
}
function createFilesFromUnresolvedLinksForAllGalleryNoteFiles() {
const galleryNoteMDFiles = app.vault.getMarkdownFiles().filter((f) => f.path.startsWith(config.path.folder.gallery));
const unresolvedLinktexts = galleryNoteMDFiles.flatMap((f) => Object.keys(app.metadataCache.unresolvedLinks?.[f.path] || {}));
console.log("unresolvedLinktexts",unresolvedLinktexts)
const propertyNames = [
"artist",
"group",
"categories",
"character",
"parody",
"language",
"cosplayer",
"female",
"location",
"male",
"mixed",
"other",
"temp",
"keywords",
"uploader",
];
const galleryMDFileCaches = galleryNoteMDFiles.map((f) => app.metadataCache.getFileCache(f) || {});
for (const linktext of uniqueArray(unresolvedLinktexts)) {
const value = `[[${linktext}]]`;
const propertyName = propertyNames.find((pn) =>
galleryMDFileCaches.filter((fc) => safeArray((fc.frontmatter || {})[pn]).includes(value)).length !== 0
);
let folderPath = config.path.folder.tag;
if (propertyName === "group") {
folderPath += "group-ns/";
} else if (propertyName === "uploader") {
folderPath = config.path.folder.uploader;
} else if (propertyName) {
folderPath += `${propertyName}/`
}
const destPath = folderPath + linktext + ".md";
try {
if (!app.vault.getAbstractFileByPath(destPath)) {
app.vault.create(destPath, "")
.then((f)=>app.metadataCache.getFileCache(f));
}
} catch (e) {
// ignore creation errors (file may exist already or race conditions)
}
}
}
function getProcessFilePromise(path, getSpecTypeFileContent) {
const file = app.vault.getAbstractFileByPath(path);
const fileProcesser = processFileWith(getSpecTypeFileContent);
return fileProcesser(file);
}
function getYear(galleryNoteFile) {
return app.metadataCache.getFileCache(galleryNoteFile)?.frontmatter?.uploaded?.slice(0, 4) || "1000";
}
function getMonth(galleryNoteFile) {
return app.metadataCache.getFileCache(galleryNoteFile)?.frontmatter?.uploaded?.slice(0, 7) || "1000-01";
}
function batchMoveGalleryNoteFilesByYearUploaded() {
const files = app.vault.getFiles();
const mdfiles = app.vault.getMarkdownFiles();
const candidates = mdfiles.filter((f) => f.path.startsWith(config.path.folder.gallery)).filter((f) =>
safeArray(app.metadataCache.getFileCache(f)?.frontmatter?.up).includes("[[gallery]]")
);
for (const f of candidates) {
if (f.path.split("/").length !== 3) continue;
const year = getYear(f);
const folderPath = `${f.parent.path}/${year}`;
if (!app.vault.getFolderByPath(folderPath)) app.vault.createFolder(folderPath);
}
for (const f of candidates) {
if (f.path.split("/").length !== 3) continue;
const year = app.metadataCache.getFileCache(f)?.frontmatter?.uploaded?.slice(0, 4);
const folderPath = `${f.parent.path}/${year}`;
if (!app.vault.getFolderByPath(folderPath)) app.vault.createFolder(folderPath);
const pathPrefix = `${f.parent.path}/${f.basename}`;
files.filter((f2) => f2.path.startsWith(pathPrefix)).forEach((f2) => {
const newPath2 = `${folderPath}/${f2.name}`;
app.vault.rename(f2, newPath2);
console.log(newPath2);
});
}
}
function stardandnizeGalleryNoteCoverFileName() {
const galleryNoteFiles = app.vault.getMarkdownFiles().filter(f=>f.path.startsWith(config.path.folder.gallery));
galleryNoteFiles.filter(f=>{
const cover = app.metadataCache.getFileCache(f)?.frontmatter?.cover;
const res = /^\[\[(?<basename>.*)\.(?<extension>.*)\]\]$/.exec(cover)
if (!res){
return;
}
const coverBasename = res.groups.basename;
const coverExtension = res.groups.extension;
const coverLinktext = `${coverBasename}.${coverExtension}`;
const coverFile = app.metadataCache.getFirstLinkpathDest(coverLinktext);
const newCoverLinktext = `${f.basename}.${coverExtension}`;
const newPath = `${coverFile.parent.path}/{newCoverLinktext}`
if (!cover?.startsWith("[["+f.basename)) {
app.fileManager.renameFile(coverFile,newCoverLinktext);
console.log(coverFile.name,newPath);
}
})
}
function refreshCache(){
app.vault.getMarkdownFiles().forEach((f)=>app.metadataCache.getFileCache(f));
}
async function main() {
console.time("run_script");
console.log(`==start (time="${new Date()}")`);
const tasks = [];
// preparatory runs
tasks.push(refreshCache());
await Promise.all(tasks);
tasks.push(createFilesFromUnresolvedLinksForAllGalleryNoteFiles());
tasks.push(batchMoveGalleryNoteFilesByYearUploaded());
tasks.push(stardandnizeGalleryNoteCoverFileName());
await Promise.all(tasks);
// single-file generators
const singleFileSpecs = [
[config.path.file.readme, getReadmeFileContent],
[config.path.file.uploader, getUploaderGroupFileContent],
[config.path.file.tag, getTagMetaFileContent],
[config.path.file.notes, getNoteMetaFileContent],
[config.path.file.gallery, getSpecGalleryMetaFileContent],
[config.path.file.exhentai, getSpecEXHentaiGalleryMetaFileContent],
[config.path.file.nhentai, getSpecNHentaiGalleryMetaFileContent],
];
for (const [path, fn] of singleFileSpecs) {
tasks.push((async () => {
try {
const timerName = "timer-"+fn.name+"-"+path;
console.time(timerName);
console.log("started:", fn.name, path);
await getProcessFilePromise(path, fn);
console.log("ended:", fn.name, path);
console.timeEnd(timerName);
} catch (e) {
console.error("error processing", path, e);
}
})());
}
await Promise.all(tasks);
tasks.push(refreshCache());
// directory-scoped generators
const dirSpecs = [
[config.path.folder.docsTag, getTagGroupFileContent],
[config.path.folder.docsYear, getYearFileContent],
[config.path.folder.property, getPropertyFileContent],
[config.path.folder.uploader, getTagFileContent],
[config.path.folder.tag, getTagFileContent],
];
for (const [rootDirPath, fn] of dirSpecs) {
tasks.push((async () => {
try {
const timerName = "timer-"+fn.name+"-"+rootDirPath;
console.time(timerName);
console.log("started:", fn.name, rootDirPath);
await Promise.all(app.vault.getMarkdownFiles().filter((f) => f.path.startsWith(rootDirPath)).map(processFileWith(fn)));
console.log("ended:", fn.name, rootDirPath);
console.timeEnd(timerName);
} catch (e) {
console.error("error processing dir", rootDirPath, e);
}
})());
}
await Promise.all(tasks);
// cleanup frontmatter
tasks.push((async () => {
try {
const timerName = "timer-removeDuplicatedValueInArrayPropertyInFrontmatterForAllMarkdownFiles";
console.time(timerName);
console.log("started:", removeDuplicatedValueInArrayPropertyInFrontmatterForAllMarkdownFiles.name);
await removeDuplicatedValueInArrayPropertyInFrontmatterForAllMarkdownFiles();
console.log("ended:", removeDuplicatedValueInArrayPropertyInFrontmatterForAllMarkdownFiles.name);
console.timeEnd(timerName);
} catch (e) {
console.error("error removing duplicates", e);
}
})());
await Promise.all(tasks);
console.log(`==end (time="${new Date()}")`);
console.timeEnd("run_script");
}
main().catch((err) => console.error("unhandled error in build-index-content main:", err));