1
1
import slugify from '@sindresorhus/slugify' ;
2
2
import filenamify from 'filenamify' ;
3
3
import { type ConfigureOptions , Environment , Template } from 'nunjucks' ;
4
- import { type CachedMetadata , Plugin , type TFile , normalizePath } from 'obsidian' ;
4
+ import { Plugin , TFile , TFolder , normalizePath } from 'obsidian' ;
5
5
import { AuthorParser } from 'services/author-parser' ;
6
6
import { DeduplicatingVaultWriter } from 'services/deduplicating-vault-writer' ;
7
7
import { FrontmatterManager } from 'services/frontmatter-manager' ;
@@ -341,12 +341,14 @@ export default class ReadwiseMirror extends Plugin {
341
341
const template = this . settings . filenameTemplate ;
342
342
const context = {
343
343
title : book . title ,
344
- author : this . settings . normalizeAuthorNames ?
345
- new AuthorParser ( {
346
- normalizeCase : true ,
347
- removeTitles : this . settings . stripTitlesFromAuthors
348
- } ) . parse ( book . author ) . join ( ', ' ) :
349
- book . author ,
344
+ author : this . settings . normalizeAuthorNames
345
+ ? new AuthorParser ( {
346
+ normalizeCase : true ,
347
+ removeTitles : this . settings . stripTitlesFromAuthors ,
348
+ } )
349
+ . parse ( book . author )
350
+ . join ( ', ' )
351
+ : book . author ,
350
352
category : book . category ,
351
353
source : book . source_url ,
352
354
book_id : book . user_book_id ,
@@ -356,7 +358,17 @@ export default class ReadwiseMirror extends Plugin {
356
358
filename = book . title ;
357
359
}
358
360
359
- const normalizedTitle = this . settings . useSlugify
361
+ return this . normalizeFilename ( filename ) ;
362
+ }
363
+
364
+ /**
365
+ * Normalizes the filename by replacing critical characters
366
+ * and ensuring it is a valid filename
367
+ * @param filename - The filename to normalize
368
+ * @returns The normalized filename
369
+ */
370
+ private normalizeFilename ( filename : string ) {
371
+ const normalizedFilename = this . settings . useSlugify
360
372
? slugify ( filename . replace ( / : / g, this . settings . colonSubstitute ?? '-' ) , {
361
373
separator : this . settings . slugifySeparator ,
362
374
lowercase : this . settings . slugifyLowercase ,
@@ -372,7 +384,7 @@ export default class ReadwiseMirror extends Plugin {
372
384
. replace ( / + / g, ' ' )
373
385
. trim ( ) ;
374
386
375
- return normalizePath ( normalizedTitle ) ;
387
+ return normalizePath ( normalizedFilename ) ;
376
388
}
377
389
378
390
async deleteLibraryFolder ( ) {
@@ -468,6 +480,72 @@ export default class ReadwiseMirror extends Plugin {
468
480
return spacetime . now ( ) . since ( spacetime ( this . settings . lastUpdated ) ) . rounded ;
469
481
}
470
482
483
+ /**
484
+ * Handles the adjustment of filenames in the Readwise folder.
485
+ */
486
+ async handleFilenameAdjustment ( ) {
487
+ const vault = this . app . vault ;
488
+ const path = `${ this . settings . baseFolderName } ` ;
489
+ const readwiseFolder = vault . getAbstractFileByPath ( path ) ;
490
+ if ( readwiseFolder && readwiseFolder instanceof TFolder ) {
491
+ // Iterate all files in the Readwise folder and "fix" their names according to the current settings using
492
+ // this.normalizeFilename()
493
+ const renamedFiles = await this . iterativeReadwiseRenamer ( readwiseFolder ) ;
494
+ if ( renamedFiles > 0 ) {
495
+ this . notify . notice ( `Readwise: Renamed ${ renamedFiles } files. Check console for renaming errors.` ) ;
496
+ } else {
497
+ this . notify . notice ( 'Readwise: No files renamed. Check console for renaming errors.' ) ;
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Iteratively renames files in the Readwise folder.
504
+ * @param folder - The folder to iterate through
505
+ * @returns
506
+ */
507
+ private async iterativeReadwiseRenamer ( folder : TFolder ) : Promise < number > {
508
+ const files = folder . children ;
509
+ let countRenamed = 0 ;
510
+ for ( const file of files ) {
511
+ if ( file instanceof TFolder ) {
512
+ // Skip folders
513
+ countRenamed += await this . iterativeReadwiseRenamer ( file ) ;
514
+ }
515
+
516
+ if ( file instanceof TFile && file . extension === 'md' ) {
517
+ const result = await this . renameReadwiseNote ( file ) ;
518
+ if ( result ) {
519
+ countRenamed ++ ;
520
+ }
521
+ }
522
+ }
523
+ return countRenamed ;
524
+ }
525
+
526
+ /**
527
+ * Formats the filename of a Readwise note based on the settings.
528
+ *
529
+ * @param file The file to format.
530
+ */
531
+ private async renameReadwiseNote ( file : TFile ) : Promise < boolean > {
532
+ const newFilename = this . normalizeFilename ( file . basename ) ;
533
+
534
+ // Only rename if there's a difference
535
+ if ( newFilename !== file . basename ) {
536
+ const newPath = `${ file . parent . path } /${ newFilename } .md` ;
537
+ try {
538
+ await this . app . fileManager . renameFile ( file , newPath ) ;
539
+ this . logger . info ( `Renamed file '${ file . name } ' to '${ newFilename } .md'` ) ;
540
+ return true ;
541
+ } catch ( error ) {
542
+ this . logger . error ( `Error renaming file: '${ file . name } ' to '${ newFilename } .md': ${ error } ` ) ;
543
+ return false ;
544
+ }
545
+ }
546
+ return false ;
547
+ }
548
+
471
549
// Reload settings after external change (e.g. after sync)
472
550
async onExternalSettingsChange ( ) {
473
551
this . logger . info ( 'Reloading settings due to external change' ) ;
@@ -566,6 +644,15 @@ export default class ReadwiseMirror extends Plugin {
566
644
callback : this . sync . bind ( this ) ,
567
645
} ) ;
568
646
647
+ this . addCommand ( {
648
+ id : 'adjust-filenames' ,
649
+ name : 'Adjust Filenames to current settings' ,
650
+ callback : async ( ) => {
651
+ this . notify . notice ( 'Readwise: Filename adjustment started' ) ;
652
+ await this . handleFilenameAdjustment ( ) ;
653
+ } ,
654
+ } ) ;
655
+
569
656
this . registerInterval (
570
657
window . setInterval ( ( ) => {
571
658
if ( / S y n c e d / . test ( this . notify . getStatusBarText ( ) ) ) {
0 commit comments