1
|
#!/usr/bin/env python
|
2
|
# coding=utf-8
|
3
|
#
|
4
|
# Copyright (C) 2012 Adam Sutton <[email protected]>
|
5
|
# Modified Feb 2015 by ulibuck to generate xmltv data
|
6
|
#
|
7
|
# This program is free software: you can redistribute it and/or modify
|
8
|
# it under the terms of the GNU General Public License as published by
|
9
|
# the Free Software Foundation, version 3 of the License.
|
10
|
#
|
11
|
# This program is distributed in the hope that it will be useful,
|
12
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
# GNU General Public License for more details.
|
15
|
#
|
16
|
# You should have received a copy of the GNU General Public License
|
17
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
18
|
#
|
19
|
"""
|
20
|
Connect to a HTSP server and generate xmltv data -- channels and epg entries -- from asynchronous initialSync
|
21
|
"""
|
22
|
|
23
|
# System imports
|
24
|
import os, sys, pprint, time
|
25
|
from optparse import OptionParser
|
26
|
from operator import itemgetter, attrgetter
|
27
|
|
28
|
# TVH imports
|
29
|
from tvh.htsp import HTSPClient
|
30
|
import tvh.log as log
|
31
|
|
32
|
# encode xml escape characters
|
33
|
def encodeXMLText(text):
|
34
|
text = text.replace("&", "&")
|
35
|
text = text.replace("\"", """)
|
36
|
text = text.replace("'", "'")
|
37
|
text = text.replace("<", "<")
|
38
|
text = text.replace(">", ">")
|
39
|
return text
|
40
|
|
41
|
# define EPG genre names
|
42
|
def genre_names ( mcat, scat ):
|
43
|
epg_genre_names = [
|
44
|
[ "Undefined content" ],
|
45
|
|
46
|
[ "Movie / Drama",
|
47
|
"Detective / Thriller",
|
48
|
"Adventure / Western / War",
|
49
|
"Science fiction / Fantasy / Horror",
|
50
|
"Comedy",
|
51
|
"Soap / Melodrama / Folkloric",
|
52
|
"Romance",
|
53
|
"Serious / Classical / Religious / Historical movie / Drama",
|
54
|
"Adult movie / Drama" ],
|
55
|
|
56
|
[ "News / Current affairs",
|
57
|
"News / Weather report",
|
58
|
"News magazine",
|
59
|
"Documentary",
|
60
|
"Discussion / Interview / Debate" ],
|
61
|
|
62
|
[ "Show / Game show",
|
63
|
"Game show / Quiz / Contest",
|
64
|
"Variety show",
|
65
|
"Talk show" ],
|
66
|
|
67
|
[ "Sports",
|
68
|
"Special events (Olympic Games, World Cup, etc.)",
|
69
|
"Sports magazines",
|
70
|
"Football / Soccer",
|
71
|
"Tennis / Squash",
|
72
|
"Team sports (excluding football)",
|
73
|
"Athletics",
|
74
|
"Motor sport",
|
75
|
"Water sport",
|
76
|
"Winter sports",
|
77
|
"Equestrian",
|
78
|
"Martial sports" ],
|
79
|
|
80
|
[ "Children's / Youth programmes",
|
81
|
"Pre-school children's programmes",
|
82
|
"Entertainment programmes for 6 to 14",
|
83
|
"Entertainment programmes for 10 to 16",
|
84
|
"Informational / Educational / School programmes",
|
85
|
"Cartoons / Puppets" ],
|
86
|
|
87
|
[ "Music / Ballet / Dance",
|
88
|
"Rock / Pop",
|
89
|
"Serious music / Classical music",
|
90
|
"Folk / Traditional music",
|
91
|
"Jazz",
|
92
|
"Musical / Opera",
|
93
|
"Ballet" ],
|
94
|
|
95
|
[ "Arts / Culture (without music)",
|
96
|
"Performing arts",
|
97
|
"Fine arts",
|
98
|
"Religion",
|
99
|
"Popular culture / Traditional arts",
|
100
|
"Literature",
|
101
|
"Film / Cinema",
|
102
|
"Experimental film / Video",
|
103
|
"Broadcasting / Press",
|
104
|
"New media",
|
105
|
"Arts / culture magazines",
|
106
|
"Fashion" ],
|
107
|
|
108
|
[ "Social / Political issues / Economics",
|
109
|
"Magazines / Reports / Documentary",
|
110
|
"Economics / Social advisory",
|
111
|
"Remarkable people" ],
|
112
|
|
113
|
[ "Education / Science / Factual topics",
|
114
|
"Nature / Animals / Environment",
|
115
|
"Technology / Natural sciences",
|
116
|
"Medicine / Physiology / Psychology",
|
117
|
"Foreign countries / Expeditions",
|
118
|
"Social / Spiritual sciences",
|
119
|
"Further education",
|
120
|
"Languages" ],
|
121
|
|
122
|
[ "Leisure hobbies",
|
123
|
"Tourism / Travel",
|
124
|
"Handicraft",
|
125
|
"Motoring",
|
126
|
"Fitness and health",
|
127
|
"Cooking",
|
128
|
"Advertisement / Shopping",
|
129
|
"Gardening" ]
|
130
|
]
|
131
|
if len(epg_genre_names) <= mcat : mcat = 0
|
132
|
if len(epg_genre_names[mcat]) <= scat: scat = 0
|
133
|
return epg_genre_names[mcat][scat]
|
134
|
|
135
|
# Formatiere Datum und Uhrzeit für die Ausgabe
|
136
|
def datumzeit (Zeit):
|
137
|
Wochentage = ["Mo","Di","Mi","Do","Fr","Sa","So"]
|
138
|
Monatsnamen = ["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]
|
139
|
Wochentag = Wochentage[Zeit.tm_wday]
|
140
|
Monatstag = Zeit.tm_mday
|
141
|
Monat = Monatsnamen [Zeit.tm_mon-1]
|
142
|
return '%s %02d %s %s' % (Wochentag, Monatstag, Monat, time.strftime('%Y %H:%M:%S',Zeit))
|
143
|
|
144
|
try:
|
145
|
|
146
|
# Command line
|
147
|
optp = OptionParser()
|
148
|
optp.add_option('-a', '--host', default='localhost',
|
149
|
help='Specify HTSP server hostname [localhost]')
|
150
|
optp.add_option('-o', '--port', default=9982, type='int',
|
151
|
help='Specify HTSP server port [9982]')
|
152
|
optp.add_option('-u', '--user', default=None,
|
153
|
help='Specify HTSP authentication username [None]')
|
154
|
optp.add_option('-p', '--passwd', default=None,
|
155
|
help='Specify HTSP authentication password [None]')
|
156
|
optp.add_option('-e', '--epg', default=True, action='store_false',
|
157
|
help='Exclude EPG data [Include]')
|
158
|
optp.add_option('-t', '--update', default=None, type='int',
|
159
|
help='Specify when to receive updates from [None]')
|
160
|
xnam = '%s.xml' % (os.path.splitext(__file__)[0])
|
161
|
optp.add_option('-x', '--xnam', default=None,
|
162
|
help='Specify xml output file name [%s]' % (xnam))
|
163
|
(opts, args) = optp.parse_args()
|
164
|
|
165
|
# Connect
|
166
|
htsp = HTSPClient((opts.host, opts.port))
|
167
|
msg = htsp.hello()
|
168
|
sernam = msg['servername']
|
169
|
server = msg['serverversion']
|
170
|
log.info('%s connected to [%s] / %s [%s] / HTSP v%d' % (htsp._name, opts.host, sernam, server, htsp._version))
|
171
|
|
172
|
# Authenticate
|
173
|
if opts.user:
|
174
|
htsp.authenticate(opts.user, opts.passwd)
|
175
|
log.info('authenticated as %s' % opts.user)
|
176
|
|
177
|
# Enable async
|
178
|
args = {}
|
179
|
if opts.epg:
|
180
|
args['epg'] = 1
|
181
|
if opts.update != None:
|
182
|
args['lastUpdate'] = opts.update
|
183
|
htsp.enableAsyncMetadata(args)
|
184
|
# log.info('generating xmltv -- channels and epg entries -- from asynchronous initialSync')
|
185
|
|
186
|
# Process messages
|
187
|
chanNum={}
|
188
|
chanAdd=[]
|
189
|
evenAdd=[]
|
190
|
while True:
|
191
|
msg = htsp.recv()
|
192
|
if 'method' in msg:
|
193
|
if msg['method'] == 'channelAdd':
|
194
|
chanNum[msg['channelId']]=msg['channelNumber']
|
195
|
chanAdd.append(msg) # store 'channelAdd' message
|
196
|
elif msg['method'] == 'eventAdd':
|
197
|
msg['channelNumber']=chanNum[msg['channelId']]
|
198
|
evenAdd.append(msg) # store 'eventAdd' message
|
199
|
elif msg['method'] == 'initialSyncCompleted':
|
200
|
break
|
201
|
# log.info('initialSync completed -- %d channels and %d events' % (len(chanAdd), len(evenAdd)))
|
202
|
|
203
|
# determine the local time offset to UTC
|
204
|
etm = int(time.time()) # seconds since the epoch
|
205
|
nlc = time.localtime(etm) # convert to struct_time in local time
|
206
|
ngm = time.gmtime(etm) # convert to struct_time in UTC
|
207
|
# convert back to seconds since the epoch
|
208
|
# - interpret struct_time in UTC in local time using mktime()
|
209
|
# - use daylight savings flag from local time
|
210
|
egm = time.mktime((ngm[0],ngm[1],ngm[2],ngm[3],ngm[4],ngm[5],ngm[6],ngm[7],nlc[8]))
|
211
|
edf = etm - egm # local time offset to UTC in seconds = difference of the two times
|
212
|
sdf = 1
|
213
|
if edf < 0: sdf = -1 # sign of the time difference
|
214
|
hrh = int(sdf*edf/36) # time difference in hours times hundred
|
215
|
mts = int((hrh%100)*3/5) # convert fraction of hour to minutes
|
216
|
hrmt = '%+05d' % (sdf*(int(hrh/100)*100 + mts)) # UTC offset in the form +HHMM or -HHMM
|
217
|
|
218
|
# generate xmltv
|
219
|
if opts.xnam != None:
|
220
|
xnam = opts.xnam
|
221
|
file = open (xnam, 'w')
|
222
|
gnam = os.path.basename(__file__)
|
223
|
file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
224
|
file.write('<!DOCTYPE tv SYSTEM "xmltv.dtd">\n')
|
225
|
out = '<tv date="%s %s" source-info-name="%s [%s]" generator-info-name="%s">\n' \
|
226
|
% (time.strftime('%Y%m%d%H%M%S'), hrmt, sernam, server, gnam)
|
227
|
file.write(out)
|
228
|
chanAddSorted = sorted(chanAdd, key=itemgetter('channelNumber'))
|
229
|
for msg in chanAddSorted:
|
230
|
out = ' <channel id="%s">\n' % (msg['channelNumber'])
|
231
|
file.write(out)
|
232
|
out = ' <display-name>%s</display-name>\n' % (encodeXMLText(msg['channelName']))
|
233
|
file.write(out)
|
234
|
if 'channelIcon' in msg:
|
235
|
out = ' <icon src="%s"/>\n' % (msg['channelIcon'])
|
236
|
file.write(out)
|
237
|
out = ' </channel>\n'
|
238
|
file.write(out)
|
239
|
evenAddSorted = sorted(evenAdd, key=itemgetter('channelNumber', 'start'))
|
240
|
for msg in evenAddSorted:
|
241
|
sta = time.localtime(msg['start'])
|
242
|
sto = time.localtime(msg['stop'])
|
243
|
out = ' <programme channel="%s" start="%s %s" stop="%s %s">\n' \
|
244
|
% (msg['channelNumber'], time.strftime('%Y%m%d%H%M%S',sta), hrmt, time.strftime('%Y%m%d%H%M%S',sto), hrmt)
|
245
|
file.write(out)
|
246
|
if 'title' in msg:
|
247
|
out = ' <title>%s</title>\n' % (encodeXMLText(msg['title']))
|
248
|
else:
|
249
|
out = ' <title>no title</title>\n'
|
250
|
file.write(out)
|
251
|
if 'description' in msg:
|
252
|
out = ' <desc>%s</desc>\n' % (encodeXMLText(msg['description']))
|
253
|
file.write(out)
|
254
|
if 'summary' in msg:
|
255
|
out = ' <sub-title>%s</sub-title>\n' % (encodeXMLText(msg['summary']))
|
256
|
file.write(out)
|
257
|
if 'contentType' in msg:
|
258
|
bits = '{0:08b}'.format(msg['contentType'])
|
259
|
mcat = int(bits[:4],2) # top 4 bits has major category
|
260
|
scat = int(bits[4:],2) # bottom 4 bits has sub-category
|
261
|
out = ' <category>%s</category>\n' % (encodeXMLText(genre_names(mcat,scat)))
|
262
|
file.write(out)
|
263
|
out = ' </programme>\n'
|
264
|
file.write(out)
|
265
|
out = '</tv>\n'
|
266
|
file.write(out)
|
267
|
file.close()
|
268
|
now = time.localtime()
|
269
|
sys.stdout.write('%s %s -- %d channels and %d epg entries\n' % (datumzeit(now), xnam, len(chanAdd), len(evenAdd)))
|
270
|
|
271
|
except KeyboardInterrupt: pass
|
272
|
except Exception, e:
|
273
|
log.error(e)
|
274
|
sys.exit(1)
|
275
|
|
276
|
# ############################################################################
|
277
|
# Editor Configuration
|
278
|
#
|
279
|
# vim:sts=2:ts=2:sw=2:et
|
280
|
# ############################################################################
|