1+ import { Storage , Collection , Logger , Dictionary } from '@freearhey/core'
2+ import { select , input } from '@inquirer/prompts'
3+ import { ChannelsParser , XML } from '../../core'
4+ import { Channel , Feed } from '../../models'
15import { DATA_DIR } from '../../constants'
2- import { Storage , Collection , Logger } from '@freearhey/core'
3- import { ChannelsParser , XML , ApiChannel } from '../../core'
4- import { Channel } from 'epg-grabber'
56import nodeCleanup from 'node-cleanup'
6- import { program } from 'commander'
7- import inquirer , { QuestionCollection } from 'inquirer'
8- import Fuse from 'fuse.js'
7+ import epgGrabber from 'epg-grabber'
8+ import { Command } from 'commander'
99import readline from 'readline'
10+ import Fuse from 'fuse.js'
11+
12+ type ChoiceValue = { type : string ; value ?: Feed | Channel }
13+ type Choice = { name : string ; short ?: string ; value : ChoiceValue }
1014
1115if ( process . platform === 'win32' ) {
1216 readline
@@ -19,105 +23,159 @@ if (process.platform === 'win32') {
1923 } )
2024}
2125
26+ const program = new Command ( )
27+
2228program . argument ( '<filepath>' , 'Path to *.channels.xml file to edit' ) . parse ( process . argv )
2329
2430const filepath = program . args [ 0 ]
25-
2631const logger = new Logger ( )
2732const storage = new Storage ( )
28- let channels = new Collection ( )
33+ let parsedChannels = new Collection ( )
2934
30- async function main ( ) {
35+ main ( filepath )
36+ nodeCleanup ( ( ) => {
37+ save ( filepath )
38+ } )
39+
40+ export default async function main ( filepath : string ) {
3141 if ( ! ( await storage . exists ( filepath ) ) ) {
3242 throw new Error ( `File "${ filepath } " does not exists` )
3343 }
3444
3545 const parser = new ChannelsParser ( { storage } )
36- channels = await parser . parse ( filepath )
46+ parsedChannels = await parser . parse ( filepath )
3747
3848 const dataStorage = new Storage ( DATA_DIR )
39- const channelsContent = await dataStorage . json ( 'channels.json' )
40- const searchIndex = new Fuse ( channelsContent , { keys : [ 'name' , 'alt_names' ] , threshold : 0.4 } )
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
58+ } )
4159
42- for ( const channel of channels . all ( ) ) {
60+ for ( const channel of parsedChannels . all ( ) ) {
4361 if ( channel . xmltv_id ) continue
44- const question : QuestionCollection = {
45- name : 'option' ,
46- message : `Select xmltv_id for "${ channel . name } " (${ channel . site_id } ):` ,
47- type : 'list' ,
48- choices : getOptions ( searchIndex , channel ) ,
49- pageSize : 10
62+ try {
63+ channel . xmltv_id = await selectChannel ( channel , searchIndex , feedsGroupedByChannelId )
64+ } catch {
65+ break
5066 }
51-
52- await inquirer . prompt ( question ) . then ( async selected => {
53- switch ( selected . option ) {
54- case 'Type...' :
55- const input = await getInput ( channel )
56- channel . xmltv_id = input . xmltv_id
57- break
58- case 'Skip' :
59- channel . xmltv_id = '-'
60- break
61- default :
62- const [ , xmltv_id ] = selected . option
63- . replace ( / \[ .* \] / , '' )
64- . split ( '|' )
65- . map ( ( i : string ) => i . trim ( ) )
66- channel . xmltv_id = xmltv_id
67- break
68- }
69- } )
7067 }
7168
72- channels . forEach ( ( channel : Channel ) => {
69+ parsedChannels . forEach ( ( channel : epgGrabber . Channel ) => {
7370 if ( channel . xmltv_id === '-' ) {
7471 channel . xmltv_id = ''
7572 }
7673 } )
7774}
7875
79- main ( )
76+ async function selectChannel (
77+ channel : epgGrabber . Channel ,
78+ searchIndex : Fuse < Channel > ,
79+ feedsGroupedByChannelId : Dictionary
80+ ) : Promise < string > {
81+ const similarChannels = searchIndex
82+ . search ( channel . name )
83+ . map ( ( result : { item : Channel } ) => result . item )
84+
85+ const selected : ChoiceValue = await select ( {
86+ message : `Select channel ID for "${ channel . name } " (${ channel . site_id } ):` ,
87+ choices : getChannelChoises ( new Collection ( similarChannels ) ) ,
88+ pageSize : 10
89+ } )
8090
81- function save ( ) {
82- if ( ! storage . existsSync ( filepath ) ) return
91+ switch ( selected . type ) {
92+ case 'skip' :
93+ return '-'
94+ case 'type' : {
95+ const typedChannelId = await input ( { message : ' Channel ID:' } )
96+ const typedFeedId = await input ( { message : ' Feed ID:' , default : 'SD' } )
97+ return [ typedChannelId , typedFeedId ] . join ( '@' )
98+ }
99+ case 'channel' : {
100+ const selectedChannel = selected . value
101+ if ( ! selectedChannel ) return ''
102+ const selectedFeedId = await selectFeed ( selectedChannel . id , feedsGroupedByChannelId )
103+ return [ selectedChannel . id , selectedFeedId ] . join ( '@' )
104+ }
105+ }
83106
84- const xml = new XML ( channels )
107+ return ''
108+ }
85109
86- storage . saveSync ( filepath , xml . toString ( ) )
110+ async function selectFeed ( channelId : string , feedsGroupedByChannelId : Dictionary ) : Promise < string > {
111+ const channelFeeds = feedsGroupedByChannelId . get ( channelId ) || [ ]
112+ if ( channelFeeds . length <= 1 ) return ''
87113
88- logger . info ( `\nFile '${ filepath } ' successfully saved` )
114+ const selected : ChoiceValue = await select ( {
115+ message : `Select feed ID for "${ channelId } ":` ,
116+ choices : getFeedChoises ( channelFeeds ) ,
117+ pageSize : 10
118+ } )
119+
120+ switch ( selected . type ) {
121+ case 'type' :
122+ return await input ( { message : ' Feed ID:' } )
123+ case 'feed' :
124+ const selectedFeed = selected . value
125+ if ( ! selectedFeed ) return ''
126+ return selectedFeed . id
127+ }
128+
129+ return ''
89130}
90131
91- nodeCleanup ( ( ) => {
92- save ( )
93- } )
132+ function getChannelChoises ( channels : Collection ) : Choice [ ] {
133+ const choises : Choice [ ] = [ ]
94134
95- async function getInput ( channel : Channel ) {
96- const name = channel . name . trim ( )
97- const input = await inquirer . prompt ( [
98- {
99- name : 'xmltv_id' ,
100- message : ' xmltv_id:' ,
101- type : 'input'
102- }
103- ] )
135+ channels . forEach ( ( channel : Channel ) => {
136+ const names = [ channel . name , ...channel . altNames . all ( ) ] . join ( ', ' )
137+
138+ choises . push ( {
139+ value : {
140+ type : 'channel' ,
141+ value : channel
142+ } ,
143+ name : `${ channel . id } (${ names } )` ,
144+ short : `${ channel . id } `
145+ } )
146+ } )
147+
148+ choises . push ( { name : 'Type...' , value : { type : 'type' } } )
149+ choises . push ( { name : 'Skip' , value : { type : 'skip' } } )
104150
105- return { name , xmltv_id : input [ 'xmltv_id' ] }
151+ return choises
106152}
107153
108- function getOptions ( index , channel : Channel ) {
109- const similar = index . search ( channel . name ) . map ( result => new ApiChannel ( result . item ) )
154+ function getFeedChoises ( feeds : Collection ) : Choice [ ] {
155+ const choises : Choice [ ] = [ ]
110156
111- const variants = new Collection ( )
112- similar . forEach ( ( _channel : ApiChannel ) => {
113- const altNames = _channel . altNames . notEmpty ( ) ? ` (${ _channel . altNames . join ( ',' ) } )` : ''
114- const closed = _channel . closed ? ` [closed:${ _channel . closed } ]` : ''
115- const replacedBy = _channel . replacedBy ? `[replaced_by:${ _channel . replacedBy } ]` : ''
157+ feeds . forEach ( ( feed : Feed ) => {
158+ let name = `${ feed . id } (${ feed . name } )`
159+ if ( feed . isMain ) name += ' [main]'
116160
117- variants . add ( `${ _channel . name } ${ altNames } | ${ _channel . id } ${ closed } ${ replacedBy } ` )
161+ choises . push ( {
162+ value : {
163+ type : 'feed' ,
164+ value : feed
165+ } ,
166+ name,
167+ short : feed . id
168+ } )
118169 } )
119- variants . add ( 'Type...' )
120- variants . add ( 'Skip' )
121170
122- return variants . all ( )
171+ choises . push ( { name : 'Type...' , value : { type : 'type' } } )
172+
173+ return choises
174+ }
175+
176+ function save ( filepath : string ) {
177+ if ( ! storage . existsSync ( filepath ) ) return
178+ const xml = new XML ( parsedChannels )
179+ storage . saveSync ( filepath , xml . toString ( ) )
180+ logger . info ( `\nFile '${ filepath } ' successfully saved` )
123181}
0 commit comments