]> kaliko git repositories - mpd-sima.git/blob - sima/client.py
e19a5aafdabbdce12ad18e747eb5d8c16e5f83ea
[mpd-sima.git] / sima / client.py
1 # -* coding: utf-8 -*-
2 """MPD client for Sima
3
4 This client is built above python-musicpd a fork of python-mpd
5 """
6
7 # standart library import
8 from select import select
9
10 # third parties componants
11 try:
12     from musicpd import (MPDClient, MPDError, CommandError)
13 except ImportError as err:
14     from sys import exit as sexit
15     print('ERROR: missing python-musicpd?\n{0}'.format(err))
16     sexit(1)
17
18 # local import
19 from .lib.player import Player
20 from .lib.track import Track
21
22
23 class PlayerError(Exception):
24     """Fatal error in poller."""
25
26 class PlayerCommandError(PlayerError):
27     """Command error"""
28
29
30 class PlayerClient(Player):
31     """MPC Client
32     From python-musicpd:
33         _fetch_nothing  …
34         _fetch_item     single str
35         _fetch_object   single dict
36         _fetch_list     list of str
37         _fetch_playlist list of str
38         _fetch_changes  list of dict
39         _fetch_database list of dict
40         _fetch_songs    list of dict, especially tracks
41         _fetch_plugins,
42     TODO: handle exception in command not going through _client_wrapper() (ie.
43           find_aa, remove…)
44     """
45     def __init__(self, host="localhost", port="6600", password=None):
46         self._host = host
47         self._port = port
48         self._password = password
49         self._client = MPDClient()
50         self._client.iterate = True
51
52     def __getattr__(self, attr):
53         command = attr
54         wrapper = self._execute
55         return lambda *args: wrapper(command, args)
56
57     def _execute(self, command, args):
58         self._write_command(command, args)
59         return self._client_wrapper()
60
61     def _write_command(self, command, args=[]):
62         self._comm = command
63         self._args = list()
64         for arg in args:
65             self._args.append(arg)
66
67     def _client_wrapper(self):
68         func = self._client.__getattr__(self._comm)
69         try:
70             ans = func(*self._args)
71         # WARNING: MPDError is an ancestor class of # CommandError
72         except CommandError as err:
73             raise PlayerCommandError('MPD command error: %s' % err)
74         except (MPDError, IOError) as err:
75             raise PlayerError(err)
76         return self._track_format(ans)
77
78     def _track_format(self, ans):
79         # TODO: ain't working for "sticker find" and "sticker list"
80         tracks_listing = ["playlistfind", "playlistid", "playlistinfo",
81                 "playlistsearch", "plchanges", "listplaylistinfo", "find",
82                 "search", "sticker find",]
83         track_obj = ['currentsong']
84         unicode_obj = ["idle", "listplaylist", "list", "sticker list",
85                 "commands", "notcommands", "tagtypes", "urlhandlers",]
86         if self._comm in tracks_listing + track_obj:
87             #  pylint: disable=w0142
88             if isinstance(ans, list):
89                 return [Track(**track) for track in ans]
90             elif isinstance(ans, dict):
91                 return Track(**ans)
92         return ans
93
94     def find_track(self, artist, title=None):
95         #return getattr(self, 'find')('artist', artist, 'title', title)
96         if title:
97             return self.find('artist', artist, 'title', title)
98         return self.find('artist', artist)
99
100     def find_album(self, artist, album):
101         """
102         Special wrapper around album search:
103         Album lookup is made through AlbumArtist/Album instead of Artist/Album
104         """
105         alb_art_search = self.find('albumartist', artist, 'album', album)
106         if alb_art_search:
107             return alb_art_search
108         return self.find('artist', artist, 'album', album)
109
110     def monitor(self):
111         try:
112             self._client.send_idle('database', 'playlist', 'player', 'options')
113             select([self._client], [], [], 60)
114             return self._client.fetch_idle()
115         except (MPDError, IOError) as err:
116             raise PlayerError("Couldn't init idle: %s" % err)
117
118     def idle(self):
119         try:
120             self._client.send_idle('database', 'playlist', 'player', 'options')
121             select([self._client], [], [], 60)
122             return self._client.fetch_idle()
123         except (MPDError, IOError) as err:
124             raise PlayerError("Couldn't init idle: %s" % err)
125
126     def remove(self, position=0):
127         self._client.delete(position)
128
129     @property
130     def state(self):
131         return str(self._client.status().get('state'))
132
133     @property
134     def current(self):
135         return self.currentsong()
136
137     def playlist(self):
138         """
139         Override deprecated MPD playlist command
140         """
141         return self.playlistinfo()
142
143     def connect(self):
144         self.disconnect()
145         try:
146             self._client.connect(self._host, self._port)
147
148         # Catch socket errors
149         except IOError as err:
150             raise PlayerError('Could not connect to "%s:%s": %s' %
151                               (self._host, self._port, err.strerror))
152
153         # Catch all other possible errors
154         # ConnectionError and ProtocolError are always fatal.  Others may not
155         # be, but we don't know how to handle them here, so treat them as if
156         # they are instead of ignoring them.
157         except MPDError as err:
158             raise PlayerError('Could not connect to "%s:%s": %s' %
159                               (self._host, self._port, err))
160
161         if self._password:
162             try:
163                 self._client.password(self._password)
164
165             # Catch errors with the password command (e.g., wrong password)
166             except CommandError as err:
167                 raise PlayerError("Could not connect to '%s': "
168                                   "password command failed: %s" %
169                                   (self._host, err))
170
171             # Catch all other possible errors
172             except (MPDError, IOError) as err:
173                 raise PlayerError("Could not connect to '%s': "
174                                   "error with password command: %s" %
175                                   (self._host, err))
176         # Controls we have sufficient rights for MPD_sima
177         needed_cmds = ['status', 'stats', 'add', 'find', \
178                        'search', 'currentsong', 'ping']
179
180         available_cmd = self._client.commands()
181         for nddcmd in needed_cmds:
182             if nddcmd not in available_cmd:
183                 self.disconnect()
184                 raise PlayerError('Could connect to "%s", '
185                                   'but command "%s" not available' %
186                                   (self._host, nddcmd))
187
188     def disconnect(self):
189         # Try to tell MPD we're closing the connection first
190         try:
191             self._client.close()
192         # If that fails, don't worry, just ignore it and disconnect
193         except (MPDError, IOError):
194             pass
195
196         try:
197             self._client.disconnect()
198         # Disconnecting failed, so use a new client object instead
199         # This should never happen.  If it does, something is seriously broken,
200         # and the client object shouldn't be trusted to be re-used.
201         except (MPDError, IOError):
202             self._client = MPDClient()
203
204 # VIM MODLINE
205 # vim: ai ts=4 sw=4 sts=4 expandtab