Ticket #3313: mythtv.3.py

File mythtv.3.py, 10.6 KB (added by hads, 17 years ago)
Line 
1#!/usr/bin/python
2
3import logging
4
5log = logging.getLogger('mythtv')
6log.setLevel(logging.DEBUG)
7ch = logging.StreamHandler()
8ch.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
9log.addHandler(ch)
10
11import os
12import sys
13import socket
14import shlex
15import socket
16import code
17
18from datetime import datetime
19
20try:
21        import MySQLdb
22except:
23        log.critical("MySQLdb (python-mysqldb) is required but is not found.")
24        sys.exit(1)
25
26RECSTATUS = {
27        'TunerBusy': -8,
28        'LowDiskSpace': -7,
29        'Cancelled': -6,
30        'Deleted': -5,
31        'Aborted': -4,
32        'Recorded': -3,
33        'Recording': -2,
34        'WillRecord': -1,
35        'Unknown': 0,
36        'DontRecord': 1,
37        'PreviousRecording': 2,
38        'CurrentRecording': 3,
39        'EarlierShowing': 4,
40        'TooManyRecordings': 5,
41        'NotListed': 6,
42        'Conflict': 7,
43        'LaterShowing': 8,
44        'Repeat': 9,
45        'Inactive': 10,
46        'NeverRecord': 11,
47}
48
49BACKEND_SEP = '[]:[]'
50PROTO_VERSION = 34
51PROGRAM_FIELDS = 43
52
53def get_database_connection():
54        """
55        A connection to the mythtv database
56        """
57        config_files = [
58                '/usr/local/share/mythtv/mysql.txt',
59                '/usr/share/mythtv/mysql.txt',
60                '/usr/local/etc/mythtv/mysql.txt',
61                '/etc/mythtv/mysql.txt',
62                os.path.expanduser('~/.mythtv/mysql.txt'),
63        ]
64        if 'MYTHCONFDIR' in os.environ:
65                config_locations.append('%s/mysql.txt' % os.environ['MYTHCONFDIR'])
66
67        found_config = False
68        for config_file in config_files:
69                try:
70                        config = shlex.shlex(open(config_file))
71                except:
72                        continue
73
74                token = config.get_token()
75                db_host = db_user = db_password = None
76                while  token != config.eof and (db_host == None or db_user == None or db_password == None):
77                        if token == "DBHostName":
78                                if config.get_token() == "=":
79                                        db_host = config.get_token()
80                        elif token == "DBUserName":
81                                if config.get_token() == "=":
82                                        db_user = config.get_token()
83                        elif token == "DBPassword":
84                                if config.get_token() == "=":
85                                        db_password = config.get_token()
86
87                        token = config.get_token()
88                log.debug('Using config %s' % config_file)
89                found_config = True
90                break
91
92        if not found_config:
93                log.critical('Unable to find config')
94                sys.exit(1)
95        return MySQLdb.connect(user=db_user, host=db_host, passwd=db_password, db="mythconverg")
96
97class MythTV:
98        """
99        A connection to MythTV
100        """
101        def __init__(self, conn_type='Monitor'):
102                self.db = get_database_connection()
103                self.master_host = self.getSetting('MasterServerIP')
104                self.master_port = int(self.getSetting('MasterServerPort'))
105               
106                if not self.master_host:
107                        log.critical('Unable to find MasterServerIP in database')
108                        sys.exit(1)
109                if not self.master_port:
110                        log.critical('Unable to find MasterServerPort in database')
111                        sys.exit(1)
112               
113                self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114                self.socket.settimeout(10)
115                self.socket.connect((self.master_host, self.master_port))
116                res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP)
117                if res[0] == 'REJECT':
118                        log.critical('Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
119                        sys.exit(1)
120                res = self.backendCommand('ANN %s %s 0' % (conn_type, socket.gethostname()))
121                if res != 'OK':
122                        log.critical('Unexpected answer to ANN command: %s', res)
123
124        def getSetting(self, value, hostname=None):
125                """
126                Returns the value for the given MythTV setting.
127               
128                Returns None if the setting was not found. If multiple rows are
129                found (multiple hostnames), returns the value of the first one.
130                """
131                log.debug('Looking for setting %s for host %s', value, hostname)
132                c = self.db.cursor()
133                if hostname is None:
134                        c.execute("""
135                                SELECT data
136                                FROM settings
137                                WHERE value LIKE(%s) AND hostname IS NULL LIMIT 1""",
138                                (value,))
139                else:
140                        c.execute("""
141                                SELECT data
142                                FROM settings
143                                WHERE value LIKE(%s) AND hostname LIKE(%s) LIMIT 1""",
144                                (value, hostname))
145                row = c.fetchone()
146                c.close()
147               
148                if row:
149                        return row[0]
150                else:
151                        return None
152
153        def backendCommand(self, data):
154                """
155                Sends a command via a socket to the mythbackend in
156                the format that it expects. Returns the result from
157                the backend
158                """
159                def recv():
160                        """
161                        Reads the data returned fomr the backend
162                        """
163                        # The first 8 bytes of the response gives us the length
164                        data = self.socket.recv(8)
165                        try:
166                                length = int(data)
167                        except:
168                                return ''
169                        data = []
170                        while length > 0:
171                                chunk = self.socket.recv(length)
172                                length = length - len(chunk)
173                                data.append(chunk)
174                        return ''.join(data)
175               
176                command = '%-8d%s' % (len(data), data)
177                log.debug('Sending command: %s' % command)
178                self.socket.send(command)
179                return recv()
180
181        def getPendingRecordings(self):
182                """
183                Returns a list of Program objects which are scheduled to be
184                recorded
185                """
186                programs = []
187                res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
188                has_conflict = int(res.pop(0))
189                num_progs = int(res.pop(0))
190                log.debug('%s pending recordings', num_progs)
191                for i in range(num_progs):
192                        programs.append(
193                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
194                return programs
195
196        def getScheduledRecordings(self):
197                """
198                Returns a list of Program objects which are scheduled to be
199                recorded
200                """
201                programs = []
202                res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
203                num_progs = int(res.pop(0))
204                log.debug('%s scheduled recordings', num_progs)
205                for i in range(num_progs):
206                        programs.append(
207                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
208                return programs
209
210        def getUpcomingRecordings(self):
211                """
212                Returns a list of Program objects for programs which are actually
213                going to be recorded.
214                """
215                def sort_programs_by_starttime(x, y):
216                        if x.starttime > y.starttime:
217                                return 1
218                        elif x.starttime == y.starttime:
219                                return 0
220                        else:
221                                return -1
222                programs = []
223                res = self.getPendingRecordings()
224                for p in res:
225                        if p.recstatus == RECSTATUS['WillRecord']:
226                                programs.append(p)
227                programs.sort(sort_programs_by_starttime)
228                return programs
229
230        def getFreeRecorderList(self):
231                """
232                Returns a list of free recorders, or an empty list if none
233                """
234                res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
235                recorders = [int(d) for d in res]
236                if recorders[0]:
237                        return recorders
238                else:
239                        return []
240
241        def getCurrentRecording(self, recorder):
242                res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder)
243                return Program(res.split(BACKEND_SEP))
244
245        def isRecording(self, recorder):
246                res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder)
247                if res == '1':
248                        return True
249                else:
250                        return False
251
252class MythVideo():
253        def __init__(self):
254                self.db = get_database_connection()
255
256        def pruneMetadata(self):
257                c = self.db.cursor()
258                c.execute("""
259                        SELECT intid, filename
260                        FROM videometadata""")
261               
262                row = c.fetchone()
263                while row is not None:
264                        intid = row[0]
265                        filename = row[1]
266                        if not os.path.exists(filename):
267                                log.info("%s not exist, removing metadata..." % filename)
268                                c2 = self.db.cursor()
269                                c2.execute("""DELETE FROM videometadata WHERE intid = %s""", (intid,))
270                                c2.close()
271                        row = c.fetchone()
272                c.close()
273
274        def getGenreId(self, genre_name):
275                """
276                Find the id of the given genre from MythDB.
277               
278                If the genre does not exist, insert it and return its id.
279                """
280                c = self.db.cursor()
281                c.execute("SELECT intid FROM videocategory WHERE lower(category) = %s", (genre_name,))
282                row = c.fetchone()
283                c.close()
284               
285                if row is not None:
286                        return row[0]
287               
288                # Insert a new genre.
289                c = self.db.cursor()
290                c.execute("INSERT INTO videocategory(category) VALUES (%s)", (genre_name.capitalize(),))
291                newid = c.lastrowid
292                c.close()
293               
294                return newid
295
296        def getMetadataId(self, videopath):
297                """
298                Finds the MythVideo metadata id for the given video path from the MythDB, if any.
299               
300                Returns None if no metadata was found.
301                """
302                c = self.db.cursor()
303                c.execute("""
304                        SELECT intid
305                        FROM videometadata
306                        WHERE filename = %s""", (videopath,))
307                row = c.fetchone()
308                c.close()
309               
310                if row is not None:
311                        return row[0]
312                else:
313                        return None
314
315        def getMetadata(self, id):
316                """
317                Finds the MythVideo metadata for the given id from the MythDB, if any.
318               
319                Returns None if no metadata was found.
320                """
321                c = self.db.cursor()
322                c.execute("""
323                        SELECT *
324                        FROM videometadata
325                        WHERE intid = %s""", (id,))
326                row = c.fetchone()
327                c.close()
328               
329                if row is not None:
330                        return row
331                else:
332                        return None
333
334        def setMetadata(self, data, id=None):
335                c = self.db.cursor()
336                if id is None:
337                        fields = ', '.join(data.keys())
338                        format_string = ', '.join(['%s' for d in data.values()])
339                        sql = "INSERT INTO videometadata(%s) VALUES(%s)" % (fields, format_string)
340                        c.execute(sql, data.values())
341                        intid = c.lastrowid
342                        c.close()
343                        return intid
344                else:
345                        log.debug('Updating metadata for %s' % id)
346                        format_string = ', '.join(['%s = %%s' % d for d in data])
347                        sql = "UPDATE videometadata SET %s WHERE intid = %%s" % format_string
348                        sql_values = data.values()
349                        sql_values.append(id)
350                        c.execute(sql, sql_values)
351                        c.close()
352
353class Program:
354        def __init__(self, data):
355                """
356                Load the list of data into the object
357                """
358                self.title = data[0]
359                self.subtitle = data[1]
360                self.description = data[2]
361                self.category = data[3]
362                try:
363                        self.chanid = int(data[4])
364                except ValueError:
365                        self.chanid = None
366                self.channum = data[5] #chanstr
367                self.callsign = data[6] #chansign
368                self.channame = data[7]
369                self.filename = data[8] #pathname
370                self.fs_high = data[9]
371                self.fs_low = data[10]
372                self.starttime = datetime.fromtimestamp(int(data[11])) # startts
373                self.endtime = datetime.fromtimestamp(int(data[12])) #endts
374                self.duplicate = int(data[13])
375                self.shareable = int(data[14])
376                self.findid = int(data[15])
377                self.hostname = data[16]
378                self.sourceid = int(data[17])
379                self.cardid = int(data[18])
380                self.inputid = int(data[19])
381                self.recpriority = int(data[20])
382                self.recstatus = int(data[21])
383                self.recordid = int(data[22])
384                self.rectype = data[23]
385                self.dupin = data[24]
386                self.dupmethod = data[25]
387                self.recstartts = datetime.fromtimestamp(int(data[26]))
388                self.recendts = datetime.fromtimestamp(int(data[27]))
389                self.repeat = int(data[28])
390                self.programflags = data[29]
391                self.recgroup = data[30]
392                self.commfree = int(data[31])
393                self.outputfilters = data[32]
394                self.seriesid = data[33]
395                self.programid = data[34]
396                self.lastmodified = data[35]
397                self.stars = float(data[36])
398                self.airdate = data[37]
399                self.hasairdate = int(data[38])
400                self.playgroup = data[39]
401                self.recpriority2 = int(data[40])
402                self.parentid = data[41]
403                self.storagegroup = data[42]
404
405if __name__ == '__main__':
406        banner = "'m' is a MythTV instance."
407        try:
408                import readline, rlcompleter
409        except:
410                pass
411        else:
412                readline.parse_and_bind("tab: complete")
413                banner = banner + " TAB completion is available."
414        m = MythTV()
415        namespace = globals().copy()
416        namespace.update(locals())
417        code.InteractiveConsole(namespace).interact(banner)