/* * Copyright (c) 2013, Sorokin Alexander (uas.sorokin@gmail.com) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * 3. The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.uas.media.aimp.api.impl; import com.uas.media.aimp.api.ApiException; import com.uas.media.aimp.api.ApiRequestException; import com.uas.media.aimp.api.IPlugin; import com.uas.media.aimp.api.Logger; import com.uas.media.aimp.api.models.CurrentSongInfo; import com.uas.media.aimp.api.models.Playlist; import com.uas.media.aimp.api.models.Song; import org.apache.http.HttpEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * User: uas.sorokin@gmail.com */ public class WebCtlPlugin implements IPlugin { public static final int DEFAULT_REQUEST_TIMEOUT = 5000; public static final int DEFAULT_PORT = 38475; interface Statuses { int VOLUME = 1; int BALANCE = 2; int SPEED = 3; int PLAY = 4; int MUTE = 5; int REVERBATION = 6; int ECHO = 7; int CHORUS = 8; int FLANGER = 9; int EQUALIZER = 10; int EQUALIZER1 = 11; int EQUALIZER2 = 12; int EQUALIZER3 = 13; int EQUALIZER4 = 14; int EQUALIZER5 = 15; int EQUALIZER6 = 16; int EQUALIZER7 = 17; int EQUALIZER8 = 18; int EQUALIZER9 = 19; int EQUALIZER10 = 20; int EQUALIZER11 = 21; int EQUALIZER12 = 22; int EQUALIZER13 = 23; int EQUALIZER14 = 24; int EQUALIZER15 = 25; int EQUALIZER16 = 26; int EQUALIZER17 = 27; int EQUALIZER18 = 28; int REPEAT_SONG = 29; int STOP = 30; int POSITION = 31; int LENGTH = 32; int REPEAT_PLAYLIST = 33; int REPEAT_PLAYLIST_1 = 34; int KBPS = 35; int KHz = 36; int MODE = 37; int RADIO = 38; int STREAM_TYPE = 39; int TIMER = 40; int SHUFFLE = 41; } private String mRemoteHost; private int mRemotePort; private int mConnectionTimeout; private String mHttpClientName; public WebCtlPlugin() { } public WebCtlPlugin(String host) { this(host, DEFAULT_PORT); } public WebCtlPlugin(String host, int port) { mRemoteHost = host; mRemotePort = port; mHttpClientName = ""; mConnectionTimeout = DEFAULT_REQUEST_TIMEOUT; } @Override public int getDefaultRemotePort() { return DEFAULT_PORT; } @Override public String getRemotePluginName() { return "AIMP Web Control Plugin"; } @Override public String getRemoteHost() { return mRemoteHost; } @Override public void setRemoteHost(String host) { if (host == null) { throw new IllegalArgumentException("Host has null value"); } mRemoteHost = host; } @Override public int getRemotePort() { return mRemotePort; } @Override public void setRemotePort(int port) { if (port < 1) { throw new IllegalArgumentException("Port value is below 0. Given value is " + port); } mRemotePort = port; } @Override public void setConnectionTimeout(int timeout) { if (timeout < 1) { throw new IllegalArgumentException("Connection timeout must be positive number. Given value is " + timeout); } mConnectionTimeout = timeout; } @Override public int getConnectionTimeout() { return mConnectionTimeout; } @Override public int getTrafficIn() { return 0; } @Override public int getTrafficOut() { return 0; } @Override public String getHttpClientName() { return mHttpClientName; } @Override public void setHttpClientName(String name) { if (name == null) { throw new IllegalArgumentException("HttpClientName is null"); } mHttpClientName = name; } // ================================================================================ // ================================================================================ @Override public boolean ping() throws InterruptedException { try { getPlaylists(); return true; } catch(InterruptedException ex) { throw ex; } catch(Exception e) { Logger.e("Ping operation failed. Reason: " + e.getMessage(), e); return false; } } @Override public boolean play() throws ApiException, IOException, InterruptedException { sendRequest("/?action=player_play"); return true; } @Override public boolean play(int playlistId, int songPosition) throws ApiException, IOException, InterruptedException { play(playlistId, songPosition, 0); return true; } @Override public boolean play(int playlistId, int songPosition, int playPosition) throws ApiException, IOException, InterruptedException { if (songPosition < 0) { throw new IllegalArgumentException("Invalid song's position value: " + songPosition); } if (playPosition < 0) { throw new IllegalArgumentException("Invalid song's play position value: " + playPosition); } sendRequest( "/?action=set_song_play" + "&playlist=" + playlistId + "&song=" + songPosition ); setSongPlayPosition(playPosition); return true; } @Override public boolean stop() throws ApiException, IOException, InterruptedException { sendRequest("/?action=player_stop"); return true; } @Override public boolean pause() throws ApiException, IOException, InterruptedException { sendRequest("/?action=player_pause"); return true; } @Override public boolean next() throws ApiException, IOException, InterruptedException { sendRequest("/?action=player_next"); return true; } @Override public boolean previous() throws ApiException, IOException, InterruptedException { sendRequest("/?action=player_prevous"); return true; } @Override public int getPlayState() throws ApiException, IOException, InterruptedException { return getCustomStatus(Statuses.PLAY).equals("1") ? PLAY_STATE_PLAYING : PLAY_STATE_STOPPED ; } @Override public int getSongPlayPosition() throws ApiException, IOException, InterruptedException { String r = getCustomStatus(Statuses.POSITION); try { int position = Integer.parseInt(r); if (position < 0) { throw new NumberFormatException(); } return position; } catch(NumberFormatException ex) { throw new ApiRequestException("Song play position expected to be a not negative integer. Response is: " + r); } } @Override public void setSongPlayPosition(int second) throws ApiException, IOException, InterruptedException { if (second < 0) { throw new IllegalArgumentException("Seconds must be not negative. Given value: " + second); } setCustomStatus(Statuses.POSITION, second); } @Override public void setRepeatSong(boolean state) throws ApiException, IOException, InterruptedException { setCustomStatus(Statuses.REPEAT_SONG, state); } @Override public boolean isRepeatSong() throws ApiException, IOException, InterruptedException { String r = getCustomStatus(Statuses.REPEAT_SONG); if (!r.equals("1") && !r.equals("0")) { throw new ApiRequestException("Repeat value is not 0 or 1. Response: " + r); } else { return r.equals("1"); } } @Override public void setVolume(int volume) throws ApiException, IOException, InterruptedException { if (volume < 0 || volume > 100) { throw new IllegalArgumentException("Volume value must be in range of [0, 100]. Given value is " + volume); } setCustomStatus(Statuses.VOLUME, volume); } @Override public int getVolume() throws ApiException, IOException, InterruptedException { String r = getCustomStatus(Statuses.VOLUME); try { int volume = Integer.parseInt(r); if (volume < 0 || volume > 100) { throw new NumberFormatException(); } return volume; } catch(NumberFormatException ex) { throw new ApiRequestException("Volume value must be a positive integer in range [0, 100]. Response: " + r); } } @Override public void setMute(boolean state) throws ApiException, IOException, InterruptedException { setCustomStatus(Statuses.MUTE, state); } @Override public boolean isMute() throws ApiException, IOException, InterruptedException { String r = getCustomStatus(Statuses.MUTE); if (!r.equals("1") && !r.equals("0")) { throw new ApiRequestException("Mute value is not 0 or 1. Response: " + r); } else { return r.equals("1"); } } @Override public void setShuffle(boolean state) throws ApiException, IOException, InterruptedException { setCustomStatus(Statuses.SHUFFLE, state); } @Override public boolean isShuffle() throws ApiException, IOException, InterruptedException { String r = getCustomStatus(Statuses.SHUFFLE); if (!r.equals("1") && !r.equals("0")) { throw new ApiRequestException("Shuffle value is not 0 or 1. Response: " + r); } else { return r.equals("1"); } } @Override public CurrentSongInfo getCurrentSongInfo() throws ApiException, IOException, InterruptedException { String response = asString(sendRequest("/?action=get_song_current")); try { JSONObject json = new JSONObject(response); if (!json.getString("status").equals("OK")) { throw new ApiRequestException("Unable to retrieve info about current playing song"); } CurrentSongInfo si = new CurrentSongInfo(); si.setPlaylistId(json.getInt("PlayingList")); si.setSongPosition(json.getInt("PlayingFile")); if (si.getSongPosition() < 0) { si.setInfo(null); } else { si.setInfo(new Song(json.getString("PlayingFileName"), json.getInt("length"))); } return si; } catch (JSONException e) { throw new ApiRequestException("Unable to retrieve info about current playing song. Error parsing response: " + response); } } @Override public List<Playlist> getPlaylists() throws ApiException, IOException, InterruptedException { ArrayList<Playlist> result = new ArrayList<Playlist>(0); String response = asString(sendRequest("/?action=get_playlist_list")); JSONArray json; try { json = new JSONArray(response); result.ensureCapacity(json.length()); for (int i = 0; i < json.length(); ++i) { JSONObject entry = json.getJSONObject(i); Playlist pl = new Playlist(); pl.setId(entry.getInt("id")); pl.setName(entry.getString("name")); pl.setSizeInBytes(entry.getLong("size")); pl.setDuration(entry.getInt("duration")); pl.setHash( getPlaylistHash(pl.getId()) ); result.add(pl); } } catch (JSONException e) { throw new ApiRequestException("Error parsing response: " + response); } return result; } @Override public String getPlaylistHash(int playlistId) throws ApiException, IOException, InterruptedException { return asString(sendRequest("/?action=get_playlist_crc&id=" + playlistId)); } @Override public List<Song> getPlaylistSongs(int playlistId) throws ApiException, IOException, InterruptedException { ArrayList<Song> result = new ArrayList<Song>(0); //TODO Jackson crashes when meets \ in song's name String response = asString(sendRequest("/?action=get_playlist_songs&id=" + playlistId)); JSONObject json; try { json = new JSONObject(response); if (!json.getString("status").equals("OK")) { throw new ApiRequestException("Retrieve playlist's songs operation failed. Playlist with ID " + playlistId + " not found"); } JSONArray songs = (JSONArray)json.get("songs"); result.ensureCapacity(songs.length()); for (int i = 0; i < songs.length(); ++i) { JSONObject entry = (JSONObject)songs.get(i); result.add(new Song(entry.getString("name"), entry.getInt("length")/1000)); } } catch (JSONException e) { throw new ApiRequestException("Error parsing response: " + response); } return result; } @Override public void removeSong(int playlistId, int songPosition) throws ApiException, IOException, InterruptedException { sendRequest( "/?action=playlist_del_file" + "&playlist=" + playlistId + "&file=" + songPosition ); } protected HttpEntity sendRequest(String request) throws IOException { final String uri = String.format( "http://%s:%d%s", mRemoteHost, mRemotePort, request ); HttpParams httpParameters = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, mConnectionTimeout); HttpConnectionParams.setSoTimeout(httpParameters, mConnectionTimeout); DefaultHttpClient httpClient = new DefaultHttpClient(httpParameters); HttpGet httpGet = new HttpGet(uri); httpGet.setHeader("User-Agent", mHttpClientName); return httpClient.execute(httpGet).getEntity(); } protected String asString(HttpEntity entity) throws IOException { return EntityUtils.toString(entity, HTTP.UTF_8); } protected InputStream asStream(HttpEntity entity) throws IOException { return entity.getContent(); } protected void setCustomStatus(String status, String value) throws ApiException, IOException, InterruptedException { sendRequest(String.format( "/?action=set_custom_status&status=%s&value=%s", status, value )); } protected void setCustomStatus(int status, boolean value) throws ApiException, IOException, InterruptedException { setCustomStatus(String.valueOf(status), value ? "1" : "0"); } protected void setCustomStatus(int status, int value) throws ApiException, IOException, InterruptedException { setCustomStatus(String.valueOf(status), String.valueOf(value)); } protected String getCustomStatus(String status) throws ApiException, IOException, InterruptedException { return asString(sendRequest(String.format( "/?action=get_custom_status&status=%s", status ))); } protected String getCustomStatus(int status) throws ApiException, IOException, InterruptedException { return getCustomStatus(String.valueOf(status)); } }