<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -127,6 +127,10 @@ The following dependencies are optional and enhance Minirok in some way:
     addition to python-dbus. Additionally, you must be running Qt 4.4.0
     or later for the DBus interface to work.)
 
+  * python-psutil - makes the scrobble lock handling more robust
+    Debian and Ubuntu: python-psutil
+    Source: http://code.google.com/p/psutil/
+
 
 Author and license
 ==================</diff>
      <filename>README</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,7 @@
 minirok (2.1-1) UNRELEASED; urgency=low
 
   * debian/control:
+    + add python-psutil to Recommends.
     + bump required Python version to 2.5.
     + ensure json or simplejson are available.
     + drop lastfmsubmitd from Suggests (submissions are done by Minirok</diff>
      <filename>debian/changelog</filename>
    </modified>
    <modified>
      <diff>@@ -14,7 +14,7 @@ Depends: python (&gt;= 2.5),
  python-gst0.10, gstreamer0.10-alsa | gstreamer0.10-audiosink,
  gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-ugly,
  ${misc:Depends}, ${python:Depends}
-Recommends: python-dbus, python-qt4-dbus
+Recommends: python-dbus, python-qt4-dbus, python-psutil
 Suggests: gstreamer0.10-plugins-bad
 Description: a small music player written in Python and inspired by Amarok
  Minirok is a small music player written in Python for the K Desktop</diff>
      <filename>debian/control</filename>
    </modified>
    <modified>
      <diff>@@ -17,11 +17,19 @@ import hashlib
 import httplib
 import urlparse
 import threading
+
 try:
     import json
 except ImportError:
     import simplejson as json
 
+try:
+    import psutil
+except ImportError:
+    _has_psutil = False
+else:
+    _has_psutil = True
+
 from PyQt4 import QtCore
 from PyKDE4 import kdecore
 
@@ -58,6 +66,9 @@ MAX_FAILURES = 3
 MAX_SLEEP_MINUTES = 120
 MAX_TRACKS_AT_ONCE = 50
 
+APPDATA_SCROBBLE = 'scrobble'
+APPDATA_SCROBBLE_LOCK = 'scrobble.lock'
+
 ##
 
 class Submission(object):
@@ -177,6 +188,77 @@ class HandshakeRequest(Request):
 
 ##
 
+class ProcInfo(object):
+
+    def __init__(self, pid=None):
+        if pid is None:
+            pid = os.getpid()
+
+        d = self.data = {}
+
+        if not _has_psutil:
+            d['pid'] = pid
+            d['version'] = '1.0'
+        else:
+            d['pid'] = pid
+            d['version'] = '1.1'
+            try:
+                d['cmdline'] = psutil.Process(pid).cmdline
+            except psutil.error:
+                d['version'] = '1.0'
+
+    def serialize(self):
+        return json.dumps(self.data, indent=4)
+
+    def isRunning(self):
+        if self.data['version'] == '1.0':
+            try:
+                os.kill(self.data['pid'], 0)
+            except OSError, e:
+                return (e.errno != errno.ESRCH) # ESRCH: No such PID
+            else:
+                return True
+
+        elif self.data['version'] == '1.1':
+            try:
+                proc = psutil.Process(self.data['pid'])
+            except psutil.error.NoSuchProcess:
+                return False
+            else:
+                return proc.cmdline == self.data['cmdline']
+
+    @classmethod
+    def load_from_fileobj(cls, fileobj):
+        try:
+            param = json.load(fileobj)
+        except ValueError:
+            return None
+        else:
+            version = param.get('version', None)
+
+            if version == '1.0':
+                keys = ['version', 'pid']
+
+            elif version == '1.1':
+                if _has_psutil:
+                    keys = ['version', 'pid', 'cmdline']
+                else: # Downgrade format
+                    param['version'] = '1.0'
+                    keys = ['version', 'pid']
+
+            else:
+                return None
+
+            obj = cls.__new__(cls)
+            try:
+                obj.data = dict((k, param[k]) for k in keys)
+            except KeyError:
+                return None
+            else:
+                return obj
+
+##
+
 class Scrobbler(QtCore.QObject, threading.Thread):
 
     def __init__(self):
@@ -207,18 +289,58 @@ class Scrobbler(QtCore.QObject, threading.Thread):
         self.apply_preferences() # Connect signals/slots, read user/passwd
 
         appdata = str(kdecore.KGlobal.dirs().saveLocation('appdata'))
-        self.spool = os.path.join(appdata, 'scrobble')
+        do_queue = False
+        self.spool = os.path.join(appdata, APPDATA_SCROBBLE)
 
+        # Spool directory handling: create it if it doesn't exist...
         if not os.path.isdir(self.spool):
             try:
                 os.mkdir(self.spool)
             except OSError, e:
                 minirok.logger.error('could not create scrobbling spool: %s', e)
                 self.spool = None
+
+        # ... else ensure it is readable and writable
         elif not os.access(self.spool, os.R_OK | os.W_OK):
             minirok.logger.error('scrobbling spool is not readable/writable')
             self.spool = None
+
+        # If not, we try to assess whether this Minirok instance should try to
+        # submit the existing entries, if any. Supposedly, the Last.fm server
+        # has some support for detecting duplicate submissions, but we're
+        # adviced not to rely on it (&lt;4A7FECF7.5030100@last.fm&gt;), so we use a
+        # lock file to signal that some Minirok process is taking care of the
+        # submissions from the spool directory. (This scheme, I realize,
+        # doesn't get all corner cases right, but will have to suffice for now.
+        # For example, if Minirok A starts, then Minirok B starts, and finally
+        # Minirok A quits and Minirok C starts, Minirok B and C will end up
+        # both trying to submit B's entries that haven't been able to be
+        # submitted yet. There's also the race-condition-du-jour, of course.)
         else:
+            scrobble_lock = os.path.join(appdata, APPDATA_SCROBBLE_LOCK)
+            try:
+                lockfile = open(scrobble_lock)
+            except IOError, e:
+                if e.errno == errno.ENOENT:
+                    do_queue = True
+                else:
+                    raise
+            else:
+                proc = ProcInfo.load_from_fileobj(lockfile)
+
+                if proc and proc.isRunning():
+                    minirok.logger.info(
+                        'Minirok already running (pid=%d), '
+                        'not scrobbling existing items', proc.data['pid'])
+                else:
+                    do_queue = True
+
+        if do_queue:
+            self.lock_file = scrobble_lock
+
+            with open(self.lock_file, 'w') as lock:
+                lock.write(ProcInfo().serialize())
+
             files = [ os.path.join(self.spool, x)
                         for x in os.listdir(self.spool) ]
             tracks = sorted(
@@ -226,6 +348,17 @@ class Scrobbler(QtCore.QObject, threading.Thread):
                             if t is not None ], key=lambda t: t.start_time)
             if tracks:
                 self.scrobble_queue.extend(tracks)
