1
- import { TFile , Notice } from "obsidian" ;
1
+ import { type App , Notice , TFile } from "obsidian" ;
2
2
import FeedParser from "./parser/feedParser" ;
3
3
import { savedFeeds } from "./store" ;
4
- import { PodcastFeed } from "./types/PodcastFeed" ;
5
-
6
- async function importOPML ( targetFile : TFile ) {
7
- const fileContent = await app . vault . cachedRead ( targetFile ) ;
8
- const dp = new DOMParser ( ) ;
9
- const dom = dp . parseFromString ( fileContent , "application/xml" ) ;
10
-
11
- const podcastEntryNodes = dom . querySelectorAll ( "outline[text][xmlUrl]" ) ;
12
- const incompletePodcastsToAdd : Pick < PodcastFeed , "title" | "url" > [ ] = [ ] ;
13
- for ( let i = 0 ; i < podcastEntryNodes . length ; i ++ ) {
14
- const node = podcastEntryNodes . item ( i ) ;
15
-
16
- const text = node . getAttribute ( "text" ) ;
17
- const xmlUrl = node . getAttribute ( "xmlUrl" ) ;
18
- if ( ! text || ! xmlUrl ) {
19
- continue ;
20
- }
4
+ import type { PodcastFeed } from "./types/PodcastFeed" ;
5
+ import { get } from "svelte/store" ;
21
6
22
- incompletePodcastsToAdd . push ( {
23
- title : text ,
24
- url : xmlUrl ,
25
- } ) ;
7
+ function TimerNotice ( heading : string , initialMessage : string ) {
8
+ let currentMessage = initialMessage ;
9
+ const startTime = Date . now ( ) ;
10
+ let stopTime : number ;
11
+ const notice = new Notice ( initialMessage , 0 ) ;
12
+
13
+ function formatMsg ( message : string ) : string {
14
+ return `${ heading } (${ getTime ( ) } ):\n\n${ message } ` ;
15
+ }
16
+
17
+ function update ( message : string ) {
18
+ currentMessage = message ;
19
+ notice . setMessage ( formatMsg ( currentMessage ) ) ;
26
20
}
27
21
28
- const podcasts : PodcastFeed [ ] = await Promise . all (
29
- incompletePodcastsToAdd . map ( async ( feed ) => {
30
- return new FeedParser ( ) . getFeed ( feed . url ) ;
31
- } )
32
- ) ;
22
+ const interval = setInterval ( ( ) => {
23
+ notice . setMessage ( formatMsg ( currentMessage ) ) ;
24
+ } , 1000 ) ;
25
+
26
+ function getTime ( ) : string {
27
+ return formatTime ( stopTime ? stopTime - startTime : Date . now ( ) - startTime ) ;
28
+ }
29
+
30
+ return {
31
+ update,
32
+ hide : ( ) => notice . hide ( ) ,
33
+ stop : ( ) => {
34
+ stopTime = Date . now ( ) ;
35
+ clearInterval ( interval ) ;
36
+ } ,
37
+ } ;
38
+ }
33
39
34
- savedFeeds . update ( ( feeds ) => {
35
- for ( const pod of podcasts ) {
36
- if ( feeds [ pod . title ] ) continue ;
37
- feeds [ pod . title ] = structuredClone ( pod ) ;
40
+ function formatTime ( ms : number ) : string {
41
+ const seconds = Math . floor ( ms / 1000 ) ;
42
+ const minutes = Math . floor ( seconds / 60 ) ;
43
+ const hours = Math . floor ( minutes / 60 ) ;
44
+ return `${ hours . toString ( ) . padStart ( 2 , "0" ) } :${ ( minutes % 60 ) . toString ( ) . padStart ( 2 , "0" ) } :${ ( seconds % 60 ) . toString ( ) . padStart ( 2 , "0" ) } ` ;
45
+ }
46
+
47
+ async function importOPML ( opml : string ) : Promise < void > {
48
+ try {
49
+ const dp = new DOMParser ( ) ;
50
+ const dom = dp . parseFromString ( opml , "application/xml" ) ;
51
+
52
+ if ( dom . documentElement . nodeName === "parsererror" ) {
53
+ throw new Error ( "Invalid XML format" ) ;
38
54
}
39
55
40
- return feeds ;
41
- } ) ;
56
+ const podcastEntryNodes = dom . querySelectorAll ( "outline[text][xmlUrl]" ) ;
57
+ const incompletePodcastsToAdd : Pick < PodcastFeed , "title" | "url" > [ ] = [ ] ;
58
+ for ( let i = 0 ; i < podcastEntryNodes . length ; i ++ ) {
59
+ const node = podcastEntryNodes . item ( i ) ;
60
+
61
+ const text = node . getAttribute ( "text" ) ;
62
+ const xmlUrl = node . getAttribute ( "xmlUrl" ) ;
63
+ if ( ! text || ! xmlUrl ) {
64
+ continue ;
65
+ }
66
+
67
+ incompletePodcastsToAdd . push ( {
68
+ title : text ,
69
+ url : xmlUrl ,
70
+ } ) ;
71
+ }
72
+
73
+ if ( incompletePodcastsToAdd . length === 0 ) {
74
+ throw new Error ( "No valid podcast entries found in OPML" ) ;
75
+ }
42
76
43
- new Notice (
44
- `${ targetFile . name } ingested. Saved ${ podcasts . length } / ${ incompletePodcastsToAdd . length } podcasts.`
45
- ) ;
77
+ const existingSavedFeeds = get ( savedFeeds ) ;
78
+ const newPodcastsToAdd = incompletePodcastsToAdd . filter (
79
+ ( pod ) =>
80
+ ! Object . values ( existingSavedFeeds ) . some (
81
+ ( savedPod ) => savedPod . url === pod . url ,
82
+ ) ,
83
+ ) ;
46
84
47
- if ( podcasts . length !== incompletePodcastsToAdd . length ) {
48
- const missingPodcasts = incompletePodcastsToAdd . filter (
49
- ( pod ) => ! podcasts . find ( ( v ) => v . url === pod . url )
85
+ const notice = TimerNotice ( "Importing podcasts" , "Preparing to import..." ) ;
86
+ let completedImports = 0 ;
87
+
88
+ const updateProgress = ( ) => {
89
+ const progress = (
90
+ ( completedImports / newPodcastsToAdd . length ) *
91
+ 100
92
+ ) . toFixed ( 1 ) ;
93
+ notice . update (
94
+ `Importing... ${ completedImports } /${ newPodcastsToAdd . length } podcasts completed (${ progress } %)` ,
95
+ ) ;
96
+ } ;
97
+
98
+ updateProgress ( ) ;
99
+
100
+ const podcasts : ( PodcastFeed | null ) [ ] = await Promise . all (
101
+ newPodcastsToAdd . map ( async ( feed ) => {
102
+ try {
103
+ const result = await new FeedParser ( ) . getFeed ( feed . url ) ;
104
+ completedImports ++ ;
105
+ updateProgress ( ) ;
106
+ return result ;
107
+ } catch ( error ) {
108
+ console . error ( `Failed to fetch feed for ${ feed . title } : ${ error } ` ) ;
109
+ completedImports ++ ;
110
+ updateProgress ( ) ;
111
+ return null ;
112
+ }
113
+ } ) ,
50
114
) ;
51
115
52
- for ( const missingPod of missingPodcasts ) {
53
- new Notice ( `Failed to save ${ missingPod . title } ...` , 60000 ) ;
116
+ notice . stop ( ) ;
117
+
118
+ const validPodcasts = podcasts . filter (
119
+ ( pod ) : pod is PodcastFeed => pod !== null ,
120
+ ) ;
121
+
122
+ savedFeeds . update ( ( feeds ) => {
123
+ for ( const pod of validPodcasts ) {
124
+ if ( feeds [ pod . title ] ) continue ;
125
+ feeds [ pod . title ] = structuredClone ( pod ) ;
126
+ }
127
+ return feeds ;
128
+ } ) ;
129
+
130
+ const skippedCount =
131
+ incompletePodcastsToAdd . length - newPodcastsToAdd . length ;
132
+ notice . update (
133
+ `OPML import complete. Saved ${ validPodcasts . length } new podcasts. Skipped ${ skippedCount } existing podcasts.` ,
134
+ ) ;
135
+
136
+ if ( validPodcasts . length !== newPodcastsToAdd . length ) {
137
+ const failedImports = newPodcastsToAdd . length - validPodcasts . length ;
138
+ console . error ( `Failed to import ${ failedImports } podcasts.` ) ;
139
+ new Notice (
140
+ `Failed to import ${ failedImports } podcasts. Check console for details.` ,
141
+ 10000 ,
142
+ ) ;
54
143
}
144
+
145
+ setTimeout ( ( ) => notice . hide ( ) , 5000 ) ;
146
+ } catch ( error ) {
147
+ console . error ( "Error importing OPML:" , error ) ;
148
+ new Notice (
149
+ `Error importing OPML: ${ error instanceof Error ? error . message : "Unknown error" } ` ,
150
+ 10000 ,
151
+ ) ;
55
152
}
56
153
}
57
154
58
155
async function exportOPML (
156
+ app : App ,
59
157
feeds : PodcastFeed [ ] ,
60
- filePath = "PodNotes_Export.opml"
158
+ filePath = "PodNotes_Export.opml" ,
61
159
) {
62
160
const header = `<?xml version="1.0" encoding="utf=8" standalone="no"?>` ;
63
161
const opml = ( child : string ) => `<opml version="1.0">${ child } </opml>` ;
64
162
const head = ( child : string ) => `<head>${ child } </head>` ;
65
- const title = ` <title>PodNotes Feeds</title>` ;
163
+ const title = " <title>PodNotes Feeds</title>" ;
66
164
const body = ( child : string ) => `<body>${ child } </body>` ;
67
165
const feedOutline = ( feed : PodcastFeed ) =>
68
166
`<outline text="${ feed . title } " type="rss" xmlUrl="${ feed . url } " />` ;
@@ -74,13 +172,17 @@ async function exportOPML(
74
172
try {
75
173
await app . vault . create ( filePath , doc ) ;
76
174
77
- new Notice (
78
- `Exported ${ feeds . length } podcast feeds to file "${ filePath } ".`
79
- ) ;
175
+ new Notice ( `Exported ${ feeds . length } podcast feeds to file "${ filePath } ".` ) ;
80
176
} catch ( error ) {
81
- new Notice (
82
- `Unable to create podcast export file:\n\n${ error } `
83
- ) ;
177
+ if ( error instanceof Error ) {
178
+ if ( error . message . includes ( "Folder does not exist" ) ) {
179
+ new Notice ( "Unable to create export file: Folder does not exist." ) ;
180
+ } else {
181
+ new Notice ( `Unable to create podcast export file:\n\n${ error . message } ` ) ;
182
+ }
183
+ } else {
184
+ new Notice ( "An unexpected error occurred during export." ) ;
185
+ }
84
186
85
187
console . error ( error ) ;
86
188
}
0 commit comments