We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
mpdstats: last_played is documented but not implemented
As pointed out [on the mailing list](https://groups.google.com/d/msg/beets-users/VW0pxtCVZG4/sq9gGsNS9zEJ), the mpdstats plugin (paging @pscn and @kljohann) does not seem to set the `last_played` field, even though the field is described in [the plugin's docs](http://beets.readthedocs.org/en/v1.3.2/plugins/mpdstats.html). Grepping in mpdstats.py for "last_played" shows that doesn't seem to be implemented. We should probably either add it to the plugin or remove it from the docs.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `beetsplug/mpdstats.py`
Content:
```
1 # coding=utf-8
2 # This file is part of beets.
3 # Copyright 2013, Peter Schnebel and Johann Klähn.
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 
16 import logging
17 import mpd
18 import socket
19 import select
20 import time
21 import os
22 
23 from beets import ui
24 from beets import config
25 from beets import plugins
26 from beets import library
27 from beets.util import displayable_path
28 
29 log = logging.getLogger('beets')
30 
31 # If we lose the connection, how many times do we want to retry and how
32 # much time should we wait between retries?
33 RETRIES = 10
34 RETRY_INTERVAL = 5
35 
36 
37 def is_url(path):
38     """Try to determine if the path is an URL.
39     """
40     return path.split('://', 1)[0] in ['http', 'https']
41 
42 
43 # Use the MPDClient internals to get unicode.
44 # see http://www.tarmack.eu/code/mpdunicode.py for the general idea
45 class MPDClient(mpd.MPDClient):
46     def _write_command(self, command, args=[]):
47         args = [unicode(arg).encode('utf-8') for arg in args]
48         super(MPDClient, self)._write_command(command, args)
49 
50     def _read_line(self):
51         line = super(MPDClient, self)._read_line()
52         if line is not None:
53             return line.decode('utf-8')
54         return None
55 
56 
57 class MPDClientWrapper(object):
58     def __init__(self):
59         self.music_directory = (
60             config['mpdstats']['music_directory'].get(unicode))
61 
62         self.client = MPDClient()
63 
64     def connect(self):
65         """Connect to the MPD.
66         """
67         host = config['mpd']['host'].get(unicode)
68         port = config['mpd']['port'].get(int)
69 
70         if host[0] in ['/', '~']:
71             host = os.path.expanduser(host)
72 
73         log.info(u'mpdstats: connecting to {0}:{1}'.format(host, port))
74         try:
75             self.client.connect(host, port)
76         except socket.error as e:
77             raise ui.UserError('could not connect to MPD: {0}'.format(e))
78 
79         password = config['mpd']['password'].get(unicode)
80         if password:
81             try:
82                 self.client.password(password)
83             except mpd.CommandError as e:
84                 raise ui.UserError(
85                     'could not authenticate to MPD: {0}'.format(e)
86                 )
87 
88     def disconnect(self):
89         """Disconnect from the MPD.
90         """
91         self.client.close()
92         self.client.disconnect()
93 
94     def get(self, command, retries=RETRIES):
95         """Wrapper for requests to the MPD server. Tries to re-connect if the
96         connection was lost (f.ex. during MPD's library refresh).
97         """
98         try:
99             return getattr(self.client, command)()
100         except (select.error, mpd.ConnectionError) as err:
101             log.error(u'mpdstats: {0}'.format(err))
102 
103         if retries <= 0:
104             # if we exited without breaking, we couldn't reconnect in time :(
105             raise ui.UserError(u'communication with MPD server failed')
106 
107         time.sleep(RETRY_INTERVAL)
108 
109         try:
110             self.disconnect()
111         except mpd.ConnectionError:
112             pass
113 
114         self.connect()
115         return self.get(command, retries=retries - 1)
116 
117     def playlist(self):
118         """Return the currently active playlist.  Prefixes paths with the
119         music_directory, to get the absolute path.
120         """
121         result = {}
122         for entry in self.get('playlistinfo'):
123             if not is_url(entry['file']):
124                 result[entry['id']] = os.path.join(
125                     self.music_directory, entry['file'])
126             else:
127                 result[entry['id']] = entry['file']
128         return result
129 
130     def status(self):
131         """Return the current status of the MPD.
132         """
133         return self.get('status')
134 
135     def events(self):
136         """Return list of events. This may block a long time while waiting for
137         an answer from MPD.
138         """
139         return self.get('idle')
140 
141 
142 class MPDStats(object):
143     def __init__(self, lib):
144         self.lib = lib
145 
146         self.do_rating = config['mpdstats']['rating'].get(bool)
147         self.rating_mix = config['mpdstats']['rating_mix'].get(float)
148         self.time_threshold = 10.0  # TODO: maybe add config option?
149 
150         self.now_playing = None
151         self.mpd = MPDClientWrapper()
152 
153     def rating(self, play_count, skip_count, rating, skipped):
154         """Calculate a new rating for a song based on play count, skip count,
155         old rating and the fact if it was skipped or not.
156         """
157         if skipped:
158             rolling = (rating - rating / 2.0)
159         else:
160             rolling = (rating + (1.0 - rating) / 2.0)
161         stable = (play_count + 1.0) / (play_count + skip_count + 2.0)
162         return (self.rating_mix * stable
163                 + (1.0 - self.rating_mix) * rolling)
164 
165     def get_item(self, path):
166         """Return the beets item related to path.
167         """
168         query = library.PathQuery('path', path)
169         item = self.lib.items(query).get()
170         if item:
171             return item
172         else:
173             log.info(u'mpdstats: item not found: {0}'.format(
174                 displayable_path(path)
175             ))
176 
177     @staticmethod
178     def update_item(item, attribute, value=None, increment=None):
179         """Update the beets item. Set attribute to value or increment the value
180         of attribute. If the increment argument is used the value is cast to the
181         corresponding type.
182         """
183         if item is None:
184             return
185 
186         if increment is not None:
187             item.load()
188             value = type(increment)(item.get(attribute, 0)) + increment
189 
190         if value is not None:
191             item[attribute] = value
192             item.store()
193 
194             log.debug(u'mpdstats: updated: {0} = {1} [{2}]'.format(
195                 attribute,
196                 item[attribute],
197                 displayable_path(item.path),
198             ))
199 
200     def update_rating(self, item, skipped):
201         """Update the rating for a beets item.
202         """
203         item.load()
204         rating = self.rating(
205             int(item.get('play_count', 0)),
206             int(item.get('skip_count', 0)),
207             float(item.get('rating', 0.5)),
208             skipped)
209 
210         self.update_item(item, 'rating', rating)
211 
212     def handle_song_change(self, song):
213         """Determine if a song was skipped or not and update its attributes.
214         To this end the difference between the song's supposed end time
215         and the current time is calculated. If it's greater than a threshold,
216         the song is considered skipped.
217         """
218         diff = abs(song['remaining'] - (time.time() - song['started']))
219 
220         skipped = diff >= self.time_threshold
221 
222         if skipped:
223             self.handle_skipped(song)
224         else:
225             self.handle_played(song)
226 
227         if self.do_rating:
228             self.update_rating(song['beets_item'], skipped)
229 
230     def handle_played(self, song):
231         """Updates the play count of a song.
232         """
233         self.update_item(song['beets_item'], 'play_count', increment=1)
234         log.info(u'mpdstats: played {0}'.format(
235             displayable_path(song['path'])
236         ))
237 
238     def handle_skipped(self, song):
239         """Updates the skip count of a song.
240         """
241         self.update_item(song['beets_item'], 'skip_count', increment=1)
242         log.info(u'mpdstats: skipped {0}'.format(
243             displayable_path(song['path'])
244         ))
245 
246     def on_stop(self, status):
247         log.info(u'mpdstats: stop')
248         self.now_playing = None
249 
250     def on_pause(self, status):
251         log.info(u'mpdstats: pause')
252         self.now_playing = None
253 
254     def on_play(self, status):
255         playlist = self.mpd.playlist()
256         path = playlist.get(status['songid'])
257 
258         if not path:
259             return
260 
261         if is_url(path):
262             log.info(u'mpdstats: playing stream {0}'.format(
263                 displayable_path(path)
264             ))
265             return
266 
267         played, duration = map(int, status['time'].split(':', 1))
268         remaining = duration - played
269 
270         if self.now_playing and self.now_playing['path'] != path:
271             self.handle_song_change(self.now_playing)
272 
273         log.info(u'mpdstats: playing {0}'.format(
274             displayable_path(path)
275         ))
276 
277         self.now_playing = {
278             'started':    time.time(),
279             'remaining':  remaining,
280             'path':       path,
281             'beets_item': self.get_item(path),
282         }
283 
284     def run(self):
285         self.mpd.connect()
286         events = ['player']
287 
288         while True:
289             if 'player' in events:
290                 status = self.mpd.status()
291 
292                 handler = getattr(self, 'on_' + status['state'], None)
293 
294                 if handler:
295                     handler(status)
296                 else:
297                     log.debug(u'mpdstats: unhandled status "{0}"'.format(status))
298 
299             events = self.mpd.events()
300 
301 
302 class MPDStatsPlugin(plugins.BeetsPlugin):
303     def __init__(self):
304         super(MPDStatsPlugin, self).__init__()
305         self.config.add({
306             'music_directory': config['directory'].as_filename(),
307             'rating':          True,
308             'rating_mix':      0.75,
309         })
310         config['mpd'].add({
311             'host':            u'localhost',
312             'port':            6600,
313             'password':        u'',
314         })
315 
316     def commands(self):
317         cmd = ui.Subcommand(
318             'mpdstats',
319             help='run a MPD client to gather play statistics')
320         cmd.parser.add_option(
321             '--host', dest='host', type='string',
322             help='set the hostname of the server to connect to')
323         cmd.parser.add_option(
324             '--port', dest='port', type='int',
325             help='set the port of the MPD server to connect to')
326         cmd.parser.add_option(
327             '--password', dest='password', type='string',
328             help='set the password of the MPD server to connect to')
329 
330         def func(lib, opts, args):
331             self.config.set_args(opts)
332 
333             # Overrides for MPD settings.
334             if opts.host:
335                 config['mpd']['host'] = opts.host.decode('utf8')
336             if opts.port:
337                 config['mpd']['host'] = int(opts.port)
338             if opts.password:
339                 config['mpd']['password'] = opts.password.decode('utf8')
340 
341             try:
342                 MPDStats(lib).run()
343             except KeyboardInterrupt:
344                 pass
345 
346         cmd.func = func
347         return [cmd]
348
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py
--- a/beetsplug/mpdstats.py
+++ b/beetsplug/mpdstats.py
@@ -281,6 +281,9 @@
             'beets_item': self.get_item(path),
         }
 
+        self.update_item(self.now_playing['beets_item'],
+                         'last_played', value=int(time.time()))
+
     def run(self):
         self.mpd.connect()
         events = ['player']

{"golden_diff": "diff --git a/beetsplug/mpdstats.py b/beetsplug/mpdstats.py\n--- a/beetsplug/mpdstats.py\n+++ b/beetsplug/mpdstats.py\n@@ -281,6 +281,9 @@\n             'beets_item': self.get_item(path),\n         }\n \n+        self.update_item(self.now_playing['beets_item'],\n+                         'last_played', value=int(time.time()))\n+\n     def run(self):\n         self.mpd.connect()\n         events = ['player']\n", "issue": "mpdstats: last_played is documented but not implemented\nAs pointed out [on the mailing list](https://groups.google.com/d/msg/beets-users/VW0pxtCVZG4/sq9gGsNS9zEJ), the mpdstats plugin (paging @pscn and @kljohann) does not seem to set the `last_played` field, even though the field is described in [the plugin's docs](http://beets.readthedocs.org/en/v1.3.2/plugins/mpdstats.html). Grepping in mpdstats.py for \"last_played\" shows that doesn't seem to be implemented. We should probably either add it to the plugin or remove it from the docs.\n\n", "before_files": [{"content": "# coding=utf-8\n# This file is part of beets.\n# Copyright 2013, Peter Schnebel and Johann Kl\u00e4hn.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nimport logging\nimport mpd\nimport socket\nimport select\nimport time\nimport os\n\nfrom beets import ui\nfrom beets import config\nfrom beets import plugins\nfrom beets import library\nfrom beets.util import displayable_path\n\nlog = logging.getLogger('beets')\n\n# If we lose the connection, how many times do we want to retry and how\n# much time should we wait between retries?\nRETRIES = 10\nRETRY_INTERVAL = 5\n\n\ndef is_url(path):\n    \"\"\"Try to determine if the path is an URL.\n    \"\"\"\n    return path.split('://', 1)[0] in ['http', 'https']\n\n\n# Use the MPDClient internals to get unicode.\n# see http://www.tarmack.eu/code/mpdunicode.py for the general idea\nclass MPDClient(mpd.MPDClient):\n    def _write_command(self, command, args=[]):\n        args = [unicode(arg).encode('utf-8') for arg in args]\n        super(MPDClient, self)._write_command(command, args)\n\n    def _read_line(self):\n        line = super(MPDClient, self)._read_line()\n        if line is not None:\n            return line.decode('utf-8')\n        return None\n\n\nclass MPDClientWrapper(object):\n    def __init__(self):\n        self.music_directory = (\n            config['mpdstats']['music_directory'].get(unicode))\n\n        self.client = MPDClient()\n\n    def connect(self):\n        \"\"\"Connect to the MPD.\n        \"\"\"\n        host = config['mpd']['host'].get(unicode)\n        port = config['mpd']['port'].get(int)\n\n        if host[0] in ['/', '~']:\n            host = os.path.expanduser(host)\n\n        log.info(u'mpdstats: connecting to {0}:{1}'.format(host, port))\n        try:\n            self.client.connect(host, port)\n        except socket.error as e:\n            raise ui.UserError('could not connect to MPD: {0}'.format(e))\n\n        password = config['mpd']['password'].get(unicode)\n        if password:\n            try:\n                self.client.password(password)\n            except mpd.CommandError as e:\n                raise ui.UserError(\n                    'could not authenticate to MPD: {0}'.format(e)\n                )\n\n    def disconnect(self):\n        \"\"\"Disconnect from the MPD.\n        \"\"\"\n        self.client.close()\n        self.client.disconnect()\n\n    def get(self, command, retries=RETRIES):\n        \"\"\"Wrapper for requests to the MPD server. Tries to re-connect if the\n        connection was lost (f.ex. during MPD's library refresh).\n        \"\"\"\n        try:\n            return getattr(self.client, command)()\n        except (select.error, mpd.ConnectionError) as err:\n            log.error(u'mpdstats: {0}'.format(err))\n\n        if retries <= 0:\n            # if we exited without breaking, we couldn't reconnect in time :(\n            raise ui.UserError(u'communication with MPD server failed')\n\n        time.sleep(RETRY_INTERVAL)\n\n        try:\n            self.disconnect()\n        except mpd.ConnectionError:\n            pass\n\n        self.connect()\n        return self.get(command, retries=retries - 1)\n\n    def playlist(self):\n        \"\"\"Return the currently active playlist.  Prefixes paths with the\n        music_directory, to get the absolute path.\n        \"\"\"\n        result = {}\n        for entry in self.get('playlistinfo'):\n            if not is_url(entry['file']):\n                result[entry['id']] = os.path.join(\n                    self.music_directory, entry['file'])\n            else:\n                result[entry['id']] = entry['file']\n        return result\n\n    def status(self):\n        \"\"\"Return the current status of the MPD.\n        \"\"\"\n        return self.get('status')\n\n    def events(self):\n        \"\"\"Return list of events. This may block a long time while waiting for\n        an answer from MPD.\n        \"\"\"\n        return self.get('idle')\n\n\nclass MPDStats(object):\n    def __init__(self, lib):\n        self.lib = lib\n\n        self.do_rating = config['mpdstats']['rating'].get(bool)\n        self.rating_mix = config['mpdstats']['rating_mix'].get(float)\n        self.time_threshold = 10.0  # TODO: maybe add config option?\n\n        self.now_playing = None\n        self.mpd = MPDClientWrapper()\n\n    def rating(self, play_count, skip_count, rating, skipped):\n        \"\"\"Calculate a new rating for a song based on play count, skip count,\n        old rating and the fact if it was skipped or not.\n        \"\"\"\n        if skipped:\n            rolling = (rating - rating / 2.0)\n        else:\n            rolling = (rating + (1.0 - rating) / 2.0)\n        stable = (play_count + 1.0) / (play_count + skip_count + 2.0)\n        return (self.rating_mix * stable\n                + (1.0 - self.rating_mix) * rolling)\n\n    def get_item(self, path):\n        \"\"\"Return the beets item related to path.\n        \"\"\"\n        query = library.PathQuery('path', path)\n        item = self.lib.items(query).get()\n        if item:\n            return item\n        else:\n            log.info(u'mpdstats: item not found: {0}'.format(\n                displayable_path(path)\n            ))\n\n    @staticmethod\n    def update_item(item, attribute, value=None, increment=None):\n        \"\"\"Update the beets item. Set attribute to value or increment the value\n        of attribute. If the increment argument is used the value is cast to the\n        corresponding type.\n        \"\"\"\n        if item is None:\n            return\n\n        if increment is not None:\n            item.load()\n            value = type(increment)(item.get(attribute, 0)) + increment\n\n        if value is not None:\n            item[attribute] = value\n            item.store()\n\n            log.debug(u'mpdstats: updated: {0} = {1} [{2}]'.format(\n                attribute,\n                item[attribute],\n                displayable_path(item.path),\n            ))\n\n    def update_rating(self, item, skipped):\n        \"\"\"Update the rating for a beets item.\n        \"\"\"\n        item.load()\n        rating = self.rating(\n            int(item.get('play_count', 0)),\n            int(item.get('skip_count', 0)),\n            float(item.get('rating', 0.5)),\n            skipped)\n\n        self.update_item(item, 'rating', rating)\n\n    def handle_song_change(self, song):\n        \"\"\"Determine if a song was skipped or not and update its attributes.\n        To this end the difference between the song's supposed end time\n        and the current time is calculated. If it's greater than a threshold,\n        the song is considered skipped.\n        \"\"\"\n        diff = abs(song['remaining'] - (time.time() - song['started']))\n\n        skipped = diff >= self.time_threshold\n\n        if skipped:\n            self.handle_skipped(song)\n        else:\n            self.handle_played(song)\n\n        if self.do_rating:\n            self.update_rating(song['beets_item'], skipped)\n\n    def handle_played(self, song):\n        \"\"\"Updates the play count of a song.\n        \"\"\"\n        self.update_item(song['beets_item'], 'play_count', increment=1)\n        log.info(u'mpdstats: played {0}'.format(\n            displayable_path(song['path'])\n        ))\n\n    def handle_skipped(self, song):\n        \"\"\"Updates the skip count of a song.\n        \"\"\"\n        self.update_item(song['beets_item'], 'skip_count', increment=1)\n        log.info(u'mpdstats: skipped {0}'.format(\n            displayable_path(song['path'])\n        ))\n\n    def on_stop(self, status):\n        log.info(u'mpdstats: stop')\n        self.now_playing = None\n\n    def on_pause(self, status):\n        log.info(u'mpdstats: pause')\n        self.now_playing = None\n\n    def on_play(self, status):\n        playlist = self.mpd.playlist()\n        path = playlist.get(status['songid'])\n\n        if not path:\n            return\n\n        if is_url(path):\n            log.info(u'mpdstats: playing stream {0}'.format(\n                displayable_path(path)\n            ))\n            return\n\n        played, duration = map(int, status['time'].split(':', 1))\n        remaining = duration - played\n\n        if self.now_playing and self.now_playing['path'] != path:\n            self.handle_song_change(self.now_playing)\n\n        log.info(u'mpdstats: playing {0}'.format(\n            displayable_path(path)\n        ))\n\n        self.now_playing = {\n            'started':    time.time(),\n            'remaining':  remaining,\n            'path':       path,\n            'beets_item': self.get_item(path),\n        }\n\n    def run(self):\n        self.mpd.connect()\n        events = ['player']\n\n        while True:\n            if 'player' in events:\n                status = self.mpd.status()\n\n                handler = getattr(self, 'on_' + status['state'], None)\n\n                if handler:\n                    handler(status)\n                else:\n                    log.debug(u'mpdstats: unhandled status \"{0}\"'.format(status))\n\n            events = self.mpd.events()\n\n\nclass MPDStatsPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super(MPDStatsPlugin, self).__init__()\n        self.config.add({\n            'music_directory': config['directory'].as_filename(),\n            'rating':          True,\n            'rating_mix':      0.75,\n        })\n        config['mpd'].add({\n            'host':            u'localhost',\n            'port':            6600,\n            'password':        u'',\n        })\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            'mpdstats',\n            help='run a MPD client to gather play statistics')\n        cmd.parser.add_option(\n            '--host', dest='host', type='string',\n            help='set the hostname of the server to connect to')\n        cmd.parser.add_option(\n            '--port', dest='port', type='int',\n            help='set the port of the MPD server to connect to')\n        cmd.parser.add_option(\n            '--password', dest='password', type='string',\n            help='set the password of the MPD server to connect to')\n\n        def func(lib, opts, args):\n            self.config.set_args(opts)\n\n            # Overrides for MPD settings.\n            if opts.host:\n                config['mpd']['host'] = opts.host.decode('utf8')\n            if opts.port:\n                config['mpd']['host'] = int(opts.port)\n            if opts.password:\n                config['mpd']['password'] = opts.password.decode('utf8')\n\n            try:\n                MPDStats(lib).run()\n            except KeyboardInterrupt:\n                pass\n\n        cmd.func = func\n        return [cmd]\n", "path": "beetsplug/mpdstats.py"}], "after_files": [{"content": "# coding=utf-8\n# This file is part of beets.\n# Copyright 2013, Peter Schnebel and Johann Kl\u00e4hn.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nimport logging\nimport mpd\nimport socket\nimport select\nimport time\nimport os\n\nfrom beets import ui\nfrom beets import config\nfrom beets import plugins\nfrom beets import library\nfrom beets.util import displayable_path\n\nlog = logging.getLogger('beets')\n\n# If we lose the connection, how many times do we want to retry and how\n# much time should we wait between retries?\nRETRIES = 10\nRETRY_INTERVAL = 5\n\n\ndef is_url(path):\n    \"\"\"Try to determine if the path is an URL.\n    \"\"\"\n    return path.split('://', 1)[0] in ['http', 'https']\n\n\n# Use the MPDClient internals to get unicode.\n# see http://www.tarmack.eu/code/mpdunicode.py for the general idea\nclass MPDClient(mpd.MPDClient):\n    def _write_command(self, command, args=[]):\n        args = [unicode(arg).encode('utf-8') for arg in args]\n        super(MPDClient, self)._write_command(command, args)\n\n    def _read_line(self):\n        line = super(MPDClient, self)._read_line()\n        if line is not None:\n            return line.decode('utf-8')\n        return None\n\n\nclass MPDClientWrapper(object):\n    def __init__(self):\n        self.music_directory = (\n            config['mpdstats']['music_directory'].get(unicode))\n\n        self.client = MPDClient()\n\n    def connect(self):\n        \"\"\"Connect to the MPD.\n        \"\"\"\n        host = config['mpd']['host'].get(unicode)\n        port = config['mpd']['port'].get(int)\n\n        if host[0] in ['/', '~']:\n            host = os.path.expanduser(host)\n\n        log.info(u'mpdstats: connecting to {0}:{1}'.format(host, port))\n        try:\n            self.client.connect(host, port)\n        except socket.error as e:\n            raise ui.UserError('could not connect to MPD: {0}'.format(e))\n\n        password = config['mpd']['password'].get(unicode)\n        if password:\n            try:\n                self.client.password(password)\n            except mpd.CommandError as e:\n                raise ui.UserError(\n                    'could not authenticate to MPD: {0}'.format(e)\n                )\n\n    def disconnect(self):\n        \"\"\"Disconnect from the MPD.\n        \"\"\"\n        self.client.close()\n        self.client.disconnect()\n\n    def get(self, command, retries=RETRIES):\n        \"\"\"Wrapper for requests to the MPD server. Tries to re-connect if the\n        connection was lost (f.ex. during MPD's library refresh).\n        \"\"\"\n        try:\n            return getattr(self.client, command)()\n        except (select.error, mpd.ConnectionError) as err:\n            log.error(u'mpdstats: {0}'.format(err))\n\n        if retries <= 0:\n            # if we exited without breaking, we couldn't reconnect in time :(\n            raise ui.UserError(u'communication with MPD server failed')\n\n        time.sleep(RETRY_INTERVAL)\n\n        try:\n            self.disconnect()\n        except mpd.ConnectionError:\n            pass\n\n        self.connect()\n        return self.get(command, retries=retries - 1)\n\n    def playlist(self):\n        \"\"\"Return the currently active playlist.  Prefixes paths with the\n        music_directory, to get the absolute path.\n        \"\"\"\n        result = {}\n        for entry in self.get('playlistinfo'):\n            if not is_url(entry['file']):\n                result[entry['id']] = os.path.join(\n                    self.music_directory, entry['file'])\n            else:\n                result[entry['id']] = entry['file']\n        return result\n\n    def status(self):\n        \"\"\"Return the current status of the MPD.\n        \"\"\"\n        return self.get('status')\n\n    def events(self):\n        \"\"\"Return list of events. This may block a long time while waiting for\n        an answer from MPD.\n        \"\"\"\n        return self.get('idle')\n\n\nclass MPDStats(object):\n    def __init__(self, lib):\n        self.lib = lib\n\n        self.do_rating = config['mpdstats']['rating'].get(bool)\n        self.rating_mix = config['mpdstats']['rating_mix'].get(float)\n        self.time_threshold = 10.0  # TODO: maybe add config option?\n\n        self.now_playing = None\n        self.mpd = MPDClientWrapper()\n\n    def rating(self, play_count, skip_count, rating, skipped):\n        \"\"\"Calculate a new rating for a song based on play count, skip count,\n        old rating and the fact if it was skipped or not.\n        \"\"\"\n        if skipped:\n            rolling = (rating - rating / 2.0)\n        else:\n            rolling = (rating + (1.0 - rating) / 2.0)\n        stable = (play_count + 1.0) / (play_count + skip_count + 2.0)\n        return (self.rating_mix * stable\n                + (1.0 - self.rating_mix) * rolling)\n\n    def get_item(self, path):\n        \"\"\"Return the beets item related to path.\n        \"\"\"\n        query = library.PathQuery('path', path)\n        item = self.lib.items(query).get()\n        if item:\n            return item\n        else:\n            log.info(u'mpdstats: item not found: {0}'.format(\n                displayable_path(path)\n            ))\n\n    @staticmethod\n    def update_item(item, attribute, value=None, increment=None):\n        \"\"\"Update the beets item. Set attribute to value or increment the value\n        of attribute. If the increment argument is used the value is cast to the\n        corresponding type.\n        \"\"\"\n        if item is None:\n            return\n\n        if increment is not None:\n            item.load()\n            value = type(increment)(item.get(attribute, 0)) + increment\n\n        if value is not None:\n            item[attribute] = value\n            item.store()\n\n            log.debug(u'mpdstats: updated: {0} = {1} [{2}]'.format(\n                attribute,\n                item[attribute],\n                displayable_path(item.path),\n            ))\n\n    def update_rating(self, item, skipped):\n        \"\"\"Update the rating for a beets item.\n        \"\"\"\n        item.load()\n        rating = self.rating(\n            int(item.get('play_count', 0)),\n            int(item.get('skip_count', 0)),\n            float(item.get('rating', 0.5)),\n            skipped)\n\n        self.update_item(item, 'rating', rating)\n\n    def handle_song_change(self, song):\n        \"\"\"Determine if a song was skipped or not and update its attributes.\n        To this end the difference between the song's supposed end time\n        and the current time is calculated. If it's greater than a threshold,\n        the song is considered skipped.\n        \"\"\"\n        diff = abs(song['remaining'] - (time.time() - song['started']))\n\n        skipped = diff >= self.time_threshold\n\n        if skipped:\n            self.handle_skipped(song)\n        else:\n            self.handle_played(song)\n\n        if self.do_rating:\n            self.update_rating(song['beets_item'], skipped)\n\n    def handle_played(self, song):\n        \"\"\"Updates the play count of a song.\n        \"\"\"\n        self.update_item(song['beets_item'], 'play_count', increment=1)\n        log.info(u'mpdstats: played {0}'.format(\n            displayable_path(song['path'])\n        ))\n\n    def handle_skipped(self, song):\n        \"\"\"Updates the skip count of a song.\n        \"\"\"\n        self.update_item(song['beets_item'], 'skip_count', increment=1)\n        log.info(u'mpdstats: skipped {0}'.format(\n            displayable_path(song['path'])\n        ))\n\n    def on_stop(self, status):\n        log.info(u'mpdstats: stop')\n        self.now_playing = None\n\n    def on_pause(self, status):\n        log.info(u'mpdstats: pause')\n        self.now_playing = None\n\n    def on_play(self, status):\n        playlist = self.mpd.playlist()\n        path = playlist.get(status['songid'])\n\n        if not path:\n            return\n\n        if is_url(path):\n            log.info(u'mpdstats: playing stream {0}'.format(\n                displayable_path(path)\n            ))\n            return\n\n        played, duration = map(int, status['time'].split(':', 1))\n        remaining = duration - played\n\n        if self.now_playing and self.now_playing['path'] != path:\n            self.handle_song_change(self.now_playing)\n\n        log.info(u'mpdstats: playing {0}'.format(\n            displayable_path(path)\n        ))\n\n        self.now_playing = {\n            'started':    time.time(),\n            'remaining':  remaining,\n            'path':       path,\n            'beets_item': self.get_item(path),\n        }\n\n        self.update_item(self.now_playing['beets_item'],\n                         'last_played', value=int(time.time()))\n\n    def run(self):\n        self.mpd.connect()\n        events = ['player']\n\n        while True:\n            if 'player' in events:\n                status = self.mpd.status()\n\n                handler = getattr(self, 'on_' + status['state'], None)\n\n                if handler:\n                    handler(status)\n                else:\n                    log.debug(u'mpdstats: unhandled status \"{0}\"'.format(status))\n\n            events = self.mpd.events()\n\n\nclass MPDStatsPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super(MPDStatsPlugin, self).__init__()\n        self.config.add({\n            'music_directory': config['directory'].as_filename(),\n            'rating':          True,\n            'rating_mix':      0.75,\n        })\n        config['mpd'].add({\n            'host':            u'localhost',\n            'port':            6600,\n            'password':        u'',\n        })\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            'mpdstats',\n            help='run a MPD client to gather play statistics')\n        cmd.parser.add_option(\n            '--host', dest='host', type='string',\n            help='set the hostname of the server to connect to')\n        cmd.parser.add_option(\n            '--port', dest='port', type='int',\n            help='set the port of the MPD server to connect to')\n        cmd.parser.add_option(\n            '--password', dest='password', type='string',\n            help='set the password of the MPD server to connect to')\n\n        def func(lib, opts, args):\n            self.config.set_args(opts)\n\n            # Overrides for MPD settings.\n            if opts.host:\n                config['mpd']['host'] = opts.host.decode('utf8')\n            if opts.port:\n                config['mpd']['host'] = int(opts.port)\n            if opts.password:\n                config['mpd']['password'] = opts.password.decode('utf8')\n\n            try:\n                MPDStats(lib).run()\n            except KeyboardInterrupt:\n                pass\n\n        cmd.func = func\n        return [cmd]\n", "path": "beetsplug/mpdstats.py"}]}