]> kaliko git repositories - mpd-sima.git/blob - sima/client.py
331c02c29bdef31e686a51ecf4a7c1a277b7d5a5
[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 remove(self, position=0):
119         self._client.delete(position)
120
121     @property
122     def state(self):
123         return str(self._client.status().get('state'))
124
125     @property
126     def current(self):
127         return self.currentsong()
128
129     @property
130     def playlist(self):
131         """
132         Override deprecated MPD playlist command
133         """
134         return self.playlistinfo()
135
136     def connect(self):
137         self.disconnect()
138         try:
139             self._client.connect(self._host, self._port)
140
141         # Catch socket errors
142         except IOError as err:
143             raise PlayerError('Could not connect to "%s:%s": %s' %
144                               (self._host, self._port, err.strerror))
145
146         # Catch all other possible errors
147         # ConnectionError and ProtocolError are always fatal.  Others may not
148         # be, but we don't know how to handle them here, so treat them as if
149         # they are instead of ignoring them.
150         except MPDError as err:
151             raise PlayerError('Could not connect to "%s:%s": %s' %
152                               (self._host, self._port, err))
153
154         if self._password:
155             try:
156                 self._client.password(self._password)
157
158             # Catch errors with the password command (e.g., wrong password)
159             except CommandError as err:
160                 raise PlayerError("Could not connect to '%s': "
161                                   "password command failed: %s" %
162                                   (self._host, err))
163
164             # Catch all other possible errors
165             except (MPDError, IOError) as err:
166                 raise PlayerError("Could not connect to '%s': "
167                                   "error with password command: %s" %
168                                   (self._host, err))
169         # Controls we have sufficient rights for MPD_sima
170         needed_cmds = ['status', 'stats', 'add', 'find', \
171                        'search', 'currentsong', 'ping']
172
173         available_cmd = self._client.commands()
174         for nddcmd in needed_cmds:
175             if nddcmd not in available_cmd:
176                 self.disconnect()
177                 raise PlayerError('Could connect to "%s", '
178                                   'but command "%s" not available' %
179                                   (self._host, nddcmd))
180
181     def disconnect(self):
182         # Try to tell MPD we're closing the connection first
183         try:
184             self._client.close()
185         # If that fails, don't worry, just ignore it and disconnect
186         except (MPDError, IOError):
187             pass
188         try:
189             self._client.disconnect()
190         # Disconnecting failed, so use a new client object instead
191         # This should never happen.  If it does, something is seriously broken,
192         # and the client object shouldn't be trusted to be re-used.
193         except (MPDError, IOError):
194             self._client = MPDClient()
195
196 # VIM MODLINE
197 # vim: ai ts=4 sw=4 sts=4 expandtab