Essentially, my goal was to create a plugin for an audio player called Exaile, that would allow management of mp3 players that used Media Transfer Protocol (MTP) within the application. This meant utilizing Exaile's built-in plugin system as well as some third party libraries to allow communication with MTP devices.
The Media Transfer Protocol was developed by Microsoft primarily as a way to facilitate driverless use of mp3 players in Windows. The protocol became very popular, being utilized in products developed by Creative, Microsoft, Samsung, and iRiver, among others. In Linux, support for MTP devices at the driver level is implemented in an open-source library called libmtp. MTP isn't an actual filesystem, and there is no real directory structure. For this reason, a connected MTP device isn't mounted as a harddisk, and there is no traditional file path associated with files on the device. Instead, each file and folder is assigned a unique id, and also stores the id of its parent. So while it is easy to start at the bottom of a path tree and work up it towards the root directory, starting from the root directory and working downward is a bit of a challenge.
Exaile is an audio player, written primarily for Linux, although a Windows version exists. Exaile is written in python, and uses GTK+ as a UI. Exaile also implements a plugin system that makes developing device plugins fairly simple, typically only requiring the plugin developer to create a single file that gets loaded by Exaile's plugin manager at start up. Once loaded, the plugin can be used by selecting the plugin's name in Exaile's built-in device panel.
The first step in my implementation was to find a way to actually use the libmtp API in a python environment. Because libmtp is written in C, there's no way for Exaile to directly call it's functions. I was able to find a set of python bindings for libmtp, called pymtp, that made making these calls possible. Pymtp works by using a ctypes library, which allows python to declare variables and parameters as valid C-types, as well as load C libraries into a python environment. It then implements libmtp API functions in python, as shown in the example from pymtp.py below, which demonstrates declaring a C data structure in python, declaring the return type of a C-library function in python, and actually defining a python method to wrap a the C-function.
LIBMTP_Playlist._fields_ = [("playlist_id", ctypes.c_uint32),
("name", ctypes.c_char_p),
("tracks", ctypes.POINTER(ctypes.c_uint32)),
("no_tracks", ctypes.c_uint32),
("next", ctypes.POINTER(LIBMTP_Playlist))]
...
_libmtp.LIBMTP_Get_Playlist.restype = ctypes.POINTER(LIBMTP_Playlist)
...
def get_playlists(self):
"""
Returns a tuple filled with L{LIBMTP_Playlist} objects
from the connected device.
The main gotcha of this function is that the tracks
variable of LIBMTP_Playlist isn't iterable (without
segfaults), so, you have to iterate over the no_tracks
(through range or xrange) and access it that way (i.e.
tracks[track_id]). Kind of sucks.
@rtype: tuple
@return: Tuple filled with LIBMTP_Playlist objects
"""
if (self.device == None):
raise NotConnected
playlists = self.mtp.LIBMTP_Get_Playlist_List(self.device)
ret = []
next = playlists
while next:
ret.append(next.contents)
if (next.contents.next == None):
break
next = next.contents.next
return ret
The next step was to familiarize myself with the types of data structures being used by both libmtp/pymtp and Exaile. A lot of the work the plugin has to do is converting libmtp structures to Exaile structures and vice versa, so it was important to have a good understanding of they work. The important data structures are as follows:
Actually creating the plugin in Exaile was a long trial and error process. I was able to get a basic skeleton structure of what a device plugin should look like by examining already existing device plugins for the iPod and Mass Storage Devices, but they differed from mtp in several very important ways (that will be explained later) which made them of minimal use, aside from providing an idea of what function names were required by Exaile to accomplish certain tasks. For example, transferring a track to the device is done by a function called put_item, which receives an media.Track object to transfer. It is then the job of the individual plugin to take that object and transfer it to the type of device the plugin is being implemented for. So my plugin must convert the media.Track object into an MTPTrack object, so it can be displayed and recognized in Exaile, and also converted to a LIBMTP_Track object, so it can be physically transferred to the device. See the code example below.
def put_item(self, item):
'''
Transfers a track to the MTP device
'''
track = item.track
metadata = pymtp.LIBMTP_Track()
self.transfer_to_root = self.exaile.settings.get_str("transfer_to_root",
plugin = plugins.name(__file__),
default="True")
# Load the metadata from the track into a LIBMTP_Track for transfer
if (hasattr(track, 'artist')):
metadata.artist = track.artist
if (hasattr(track, 'title')):
metadata.title = track.title
if (hasattr(track, 'album')):
metadata.album = track.album
if (hasattr(track, 'track')):
metadata.tracknumber = track.track
if (hasattr(track, 'date')):
metadata.date = track.date
if (hasattr(track, 'genre')):
metadata.genre = track.genre
if (hasattr(track, '_len')):
# Convert length to milliseconds so the MTP device
# will display it properly
metadata.duration = track.get_duration() * 1000
song = MTPTrack(track.title, track.artist, track.album, track.track,
track.date, metadata.duration, track.genre)
# Determine the directory to transfer the track to.
if self.transfer_to_root == True:
parent = 0
else:
parent = self.create_folders_for_transfer(track.artist, track.album)
# Transfer the file
filename = str(track.get_filename())
loc = str(track.get_loc())
track_id = self.mtp.send_track_from_file(loc, filename, metadata, parent)
song.mtp_item_id = track_id
# Assign the track a unique "location". Since there is no path
# associated with an MTP track, we use it's track_id.
song.loc = str(track_id)
gobject.idle_add(self.all.append, song)
self.all_dict[track_id] = song
Similar types of conversions were needed to displaying, editing, and transferring playlists, as well as transferring tracks from the device to the harddisk.
The most difficult thing to deal with was when the data structures that libmtp returned were particularly difficult to use in python. The structure returned when requests the list of all folders on the device was particularly difficult to deal with. Pymtp hadn't implemented this function yet, so I had to write one myself. Ultimately I decided that the best way to represent the list so that it could be use practically and quickly was to create two data structures. One is simply a dict where the key is a folder's unique id, and the value is the LIBMTP_Folder object associated with it. The other is also a dict, but in this case the key is a folder id, and the value is a list of all folder ids that have the key as a parent. This makes finding all the children of a particular folder much faster, which is an important part of transferring tracks to their respective Artist/Album directory.
Another roadblock I ran into was discovering that it isn't possible to play tracks directly from an MTP device. I decided to simulate this feature by transferring the track the user wanted to play to a temporary folder on the local harddisk, then passing Exaile a track object that pointed to the track at the temp location, instead of the track on the device. There are two major downsides to this approach, one is that the transferring process is fairly slow, especially if a user tries to select a whole album, and transferring tracks to a temporary directory takes up space. I was able to get around the latter problem by having the plugin search for transferred tracks in the temporary directory and deleting them when Exaile is closed. The slowdown problem doesn't have an easy solution, since transferring in a separate thread causes some problems with how exaile expects a plugin to react when a track is requested to be added to the current playlist. Finding a good solution would probably require rewriting a sizable amount of Exaile code, so I decided that for now the delay is an acceptable shortcoming. Another issue I ran into while implementing this feature was that Exaile didn't actually allow the plugin to implement its own method of passing a device track to the current playlist. It would just grab the location of the track that was stored in the MTPTrack object and try to load the track from there. Since the location it would find was just the MTP track id, Exaile would return an error. I was able to remedy this by creating a patch that made Exaile check to see if a device plugin implements its own way of passing a track to the playlist, instead of just grabbing the location. The developers of Exaile were very accommodating in applying this patch to their latest development builds. The patch was fairly simple:
Original Code in xl/panels/device.py:
def get_song(self, loc):
return self.all.for_path(loc.replace('device_%s://' % self.driver.name, ''))
Patched code in xl/panels/device.py:
def get_song(self, loc):
song = self.all.for_path(loc.replace('device_%s://' % self.driver.name, ''))
if hasattr(self.driver, 'get_song'):
return self.driver.get_song(song)
else:
return song
Aside from directly working with libmtp/pymtp, the majority of the remaining work was mostly getting the tracks and playlists to display correctly in Exaile, as well as creating right-click menus for MTPTracks, MTPPlaylists, and MTPPlaylistTracks. I was able to use the iPod plugin as a guide to get started in displaying everything properly, although I had to make some alterations because of the differences in the nature of MTP playlists and iPod playlists. In particular, iPod playlists aren't designed to be viewed in the device panel, but instead were imported into a new Exaile playlist. This wasn't a good solution for my plugin because of how long it takes to load tracks from the device to the local disk, so they can be added to a playlist. I decided the best solution was to display the playlist tracks in the device panel itself, and allow the user to drag MTP playlist tracks into Exaile's playlist track by track. This process was fairly time consuming, though not particularly difficult in terms of logic. It was mostly a matter of reading a lot of pygtk documentation and studying Exaile's code, in order to determine what exactly it was expecting and how certain things were handled in general. I had to submit a few small patches to Exaile's developers to get everything displayed the way I wanted. For example, there wasn't a way to display an item in the device panel that wasn't associated with some kind of object like a track or playlist. I submitted a patch that added support for this, enabling me to display a "Playlists" header and "Music Collection" header in the panel, as shown in the image below.
In general, I was very happy with the results of my work. I was able to reach all the implementation goals I laid out in the proposal of my project. However, a few of them can't be included in a public release until the changes I made to pymtp are merged into the main release by its developer. However, a version of the plugin missing features that rely on these changes is already being included in development builds of Exaile. So my plugin should be included by default with its next release. Aside from getting the features missing because pymtp problems included, I still would like to add a few more features to the plugin. My next goal is to try to transfer album art associated with tracks being transfered to the device, when possible. I'd also like to improve the behavior of the plugin in cases where the user is trying to do "strange" things, like add tracks to the device that are already there, or add the same track to a playlist twice. Another possibility is creating a way to display general information about the connected device, like hard drive space, device name, free space, etc.