| 1 | #!/usr/bin/python |
| 2 | |
| 3 | # vim:ts=4 sw=4 nowrap: |
| 4 | |
| 5 | # version: 0.1 |
| 6 | # date: 2008-02-06 |
| 7 | # by: trisooma |
| 8 | # description: Part of mythtv python bindings. Provides the base class for |
| 9 | # using mythtv from python |
| 10 | |
| 11 | # system imports |
| 12 | import os |
| 13 | import sys |
| 14 | import socket |
| 15 | import shlex |
| 16 | import socket |
| 17 | import code |
| 18 | from datetime import datetime |
| 19 | # package deps |
| 20 | from log import * |
| 21 | from db import * |
| 22 | |
| 23 | log = Log(INFO, '%(levelname)s - %(message)s', 'MythTV') |
| 24 | |
| 25 | RECSTATUS = { |
| 26 | 'TunerBusy': -8, |
| 27 | 'LowDiskSpace': -7, |
| 28 | 'Cancelled': -6, |
| 29 | 'Deleted': -5, |
| 30 | 'Aborted': -4, |
| 31 | 'Recorded': -3, |
| 32 | 'Recording': -2, |
| 33 | 'WillRecord': -1, |
| 34 | 'Unknown': 0, |
| 35 | 'DontRecord': 1, |
| 36 | 'PreviousRecording': 2, |
| 37 | 'CurrentRecording': 3, |
| 38 | 'EarlierShowing': 4, |
| 39 | 'TooManyRecordings': 5, |
| 40 | 'NotListed': 6, |
| 41 | 'Conflict': 7, |
| 42 | 'LaterShowing': 8, |
| 43 | 'Repeat': 9, |
| 44 | 'Inactive': 10, |
| 45 | 'NeverRecord': 11, |
| 46 | } |
| 47 | |
| 48 | BACKEND_SEP = '[]:[]' |
| 49 | PROTO_VERSION = 38 |
| 50 | PROGRAM_FIELDS = 46 |
| 51 | |
| 52 | class Base: |
| 53 | """ |
| 54 | A connection to MythTV backend. |
| 55 | """ |
| 56 | def __init__(self, conn_type='Monitor'): |
| 57 | self.db = DB(sys.argv[1:]) |
| 58 | self.master_host = self.db.getSetting('MasterServerIP') |
| 59 | self.master_port = int(self.db.getSetting('MasterServerPort')) |
| 60 | |
| 61 | if not self.master_host: |
| 62 | log.Msg(CRITICAL, 'Unable to find MasterServerIP in database') |
| 63 | sys.exit(1) |
| 64 | if not self.master_port: |
| 65 | log.Msg(CRITICAL, 'Unable to find MasterServerPort in database') |
| 66 | sys.exit(1) |
| 67 | |
| 68 | try: |
| 69 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 70 | self.socket.settimeout(10) |
| 71 | self.socket.connect((self.master_host, self.master_port)) |
| 72 | res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP) |
| 73 | if res[0] == 'REJECT': |
| 74 | log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION) |
| 75 | sys.exit(1) |
| 76 | res = self.backendCommand('ANN %s %s 0' % (conn_type, socket.gethostname())) |
| 77 | if res != 'OK': |
| 78 | log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res) |
| 79 | else: |
| 80 | log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port) |
| 81 | except socket.error, e: |
| 82 | #log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port) |
| 83 | raise Error('Couldn\'t connect to %s:%d (is the backend running)' % (self.master_host, self.master_port)) |
| 84 | |
| 85 | def backendCommand(self, data): |
| 86 | """ |
| 87 | Sends a formatted command via a socket to the mythbackend. |
| 88 | |
| 89 | Returns the result from the backend. |
| 90 | """ |
| 91 | def recv(): |
| 92 | """ |
| 93 | Reads the data returned from the backend. |
| 94 | """ |
| 95 | # The first 8 bytes of the response gives us the length |
| 96 | data = self.socket.recv(8) |
| 97 | try: |
| 98 | length = int(data) |
| 99 | except: |
| 100 | return '' |
| 101 | data = [] |
| 102 | while length > 0: |
| 103 | chunk = self.socket.recv(length) |
| 104 | length = length - len(chunk) |
| 105 | data.append(chunk) |
| 106 | return ''.join(data) |
| 107 | |
| 108 | command = '%-8d%s' % (len(data), data) |
| 109 | log.Msg(DEBUG, 'Sending command: %s', command) |
| 110 | self.socket.send(command) |
| 111 | return recv() |
| 112 | |
| 113 | def getPendingRecordings(self): |
| 114 | """ |
| 115 | Returns a list of Program objects which are scheduled to be recorded. |
| 116 | """ |
| 117 | programs = [] |
| 118 | res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP) |
| 119 | has_conflict = int(res.pop(0)) |
| 120 | num_progs = int(res.pop(0)) |
| 121 | log.Msg(DEBUG, '%s pending recordings', num_progs) |
| 122 | for i in range(num_progs): |
| 123 | programs.append( |
| 124 | Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS])) |
| 125 | return programs |
| 126 | |
| 127 | def getScheduledRecordings(self): |
| 128 | """ |
| 129 | Returns a list of Program objects which are scheduled to be recorded. |
| 130 | """ |
| 131 | programs = [] |
| 132 | res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP) |
| 133 | num_progs = int(res.pop(0)) |
| 134 | log.Msg(DEBUG, '%s scheduled recordings', num_progs) |
| 135 | for i in range(num_progs): |
| 136 | programs.append( |
| 137 | Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS])) |
| 138 | return programs |
| 139 | |
| 140 | def getUpcomingRecordings(self): |
| 141 | """ |
| 142 | Returns a list of Program objects which are scheduled to be recorded. |
| 143 | |
| 144 | Sorts the list by recording start time and only returns those with |
| 145 | record status of WillRecord. |
| 146 | """ |
| 147 | def sort_programs_by_starttime(x, y): |
| 148 | if x.starttime > y.starttime: |
| 149 | return 1 |
| 150 | elif x.starttime == y.starttime: |
| 151 | return 0 |
| 152 | else: |
| 153 | return -1 |
| 154 | programs = [] |
| 155 | res = self.getPendingRecordings() |
| 156 | for p in res: |
| 157 | if p.recstatus == RECSTATUS['WillRecord']: |
| 158 | programs.append(p) |
| 159 | programs.sort(sort_programs_by_starttime) |
| 160 | return programs |
| 161 | |
| 162 | def getRecorderList(self): |
| 163 | """ |
| 164 | Returns a list of recorders, or an empty list if none. |
| 165 | """ |
| 166 | recorders = [] |
| 167 | c = self.db.cursor() |
| 168 | c.execute('SELECT cardid FROM capturecard') |
| 169 | row = c.fetchone() |
| 170 | while row is not None: |
| 171 | recorders.append(int(row[0])) |
| 172 | row = c.fetchone() |
| 173 | c.close() |
| 174 | return recorders |
| 175 | |
| 176 | def getFreeRecorderList(self): |
| 177 | """ |
| 178 | Returns a list of free recorders, or an empty list if none. |
| 179 | """ |
| 180 | res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP) |
| 181 | recorders = [int(d) for d in res] |
| 182 | return recorders |
| 183 | |
| 184 | def getRecorderDetails(self, recorder_id): |
| 185 | """ |
| 186 | Returns a Recorder object with details of the recorder. |
| 187 | """ |
| 188 | c = self.db.cursor() |
| 189 | c.execute("""SELECT cardid, cardtype, videodevice, hostname |
| 190 | FROM capturecard WHERE cardid = %s""", recorder_id) |
| 191 | row = c.fetchone() |
| 192 | if row: |
| 193 | recorder = Recorder(row) |
| 194 | return recorder |
| 195 | else: |
| 196 | return None |
| 197 | |
| 198 | def getCurrentRecording(self, recorder): |
| 199 | """ |
| 200 | Returns a Program object for the current recorders recording. |
| 201 | """ |
| 202 | res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder) |
| 203 | return Program(res.split(BACKEND_SEP)) |
| 204 | |
| 205 | def isRecording(self, recorder): |
| 206 | """ |
| 207 | Returns a boolean as to whether the given recorder is recording. |
| 208 | """ |
| 209 | res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder) |
| 210 | if res == '1': |
| 211 | return True |
| 212 | else: |
| 213 | return False |
| 214 | |
| 215 | def isActiveBackend(self, hostname): |
| 216 | """ |
| 217 | Returns a boolean as to whether the given host is an active backend |
| 218 | """ |
| 219 | res = self.backendCommand('QUERY_IS_ACTIVE_BACKEND[]:[]%s' % hostname) |
| 220 | if res == 'TRUE': |
| 221 | return True |
| 222 | else: |
| 223 | return False |
| 224 | |
| 225 | class MythVideo: |
| 226 | def __init__(self): |
| 227 | self.db = MythDB() |
| 228 | |
| 229 | def pruneMetadata(self): |
| 230 | """ |
| 231 | Removes metadata from the database for files that no longer exist. |
| 232 | """ |
| 233 | c = self.db.cursor() |
| 234 | c.execute(""" |
| 235 | SELECT intid, filename |
| 236 | FROM videometadata""") |
| 237 | |
| 238 | row = c.fetchone() |
| 239 | while row is not None: |
| 240 | intid = row[0] |
| 241 | filename = row[1] |
| 242 | if not os.path.exists(filename): |
| 243 | log.Msg(INFO, '%s not exist, removing metadata...', filename) |
| 244 | c2 = self.db.cursor() |
| 245 | c2.execute("""DELETE FROM videometadata WHERE intid = %s""", (intid,)) |
| 246 | c2.close() |
| 247 | row = c.fetchone() |
| 248 | c.close() |
| 249 | |
| 250 | def getGenreId(self, genre_name): |
| 251 | """ |
| 252 | Find the id of the given genre from MythDB. |
| 253 | |
| 254 | If the genre does not exist, insert it and return its id. |
| 255 | """ |
| 256 | c = self.db.cursor() |
| 257 | c.execute("SELECT intid FROM videocategory WHERE lower(category) = %s", (genre_name,)) |
| 258 | row = c.fetchone() |
| 259 | c.close() |
| 260 | |
| 261 | if row is not None: |
| 262 | return row[0] |
| 263 | |
| 264 | # Insert a new genre. |
| 265 | c = self.db.cursor() |
| 266 | c.execute("INSERT INTO videocategory(category) VALUES (%s)", (genre_name.capitalize(),)) |
| 267 | newid = c.lastrowid |
| 268 | c.close() |
| 269 | |
| 270 | return newid |
| 271 | |
| 272 | def getMetadataId(self, videopath): |
| 273 | """ |
| 274 | Finds the MythVideo metadata id for the given video path from the MythDB, if any. |
| 275 | |
| 276 | Returns None if no metadata was found. |
| 277 | """ |
| 278 | c = self.db.cursor() |
| 279 | c.execute(""" |
| 280 | SELECT intid |
| 281 | FROM videometadata |
| 282 | WHERE filename = %s""", (videopath,)) |
| 283 | row = c.fetchone() |
| 284 | c.close() |
| 285 | |
| 286 | if row is not None: |
| 287 | return row[0] |
| 288 | else: |
| 289 | return None |
| 290 | |
| 291 | def hasMetadata(self, videopath): |
| 292 | """ |
| 293 | Determines if the given videopath has any metadata in the DB |
| 294 | |
| 295 | Returns False if no metadata was found. |
| 296 | """ |
| 297 | c = self.db.cursor() |
| 298 | c.execute(""" |
| 299 | SELECT category, year |
| 300 | FROM videometadata |
| 301 | WHERE filename = %s""", (videopath,)) |
| 302 | row = c.fetchone() |
| 303 | c.close() |
| 304 | |
| 305 | if row is not None: |
| 306 | # If category is 0 and year is 1895, we can safely assume no metadata |
| 307 | if (row[0] == 0) and (row[1] == 1895): |
| 308 | return False |
| 309 | else: |
| 310 | return True |
| 311 | else: |
| 312 | return False |
| 313 | |
| 314 | def getMetadata(self, id): |
| 315 | """ |
| 316 | Finds the MythVideo metadata for the given id from the MythDB, if any. |
| 317 | |
| 318 | Returns None if no metadata was found. |
| 319 | """ |
| 320 | c = self.db.cursor() |
| 321 | c.execute(""" |
| 322 | SELECT * |
| 323 | FROM videometadata |
| 324 | WHERE intid = %s""", (id,)) |
| 325 | row = c.fetchone() |
| 326 | c.close() |
| 327 | |
| 328 | if row is not None: |
| 329 | return row |
| 330 | else: |
| 331 | return None |
| 332 | |
| 333 | def setMetadata(self, data, id=None): |
| 334 | """ |
| 335 | Adds or updates the metadata in the database for a video item. |
| 336 | """ |
| 337 | c = self.db.cursor() |
| 338 | if id is None: |
| 339 | fields = ', '.join(data.keys()) |
| 340 | format_string = ', '.join(['%s' for d in data.values()]) |
| 341 | sql = "INSERT INTO videometadata(%s) VALUES(%s)" % (fields, format_string) |
| 342 | c.execute(sql, data.values()) |
| 343 | intid = c.lastrowid |
| 344 | c.close() |
| 345 | return intid |
| 346 | else: |
| 347 | log.Msg(DEBUG, 'Updating metadata for %s', id) |
| 348 | format_string = ', '.join(['%s = %%s' % d for d in data]) |
| 349 | sql = "UPDATE videometadata SET %s WHERE intid = %%s" % format_string |
| 350 | sql_values = data.values() |
| 351 | sql_values.append(id) |
| 352 | c.execute(sql, sql_values) |
| 353 | c.close() |
| 354 | |
| 355 | class Recorder: |
| 356 | def __str__(self): |
| 357 | return "Recorder %s (%s)" % (self.cardid, self.cardtype) |
| 358 | |
| 359 | def __repr__(self): |
| 360 | return "Recorder %s (%s)" % (self.cardid, self.cardtype) |
| 361 | |
| 362 | def __init__(self, data): |
| 363 | """ |
| 364 | Load the list of data into the object. |
| 365 | """ |
| 366 | self.cardid = data[0] |
| 367 | self.cardtype = data[1] |
| 368 | self.videodevice = data[2] |
| 369 | self.hostname = data[3] |
| 370 | |
| 371 | class Program: |
| 372 | def __str__(self): |
| 373 | return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S')) |
| 374 | |
| 375 | def __repr__(self): |
| 376 | return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S')) |
| 377 | |
| 378 | def __init__(self, data): |
| 379 | """ |
| 380 | Load the list of data into the object. |
| 381 | """ |
| 382 | self.title = data[0] |
| 383 | self.subtitle = data[1] |
| 384 | self.description = data[2] |
| 385 | self.category = data[3] |
| 386 | try: |
| 387 | self.chanid = int(data[4]) |
| 388 | except ValueError: |
| 389 | self.chanid = None |
| 390 | self.channum = data[5] #chanstr |
| 391 | self.callsign = data[6] #chansign |
| 392 | self.channame = data[7] |
| 393 | self.filename = data[8] #pathname |
| 394 | self.fs_high = data[9] |
| 395 | self.fs_low = data[10] |
| 396 | self.starttime = datetime.fromtimestamp(int(data[11])) # startts |
| 397 | self.endtime = datetime.fromtimestamp(int(data[12])) #endts |
| 398 | self.duplicate = int(data[13]) |
| 399 | self.shareable = int(data[14]) |
| 400 | self.findid = int(data[15]) |
| 401 | self.hostname = data[16] |
| 402 | self.sourceid = int(data[17]) |
| 403 | self.cardid = int(data[18]) |
| 404 | self.inputid = int(data[19]) |
| 405 | self.recpriority = int(data[20]) |
| 406 | self.recstatus = int(data[21]) |
| 407 | self.recordid = int(data[22]) |
| 408 | self.rectype = data[23] |
| 409 | self.dupin = data[24] |
| 410 | self.dupmethod = data[25] |
| 411 | self.recstartts = datetime.fromtimestamp(int(data[26])) |
| 412 | self.recendts = datetime.fromtimestamp(int(data[27])) |
| 413 | self.repeat = int(data[28]) |
| 414 | self.programflags = data[29] |
| 415 | self.recgroup = data[30] |
| 416 | self.commfree = int(data[31]) |
| 417 | self.outputfilters = data[32] |
| 418 | self.seriesid = data[33] |
| 419 | self.programid = data[34] |
| 420 | self.lastmodified = data[35] |
| 421 | self.stars = float(data[36]) |
| 422 | self.airdate = data[37] |
| 423 | self.hasairdate = int(data[38]) |
| 424 | self.playgroup = data[39] |
| 425 | self.recpriority2 = int(data[40]) |
| 426 | self.parentid = data[41] |
| 427 | self.storagegroup = data[42] |
| 428 | self.audio_props = data[43] |
| 429 | self.video_props = data[44] |
| 430 | self.subtitle_type = data[45] |
| 431 | |
| 432 | if __name__ == '__main__': |
| 433 | print 'Base is now part of the mythtv package' |