Index: __init__.py =================================================================== --- __init__.py (revision 25163) +++ __init__.py (working copy) @@ -25,10 +25,11 @@ \ 'Record', 'Recorded', 'RecordedProgram', 'OldRecorded', 'Job', \ 'Channel', 'Guide', 'Video', 'VideoGrabber', 'InternetContent', \ - 'InternetContentArticles', 'InternetSource', \ + 'InternetContentArticles', 'InternetSource', 'Song', 'Album', \ + 'Artist', 'MusicPlaylist', 'MusicDirectory', \ \ 'MythBE', 'BEEventMonitor', 'MythSystemEvent', 'SystemEvent', \ - 'Frontend', 'MythDB', 'MythVideo', 'MythXML'] + 'Frontend', 'MythDB', 'MythVideo', 'MythXML', 'MythMusic'] import26 = """ import warnings @@ -61,16 +62,3 @@ __version__ = OWN_VERSION static.mysqldb = MySQLdb.__version__ -if __name__ == '__main__': - banner = 'MythTV Python interactive shell.' - import code - try: - import readline, rlcompleter - except: - pass - else: - readline.parse_and_bind("tab: complete") - banner += ' TAB completion available.' - namespace = globals().copy() - namespace.update(locals()) - code.InteractiveConsole(namespace).interact(banner) Index: database.py =================================================================== --- database.py (revision 25163) +++ database.py (working copy) @@ -58,10 +58,14 @@ @classmethod def getAllEntries(cls, db=None): """cls.getAllEntries() -> tuple of DBData objects""" + return cls._fromQuery("", ()) + + @classmethod + def _fromQuery(cls, where, args, db=None): db = DBCache(db) - c = db.cursor() - count = c.execute("""SELECT * FROM %s""" % cls._table) + count = c.execute("""SELECT * FROM %s %s""" \ + % (cls._table, where), args) for i in xrange(count): res = c.fetchone() yield cls.fromRaw(res, db) Index: dataheap.py =================================================================== --- dataheap.py (revision 25163) +++ dataheap.py (working copy) @@ -783,4 +783,189 @@ for item in xmldat.findall('item'): yield InternetMetadata(item) +#### MYTHMUSIC #### +class MythMusicSchema( object ): + _schema_value = 'MusicDBSchemaVer' + _schema_local = MUSICSCHEMA_VERSION + _schema_name = 'MythMusic' + +class Song( DBDataWrite, MythMusicSchema ): + _table = 'music_songs' + _where = 'song_id=%s' + _setwheredat = 'self.song_id,' + _defaults = {'song_id':None} + _logmodule = 'Python Song' + + def __init__(self, id, db=None): + DBDataWrite.__init__(self, (id,), db) + + def create(self, data=None): + """Song.create(data=None) -> Song object""" + self._wheredat = (DBDataWrite.create(self, data),) + self._pull() + return self + + @classmethod + def fromAlbum(cls, album, db=None): + """Returns iterable of songs from given album.""" + try: + db = album._db + album = album.album_id + except AttributeError: pass + + return cls._fromQuery("WHERE album_id=%s", [album], db) + + @classmethod + def fromArtist(cls, artist, db=None): + """Returns iterable of songs from given artist.""" + try: + db = artist._db + artist = artist.artist_id + except AttributeError: pass + + return cls._fromQuery("WHERE artist_id=%s", [artist], db) + + @classmethod + def fromPlaylist(cls, playlist, db=None): + """Returns iterable of songs from given playlist.""" + try: + songs = playlist._songstring() + db = playlist._db + except AttributeError: + db = DBCache(db) + songs = MusicPlaylist(playlist, db)._songstring() + + return cls._fromQuery("WHERE LOCATE(song_id, %s)", songs, db) + +class Album( DBDataWrite, MythMusicSchema ): + _table = 'music_albums' + _where = 'album_id=%s' + _setwheredat = 'self.album_id,' + _defaults = {'album_id':None} + _logmodule = 'Python Album' + + def __init__(self, id, db=None): + DBDataWrite.__init__(self, (id,), db) + + def create(self, data=None): + """Album.create(data=None) -> Album object""" + self._wheredat = (DBDataWrite.create(self, data),) + self._pull() + return self + + @classmethod + def fromArtist(cls, artist, db=None): + """Returns iterable of albums from given artist.""" + try: + db = artist._db + artist = artist.artist_id + except AttributeError: + pass + return cls._fromQuery("WHERE artist_id=%s", [artist], db) + + @classmethod + def fromSong(cls, song, db=None): + """Returns the album for the given song.""" + try: + album = song.album_id + db = song._db + except AttributeError: + db = DBCache(db) + album = Song(song, db).album_id + return cls(album, db) + +class Artist( DBDataWrite, MythMusicSchema ): + _table = 'music_artists' + _where = 'artist_id=%s' + _setwheredat = 'self.artist_id,' + _defaults = {'artist_id':None} + _logmodule = 'Python Artist' + + def __init__(self, id, db=None): + DBDataWrite.__init__(self, (id,), db) + + def create(self, data=None): + """Artist.create(data=None) -> Artist object""" + self._wheredat = (DBDataWrite.create(self, data),) + self._pull() + return self + + @classmethod + def fromName(cls, name, db=None): + db = MythDB(db) + c = db.cursor() + count = c.execute("""SELECT * FROM %s WHERE artist_name=%s""", (cls._table, name)) + if count > 1: + raise MythDBError('Non-unique music_artist entry') + elif count == 1: + return cls.fromRaw(c.fetchone(), db) + else: + artist = cls(db=db) + artist.artist_name = name + return artist.create() + + @classmethod + def fromSong(cls, song, db=None): + """Returns the artist for the given song.""" + try: + artist = song.artist_id + db = song._db + except AttributeError: + db = DBCache(db) + artist = Song(song, db).artist_id + return cls(artist, db) + + @classmethod + def fromAlbum(cls, album, db=None): + """Returns the artist for the given album.""" + try: + artist = album.artist_id + db = album._db + except AttributeError: + db = DBCache(db) + artist = Album(album, db).artist_id + return cls(artist, db) + +class MusicPlaylist( DBDataWrite, MythMusicSchema ): + _table = 'music_playlists' + _where = 'playlist_id=%s' + _setwheredat = 'self.playlist_id,' + _defaults = {'playlist_id':None} + _logmodule = 'Python Music Playlist' + + def __init__(self, id, db=None): + DBDataWrite.__init__(self, (id,), db) + + def create(self, data=None): + """MusicPlaylist.create(data=None) -> MusicPlaylist object""" + self._wheredat = (DBDataWrite.create(self, data),) + self._pull() + return self + + @classmethod + def fromSong(cls, song, db=None): + """Returns an iterable of playlists containing the given song.""" + try: + db = song._db + song = song.song_id + except AttributeError: + db = DBCache(db) + song = Song(song, db).song_id + return cls._fromQuery("WHERE LOCATE(%s, playlist_songs)", song, db) + +class MusicDirectory( DBDataWrite, MythMusicSchema ): + _table = 'music_directories' + _where = 'directory_id=%s' + _setwheredat = 'self.directory_id,' + _defaults = {'directory_id':None} + _logmodule = 'Python Music Directory' + + def __init__(self, id, db=None): + DBDataWrite.__init__(self, (id,), db) + + def create(self, data=None): + """MusicDirectory.create(data=None) -> MusicDirectory object""" + self._wheredat = (DBDataWrite.create(self, data),) + self._pull() + return self Index: methodheap.py =================================================================== --- methodheap.py (revision 25163) +++ methodheap.py (working copy) @@ -13,7 +13,8 @@ from mythproto import BEEvent, FileOps, Program from dataheap import Record, Recorded, RecordedProgram, \ OldRecorded, Job, Guide, Video, \ - InternetSource, InternetContentArticles + InternetSource, InternetContentArticles, \ + Song import re @@ -1029,4 +1030,42 @@ except StopIteration: return None +class MythMusic( DBCache ): + """ + Provides convenient methods to access the MythTV MythMusic database. + """ + def __init__(self,db=None): + """ + Initialise the MythDB connection. + """ + DBCache.__init__(self,db) + # check schema version + self._check_schema('MusicDBSchemaVer', + MUSICSCHEMA_VERSION, 'MythMusic') + + @databaseSearch + def searchMusic(self, init=False, key=None, value=None): + """ + obj.searchVideos(**kwargs) -> iterable of Song objects + + Supports the following keywords: + name, track, disc_number, artist, album, year + genre, rating, format, sample_rate, bitrate + """ + if init: + return ('music_songs', Song, (), + ('music_artists','music_songs',('artist_id',)), #1 + ('music_albums', 'music_songs',('album_id',)), #2 + ('music_genres', 'music_songs',('genre_id',))) #4 + if key in ('name','track','disc_number','rating', + 'format','sample_rate','bitrate'): + return ('music_songs.%s=%%s' % key, value, 0) + if key == 'artist': + return ('music_artists.artist_name=%s', value, 1) + if key == 'album': + return ('music_albums.album_name=%s', value, 2) + if key == 'year': + return ('music_albums.year=%s', value, 2) + if key == 'genre': + return ('music_genres.genre=%s', value, 4) Index: connections.py =================================================================== --- connections.py (revision 25163) +++ connections.py (working copy) @@ -14,6 +14,7 @@ from urllib import urlopen from thread import start_new_thread, allocate_lock from time import sleep, time +import traceback import socket import re import weakref @@ -177,12 +178,16 @@ def disconnect(self): if not self.connected: return - self.log(MythLog.SOCKET|MythLog.NETWORK, + try: + self.log(MythLog.SOCKET|MythLog.NETWORK, "Terminating connection to %s:%d" % (self.host, self.port)) - self.backendCommand('DONE',0) - self.connected = False - self.socket.shutdown(1) - self.socket.close() + self.backendCommand('DONE',0) + self.connected = False + self.socket.shutdown(1) + self.socket.close() + except: + traceback.print_exc() + raise def reconnect(self, force=False): # compute new connection options Index: static.py =================================================================== --- static.py (revision 25163) +++ static.py (working copy) @@ -4,10 +4,11 @@ Contains any static and global variables for MythTV Python Bindings """ -OWN_VERSION = (0,23,0,12) +OWN_VERSION = (0,23,0,13) SCHEMA_VERSION = 1259 MVSCHEMA_VERSION = 1036 NVSCHEMA_VERSION = 1007 +MUSICSCHEMA_VERSION = 1017 PROTO_VERSION = 57 BACKEND_SEP = '[]:[]'