diff options
Diffstat (limited to 'bindings/python')
-rw-r--r-- | bindings/python/Makefile.am | 13 | ||||
-rw-r--r-- | bindings/python/__init__.py | 8 | ||||
-rw-r--r-- | bindings/python/gpod.i.in | 14 | ||||
-rw-r--r-- | bindings/python/gtkpod.py | 23 | ||||
-rw-r--r-- | bindings/python/ipod.py | 239 |
5 files changed, 279 insertions, 18 deletions
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am index e65cf0d..0951303 100644 --- a/bindings/python/Makefile.am +++ b/bindings/python/Makefile.am @@ -4,6 +4,8 @@ EXTRA_DIST = \ README.in \ __init__.py \ gpod.i.in \ + gpod_doc.i.in \ + gtkdoc-to-swig.xsl \ gtkpod.py \ ipod.py @@ -12,6 +14,7 @@ CLEANFILES = \ *.pyo \ _gpod.so \ gpod.py \ + gpod_doc.i \ gpod_wrap.* DISTCLEANFILES = \ @@ -28,7 +31,7 @@ README: README.in gpod.i if HAVE_PYTHON BUILT_SOURCES = gpod_wrap.c -SWIG_INTERFACES = gpod.i +SWIG_INTERFACES = gpod.i gpod_doc.i LIBGPOD_CFLAGS += -fno-strict-aliasing INCLUDES = -I$(top_srcdir)/src @@ -41,6 +44,14 @@ _gpod_la_CPPFLAGS = $(PYTHON_INCLUDES) $(LIBGPOD_CFLAGS) $(INCLUDES) _gpod_la_LDFLAGS = -module -avoid-version _gpod_la_LIBADD = $(LIBGPOD_LIBS) $(top_builddir)/src/libgpod.la +gpod_doc.i: $(srcdir)/gpod_doc.i.in $(srcdir)/gtkdoc-to-swig.xsl + cat $< > $@ +if ENABLE_GTK_DOC + -for xml in $(top_srcdir)/docs/reference/xml/*.xml; do \ + xsltproc $(srcdir)/gtkdoc-to-swig.xsl $$xml; \ + done >> $@ +endif + gpod_wrap.c: $(SWIG_INTERFACES) $(nodist_gpod_PYTHON) $(SWIG) -python $(INCLUDES) -o $@ $< diff --git a/bindings/python/__init__.py b/bindings/python/__init__.py index 05a8159..b0640cf 100644 --- a/bindings/python/__init__.py +++ b/bindings/python/__init__.py @@ -1,6 +1,12 @@ +"""Manage tracks and playlists on an iPod. + +The gpod module allows you to add and remove tracks, create and edit +playlists, and other iPod tasks. + +""" + from gpod import * from ipod import * __all__ = ["DatabaseException", "TrackException", "Database","Track","Playlist"] - diff --git a/bindings/python/gpod.i.in b/bindings/python/gpod.i.in index ef6e65a..a908e02 100644 --- a/bindings/python/gpod.i.in +++ b/bindings/python/gpod.i.in @@ -28,7 +28,17 @@ Please send any fixes, improvements or suggestions to */ -%module gpod +%define DOCSTRING +"This module gives access to an iPod's content. It provides an easy to +use API to retrieve the list of files and playlists stored on an iPod, +modify them, and save them back to the iPod. + +This module implements the libgpod C API as directly as possible in +Python. See the main gpod module for a more traditional Python +interface." +%enddef + +%module(docstring=DOCSTRING) gpod %{ #include "db-artwork-debug.h" #include "db-artwork-parser.h" @@ -163,6 +173,8 @@ PyObject* sw_get_playlists(Itdb_iTunesDB *itdb) { %} +%include "gpod_doc.i" + # be nicer to decode these utf8 strings into Unicode objects in the C # layer. Here we are leaving it to the Python side, and just giving # them utf8 encoded Strings. diff --git a/bindings/python/gtkpod.py b/bindings/python/gtkpod.py index 9eebf36..54a68dc 100644 --- a/bindings/python/gtkpod.py +++ b/bindings/python/gtkpod.py @@ -1,3 +1,5 @@ +"""Read and write Gtkpod extended info files.""" + import sha import os import socket @@ -9,12 +11,15 @@ import socket hostname = socket.gethostname() class ParseError(Exception): + """Exception for parse errors.""" pass class SyncError(Exception): + """Exception for sync errors.""" pass def sha1_hash(filename): + """Return an SHA1 hash on the first 16k of a file.""" import struct # only hash the first 16k hash_len = 4*4096 @@ -25,6 +30,15 @@ def sha1_hash(filename): return hash.hexdigest() def write(filename, db, itunesdb_file): + """Save extended info to a file. + + db is a gpod.Database instance + + Extended info is written for the iTunesDB specified in + itunesdb_file + + """ + file = open(filename, "w") def write_pair(name, value): @@ -53,6 +67,15 @@ def write(filename, db, itunesdb_file): write_pair("id", "xxx") def parse(filename, db, itunesdb_file=None): + """Load extended info from a file. + + db is a gpod.Database instance + + If itunesdb_file is set and it's hash is valid some expensive + checks are skipped. + + """ + tracks_by_id = {} tracks_by_sha = {} id = 0 diff --git a/bindings/python/ipod.py b/bindings/python/ipod.py index a3ac1c0..4c0d14a 100644 --- a/bindings/python/ipod.py +++ b/bindings/python/ipod.py @@ -1,6 +1,8 @@ -"""Documentation for this module. +"""Manage tracks and playlists on an iPod. + +You should normally just import the gpod module which will import the +classes and methods provided here. -More details. """ import gpod @@ -10,13 +12,41 @@ import gtkpod import os class DatabaseException(RuntimeError): + """Exception for database errors.""" pass class TrackException(RuntimeError): + """Exception for track errors.""" pass class Database: + """An iTunes database. + + A database contains playlists and tracks. + + """ + def __init__(self, mountpoint="/mnt/ipod", local=False, localdb=None): + """Create a Database object. + + You can create the object from a mounted iPod or from a local + database file. + + To use a mounted iPod: + + db = gpod.Database('/mnt/ipod') + + To use a local database file: + + db = gpod.Database(localdb='/path/to/iTunesDB') + + If you specify local=True then the default local database from + gtkpod will be used (~/.gtkpod/local_0.itdb): + + db = gpod.Database(local=True) + + """ + if local or localdb: if localdb: self._itdb_file = localdb @@ -37,7 +67,7 @@ class Database: def __str__(self): return self.__repr__() - + def __repr__(self): return "<Database Filename:%s Playlists:%s Tracks:%s>" % ( repr(self._itdb.filename), @@ -45,12 +75,21 @@ class Database: len(self)) def _load_gtkpod_extended_info(self): + """Read extended information from a gtkpod .ext file.""" itdbext_file = "%s.ext" % (self._itdb_file) if os.path.exists(self._itdb_file) and os.path.exists(itdbext_file): gtkpod.parse(itdbext_file, self, self._itdb_file) def close(self): + """Save and close the database. + + Note: If you are working with an iPod you should normally call + copy_delayed_files() prior to close() to ensure that newly + added or updated tracks are transferred to the iPod. + + """ + if not gpod.itdb_write_file(self._itdb, self._itdb_file, None): raise DatabaseException("Unable to save iTunes database %s" % self) itdbext_file = "%s.ext" % (self._itdb_file) @@ -68,6 +107,11 @@ class Database: return gpod.sw_get_list_len(self._itdb.tracks) def import_file(self, filename): + """Import a file. + + filename is added to the end of the master playlist. + + """ track = Track(filename) track.copy_to_ipod() gpod.itdb_playlist_add_track(gpod.itdb_playlist_mpl(self._itdb), @@ -77,9 +121,26 @@ class Database: gpod.itdb_free(self._itdb) def add(self, track, pos=-1): + """Add a track to a database. + + If pos is set the track will be inserted at that position. By + default the track will be added at the end of the database. + + """ + gpod.itdb_track_add(self._itdb, track._track, pos) def remove(self, item, harddisk=False, ipod=True): + """Remove a playlist or track from a database. + + item is either a playlist or track object. + + If harddisk is True the item will be removed from the the hard drive. + + If ipod is True the item will be removed from the iPod. + + """ + if isinstance(item, Playlist): if ipod or harddisk: # remove all the tracks that were in this playlist @@ -109,12 +170,15 @@ class Database: raise DatabaseException("Unable to remove a %s from database" % type(item)) def get_master(self): + """Get the Master playlist.""" return Playlist(self,proxied_playlist=gpod.itdb_playlist_mpl(self._itdb)) def get_podcasts(self): + """Get the podcasts playlist.""" return Playlist(self,proxied_playlist=gpod.itdb_playlist_podcasts(self._itdb)) def get_playlists(self): + """Get all playlists.""" return _Playlists(self) Master = property(get_master) @@ -122,12 +186,25 @@ class Database: Playlists= property(get_playlists) def smart_update(self): + """Update all smart playlists.""" gpod.itdb_spl_update_all(self._itdb) def new_Playlist(self,*args,**kwargs): + """Create a new Playlist. + + See Playlist.__init__() for details. + + """ + return Playlist(self, *args,**kwargs) def new_Track(self,**kwargs): + """Create a new Track. + + See Track.__init__() for details. + + """ + track = Track(**kwargs) self.add(track) if kwargs.has_key('podcast') and kwargs['podcast'] == True: @@ -137,6 +214,19 @@ class Database: return track def copy_delayed_files(self,callback=False): + """Copy files not marked as transferred to the iPod. + + callback is an optional function that will be called for each + track that is copied. It will be passed the following + arguments: + + database -> the database object + track -> the track being copied + iterator -> the current track number being copied + total -> the total tracks to be copied + + """ + if not gpod.itdb_get_mountpoint(self._itdb): # we're not working with a real ipod. return @@ -157,10 +247,22 @@ class Database: track.copy_to_ipod() class Track: + """A track in an iTunes database. + + A track contains information like the artist, title, album, etc. + It also contains data like the location on the iPod, whether there + is artwork stored, the track has lyrics, etc. + + Information from a gtkpod extended info file (if one exists for + the iTunesDB) is also available in Track['userdata']. + + The information is stored in a dictionary. + + """ + # Note we don't free the underlying structure, as it's still used # by the itdb. - - + _proxied_attributes = ("title","ipod_path","album","artist","genre","filetype", "comment","category","composer","grouping","description", "podcasturl","podcastrss","chapterdata","subtitle","id", @@ -179,6 +281,20 @@ class Track: def __init__(self, filename=None, from_file=None, proxied_track=None, podcast=False): + """Create a Track object. + + If from_file or filename is set, the file specified will be + used to create the track. + + If proxied_track is set, it is expected to be an Itdb_Track + object. + + If podcast is True then the track will be setup as a Podcast, + unless proxied_track is set. + + """ + + # XXX couldn't from_file and filename be merged? if from_file: filename = from_file if filename: @@ -218,9 +334,10 @@ class Track: self._track = proxied_track else: self._track = gpod.itdb_track_new() - self.set_podcast(podcast) + self.set_podcast(podcast) def copy_to_ipod(self): + """Copy the track to the iPod.""" self['userdata']['md5_hash'] = gtkpod.sha1_hash( self['userdata']['filename_locale']) if not gpod.itdb_get_mountpoint(self._track.itdb): @@ -234,21 +351,37 @@ class Track: return True def ipod_filename(self): + """Get the full path to the track on the iPod. + + Note (from the libgpod documentation): This code works around + a problem on some systems and might return a filename with + different case than the original filename. Don't copy it back + to track unless you must. + + """ + return gpod.itdb_filename_on_ipod(self._track) def set_podcast(self, value): + """Mark the track as a podcast. + + If value is True flags appropriate for podcasts are set, + otherwise those flags are unset. + + """ + if value: self['skip_when_shuffling'] = 0x01 self['remember_playback_position'] = 0x01 - self['flag4'] = 0x01 # Show Title/Album on the 'Now Playing' page + self['flag4'] = 0x01 # Show Title/Album on the 'Now Playing' page else: self['skip_when_shuffling'] = 0x00 self['remember_playback_position'] = 0x00 - self['flag4'] = 0x00 # Show Title/Album/Artist on the 'New Playing' page + self['flag4'] = 0x00 # Show Title/Album/Artist on the 'Now Playing' page def __str__(self): return self.__repr__() - + def __repr__(self): return "<Track Artist:%s Title:%s Album:%s>" % ( repr(self['artist']), @@ -284,6 +417,8 @@ class Track: else: raise KeyError('No such key: %s' % item) +# XXX would this be better as a public variable so that the docs would +# list valid sort order values? _playlist_sorting = { 1:'playlist', 2:'unknown2', @@ -323,7 +458,7 @@ class _Playlists: def __nonzero__(self): return True - + def __getitem__(self, index): if type(index) == types.SliceType: return [self[i] for i in xrange(*index.indices(len(self)))] @@ -336,7 +471,7 @@ class _Playlists: def __repr__(self): return "<Playlists from %s>" % self._db - + def __call__(self, id=None, number=None, name=None): if ((id and (number or name)) or (number and name)): raise ValueError("Only specify id, number OR name") @@ -373,10 +508,30 @@ class _Playlists: proxied_playlist=pl) else: raise KeyError("Playlist with number %s not found." % repr(number)) - + class Playlist: + """A playlist in an iTunes database.""" + def __init__(self, parent_db, title="New Playlist", smart=False, pos=-1, proxied_playlist=None): + """Create a playlist object. + + parent_db is the database object to which the playlist + belongs. + + title is a string that provides a name for the playlist. + + If smart is true the playlist will be a smart playlist. + + If pos is set the track will be inserted at that position. By + default the playlist will be added at the end of the database. + + If proxied_playlist is set it is expected to be an + Itdb_Playlist object (as returned by gpod.sw_get_playlist() or + similar functions). + + """ + self._db = parent_db if proxied_playlist: self._pl = proxied_playlist @@ -389,32 +544,71 @@ class Playlist: gpod.itdb_playlist_add(self._db._itdb, self._pl, pos) def smart_update(self): + """Update the content of the smart playlist.""" gpod.itdb_spl_update(self._pl) def randomize(self): + """Randomizes the playlist.""" gpod.itdb_playlist_randomize(self._pl) def get_name(self): + """Get the name of the playlist.""" return self._pl.name + def set_name(self, name): + """Set the name for the playlist.""" self._pl.name = name + def get_id(self): + """Get the id of the playlist.""" return self._pl.id + + # XXX would this be more aptly named is_smart? If it was, I think + # the docstring could skip the explanation of the return value. + # (Same question for get_master and get_podcast.) def get_smart(self): + """Check if the playlist is smart or not. + + Returns True for a smart playlist, False for a regular + playlist. + + """ + if self._pl.is_spl == 1: return True return False + def get_master(self): + """Check if the playlist is the master playlist (MPL). + + Returns True if the playlist is the MPL, False if not. + + """ + if gpod.itdb_playlist_is_mpl(self._pl) == 1: return True return False + def get_podcast(self): + """Check if the playlist is the podcasts playlist. + + Returns True if the playlist is the podcasts playlist, False + if not. + + """ + if gpod.itdb_playlist_is_podcasts(self._pl) == 1: return True return False + def get_sort(self): + """Get the sort order for the playlist.""" return _playlist_sorting[self._pl.sortorder] + + # XXX how does one find out what values are allowed for order without + # poking into this file? (See similar question @ _playlist_order) def set_sort(self, order): + """Set the sort order for the playlist.""" order = order.lower() for k, v in _playlist_sorting.items(): if v == order: @@ -431,7 +625,7 @@ class Playlist: def __str__(self): return self.__repr__() - + def __repr__(self): return "<Playlist Title:%s Sort:%s Smart:%s Master:%s Podcast:%s Tracks:%d>" % ( repr(self.name), @@ -440,7 +634,7 @@ class Playlist: repr(self.master), repr(self.podcast), len(self)) - + def __getitem__(self, index): if type(index) == types.SliceType: return [self[i] for i in xrange(*index.indices(len(self)))] @@ -461,11 +655,26 @@ class Playlist: return True else: return False - + def add(self, track, pos=-1): + """Add a track to the playlist. + + track is a track object to add. + + If pos is set the track will be inserted at that position. By + default the track will be added at the end of the playlist. + + """ + gpod.itdb_playlist_add_track(self._pl, track._track, pos) def remove(self, track): + """Remove a track from the playlist. + + track is a track object to remove. + + """ + if self.__contains__(track): gpod.itdb_playlist_remove_track(self._pl, track._track) else: |