├── .gitignore ├── LICENSE ├── README.md ├── app ├── Global.java ├── controllers │ ├── App.scala │ ├── Application.java │ ├── Credential.java │ └── Secured.java ├── helper │ ├── ConstantFuture.java │ ├── DefaultPage.java │ ├── DefaultPagingList.java │ ├── Digester.java │ ├── EmptyPage.java │ ├── MpdMonitor.java │ └── UrlParser.java ├── models │ ├── Database.java │ ├── Playlist.java │ └── User.java └── views │ ├── addweb.scala.html │ ├── database.scala.html │ ├── info.scala.html │ ├── login.scala.html │ ├── main.scala.html │ ├── playlist.scala.html │ ├── playlists.scala.html │ └── saveplaylist.scala.html ├── conf ├── application.conf ├── evolutions │ └── default │ │ └── 1.sql ├── messages └── routes ├── images ├── 2013-08-22.png ├── 2013-08-28.png ├── 2013-08-29.png └── 2013_08_25_8179f96.png ├── lib ├── javampd-4.2-SNAPSHOT-sources.jar └── javampd-4.2-SNAPSHOT.jar ├── project ├── Build.scala ├── build.properties └── plugins.sbt ├── public ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── images │ ├── favicon.png │ └── favicon.xcf ├── javascripts │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-1.10.2.js │ ├── jquery-1.10.2.min.js │ ├── jquery-ui-1.10.3.js │ ├── jquery-ui-1.10.3.min.js │ └── main.js └── stylesheets │ ├── bootstrap-theme.css │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── jquery-ui-1.10.3 │ └── redmond │ │ ├── images │ │ ├── animated-overlay.gif │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ ├── ui-bg_flat_55_fbec88_40x100.png │ │ ├── ui-bg_glass_75_d0e5f5_1x400.png │ │ ├── ui-bg_glass_85_dfeffc_1x400.png │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ ├── ui-bg_gloss-wave_55_5c9ccc_500x100.png │ │ ├── ui-bg_inset-hard_100_f5f8f9_1x100.png │ │ ├── ui-bg_inset-hard_100_fcfdfd_1x100.png │ │ ├── ui-icons_217bc0_256x240.png │ │ ├── ui-icons_2e83ff_256x240.png │ │ ├── ui-icons_469bdd_256x240.png │ │ ├── ui-icons_6da8d5_256x240.png │ │ ├── ui-icons_cd0a0a_256x240.png │ │ ├── ui-icons_d8e7f3_256x240.png │ │ └── ui-icons_f9bd01_256x240.png │ │ ├── jquery-ui.css │ │ └── jquery-ui.min.css │ ├── main.css │ ├── ui-bg_gloss-wave_55_5c9ccc_60x12.png │ └── ui-bg_gloss-wave_55_cc9c5c_60x12.png └── test ├── FunctionalTest.java ├── IntegrationTest.java └── ModelTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Lines that start with '#' are comments. 2 | 3 | # generic part (can be overridden locally using !pattern) 4 | bin/ 5 | doc/ 6 | doc-files/ 7 | 8 | # eclipse-related content 9 | .project 10 | .classpath 11 | .metadata/ 12 | .settings/ 13 | 14 | # Generated output 15 | logs/ 16 | target/ 17 | .target/ 18 | project/target 19 | project/project/target 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Martin Steiger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Play! MPC 2 | =============== 3 | 4 | This is a simple and fast web frontend for [Music Player Daemon](http://www.musicpd.org/). 5 | 6 | ## Live Demo 7 | A non-functional demo can be accessed here: [http://msteiger.github.io/play-mpc](http://msteiger.github.io/play-mpc) 8 | 9 | ## Features 10 | 11 | - MPD Player controls 12 | - Playlist view and modification 13 | - Filtering and sorting of database 14 | - User authentification 15 | - Device-specific layout (Desktop, Tablet, Smartphone) 16 | - Web-Radio support 17 | 18 | It uses the following technologies: 19 | 20 | - [Play! 2](http://www.playframework.com) 21 | - [Bootstrap 3](http://getbootstrap.com) 22 | - [JavaMPD 4](http://www.thejavashop.net/javampd) 23 | 24 | ## Installation 25 | 26 | 1) Download and install Play! 2.1 or later 27 | 28 | 2) Either download the latest (tagged) version as zip file or use git to clone the repository. 29 | 30 | 3) Edit `conf/application.conf` to suit your needs 31 | 32 | 4) Enter the directory and run `play start` 33 | 34 | 5) The website is served at `localhost:9000` 35 | 36 | 6) The default login is bob@example.com // secret 37 | 38 | ## License 39 | 40 | This project is licensed under the **MIT License** 41 | 42 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 47 | 48 | -------------------------------------------------------------------------------- /app/Global.java: -------------------------------------------------------------------------------- 1 | import helper.MpdMonitor; 2 | 3 | import org.bff.javampd.exception.MPDConnectionException; 4 | 5 | import play.Application; 6 | import play.GlobalSettings; 7 | import play.Logger; 8 | 9 | /** 10 | * Global is instantiated by the framework when an application starts, to let 11 | * you perform specific tasks at start-up or shut-down. 12 | * @author Martin Steiger 13 | */ 14 | public class Global extends GlobalSettings 15 | { 16 | @Override 17 | public void onStart(Application app) 18 | { 19 | // nothing to do 20 | } 21 | 22 | @Override 23 | public void onStop(Application app) 24 | { 25 | Logger.info("Disconnecting .. "); 26 | 27 | try 28 | { 29 | MpdMonitor.getInstance().stop(); 30 | } 31 | catch (MPDConnectionException e) 32 | { 33 | // ignore 34 | } 35 | 36 | super.onStop(app); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/controllers/App.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc._ 4 | import play.api.Logger 5 | import play.api.libs.json._ 6 | import play.api.libs.concurrent.Execution.Implicits._ 7 | import play.api.libs.iteratee._ 8 | 9 | import java.util.concurrent.atomic.AtomicLong 10 | 11 | object App extends Controller { 12 | 13 | // Concurrent.broadcast returns (Enumerator, Concurrent.Channel) 14 | private lazy val (out, channel) = Concurrent.broadcast[JsValue] 15 | 16 | private val noOfUsers = new AtomicLong(0) 17 | 18 | def sockHandler = WebSocket.using[JsValue] { request => 19 | Logger.info(s"New browser connected (${ noOfUsers.incrementAndGet } browsers currently connected)") 20 | 21 | val in = Iteratee.ignore[JsValue].map { _ => 22 | Logger.info(s"Browser disconnected (${ noOfUsers.decrementAndGet } browsers currently connected)") 23 | } 24 | 25 | (in, out) 26 | } 27 | 28 | def sendWebsocketMessage(kind: String, value: Long): Unit = 29 | broadcast(kind, JsNumber(value)) 30 | 31 | def sendWebsocketMessage(kind: String, value: String): Unit = 32 | broadcast(kind, JsString(value)) 33 | 34 | def broadcast(kind: String, value: JsValue): Unit = { 35 | val msg = Json.obj( 36 | "type" -> kind, 37 | "value" -> value 38 | ) 39 | Logger.debug(msg.toString) 40 | 41 | channel.push(msg) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/controllers/Application.java: -------------------------------------------------------------------------------- 1 | 2 | package controllers; 3 | 4 | import static org.bff.javampd.MPDPlayer.PlayerStatus.STATUS_PLAYING; 5 | import helper.EmptyPage; 6 | import helper.MpdMonitor; 7 | import helper.UrlParser; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import models.Database; 14 | import models.Playlist; 15 | 16 | import org.bff.javampd.MPD; 17 | import org.bff.javampd.MPDAdmin; 18 | import org.bff.javampd.MPDFile; 19 | import org.bff.javampd.MPDOutput; 20 | import org.bff.javampd.MPDPlayer; 21 | import org.bff.javampd.MPDPlayer.PlayerStatus; 22 | import org.bff.javampd.MPDPlaylist; 23 | import org.bff.javampd.events.PlayerBasicChangeEvent; 24 | import org.bff.javampd.events.PlayerBasicChangeListener; 25 | import org.bff.javampd.events.PlaylistBasicChangeEvent; 26 | import org.bff.javampd.events.PlaylistBasicChangeListener; 27 | import org.bff.javampd.events.TrackPositionChangeEvent; 28 | import org.bff.javampd.events.TrackPositionChangeListener; 29 | import org.bff.javampd.events.VolumeChangeEvent; 30 | import org.bff.javampd.events.VolumeChangeListener; 31 | import org.bff.javampd.exception.MPDAdminException; 32 | import org.bff.javampd.exception.MPDConnectionException; 33 | import org.bff.javampd.exception.MPDException; 34 | import org.bff.javampd.exception.MPDPlayerException; 35 | import org.bff.javampd.monitor.MPDStandAloneMonitor; 36 | import org.bff.javampd.objects.MPDSavedPlaylist; 37 | import org.bff.javampd.objects.MPDSong; 38 | 39 | import play.Logger; 40 | import play.Routes; 41 | import play.libs.F.Callback0; 42 | import play.libs.Json; 43 | import play.mvc.Controller; 44 | import play.mvc.Result; 45 | import play.mvc.Security; 46 | import views.html.database; 47 | import views.html.info; 48 | import views.html.main; 49 | import views.html.playlist; 50 | import views.html.playlists; 51 | 52 | import com.avaje.ebean.Page; 53 | 54 | import static controllers.App.sendWebsocketMessage; 55 | 56 | /** 57 | * Manage a database of computers 58 | */ 59 | @Security.Authenticated(Secured.class) 60 | public class Application extends Controller 61 | { 62 | /** 63 | * This result directly redirect to application home. 64 | */ 65 | public static Result GO_HOME = redirect(routes.Application.playlist(0)); 66 | 67 | static 68 | { 69 | try 70 | { 71 | MPDStandAloneMonitor monitor = MpdMonitor.getInstance().getMonitor(); 72 | 73 | Logger.info("Start monitoring ..."); 74 | monitor.addPlayerChangeListener(new PlayerBasicChangeListener() 75 | { 76 | @Override 77 | public void playerBasicChange(PlayerBasicChangeEvent event) 78 | { 79 | int id = event.getId(); 80 | 81 | try 82 | { 83 | MPDPlayer player = MpdMonitor.getInstance().getMPD().getMPDPlayer(); 84 | 85 | switch (id) 86 | { 87 | case PlayerBasicChangeEvent.PLAYER_CONSUME_CHANGE: 88 | sendWebsocketMessage("consume", player.isConsuming() ? 1 : 0); 89 | break; 90 | 91 | case PlayerBasicChangeEvent.PLAYER_SINGLE_CHANGE: 92 | sendWebsocketMessage("single", player.isSingleMode() ? 1 : 0); 93 | break; 94 | 95 | case PlayerBasicChangeEvent.PLAYER_REPEAT_CHANGE: 96 | sendWebsocketMessage("repeat", player.isRepeat() ? 1 : 0); 97 | break; 98 | 99 | case PlayerBasicChangeEvent.PLAYER_RANDOM_CHANGE: 100 | sendWebsocketMessage("shuffle", player.isRandom() ? 1 : 0); 101 | break; 102 | 103 | case PlayerBasicChangeEvent.PLAYER_PAUSED: 104 | sendWebsocketMessage("status", "pause"); 105 | break; 106 | 107 | case PlayerBasicChangeEvent.PLAYER_STOPPED: 108 | sendWebsocketMessage("status", "stop"); 109 | break; 110 | 111 | case PlayerBasicChangeEvent.PLAYER_UNPAUSED: 112 | case PlayerBasicChangeEvent.PLAYER_STARTED: 113 | sendWebsocketMessage("status", "play"); 114 | break; 115 | 116 | case PlayerBasicChangeEvent.PLAYER_BITRATE_CHANGE: 117 | // ignore silently - changes from 0 to x and back occur frequently 118 | break; 119 | 120 | default: 121 | Logger.info("Ignored player change message " + id); 122 | break; 123 | } 124 | } 125 | catch (MPDException e) 126 | { 127 | Logger.warn("Error on event " + id, e); 128 | } 129 | } 130 | }); 131 | 132 | monitor.addTrackPositionChangeListener(new TrackPositionChangeListener() 133 | { 134 | @Override 135 | public void trackPositionChanged(TrackPositionChangeEvent event) 136 | { 137 | sendWebsocketMessage("songpos", event.getElapsedTime()); 138 | } 139 | }); 140 | 141 | monitor.addVolumeChangeListener(new VolumeChangeListener() 142 | { 143 | @Override 144 | public void volumeChanged(VolumeChangeEvent event) 145 | { 146 | sendWebsocketMessage("volume", event.getVolume()); 147 | } 148 | }); 149 | 150 | monitor.addPlaylistChangeListener(new PlaylistBasicChangeListener() 151 | { 152 | @Override 153 | public void playlistBasicChange(PlaylistBasicChangeEvent event) 154 | { 155 | try 156 | { 157 | MPDPlayer player = MpdMonitor.getInstance().getMPD().getMPDPlayer(); 158 | switch (event.getId()) 159 | { 160 | case PlaylistBasicChangeEvent.SONG_ADDED: 161 | case PlaylistBasicChangeEvent.SONG_DELETED: 162 | sendWebsocketMessage("reload", event.getId()); 163 | break; 164 | 165 | case PlaylistBasicChangeEvent.PLAYLIST_ENDED: 166 | case PlaylistBasicChangeEvent.PLAYLIST_CHANGED: 167 | // just don't care 168 | break; 169 | 170 | case PlaylistBasicChangeEvent.SONG_CHANGED: 171 | sendWebsocketMessage("select", player.getCurrentSong().getPosition()); 172 | sendWebsocketMessage("songlength", player.getCurrentSong().getLength()); 173 | break; 174 | } 175 | } 176 | catch (MPDException e) 177 | { 178 | Logger.warn("Error on event " + event.getId(), e); 179 | } 180 | } 181 | }); 182 | } 183 | catch (MPDConnectionException e) 184 | { 185 | Logger.warn("Could not connect", e); 186 | } 187 | } 188 | 189 | 190 | /** 191 | * Handle default path requests, redirect to computers list 192 | * @return an action result 193 | */ 194 | public static Result index() 195 | { 196 | return GO_HOME; 197 | } 198 | 199 | public static Result javascriptRoutes() 200 | { 201 | response().setContentType("text/javascript"); 202 | return ok(Routes.javascriptRouter("jsRoutes", 203 | controllers.routes.javascript.Application.prevSong(), 204 | controllers.routes.javascript.Application.playSong(), 205 | controllers.routes.javascript.Application.nextSong(), 206 | controllers.routes.javascript.Application.stopSong(), 207 | 208 | controllers.routes.javascript.Application.toggleShuffle(), 209 | controllers.routes.javascript.Application.toggleRepeat(), 210 | controllers.routes.javascript.Application.toggleSingleMode(), 211 | controllers.routes.javascript.Application.toggleConsuming(), 212 | 213 | controllers.routes.javascript.Application.setVolume(), 214 | controllers.routes.javascript.Application.selectSong(), 215 | controllers.routes.javascript.Application.setSongPos(), 216 | controllers.routes.javascript.Application.addUrl(), 217 | controllers.routes.javascript.Application.addDbEntry(), 218 | controllers.routes.javascript.Application.remove(), 219 | 220 | controllers.routes.javascript.Application.playlistContent(), 221 | controllers.routes.javascript.Application.playlistDelete(), 222 | controllers.routes.javascript.Application.playlistLoad(), 223 | controllers.routes.javascript.Application.playlistSave(), 224 | 225 | controllers.routes.javascript.Application.toggleOutput() 226 | ) 227 | ); 228 | } 229 | 230 | /** 231 | * Display the paginated list of playlist entries. 232 | * @param page Current page number (starts from 0) 233 | * @return an action result 234 | */ 235 | public static Result playlist(int page) 236 | { 237 | try 238 | { 239 | MPD mpd = MpdMonitor.getInstance().getMPD(); 240 | MPDPlayer player = mpd.getMPDPlayer(); 241 | Page songs = Playlist.getSongs(page, 10); 242 | 243 | return ok(playlist.render(player, songs)); 244 | } 245 | catch (MPDException e) 246 | { 247 | Logger.error("MPD error", e); 248 | 249 | flash("error", "Command failed! " + e.getMessage()); 250 | return ok(playlist.render(null, new EmptyPage())); 251 | } 252 | 253 | } 254 | 255 | /** 256 | * Display all available playlists 257 | * @return the rendered html content 258 | */ 259 | public static Result playlists() 260 | { 261 | try 262 | { 263 | MPD mpd = MpdMonitor.getInstance().getMPD(); 264 | 265 | List savedlists = mpd.getMPDDatabase().listSavedPlaylists(); 266 | 267 | // List savedlists = mpd.getMPDDatabase().listPlaylists(); 268 | 269 | return ok(playlists.render(savedlists)); 270 | } 271 | catch (MPDException e) 272 | { 273 | Logger.error("MPD error", e); 274 | 275 | flash("error", "Command failed! " + e.getMessage()); 276 | return ok(playlists.render(Collections.emptyList())); 277 | } 278 | } 279 | 280 | /** 281 | * Return all songs of a given playlist 282 | * @return the rendered html content 283 | */ 284 | public static Result playlistContent(String id) 285 | { 286 | try 287 | { 288 | MPD mpd = MpdMonitor.getInstance().getMPD(); 289 | 290 | List songs = mpd.getMPDDatabase().listPlaylistSongs(id); 291 | 292 | return ok(Json.toJson(songs)); 293 | } 294 | catch (MPDException e) 295 | { 296 | Logger.error("MPD error", e); 297 | 298 | flash("error", "Command failed! " + e.getMessage()); 299 | return ok(playlists.render(Collections.emptyList())); 300 | } 301 | } 302 | 303 | /** 304 | * Load a playlist 305 | * @return an empty ok 306 | */ 307 | public static Result playlistLoad(String id) 308 | { 309 | try 310 | { 311 | MPD mpd = MpdMonitor.getInstance().getMPD(); 312 | MPDPlaylist playlist = mpd.getMPDPlaylist(); 313 | 314 | playlist.clearPlaylist(); 315 | playlist.loadPlaylist(id); 316 | 317 | return ok(""); 318 | } 319 | catch (MPDException e) 320 | { 321 | Logger.error("MPD error", e); 322 | 323 | flash("error", e.getMessage()); 324 | return internalServerError(e.getMessage()); 325 | } 326 | } 327 | 328 | /** 329 | * Save a playlist 330 | * @return an empty ok 331 | */ 332 | public static Result playlistSave(String id) 333 | { 334 | Logger.info("Saving playlist \"" + id + "\""); 335 | 336 | try 337 | { 338 | MPD mpd = MpdMonitor.getInstance().getMPD(); 339 | MPDPlaylist playlist = mpd.getMPDPlaylist(); 340 | 341 | playlist.savePlaylist(id); 342 | 343 | return ok(""); 344 | } 345 | catch (MPDException e) 346 | { 347 | Logger.error("MPD error", e); 348 | 349 | flash("error", e.getMessage()); 350 | return internalServerError(e.getMessage()); 351 | } 352 | } 353 | 354 | /** 355 | * Delete a given playlist 356 | * @return an empty ok 357 | */ 358 | public static Result playlistDelete(String id) 359 | { 360 | try 361 | { 362 | MPD mpd = MpdMonitor.getInstance().getMPD(); 363 | mpd.getMPDPlaylist().deletePlaylist(id); 364 | 365 | return ok(""); 366 | } 367 | catch (MPDException e) 368 | { 369 | Logger.error("MPD error", e); 370 | 371 | flash("error", e.getMessage()); 372 | return internalServerError(e.getMessage()); 373 | } 374 | } 375 | 376 | /** 377 | * Display the paginated list of computers. 378 | * @param page Current page number (starts from 0) 379 | * @param sortBy Column to be sorted 380 | * @param order Sort order (either asc or desc) 381 | * @param filter Filter applied on computer names 382 | * @return an action result 383 | */ 384 | public static Result browseDb(int page, String sortBy, String order, String filter) 385 | { 386 | Page songs = null; 387 | List playlistfiles = new ArrayList<>(); 388 | 389 | try 390 | { 391 | MPD mpd = MpdMonitor.getInstance().getMPD(); 392 | 393 | List playlist = mpd.getMPDPlaylist().getSongList(); 394 | for (MPDSong song : playlist) 395 | { 396 | playlistfiles.add(song.getFile()); 397 | } 398 | 399 | songs = Database.getSongs(page, 10, sortBy, order, filter); 400 | } 401 | catch (MPDException e) 402 | { 403 | Logger.error("MPD error", e); 404 | 405 | flash("error", "Command failed! " + e.getMessage()); 406 | songs = new EmptyPage<>(); 407 | } 408 | 409 | return ok(database.render(songs, playlistfiles, sortBy, order, filter)); 410 | } 411 | 412 | /** 413 | * Performs POST /addUrl 414 | * Display the 'Add from URL form'. 415 | * @return an action result 416 | */ 417 | public static Result addUrl(String url) 418 | { 419 | try 420 | { 421 | Logger.info("Adding to playlist: " + url); 422 | 423 | MPD mpd = MpdMonitor.getInstance().getMPD(); 424 | 425 | UrlParser parser = new UrlParser(); 426 | List files = parser.getAll(url); 427 | 428 | for (MPDFile file : files) 429 | { 430 | mpd.getMPDPlaylist().addFileOrDirectory(file); 431 | } 432 | 433 | return ok("Added " + files.size() + " files"); 434 | } 435 | catch (Exception e) 436 | { 437 | Logger.error("MPD error", e); 438 | flash("error", e.getMessage()); 439 | 440 | return internalServerError(e.getMessage()); 441 | } 442 | 443 | } 444 | /** 445 | * Performs POST /addDbEntry 446 | * @return an action result 447 | */ 448 | public static Result addDbEntry(String path) 449 | { 450 | try 451 | { 452 | Logger.info("Adding db entry to playlist: " + path); 453 | 454 | MPD mpd = MpdMonitor.getInstance().getMPD(); 455 | MPDSong song = new MPDSong(); 456 | song.setFile(path); 457 | 458 | mpd.getMPDPlaylist().addSong(song); 459 | 460 | return ok(path); 461 | } 462 | catch (MPDException e) 463 | { 464 | Logger.error("MPD error", e); 465 | flash("error", "Command failed! " + e.getMessage()); 466 | 467 | return notFound(path); 468 | } 469 | } 470 | 471 | /** 472 | * Performs GET /playSong 473 | * @return an action result 474 | */ 475 | public static Result playSong() 476 | { 477 | try 478 | { 479 | MPD mpd = MpdMonitor.getInstance().getMPD(); 480 | MPDPlayer player = mpd.getMPDPlayer(); 481 | PlayerStatus status = player.getStatus(); 482 | 483 | if (status == STATUS_PLAYING) 484 | player.pause(); else 485 | player.play(); 486 | } 487 | catch (MPDException e) 488 | { 489 | Logger.error("MPD error", e); 490 | flash("error", "Command failed! " + e.getMessage()); 491 | } 492 | 493 | return ok(""); 494 | } 495 | 496 | /** 497 | * Performs GET /toggleRepeat 498 | * @return an action result 499 | */ 500 | public static Result toggleRepeat() 501 | { 502 | try 503 | { 504 | MPD mpd = MpdMonitor.getInstance().getMPD(); 505 | MPDPlayer player = mpd.getMPDPlayer(); 506 | player.setRepeat(!player.isRepeat()); 507 | 508 | Logger.info("Setting repeat: " + player.isRepeat()); 509 | } 510 | catch (MPDException e) 511 | { 512 | Logger.error("MPD error", e); 513 | flash("error", "Command failed! " + e.getMessage()); 514 | } 515 | 516 | return ok(""); 517 | } 518 | 519 | /** 520 | * Performs GET /toggleRandome 521 | * @return an action result 522 | */ 523 | public static Result toggleShuffle() 524 | { 525 | try 526 | { 527 | MPD mpd = MpdMonitor.getInstance().getMPD(); 528 | MPDPlayer player = mpd.getMPDPlayer(); 529 | player.setRandom(!player.isRandom()); 530 | 531 | Logger.info("Setting shuffle: " + player.isRandom()); 532 | } 533 | catch (MPDException e) 534 | { 535 | Logger.error("MPD error", e); 536 | flash("error", "Command failed! " + e.getMessage()); 537 | } 538 | 539 | return ok(""); 540 | } 541 | 542 | /** 543 | * Performs GET /toggleConsuming 544 | * @return an action result 545 | */ 546 | public static Result toggleConsuming() 547 | { 548 | try 549 | { 550 | MPD mpd = MpdMonitor.getInstance().getMPD(); 551 | MPDPlayer player = mpd.getMPDPlayer(); 552 | player.setConsuming(!player.isConsuming()); 553 | 554 | Logger.info("Setting consuming: " + player.isConsuming()); 555 | } 556 | catch (MPDException e) 557 | { 558 | Logger.error("MPD error", e); 559 | flash("error", "Command failed! " + e.getMessage()); 560 | } 561 | 562 | return ok(""); 563 | } 564 | 565 | /** 566 | * Performs GET /toggleSingleMode 567 | * @return an action result 568 | */ 569 | public static Result toggleSingleMode() 570 | { 571 | try 572 | { 573 | MPD mpd = MpdMonitor.getInstance().getMPD(); 574 | MPDPlayer player = mpd.getMPDPlayer(); 575 | player.setSingleMode(!player.isSingleMode()); 576 | 577 | Logger.info("Setting single mode: " + player.isSingleMode()); 578 | } 579 | catch (MPDException e) 580 | { 581 | Logger.error("MPD error", e); 582 | flash("error", "Command failed! " + e.getMessage()); 583 | } 584 | 585 | return ok(""); 586 | } 587 | 588 | /** 589 | * Performs GET /nextSong 590 | * @return an action result 591 | */ 592 | public static Result nextSong() 593 | { 594 | try 595 | { 596 | MPD mpd = MpdMonitor.getInstance().getMPD(); 597 | MPDPlayer player = mpd.getMPDPlayer(); 598 | 599 | player.playNext(); 600 | } 601 | catch (MPDException e) 602 | { 603 | Logger.error("MPD error", e); 604 | flash("error", "Command failed! " + e.getMessage()); 605 | } 606 | 607 | return ok(""); 608 | } 609 | 610 | /** 611 | * Performs GET /prevSong 612 | * @return an action result 613 | */ 614 | public static Result prevSong() 615 | { 616 | try 617 | { 618 | MPD mpd = MpdMonitor.getInstance().getMPD(); 619 | MPDPlayer player = mpd.getMPDPlayer(); 620 | 621 | player.playPrev(); 622 | } 623 | catch (MPDException e) 624 | { 625 | Logger.error("MPD error", e); 626 | flash("error", "Command failed! " + e.getMessage()); 627 | } 628 | 629 | return ok(""); 630 | } 631 | 632 | 633 | /** 634 | * Performs GET /stopSong 635 | * @return an action result 636 | */ 637 | public static Result stopSong() 638 | { 639 | try 640 | { 641 | MPD mpd = MpdMonitor.getInstance().getMPD(); 642 | MPDPlayer player = mpd.getMPDPlayer(); 643 | 644 | player.stop(); 645 | } 646 | catch (MPDException e) 647 | { 648 | Logger.error("MPD error", e); 649 | flash("error", "Command failed! " + e.getMessage()); 650 | } 651 | 652 | return ok(""); 653 | } 654 | 655 | /** 656 | * Performs POST /setsongpos 657 | * @param pos the new song position in seconds 658 | * @return an action result 659 | */ 660 | public static Result setSongPos(int pos) 661 | { 662 | Logger.info("Set song pos " + pos); 663 | 664 | try 665 | { 666 | MPD mpd = MpdMonitor.getInstance().getMPD(); 667 | mpd.getMPDPlayer().seek(pos); 668 | } 669 | catch (MPDPlayerException | MPDConnectionException e) 670 | { 671 | Logger.error("MPD error", e); 672 | flash("error", "Changing song position failed! " + e.getMessage()); 673 | } 674 | 675 | return ok(""); 676 | } 677 | 678 | /** 679 | * Performs POST /volume 680 | * @param volume the new volume level 681 | * @return an action result 682 | */ 683 | public static Result setVolume(int volume) 684 | { 685 | Logger.info("Set volume " + volume); 686 | 687 | try 688 | { 689 | MPD mpd = MpdMonitor.getInstance().getMPD(); 690 | mpd.getMPDPlayer().setVolume(volume); 691 | } 692 | catch (MPDException e) 693 | { 694 | Logger.error("MPD error", e); 695 | flash("error", "Changing volume failed! " + e.getMessage()); 696 | } 697 | 698 | return ok(""); 699 | } 700 | 701 | /** 702 | * Performs POST /selectsong/:pos 703 | * @return an action result 704 | */ 705 | public static Result selectSong(int pos) 706 | { 707 | Logger.info("Play Song " + pos); 708 | 709 | try 710 | { 711 | MPD mpd = MpdMonitor.getInstance().getMPD(); 712 | MPDSong song = mpd.getMPDPlaylist().getSongList().get(pos); 713 | mpd.getMPDPlayer().playId(song); 714 | } 715 | catch (MPDException e) 716 | { 717 | Logger.error("MPD error", e); 718 | flash("error", "Changing song failed! " + e.getMessage()); 719 | } 720 | 721 | return ok(""); 722 | } 723 | 724 | /** 725 | * Performs GET /update 726 | * @return an action result 727 | */ 728 | public static Result updateDb() 729 | { 730 | try 731 | { 732 | MPD mpd = MpdMonitor.getInstance().getMPD(); 733 | 734 | mpd.getMPDAdmin().updateDatabase(); 735 | 736 | flash("success", "Database updated!"); 737 | } 738 | catch (MPDException e) 739 | { 740 | Logger.error("MPD error", e); 741 | flash("error", "Updating database failed!" + e.getMessage()); 742 | } 743 | 744 | return browseDb(0, "name", "asc", ""); // defaults - same as in routes files 745 | } 746 | 747 | /** 748 | * Remove entry from playlist 749 | * @param id the playlist entry pos 750 | * @return an action result 751 | */ 752 | public static Result remove(int id) 753 | { 754 | Logger.info("Removing entry from playlist: " + id); 755 | 756 | try 757 | { 758 | MPD mpd = MpdMonitor.getInstance().getMPD(); 759 | MPDPlaylist mpdPlaylist = mpd.getMPDPlaylist(); 760 | MPDSong song = mpdPlaylist.getSongList().get(id); 761 | 762 | mpdPlaylist.removeSong(song); 763 | } 764 | catch (MPDException e) 765 | { 766 | Logger.error("MPD error", e); 767 | flash("error", "Removing entry from playlist failed! " + e.getMessage()); 768 | } 769 | 770 | return ok(""); 771 | } 772 | 773 | /** 774 | * Render info page GET /info 775 | * @return the info page 776 | */ 777 | public static Result info() 778 | { 779 | try 780 | { 781 | MPD mpd = MpdMonitor.getInstance().getMPD(); 782 | return ok(info.render(mpd)); 783 | } 784 | catch (MPDException e) 785 | { 786 | Logger.error("MPD error", e); 787 | flash("error", e.getMessage()); 788 | return ok(main.render(null, null)); 789 | } 790 | } 791 | 792 | /** 793 | * Toggle MPD output 794 | * @param id the output id 795 | * @return ok 796 | */ 797 | public static Result toggleOutput(int id, boolean check) 798 | { 799 | try 800 | { 801 | MPD mpd = MpdMonitor.getInstance().getMPD(); 802 | MPDAdmin admin = mpd.getMPDAdmin(); 803 | List outputs = (List)admin.getOutputs(); 804 | 805 | if (id < 0 || id >= outputs.size()) 806 | throw new MPDAdminException("Output ID invalid", new IllegalArgumentException()); 807 | 808 | MPDOutput output = outputs.get(id); 809 | 810 | if (check) 811 | admin.enableOutput(output); else 812 | admin.disableOutput(output); 813 | } 814 | catch (MPDException e) 815 | { 816 | Logger.error("MPD error", e); 817 | flash("error", e.getMessage()); 818 | return notFound(e.getMessage()); 819 | } 820 | 821 | return ok(""); 822 | } 823 | } 824 | -------------------------------------------------------------------------------- /app/controllers/Credential.java: -------------------------------------------------------------------------------- 1 | 2 | package controllers; 3 | 4 | import helper.Digester; 5 | 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.Objects; 8 | 9 | import play.Configuration; 10 | import play.Play; 11 | import play.data.Form; 12 | import play.mvc.Controller; 13 | import play.mvc.Result; 14 | import views.html.login; 15 | 16 | /** 17 | * Deals with user authorization 18 | * @author Martin Steiger 19 | */ 20 | public class Credential extends Controller 21 | { 22 | /** 23 | * Login credentials 24 | */ 25 | public static class Login 26 | { 27 | public String email; 28 | public String password; 29 | 30 | /** 31 | * This method seems to be called in authenticate() 32 | * when bindFromRequest() is invoked. The return value 33 | * seems to be some kind of error message 34 | * @return null if successful or an error string 35 | */ 36 | public String validate() 37 | { 38 | try 39 | { 40 | String digest = Digester.digest(password); 41 | 42 | Configuration config = Play.application().configuration(); 43 | 44 | String username = config.getString("login.user"); 45 | String userpass = config.getString("login.pass"); 46 | 47 | if (Objects.equals(username, email) && 48 | Objects.equals(userpass, digest)) 49 | return null; 50 | 51 | return "Invalid user or password"; 52 | } 53 | catch (NoSuchAlgorithmException e) 54 | { 55 | return "No valid encryption algorithm implemented"; 56 | } 57 | } 58 | 59 | } 60 | 61 | /** 62 | * Performs GET /login 63 | * @return an action result 64 | */ 65 | public static Result login() 66 | { 67 | return ok(login.render(Form.form(Login.class))); 68 | } 69 | 70 | /** 71 | * Performs POST /login 72 | * @return an action result 73 | */ 74 | public static Result authenticate() 75 | { 76 | Form loginForm = Form.form(Login.class).bindFromRequest(); 77 | if (loginForm.hasErrors()) 78 | { 79 | return unauthorized(login.render(loginForm)); 80 | } 81 | else 82 | { 83 | session().clear(); 84 | session("email", loginForm.get().email); 85 | return redirect(routes.Application.index()); 86 | } 87 | } 88 | 89 | /** 90 | * Performs GET /logout 91 | * @return an action result 92 | */ 93 | public static Result logout() 94 | { 95 | session().clear(); 96 | flash("success", "You've been logged out"); 97 | return redirect(routes.Credential.login()); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /app/controllers/Secured.java: -------------------------------------------------------------------------------- 1 | 2 | package controllers; 3 | 4 | import play.mvc.Http; 5 | import play.mvc.Result; 6 | import play.mvc.Security; 7 | 8 | /** 9 | * Handles authentication. 10 | * @author Martin Steiger 11 | */ 12 | public class Secured extends Security.Authenticator 13 | { 14 | @Override 15 | public String getUsername(Http.Context ctx) 16 | { 17 | return ctx.session().get("email"); 18 | } 19 | 20 | @Override 21 | public Result onUnauthorized(Http.Context ctx) 22 | { 23 | return redirect(routes.Credential.login()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/helper/ConstantFuture.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.util.concurrent.ExecutionException; 21 | import java.util.concurrent.Future; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.concurrent.TimeoutException; 24 | 25 | /** 26 | * A {@link Future} that represents a constant value 27 | * @author Martin Steiger 28 | */ 29 | public final class ConstantFuture implements Future 30 | { 31 | private final T value; 32 | 33 | /** 34 | * @param value the value 35 | */ 36 | public ConstantFuture(T value) 37 | { 38 | this.value = value; 39 | } 40 | 41 | @Override 42 | public boolean cancel(boolean mayInterruptIfRunning) 43 | { 44 | return false; // because isDone() is true 45 | } 46 | 47 | @Override 48 | public boolean isCancelled() 49 | { 50 | return false; 51 | } 52 | 53 | @Override 54 | public boolean isDone() 55 | { 56 | return true; 57 | } 58 | 59 | @Override 60 | public T get() throws InterruptedException, ExecutionException 61 | { 62 | return value; 63 | } 64 | 65 | @Override 66 | public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 67 | { 68 | return get(); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /app/helper/DefaultPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.util.List; 21 | 22 | import com.avaje.ebean.Page; 23 | import com.avaje.ebean.PagingList; 24 | 25 | /** 26 | * The "default" implementation of Page. 27 | * It redirects all calls to {@link PagingList} 28 | * @author Martin Steiger 29 | */ 30 | public class DefaultPage implements Page 31 | { 32 | private PagingList pagingList; 33 | private int index; 34 | 35 | DefaultPage(PagingList pagingList, int index) 36 | { 37 | this.pagingList = pagingList; 38 | this.index = index; 39 | } 40 | 41 | @Override 42 | public Page prev() 43 | { 44 | return pagingList.getPage(index - 1); 45 | } 46 | 47 | @Override 48 | public Page next() 49 | { 50 | return pagingList.getPage(index + 1); 51 | } 52 | 53 | @Override 54 | public boolean hasPrev() 55 | { 56 | return index > 0; 57 | } 58 | 59 | @Override 60 | public boolean hasNext() 61 | { 62 | return index < pagingList.getTotalPageCount() - 1; 63 | } 64 | 65 | @Override 66 | public int getTotalRowCount() 67 | { 68 | return pagingList.getTotalRowCount(); 69 | } 70 | 71 | @Override 72 | public int getTotalPageCount() 73 | { 74 | return pagingList.getTotalPageCount(); 75 | } 76 | 77 | @Override 78 | public int getPageIndex() 79 | { 80 | return index; 81 | } 82 | 83 | @Override 84 | public String getDisplayXtoYofZ(String to, String of) 85 | { 86 | int first = index * pagingList.getPageSize() + 1; 87 | int last = Math.min(getTotalRowCount(), (index + 1) * pagingList.getPageSize()); 88 | 89 | int total = getTotalRowCount(); 90 | 91 | return first+to+last+of+total; 92 | } 93 | 94 | @Override 95 | public List getList() 96 | { 97 | int from = index * pagingList.getPageSize(); 98 | int to = Math.min(getTotalRowCount(), (index + 1) * pagingList.getPageSize()); 99 | 100 | return pagingList.getAsList().subList(from, to); 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /app/helper/DefaultPagingList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.Future; 24 | 25 | import play.Logger; 26 | 27 | import com.avaje.ebean.Page; 28 | import com.avaje.ebean.PagingList; 29 | 30 | /** 31 | * TODO Type description 32 | * @author Martin Steiger 33 | */ 34 | public class DefaultPagingList implements PagingList 35 | { 36 | private List list; 37 | private int pageSize; 38 | 39 | public DefaultPagingList(List list, int pageSize) 40 | { 41 | this.list = list; 42 | this.pageSize = pageSize; 43 | } 44 | 45 | @Override 46 | public List getAsList() 47 | { 48 | return Collections.unmodifiableList(list); 49 | } 50 | 51 | @Override 52 | public Future getFutureRowCount() 53 | { 54 | return new ConstantFuture(list.size()); 55 | } 56 | 57 | @Override 58 | public Page getPage(int index) 59 | { 60 | return new DefaultPage(this, index); 61 | } 62 | 63 | @Override 64 | public int getPageSize() 65 | { 66 | return pageSize; 67 | } 68 | 69 | @Override 70 | public int getTotalPageCount() 71 | { 72 | int rowCount = getTotalRowCount(); 73 | if (rowCount == 0) 74 | return 0; else 75 | return ((rowCount-1) / getPageSize()) + 1; 76 | } 77 | 78 | @Override 79 | public int getTotalRowCount() 80 | { 81 | try 82 | { 83 | return getFutureRowCount().get(); 84 | } 85 | catch (InterruptedException | ExecutionException e) 86 | { 87 | Logger.warn("Error getting row count", e); 88 | return 0; 89 | } 90 | } 91 | 92 | @Override 93 | public void refresh() 94 | { 95 | // ignore 96 | } 97 | 98 | @Override 99 | public PagingList setFetchAhead(boolean fetchAhead) 100 | { 101 | return this; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /app/helper/Digester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.nio.charset.Charset; 21 | import java.security.MessageDigest; 22 | import java.security.NoSuchAlgorithmException; 23 | 24 | import org.apache.commons.codec.binary.Base64; 25 | 26 | import play.Configuration; 27 | import play.Play; 28 | 29 | /** 30 | * Digests string data in the form
31 | * result = Base64(SHA(str + application.secret)) 32 | * @author Martin Steiger 33 | */ 34 | public class Digester 35 | { 36 | /** 37 | * @param str the input string 38 | * @return the digested string 39 | * @throws NoSuchAlgorithmException encryption algorithm not installed 40 | */ 41 | public static String digest(String str) throws NoSuchAlgorithmException 42 | { 43 | Configuration config = Play.application().configuration(); 44 | 45 | String salt = config.getString("application.secret"); 46 | 47 | String saltedStr = str + salt; 48 | 49 | MessageDigest md = MessageDigest.getInstance("SHA"); 50 | md.update(saltedStr.getBytes()); 51 | 52 | byte byteData[] = md.digest(); 53 | 54 | byte[] base64 = Base64.encodeBase64(byteData); 55 | 56 | String result = new String(base64, Charset.defaultCharset()); 57 | 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/helper/EmptyPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.util.Collections; 21 | import java.util.List; 22 | 23 | import com.avaje.ebean.Page; 24 | 25 | /** 26 | * An empty implementation of Page. 27 | * @author Martin Steiger 28 | */ 29 | public final class EmptyPage implements Page 30 | { 31 | @Override 32 | public Page prev() 33 | { 34 | // maybe null, but in doubt avoid NPE 35 | return this; 36 | } 37 | 38 | @Override 39 | public Page next() 40 | { 41 | // maybe null, but in doubt avoid NPE 42 | return this; 43 | } 44 | 45 | @Override 46 | public boolean hasPrev() 47 | { 48 | return false; 49 | } 50 | 51 | @Override 52 | public boolean hasNext() 53 | { 54 | return false; 55 | } 56 | 57 | @Override 58 | public int getTotalRowCount() 59 | { 60 | return 0; 61 | } 62 | 63 | @Override 64 | public int getTotalPageCount() 65 | { 66 | // not sure whether 0 is a valid value, could be 1 also 67 | return 0; 68 | } 69 | 70 | @Override 71 | public int getPageIndex() 72 | { 73 | return 0; 74 | } 75 | 76 | @Override 77 | public String getDisplayXtoYofZ(String to, String of) 78 | { 79 | return "- " + to + " - " + of + " -"; 80 | } 81 | 82 | @Override 83 | public List getList() 84 | { 85 | return Collections.emptyList(); 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /app/helper/MpdMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.net.UnknownHostException; 21 | 22 | import org.bff.javampd.MPD; 23 | import org.bff.javampd.exception.MPDConnectionException; 24 | import org.bff.javampd.exception.MPDException; 25 | import org.bff.javampd.monitor.MPDStandAloneMonitor; 26 | 27 | import play.Configuration; 28 | import play.Logger; 29 | import play.Play; 30 | 31 | /** 32 | * A singleton that represents the connection to MPD 33 | * @author Martin Steiger 34 | */ 35 | public class MpdMonitor 36 | { 37 | private static MpdMonitor instance = null; 38 | private MPDStandAloneMonitor monitor; 39 | private Thread thread; 40 | private MPD mpd; 41 | 42 | /** 43 | * @return returns the current instance or creates it if necessary 44 | * @throws MPDConnectionException if connection to MPD fails 45 | */ 46 | public static MpdMonitor getInstance() throws MPDConnectionException 47 | { 48 | // TODO: implement circuit breaker pattern 49 | 50 | if (instance == null) 51 | { 52 | try 53 | { 54 | instance = new MpdMonitor(); 55 | } 56 | catch (UnknownHostException e) 57 | { 58 | throw new MPDConnectionException(e); 59 | } 60 | } 61 | 62 | return instance; 63 | } 64 | 65 | private MpdMonitor() throws UnknownHostException, MPDConnectionException 66 | { 67 | Configuration config = Play.application().configuration(); 68 | 69 | String hostname = config.getString("mpd.hostname"); 70 | int port = config.getInt("mpd.port"); 71 | String password = config.getString("mpd.password"); 72 | int timeout = config.getInt("mpd.timeout", 10) * 1000; 73 | 74 | Logger.info("Connecting to MPD"); 75 | 76 | mpd = new MPD(hostname, port, password, timeout); 77 | monitor = new MPDStandAloneMonitor(mpd, 1000); 78 | 79 | thread = new Thread(monitor); 80 | thread.start(); 81 | } 82 | 83 | /** 84 | * @return the MPD instance 85 | */ 86 | public MPD getMPD() 87 | { 88 | return mpd; 89 | } 90 | 91 | /** 92 | * @return the MPD monitor instance 93 | */ 94 | public MPDStandAloneMonitor getMonitor() 95 | { 96 | return monitor; 97 | } 98 | 99 | /** 100 | * Stops the monitoring and waits for the query thread to terminate 101 | */ 102 | public void stop() 103 | { 104 | monitor.stop(); 105 | 106 | try 107 | { 108 | thread.join(); 109 | mpd.close(); 110 | } 111 | catch (InterruptedException e) 112 | { 113 | Logger.warn("Monitor thread has not terminated"); 114 | } 115 | catch (MPDException e) 116 | { 117 | Logger.warn("Error closing connection"); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/helper/UrlParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012-2013 Martin Steiger 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package helper; 19 | 20 | import java.io.BufferedReader; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.InputStreamReader; 24 | import java.net.URL; 25 | import java.net.URLConnection; 26 | import java.nio.charset.Charset; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | import org.bff.javampd.MPDFile; 31 | 32 | /** 33 | * Parses a given URL and returns 34 | * a list of referenced mpd files 35 | * @author Martin Steiger 36 | */ 37 | public class UrlParser 38 | { 39 | /** 40 | * @param url the url 41 | * @return a list of referenced mpd files 42 | * @throws IOException if the url or its contents cannot be read 43 | * @throws IllegalArgumentException if the url or its contents are invalid 44 | */ 45 | public List getAll(String url) throws IOException 46 | { 47 | List list = new ArrayList<>(); 48 | 49 | int dp = url.lastIndexOf('.'); 50 | 51 | if (dp == -1) 52 | throw new IllegalArgumentException("URL does not have a valid file ending"); 53 | 54 | String ext = url.substring(dp + 1); 55 | ext = ext.trim().toLowerCase(); 56 | 57 | switch (ext) 58 | { 59 | case ".m3u": 60 | parsePlaylistM3u(url); 61 | break; 62 | 63 | case ".mp3": 64 | list.add(createSingle(url)); 65 | break; 66 | 67 | // TODO: check other file types or test with "ffmpeg -i " 68 | } 69 | 70 | return list; 71 | } 72 | 73 | private MPDFile createSingle(String url) 74 | { 75 | String name = ""; 76 | int from = url.lastIndexOf('/'); 77 | 78 | if (from != -1) 79 | name = url.substring(from); 80 | 81 | MPDFile file = new MPDFile(); 82 | file.setDirectory(false); 83 | file.setPath(url); 84 | file.setName(name); 85 | 86 | return file; 87 | } 88 | 89 | private void parsePlaylistM3u(String url) throws IOException 90 | { 91 | URL website = new URL(url); 92 | URLConnection conn = website.openConnection(); 93 | 94 | int size = conn.getContentLength(); 95 | 96 | if (size > 256 * 1024) 97 | throw new IllegalArgumentException("File suspiciously big"); 98 | 99 | try (InputStream is = conn.getInputStream()) 100 | { 101 | InputStreamReader read = new InputStreamReader(is, Charset.defaultCharset()); 102 | BufferedReader reader = new BufferedReader(read); 103 | 104 | String line; 105 | while ((line = reader.readLine()) != null) 106 | { 107 | int comIdx = line.indexOf('#'); 108 | if (comIdx >= 0) 109 | line = line.substring(0, comIdx); 110 | line = line.trim(); 111 | 112 | if (!line.isEmpty()) 113 | getAll(line); 114 | } 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/models/Database.java: -------------------------------------------------------------------------------- 1 | 2 | package models; 3 | 4 | import helper.DefaultPagingList; 5 | import helper.MpdMonitor; 6 | 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.Comparator; 10 | import java.util.List; 11 | 12 | import org.bff.javampd.MPD; 13 | import org.bff.javampd.MPDDatabase; 14 | import org.bff.javampd.exception.MPDException; 15 | import org.bff.javampd.objects.MPDItem; 16 | import org.bff.javampd.objects.MPDSong; 17 | 18 | import com.avaje.ebean.Page; 19 | 20 | /** 21 | * MPD Database page 22 | */ 23 | public class Database 24 | { 25 | /** 26 | * Returns a page of the current playlist 27 | * @param page Page to display 28 | * @param pageSize Number of computers per page 29 | * @param sortBy Computer property used for sorting 30 | * @param order Sort order (either or asc or desc) 31 | * @param filter Filter applied on the name column 32 | * @return the page with all relevant entries 33 | * @throws MPDException if MPD reports an error 34 | */ 35 | public static Page getSongs(int page, final int pageSize, final String sortBy, final String order, String filter) throws MPDException 36 | { 37 | MPD mpd = MpdMonitor.getInstance().getMPD(); 38 | 39 | MPDDatabase database = mpd.getMPDDatabase(); 40 | Collection hits; 41 | 42 | if (filter == null || filter.isEmpty()) 43 | hits = database.listAllSongs(); else 44 | hits = database.searchAny(filter); 45 | 46 | // HACK: unfortunately casting is necessary here 47 | List songs = (List) hits; 48 | 49 | DefaultPagingList pagingList = new DefaultPagingList<>(songs, pageSize); 50 | 51 | Collections.sort(songs, new Comparator() 52 | { 53 | private int cmp(String c1, String c2) 54 | { 55 | if (c1 == null) 56 | { 57 | if (c2 == null) 58 | return 0; 59 | 60 | return -1; 61 | } 62 | 63 | if (c2 == null) 64 | return 1; 65 | 66 | return c1.compareToIgnoreCase(c2); 67 | } 68 | 69 | private int cmp(MPDItem c1, MPDItem c2) 70 | { 71 | if (c1 == null) 72 | { 73 | if (c2 == null) 74 | return 0; 75 | 76 | return -1; 77 | } 78 | 79 | if (c2 == null) 80 | return 1; 81 | 82 | return cmp(c1.getName(), c2.getName()); 83 | } 84 | 85 | @Override 86 | public int compare(MPDSong o1, MPDSong o2) 87 | { 88 | int result = 0; 89 | 90 | switch (sortBy.toLowerCase()) 91 | { 92 | case "title": 93 | result = cmp(o1.getTitle(), o2.getTitle()); 94 | break; 95 | 96 | case "artist": 97 | result = cmp(o1.getArtist(), o2.getArtist()); 98 | break; 99 | 100 | case "album": 101 | result = cmp(o1.getAlbum(), o2.getAlbum()); 102 | break; 103 | 104 | case "file": 105 | result = cmp(o1.getFile(), o2.getFile()); 106 | break; 107 | } 108 | 109 | if ("desc".equals(order)) 110 | result = -result; 111 | 112 | return result; 113 | } 114 | }); 115 | 116 | return pagingList.getPage(page); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/models/Playlist.java: -------------------------------------------------------------------------------- 1 | 2 | package models; 3 | 4 | import helper.DefaultPagingList; 5 | import helper.MpdMonitor; 6 | 7 | import java.util.List; 8 | 9 | import org.bff.javampd.MPD; 10 | import org.bff.javampd.MPDPlaylist; 11 | import org.bff.javampd.exception.MPDException; 12 | import org.bff.javampd.objects.MPDSong; 13 | 14 | import com.avaje.ebean.Page; 15 | 16 | /** 17 | * MPD Playlist 18 | */ 19 | public class Playlist 20 | { 21 | /** 22 | * Returns a page of the current playlist 23 | * @param page Page to display 24 | * @param pageSize Number of computers per page 25 | * @return a page with all relevant entries 26 | * @throws MPDException if MPD reports an error 27 | */ 28 | public static Page getSongs(int page, final int pageSize) throws MPDException 29 | { 30 | MPD mpd = MpdMonitor.getInstance().getMPD(); 31 | MPDPlaylist playlist = mpd.getMPDPlaylist(); 32 | 33 | List songs = playlist.getSongList(); 34 | 35 | DefaultPagingList pagingList = new DefaultPagingList<>(songs, pageSize); 36 | 37 | return pagingList.getPage(page); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/models/User.java: -------------------------------------------------------------------------------- 1 | package models; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.Id; 5 | 6 | import play.db.ebean.Model; 7 | 8 | @Entity 9 | public class User extends Model 10 | { 11 | private static final long serialVersionUID = 1L; 12 | 13 | @Id 14 | public String email; 15 | public String firstName; 16 | public String lastName; 17 | public String password; 18 | 19 | public static Finder find = new Finder(String.class, User.class); 20 | 21 | public User(String email, String firstName, String lastName, String password) 22 | { 23 | this.email = email; 24 | this.firstName = firstName; 25 | this.lastName = lastName; 26 | this.password = password; 27 | } 28 | 29 | public static User authenticate(String email, String password) 30 | { 31 | return find.where().eq("email", email).eq("password", password).findUnique(); 32 | } 33 | 34 | @Override 35 | public int hashCode() 36 | { 37 | return email.hashCode(); 38 | } 39 | 40 | public static int count() 41 | { 42 | return find.findRowCount(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/views/addweb.scala.html: -------------------------------------------------------------------------------- 1 | @() 2 | 3 | 4 | 32 | 33 | 70 | -------------------------------------------------------------------------------- /app/views/database.scala.html: -------------------------------------------------------------------------------- 1 | @(currentPage: com.avaje.ebean.Page[org.bff.javampd.objects.MPDSong], playlistfiles: List[String], currentSortBy: String, currentOrder: String, currentFilter: String) 2 | 3 | @**************************************** 4 | * Helper generating navigation links * 5 | ****************************************@ 6 | @link(newPage:Int, newSortBy:String) = @{ 7 | 8 | var sortBy = currentSortBy 9 | var order = currentOrder 10 | 11 | if(newSortBy != null) { 12 | sortBy = newSortBy 13 | if(currentSortBy == newSortBy) { 14 | if(currentOrder == "asc") { 15 | order = "desc" 16 | } else { 17 | order = "asc" 18 | } 19 | } else { 20 | order = "asc" 21 | } 22 | } 23 | 24 | // Generate the link 25 | routes.Application.browseDb(newPage, sortBy, order, currentFilter) 26 | 27 | } 28 | 29 | @********************************** 30 | * Helper generating table headers * 31 | ***********************************@ 32 | @header(key:String, title:String) = { 33 | 34 | @title 35 | @if(currentSortBy == key) @{if(currentOrder == "asc") { 36 | } else { 37 | } } 38 | 39 | } 40 | 41 | @main(routes.Application.browseDb()) { 42 | 43 |

@Messages("database.list.title", currentPage.getTotalRowCount)

44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | @header("title", "Title") 61 | @header("artist", "Artist") 62 | @header("album", "Album") 63 | @header("file", "File") 64 | 65 | 66 | 67 | 68 | 69 | @for(song <- currentPage.getList) { 70 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | } 83 | 84 | 85 |
@song.getTitle()@song.getArtist()@song.getAlbum()@song.getFile() 76 | 79 | 80 |
86 | 87 |
    88 | @if(currentPage.hasPrev) { 89 |
  • 90 | « 91 |
  • 92 | } else { 93 |
  • 94 | « 95 |
  • 96 | } 97 | 98 | @for(idx <- Math.max(0, currentPage.getPageIndex - 2) to Math.min(currentPage.getTotalPageCount - 1, currentPage.getPageIndex + 2)) { 99 | 100 |
  • 101 | @idx 102 |
  • 103 | } 104 | 105 | @if(currentPage.hasNext) { 106 |
  • 107 | » 108 |
  • 109 | } else { 110 |
  • 111 | » 112 |
  • 113 | } 114 |
115 | 116 | 117 | Update Database 118 | 119 | 120 | 121 | } 122 | 123 | 124 | -------------------------------------------------------------------------------- /app/views/info.scala.html: -------------------------------------------------------------------------------- 1 | @(mpd: org.bff.javampd.MPD) 2 | 3 | @********************@ 4 | 5 | @format_time(secs: Long) = @{ 6 | val hr = secs / 3600; 7 | val min = (secs / 60) % 60; 8 | val sec = secs % 60; 9 | 10 | "%d:%02d:%02d".format(hr, min, sec); 11 | } 12 | 13 | @dts = @{mpd.getMPDPlayer().getAudioDetails} 14 | 15 | @main(routes.Application.info()) { 16 | 17 |

@Messages("project.name")

18 | 19 |

Outputs

20 | 21 |
22 | @for(output <- mpd.getMPDAdmin.getOutputs) { 23 |
@output.getName
24 |
25 | } 26 |
27 | 28 | 43 | 44 |

Public info

45 | 46 |
47 | 48 |
@Messages("info.uptime")
49 |
@format_time(mpd.getUptime)
50 | 51 |
Sample Rate
52 |
@if(dts != null) {@dts.getSampleRate Hz} else {-}
53 | 54 |
Precision
55 |
@if(dts != null) {@dts.getBits Bit} else {-}
56 | 57 |
Channels
58 |
@if(dts != null) {@dts.getChannels} else {-}
59 | 60 |
Play!
61 |
@play.core.PlayVersion.current
62 | 63 |
Scala
64 |
@play.core.PlayVersion.scalaVersion
65 | 66 |
MPD
67 |
@mpd.getVersion
68 |
69 | 70 |

Internal info

71 |
72 | @for((key, value) <- mpd.getStatus) {
@key
@value
} 73 |
74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/views/login.scala.html: -------------------------------------------------------------------------------- 1 | @(form: Form[Credential.Login]) 2 | 3 | @main(routes.Credential.login()) { 4 | 5 | @if(form.hasGlobalErrors) { 6 |
@form.globalError.message
7 | } 8 | 9 |
10 | 11 | @helper.form(routes.Credential.authenticate) { 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |

24 | 25 |

26 | } 27 |
28 | } 29 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(curlink: play.api.mvc.Call)(content: Html) 2 | 3 | @********************************** 4 | * Helper generating table headers * 5 | ***********************************@ 6 | @entry(link : Call, title:String) = { 7 |
  • @title
  • 8 | 9 | } 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Play! MPC 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 |
    51 | 76 |
    77 | 78 |
    79 |
    80 | 84 | 85 | 89 | 90 | 93 | 94 | 97 | 98 | @content 99 |
    100 |
    101 | 102 |