+        else:
+            self.lock_file = None
+
+        util.CallbackRegistry.register_save_config(self.cleanup)
+
+    def cleanup(self):
+        if self.lock_file is not None:
+            try:
+                os.unlink(self.lock_file)
+            except:
+                pass
 
     def slot_new_track(self):
         self.timer.stop()</diff>
      <filename>minirok/scrobble.py</filename>
    </modified>
    <modified>
      <diff>@@ -196,6 +196,8 @@ def creat_excl(path, mode=0644):
 
 class CallbackRegistry(object):
 
+    # TODO: rename &quot;save_config&quot; to something else, eg. &quot;at_exit&quot;.
+
     SAVE_CONFIG = object()
     APPLY_PREFS = object()
 </diff>
      <filename>minirok/util.py</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>97907d7d0f0fb6c3dc375b38234cab8d2fdaf2cf</id>
    </parent>
  </parents>
  <author>
    <name>Adeodato Sim&#243;</name>
    <email>dato@net.com.org.es</email>
  </author>
  <url>http://github.com/dato/minirok/commit/bad9da29840affd0f5e7a5fef75677deb700c47b</url>
  <id>bad9da29840affd0f5e7a5fef75677deb700c47b</id>
  <committed-date>2009-08-18T06:16:44-07:00</committed-date>
  <authored-date>2009-08-18T06:11:27-07:00</authored-date>
  <message>scrobble.py: introduce some rudimentary locking to the scrobble queue.</message>
  <tree>3ce43dd845b6b2c79fe4374fb0160ea3eee78862</tree>
  <committer>
    <name>Adeodato Sim&#243;</name>
    <email>dato@net.com.org.es</email>
  </committer>
</commit>
