@@ -4,13 +4,17 @@ import { ChannelsParser, XML } from '../../core'
4
4
import { Channel , Feed } from '../../models'
5
5
import { DATA_DIR } from '../../constants'
6
6
import nodeCleanup from 'node-cleanup'
7
- import epgGrabber from 'epg-grabber'
8
7
import { Command } from 'commander'
9
8
import readline from 'readline'
10
- import Fuse from 'fuse.js'
9
+ import sjs from '@freearhey/search-js'
10
+ import { DataProcessor , DataLoader } from '../../core'
11
+ import type { DataLoaderData } from '../../types/dataLoader'
12
+ import type { DataProcessorData } from '../../types/dataProcessor'
13
+ import epgGrabber from 'epg-grabber'
14
+ import { ChannelSearchableData } from '../../types/channel'
11
15
12
16
type ChoiceValue = { type : string ; value ?: Feed | Channel }
13
- type Choice = { name : string ; short ?: string ; value : ChoiceValue }
17
+ type Choice = { name : string ; short ?: string ; value : ChoiceValue ; default ?: boolean }
14
18
15
19
if ( process . platform === 'win32' ) {
16
20
readline
@@ -42,31 +46,48 @@ export default async function main(filepath: string) {
42
46
throw new Error ( `File "${ filepath } " does not exists` )
43
47
}
44
48
49
+ logger . info ( 'loading data from api...' )
50
+ const processor = new DataProcessor ( )
51
+ const dataStorage = new Storage ( DATA_DIR )
52
+ const loader = new DataLoader ( { storage : dataStorage } )
53
+ const data : DataLoaderData = await loader . load ( )
54
+ const { feedsGroupedByChannelId, channels, channelsKeyById } : DataProcessorData =
55
+ processor . process ( data )
56
+
57
+ logger . info ( 'loading channels...' )
45
58
const parser = new ChannelsParser ( { storage } )
46
59
parsedChannels = await parser . parse ( filepath )
47
-
48
- const dataStorage = new Storage ( DATA_DIR )
49
- const channelsData = await dataStorage . json ( 'channels.json' )
50
- const channels = new Collection ( channelsData ) . map ( data => new Channel ( data ) )
51
- const feedsData = await dataStorage . json ( 'feeds.json' )
52
- const feeds = new Collection ( feedsData ) . map ( data => new Feed ( data ) )
53
- const feedsGroupedByChannelId = feeds . groupBy ( ( feed : Feed ) => feed . channelId )
54
-
55
- const searchIndex : Fuse < Channel > = new Fuse ( channels . all ( ) , {
56
- keys : [ 'name' , 'alt_names' ] ,
57
- threshold : 0.4
60
+ const parsedChannelsWithoutId = parsedChannels . filter (
61
+ ( channel : epgGrabber . Channel ) => ! channel . xmltv_id
62
+ )
63
+
64
+ logger . info (
65
+ `found ${ parsedChannels . count ( ) } channels (including ${ parsedChannelsWithoutId . count ( ) } without ID)`
66
+ )
67
+
68
+ logger . info ( 'creating search index...' )
69
+ const items = channels . map ( ( channel : Channel ) => channel . getSearchable ( ) ) . all ( )
70
+ const searchIndex = sjs . createIndex ( items , {
71
+ searchable : [ 'name' , 'altNames' , 'guideNames' , 'streamNames' , 'feedFullNames' ]
58
72
} )
59
73
60
- for ( const channel of parsedChannels . all ( ) ) {
61
- if ( channel . xmltv_id ) continue
74
+ logger . info ( 'starting...\n' )
75
+
76
+ for ( const parsedChannel of parsedChannelsWithoutId . all ( ) ) {
62
77
try {
63
- channel . xmltv_id = await selectChannel ( channel , searchIndex , feedsGroupedByChannelId )
64
- } catch {
78
+ parsedChannel . xmltv_id = await selectChannel (
79
+ parsedChannel ,
80
+ searchIndex ,
81
+ feedsGroupedByChannelId ,
82
+ channelsKeyById
83
+ )
84
+ } catch ( err ) {
85
+ logger . info ( err . message )
65
86
break
66
87
}
67
88
}
68
89
69
- parsedChannels . forEach ( ( channel : epgGrabber . Channel ) => {
90
+ parsedChannelsWithoutId . forEach ( ( channel : epgGrabber . Channel ) => {
70
91
if ( channel . xmltv_id === '-' ) {
71
92
channel . xmltv_id = ''
72
93
}
@@ -75,12 +96,14 @@ export default async function main(filepath: string) {
75
96
76
97
async function selectChannel (
77
98
channel : epgGrabber . Channel ,
78
- searchIndex : Fuse < Channel > ,
79
- feedsGroupedByChannelId : Dictionary
99
+ searchIndex ,
100
+ feedsGroupedByChannelId : Dictionary ,
101
+ channelsKeyById : Dictionary
80
102
) : Promise < string > {
103
+ const query = escapeRegex ( channel . name )
81
104
const similarChannels = searchIndex
82
- . search ( channel . name )
83
- . map ( ( result : { item : Channel } ) => result . item )
105
+ . search ( query )
106
+ . map ( ( item : ChannelSearchableData ) => channelsKeyById . get ( item . id ) )
84
107
85
108
const selected : ChoiceValue = await select ( {
86
109
message : `Select channel ID for "${ channel . name } " (${ channel . site_id } ):` ,
@@ -93,13 +116,16 @@ async function selectChannel(
93
116
return '-'
94
117
case 'type' : {
95
118
const typedChannelId = await input ( { message : ' Channel ID:' } )
96
- const typedFeedId = await input ( { message : ' Feed ID:' , default : 'SD' } )
97
- return [ typedChannelId , typedFeedId ] . join ( '@' )
119
+ if ( ! typedChannelId ) return ''
120
+ const selectedFeedId = await selectFeed ( typedChannelId , feedsGroupedByChannelId )
121
+ if ( selectedFeedId === '-' ) return typedChannelId
122
+ return [ typedChannelId , selectedFeedId ] . join ( '@' )
98
123
}
99
124
case 'channel' : {
100
125
const selectedChannel = selected . value
101
126
if ( ! selectedChannel ) return ''
102
127
const selectedFeedId = await selectFeed ( selectedChannel . id , feedsGroupedByChannelId )
128
+ if ( selectedFeedId === '-' ) return selectedChannel . id
103
129
return [ selectedChannel . id , selectedFeedId ] . join ( '@' )
104
130
}
105
131
}
@@ -108,18 +134,22 @@ async function selectChannel(
108
134
}
109
135
110
136
async function selectFeed ( channelId : string , feedsGroupedByChannelId : Dictionary ) : Promise < string > {
111
- const channelFeeds = feedsGroupedByChannelId . get ( channelId ) || [ ]
112
- if ( channelFeeds . length <= 1 ) return ''
137
+ const channelFeeds = feedsGroupedByChannelId . has ( channelId )
138
+ ? new Collection ( feedsGroupedByChannelId . get ( channelId ) )
139
+ : new Collection ( )
140
+ const choices = getFeedChoises ( channelFeeds )
113
141
114
142
const selected : ChoiceValue = await select ( {
115
143
message : `Select feed ID for "${ channelId } ":` ,
116
- choices : getFeedChoises ( channelFeeds ) ,
144
+ choices,
117
145
pageSize : 10
118
146
} )
119
147
120
148
switch ( selected . type ) {
149
+ case 'skip' :
150
+ return '-'
121
151
case 'type' :
122
- return await input ( { message : ' Feed ID:' } )
152
+ return await input ( { message : ' Feed ID:' , default : 'SD' } )
123
153
case 'feed' :
124
154
const selectedFeed = selected . value
125
155
if ( ! selectedFeed ) return ''
@@ -133,7 +163,7 @@ function getChannelChoises(channels: Collection): Choice[] {
133
163
const choises : Choice [ ] = [ ]
134
164
135
165
channels . forEach ( ( channel : Channel ) => {
136
- const names = [ channel . name , ...channel . altNames . all ( ) ] . join ( ', ' )
166
+ const names = new Collection ( [ channel . name , ...channel . getAltNames ( ) . all ( ) ] ) . uniq ( ) . join ( ', ' )
137
167
138
168
choises . push ( {
139
169
value : {
@@ -163,12 +193,14 @@ function getFeedChoises(feeds: Collection): Choice[] {
163
193
type : 'feed' ,
164
194
value : feed
165
195
} ,
196
+ default : feed . isMain ,
166
197
name,
167
198
short : feed . id
168
199
} )
169
200
} )
170
201
171
202
choises . push ( { name : 'Type...' , value : { type : 'type' } } )
203
+ choises . push ( { name : 'Skip' , value : { type : 'skip' } } )
172
204
173
205
return choises
174
206
}
@@ -179,3 +211,7 @@ function save(filepath: string) {
179
211
storage . saveSync ( filepath , xml . toString ( ) )
180
212
logger . info ( `\nFile '${ filepath } ' successfully saved` )
181
213
}
214
+
215
+ function escapeRegex ( string : string ) {
216
+ return string . replace ( / [ / \- \\ ^ $ * + ? . ( ) | [ \] { } ] / g, '\\$&' )
217
+ }
0 commit comments