README

Note

Web Clipper

Folder Struct

DFC stands for the total number of descendant files

Note

artist

seealso: artist

  1. kiira | 5 | kiira
  2. henreader | 5 | henreader
  3. utatane | 4 | utatane
  4. wancho | 5 | wancho
  5. custom-udon | 3 | custom-udon
  6. komugi | 3 | komugi
  7. hikami-izuto | 2 | hikami-izuto
  8. murai-renji | 1 | murai-renji
  9. yoyomax | 1 | yoyomax
  10. kani-biimu | 1 | kani-biimu
  11. baku-p | 1 | baku-p

categories

seealso: categories

  1. doujinshi | 463 | doujinshi
  2. manga | 114 | manga
  3. image-set | 37 | image-set
  4. misc | 23 | misc
  5. artist-cg | 33 | artist-cg
  6. game-cg | 8 | game-cg
  7. non-h | 4 | non-h
  8. western | 1 | western

parody

seealso: parody

  1. original | 193 | original
  2. blue-archive | 93 | blue-archive
  3. touhou-project | 25 | touhou-project
  4. mahoujin-guru-guru | 20 | mahoujin-guru-guru

female

  1. lolicon | 644 | lolicon
  2. rape | 106 | rape

male

  1. sole-male | 293 | sole-male

mixed

  1. kodomo-doushi | 27 | kodomo-doushi

character

  1. kukuri | 19 | kukuri

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- ![200](${coverField})`;
    }
 
    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));