Example #1
0
 public function getCurrentlyPlayedTrack()
 {
     $status = $this->mpd('status');
     $listpos = isset($status['song']) ? $status['song'] : 0;
     $files = $this->mpd('playlist');
     $listlength = $status['playlistlength'];
     if ($listlength > 0) {
         $track = \Slimpd\Track::getInstanceByPath($files[$listpos]);
         // obviously the played track is not imported in slimpd-database...
         // TODO: trigger whole update procedure for this single track
         // for now we simply create a dummy instance
         if ($track === NULL) {
             $track = new \Slimpd\Track();
             $track->setRelativePath($files[$listpos]);
             $track->setRelativePathHash(getFilePathHash($files[$listpos]));
         }
         return $track;
     }
     return NULL;
 }
Example #2
0
 public function __construct($arg)
 {
     $config = \Slim\Slim::getInstance()->config['mpd'];
     $arg = join(DS, $arg);
     if (is_numeric($arg) === TRUE) {
         $t = \Slimpd\Track::getInstanceByAttributes(array('id' => (int) $arg));
         if (is_object($t) === TRUE) {
             $this->absolutePath = $config['musicdir'] . $t->getRelativePath();
             $this->fingerprint = $t->getFingerprint();
             $this->ext = $t->getAudioDataFormat();
         }
     } else {
         if (is_file($config['alternative_musicdir'] . $arg) === TRUE) {
             $arg = $config['alternative_musicdir'] . $arg;
         }
         if (is_file($config['musicdir'] . $arg) === TRUE) {
             $arg = $config['musicdir'] . $arg;
         }
         if (is_file($arg) === TRUE) {
             $this->absolutePath = $arg;
             $this->ext = pathinfo($arg, PATHINFO_EXTENSION);
         }
     }
     if (is_file($this->absolutePath) === FALSE) {
         // TODO: should we serve a default waveform svg?
         return NULL;
     }
     if (!preg_match("/^([a-f0-9]){32}\$/", $this->fingerprint)) {
         // extract the fingerprint
         if ($fp = \Slimpd\Importer::extractAudioFingerprint($this->absolutePath)) {
             $this->fingerprint = $fp;
         } else {
             # TODO: handle missing fingerprint
             die('invalid fingerprint: ' . $this->absolutePath);
         }
     }
     $this->setPeakFilePath();
     if (is_file($this->peakValuesFilePath) === FALSE) {
         session_write_close();
         // do not block other requests during processing
         $tmpFileName = APP_ROOT . 'cache' . DS . $this->ext . '.' . $this->fingerprint . '.';
         if (is_file($tmpFileName . 'mp3') === TRUE || is_file($tmpFileName . 'wav') === TRUE) {
             # make sure same file isnt processed twice simultaneously by different client-requests...
             # TODO: send a message to client for requesting waveform again after a few seconds?
             # or sleep here until tmp files had been deleted?
             # or redirect to same route with increasing counter until a maximum is reached
             return NULL;
         }
         $this->generatePeakFile();
     }
 }
Example #3
0
 public function cmd($cmd, $item = NULL)
 {
     // TODO: check access
     // @see: http://www.musicpd.org/doc/protocol/playback_commands.html
     // validate commands
     switch ($cmd) {
         case 'update':
             $config = \Slim\Slim::getInstance()->config['mpd'];
             # TODO: move 'disallow_full_database_update' from config.ini to user-previleges
             if (!$item && $config['disallow_full_database_update'] == '0') {
                 return $this->mpd($cmd);
             }
             if (is_string($item) === TRUE) {
                 $item = $item;
             }
             if (is_array($item) === TRUE) {
                 $item = join(DS, $item);
             }
             if (is_file($config['musicdir'] . $item) === FALSE && is_dir($config['musicdir'] . $item) === FALSE) {
                 // error - invalid $item
                 return FALSE;
             }
             \Slimpd\importer::queDirectoryUpdate($item);
             return $this->mpd('update "' . str_replace("\"", "\\\"", $item) . '"');
             // tracks that hasnt been importet in mpd database have to get inserted befor playing
             // TODO: should this also trigger a mysql-db-insert of this track?
             // TODO: should we allow this also for directories or limit this function to single music files?
         // tracks that hasnt been importet in mpd database have to get inserted befor playing
         // TODO: should this also trigger a mysql-db-insert of this track?
         // TODO: should we allow this also for directories or limit this function to single music files?
         case 'updateMpdAndPlay':
             $config = \Slim\Slim::getInstance()->config['mpd'];
             # TODO: move 'disallow_full_database_update' from config.ini to user-previleges
             if (!$item && $config['disallow_full_database_update'] == '0') {
                 return $this->mpd($cmd);
             }
             if (is_string($item) === TRUE) {
                 $item = $item;
             }
             if (is_array($item) === TRUE) {
                 $item = join(DS, $item);
             }
             if (is_file($config['musicdir'] . $item) === FALSE) {
                 // error - invalid $item or $item is a directory
                 # TODO: send warning to client?
                 return FALSE;
             }
             // now we have to find the nearest parent directory that already exists in mpd-database
             $closestExistingItemInMpdDatabase = $this->findClosestExistingItem($item);
             // special case when we try to play a single new file (without parent-dir) out of mpd root
             if ($closestExistingItemInMpdDatabase === NULL && $config['disallow_full_database_update'] == '1') {
                 # TODO: send warning to client?
                 return FALSE;
             }
             if ($closestExistingItemInMpdDatabase !== $item) {
                 $this->cmd('update', $closestExistingItemInMpdDatabase);
                 // TODO: replace dirty sleep with mpd-status-poll and continue as soon as the item is imported
                 sleep(1);
             }
             return $this->cmd('addSelect', $item);
         case 'seekPercent':
             $currentSong = $this->mpd('currentsong');
             $cmd = 'seek ' . $currentSong['Pos'] . ' ' . round($item * ($currentSong['Time'] / 100)) . '';
             $this->mpd($cmd);
         case 'status':
         case 'stats':
         case 'currentsong':
             return $this->mpd($cmd);
         case 'play':
         case 'pause':
         case 'stop':
         case 'previous':
         case 'next':
             $this->mpd($cmd);
             break;
         case 'toggleRepeat':
             $status = $this->mpd('status');
             $this->mpd('repeat ' . (int) ($status['repeat'] xor 1));
             break;
         case 'toggleRandom':
             $status = $this->mpd('status');
             $this->mpd('random ' . (int) ($status['random'] xor 1));
             break;
         case 'toggleConsume':
             $status = $this->mpd('status');
             $this->mpd('consume ' . (int) ($status['consume'] xor 1));
             break;
         case 'playlistStatus':
             $this->playlistStatus();
             break;
         case 'addSelect':
             # TODO: general handling of position to add
             # TODO: general handling of playing immediately or simply appending to playlist
             $path = '';
             if (is_string($item) === TRUE) {
                 $path = $item;
             }
             if (is_numeric($item) === TRUE) {
                 $path = \Slimpd\Track::getInstanceByAttributes(array('id' => $item))->getRelativePath();
             }
             if (is_array($item) === TRUE) {
                 $path = join(DS, $item);
             }
             if (is_file(\Slim\Slim::getInstance()->config['mpd']['musicdir'] . $path) === TRUE) {
                 $this->mpd('addid "' . str_replace("\"", "\\\"", $path) . '" 0');
                 $this->mpd('play 0');
             } else {
                 // trailing slash on directories will not work - lets remove it
                 if (substr($path, -1) === DS) {
                     $path = substr($path, 0, -1);
                 }
                 $this->mpd('add "' . str_replace("\"", "\\\"", $path) . '"');
             }
             break;
         case 'playIndex':
             $this->mpd('play ' . $item);
             break;
         case 'deleteIndex':
             $this->mpd('delete ' . $item);
             break;
         case 'clearPlaylist':
             $this->mpd('clear');
             break;
         case 'clearPlaylistNotCurrent':
             $status = $this->mpd('status');
             $songId = isset($status['songid']) ? $status['songid'] : 0;
             if ($songId > 0) {
                 // move current song to first position
                 $this->mpd('moveid ' . $songId . ' 0');
                 $playlistLength = isset($status['playlistlength']) ? $status['playlistlength'] : 0;
                 if ($playlistLength > 1) {
                     $this->mpd('delete 1:' . $playlistLength);
                 }
             }
             break;
         case 'playSelect':
             //		playSelect();
         //		playSelect();
         case 'addSelect':
             //		addSelect();
         //		addSelect();
         case 'deleteIndexAjax':
             //	deleteIndexAjax();
         //	deleteIndexAjax();
         case 'deletePlayed':
             //		deletePlayed();
         //		deletePlayed();
         case 'volumeImageMap':
             //	volumeImageMap();
         //	volumeImageMap();
         case 'toggleMute':
             //		toggleMute();
         //		toggleMute();
         case 'loopGain':
             //			loopGain();
         //			loopGain();
         case 'playlistTrack':
             //	playlistTrack();
             die('sorry, not implemented yet');
             break;
         default:
             die('unsupported');
             break;
     }
 }
Example #4
0
 public function run()
 {
     // first of all - try to guess if this dir should be
     // treated as an album or as a bunch of loose tracks
     // further this method is adding score to several attributes which will be migrated to production db-table
     $this->setHandleAsAlbum();
     #print_r($this->r);
     cliLog("handleAsAlbumScore " . $this->handleAsAlbumScore, 3, 'purple');
     #die();
     #if($this->tracks[0]['relativePath'] == 'newroot/crse002cd--Calibre-Musique_Concrete-2CD-CRSE002CD-2001-sour/101-calibre-deep_everytime.mp3') {
     #print_r($this->r); die();
     #}
     // extract some attributes from tracks
     // those will be used for album stuff
     $mergedFromTracks = array('artist' => array(), 'genre' => array(), 'label' => array(), 'catalogNr' => array());
     foreach (array_keys($mergedFromTracks) as $what) {
         foreach ($this->tracks as $idx => $rawTagData) {
             $mergedFromTracks[$what][] = $this->getMostScored($idx, $what);
         }
         $mergedFromTracks[$what][] = $this->getMostScored('album', $what);
         $mergedFromTracks[$what] = join(',', array_unique($mergedFromTracks[$what]));
     }
     $albumArtists = count(trimExplode(",", $mergedFromTracks['artist'])) > 3 ? 'Various Artists' : $mergedFromTracks['artist'];
     $a = new Album();
     $a->setArtistId(join(",", Artist::getIdsByString($albumArtists)));
     $a->setGenreId(join(",", Genre::getIdsByString($mergedFromTracks['genre'])));
     #$a->setLabelId(join(",", Label::getIdsByString($mergedFromTracks['label'])));
     $a->setCatalogNr($this->mostScored['album']['catalogNr']);
     $a->setRelativePath($this->getRelativeDirectoryPath());
     $a->setRelativePathHash($this->getRelativeDirectoryPathHash());
     $a->setAdded($this->getDirectoryMtime());
     $a->setFilemtime($this->getDirectoryMtime());
     $a->setTitle($this->mostScored['album']['title']);
     $a->setYear($this->mostScored['album']['year']);
     $a->setIsJumble($this->handleAsAlbum === TRUE ? 0 : 1);
     $a->setTrackCount(count($this->tracks));
     #print_r($a); die();
     $a->update();
     $albumId = $a->getId();
     // add the whole bunch of valid and indvalid attributes to albumindex table
     $this->updateAlbumIndex($albumId);
     foreach ($this->tracks as $idx => $rawTagData) {
         $t = $this->migrateNonGuessableData($rawTagData);
         $t->setArtistId($this->mostScored[$idx]['artist']);
         // currently the string insted of an artistId
         $t->setTitle($this->mostScored[$idx]['title']);
         $t->setFeaturedArtistsAndRemixers();
         # setFeaturedArtistsAndRemixers() is processing:
         # $t->setArtistId();
         # $t->setFeaturingId();
         # $t->setRemixerId();
         $t->setGenreId(join(",", Genre::getIdsByString($this->getMostScored($idx, 'genre'))));
         $t->setLabelId(join(",", Label::getIdsByString($this->getMostScored($idx, 'label'))));
         $t->setCatalogNr($this->mostScored[$idx]['catalogNr']);
         $t->setDisc($this->mostScored[$idx]['disc']);
         $t->setNumber($this->mostScored[$idx]['number']);
         $t->setComment($this->mostScored[$idx]['comment']);
         $t->setYear($this->mostScored[$idx]['year']);
         $t->setAlbumId($albumId);
         // make sure to use identical ids in table:rawtagdata and table:track
         \Slimpd\Track::ensureRecordIdExists($t->getId());
         $t->update();
         // make sure extracted images will be referenced to an album
         \Slimpd\Bitmap::addAlbumIdToTrackId($t->getId(), $albumId);
         #
         // add the whole bunch of valid and indvalid attributes to trackindex table
         $this->updateTrackIndex($t->getId(), $idx);
     }
     unset($this->r['album']);
     if ($this->handleAsAlbum === TRUE) {
         // try to guess if all tracks of this album has obviously invalid fixable attributes
     }
     return;
     print_r($this->r);
     #die();
 }
Example #5
0
 public function processMpdDatabasefile()
 {
     $this->jobPhase = 1;
     $app = \Slim\Slim::getInstance();
     $this->beginJob(array('msg' => $app->ll->str('importer.processing.mpdfile')), __FUNCTION__);
     // check if mpd_db_file exists
     if (is_file($app->config['mpd']['dbfile']) == FALSE || is_readable($app->config['mpd']['dbfile']) === FALSE) {
         $msg = $app->ll->str('error.mpd.dbfile', array($app->config['mpd']['dbfile']));
         cliLog($msg, 1, 'red', TRUE);
         $this->finishJob(array('msg' => $msg));
         $app->stop();
     }
     # TODO: check if mpd-database file is plaintext or gzipped or sqlite
     # TODO: processing mpd-sqlite db or gzipped db
     $this->updateJob(array('msg' => $app->ll->str('importer.collecting.mysqlitems')));
     // get timestamps of all tracks and directories from mysql database
     $fileTimestampsMysql = array();
     $directoryTimestampsMysql = array();
     // get all existing track-ids to determine orphans
     $deadMysqlFiles = array();
     $query = "SELECT id, relativePathHash, relativeDirectoryPathHash, filemtime, directoryMtime FROM rawtagdata;";
     $result = $app->db->query($query);
     while ($record = $result->fetch_assoc()) {
         $deadMysqlFiles[$record['relativePathHash']] = $record['id'];
         $fileTimestampsMysql[$record['relativePathHash']] = $record['filemtime'];
         // get the oldest directory timestamp stored in rawtagdata
         if (isset($directoryTimestampsMysql[$record['relativeDirectoryPathHash']]) === FALSE) {
             $directoryTimestampsMysql[$record['relativeDirectoryPathHash']] = 9999999999;
         }
         if ($record['directoryMtime'] < $directoryTimestampsMysql[$record['relativeDirectoryPathHash']]) {
             $directoryTimestampsMysql[$record['relativeDirectoryPathHash']] = $record['directoryMtime'];
         }
     }
     $dbFilePath = $app->config['mpd']['dbfile'];
     $this->updateJob(array('msg' => $app->ll->str('importer.testdbfile')));
     // check if we have a plaintext or gzipped mpd-databasefile
     $isBinary = testBinary($dbFilePath);
     if ($isBinary === TRUE) {
         $this->updateJob(array('msg' => $app->ll->str('importer.gunzipdbfile')));
         // decompress databasefile
         $bufferSize = 4096;
         // read 4kb at a time (raising this value may increase performance)
         $outFileName = APP_ROOT . 'cache/mpd-database-plaintext';
         // Open our files (in binary mode)
         $inFile = gzopen($app->config['mpd']['dbfile'], 'rb');
         $outFile = fopen($outFileName, 'wb');
         // Keep repeating until the end of the input file
         while (!gzeof($inFile)) {
             // Read buffer-size bytes
             // Both fwrite and gzread and binary-safe
             fwrite($outFile, gzread($inFile, $bufferSize));
         }
         // Files are done, close files
         fclose($outFile);
         gzclose($inFile);
         $dbFilePath = $outFileName;
     }
     $dbfile = explode("\n", file_get_contents($dbFilePath));
     $currentDirectory = "";
     $currentSong = "";
     $currentPlaylist = "";
     $currentSection = "";
     $dirs = array();
     //$songs = array();
     //$playlists = array();
     $dircount = 0;
     $unmodifiedFiles = 0;
     $level = -1;
     $opendirs = array();
     // set initial attributes
     $mtime = 0;
     $time = 0;
     $artist = '';
     $title = '';
     $track = '';
     $album = '';
     $date = '';
     $genre = '';
     $mtimeDirectory = 0;
     foreach ($dbfile as $line) {
         if (trim($line) === "") {
             continue;
             // skip empty lines
         }
         $attr = explode(": ", $line, 2);
         array_map('trim', $attr);
         if (count($attr === 1)) {
             switch ($attr[0]) {
                 case 'info_begin':
                     break;
                 case 'info_end':
                     break;
                 case 'playlist_end':
                     // TODO: what to do with playlists fetched by mpd-database???
                     //$playlists[] = $currentDirectory . DS . $currentPlaylist;
                     $currentPlaylist = "";
                     $currentSection = "";
                     break;
                 case 'song_end':
                     $this->itemCountChecked++;
                     // single music files directly in mpd-musicdir-root must not get a leading slash
                     $dirRelativePath = $currentDirectory === '' ? '' : $currentDirectory . DS;
                     $directoryHash = getFilePathHash($dirRelativePath);
                     // further we have to read directory-modified-time manually because there is no info
                     // about mpd-root-directory in mpd-database-file
                     $mtimeDirectory = $currentDirectory === '' ? filemtime($app->config['mpd']['musicdir']) : $mtimeDirectory;
                     $trackRelativePath = $dirRelativePath . $currentSong;
                     $trackHash = getFilePathHash($trackRelativePath);
                     $this->updateJob(array('msg' => 'processed ' . $this->itemCountChecked . ' files', 'currentfile' => $currentDirectory . DS . $currentSong, 'deadfiles' => count($deadMysqlFiles), 'unmodified_files' => $unmodifiedFiles));
                     $insertOrUpdateRawtagData = FALSE;
                     // compare timestamps of mysql-database-entry(rawtagdata) and mpddatabase
                     if (isset($fileTimestampsMysql[$trackHash]) === FALSE) {
                         cliLog('mpd-file does not exist in rawtagdata: ' . $trackRelativePath, 5);
                         $insertOrUpdateRawtagData = TRUE;
                     } else {
                         if ($mtime > $fileTimestampsMysql[$trackHash]) {
                             cliLog('mpd-file timestamp is newer: ' . $trackRelativePath, 5);
                             $insertOrUpdateRawtagData = TRUE;
                         }
                     }
                     if (isset($directoryTimestampsMysql[$directoryHash]) === FALSE) {
                         cliLog('mpd-directory does not exist in rawtagdata: ' . $dirRelativePath, 5);
                         $insertOrUpdateRawtagData = TRUE;
                     } else {
                         if ($mtimeDirectory > $directoryTimestampsMysql[$directoryHash]) {
                             cliLog('mpd-directory timestamp is newer: ' . $trackRelativePath, 5);
                             $insertOrUpdateRawtagData = TRUE;
                         }
                     }
                     if ($insertOrUpdateRawtagData === FALSE) {
                         // track has not been modified - no need for updating
                         unset($fileTimestampsMysql[$trackHash]);
                         unset($deadMysqlFiles[$trackHash]);
                         $unmodifiedFiles++;
                     } else {
                         $t = new Rawtagdata();
                         if (isset($deadMysqlFiles[$trackHash])) {
                             $t->setId($deadMysqlFiles[$trackHash]);
                             // file is alive - remove it from dead items
                             unset($deadMysqlFiles[$trackHash]);
                         }
                         $t->setArtist($artist);
                         $t->setTitle($title);
                         $t->setAlbum($album);
                         $t->setGenre($genre);
                         $t->setYear($date);
                         $t->setTrackNumber($track);
                         $t->setRelativePath($trackRelativePath);
                         $t->setRelativePathHash($trackHash);
                         $t->setRelativeDirectoryPath($dirRelativePath);
                         $t->setRelativeDirectoryPathHash($directoryHash);
                         $t->setDirectoryMtime($mtimeDirectory);
                         $t->setFilemtime($mtime);
                         $t->setMiliseconds($time * 1000);
                         $t->setlastScan(0);
                         $t->setImportStatus(1);
                         $t->update();
                         unset($t);
                         $this->itemCountProcessed++;
                     }
                     cliLog("#" . $this->itemCountChecked . " " . $currentDirectory . DS . $currentSong, 2);
                     //$songs[] = $currentDirectory . DS . $currentSong;
                     $currentSong = "";
                     $currentSection = "";
                     // reset song attributes
                     $mtime = 0;
                     $time = 0;
                     $artist = '';
                     $title = '';
                     $track = '';
                     $album = '';
                     $date = '';
                     $genre = '';
                     break;
                 default:
                     break;
             }
         }
         if (isset($attr[1]) === TRUE) {
             // believe it or not - some people store html in their tags
             $attr[1] = preg_replace('!\\s+!', ' ', trim(strip_tags($attr[1])));
         }
         switch ($attr[0]) {
             case 'directory':
                 $currentSection = "directory";
                 break;
             case 'begin':
                 $level++;
                 $opendirs = explode(DS, $attr[1]);
                 $currentSection = "directory";
                 $currentDirectory = $attr[1];
                 break;
             case 'song_begin':
                 $currentSection = "song";
                 $currentSong = $attr[1];
                 break;
             case 'playlist_begin':
                 $currentSection = "playlist";
                 $currentPlaylist = $attr[1];
                 break;
             case 'end':
                 $level--;
                 //$dirs[$currentDirectory] = TRUE;
                 $dircount++;
                 array_pop($opendirs);
                 $currentDirectory = join(DS, $opendirs);
                 $currentSection = "";
                 break;
             case 'mtime':
                 if ($currentSection == "directory") {
                     $mtimeDirectory = $attr[1];
                 } else {
                     $mtime = $attr[1];
                 }
                 break;
             case 'Time':
                 $time = $attr[1];
                 break;
             case 'Artist':
                 $artist = $attr[1];
                 break;
             case 'Title':
                 $title = $attr[1];
                 break;
             case 'Track':
                 $track = $attr[1];
                 break;
             case 'Album':
                 $album = $attr[1];
                 break;
             case 'Genre':
                 $genre = $attr[1];
                 break;
             case 'Date':
                 $date = $attr[1];
                 break;
         }
     }
     // delete dead items in table:rawtagdata & table:track
     if (count($deadMysqlFiles) > 0) {
         \Slimpd\Rawtagdata::deleteRecordsByIds($deadMysqlFiles);
         \Slimpd\Track::deleteRecordsByIds($deadMysqlFiles);
     }
     cliLog("dircount: " . $dircount);
     cliLog("songs: " . $this->itemCountChecked);
     //cliLog("playlists: " . count($playlists));
     # TODO: flag&handle dead items in mysql-database
     //cliLog("dead dirs: " . count($deadMysqlDirectories));
     cliLog("dead songs: " . count($deadMysqlFiles));
     #print_r($deadMysqlFiles);
     $this->itemCountTotal = $this->itemCountChecked;
     $this->finishJob(array('msg' => 'processed ' . $this->itemCountChecked . ' files', 'directorycount' => $dircount, 'deletedRecords' => count($deadMysqlFiles), 'unmodified_files' => $unmodifiedFiles), __FUNCTION__);
     // destroy large arrays
     unset($deadMysqlFiles);
     unset($fileTimestampsMysql);
     unset($directoryTimestampsMysql);
     return;
 }
Example #6
0
        foreach ($rows as $row) {
            $excerped = $cl->BuildExcerpts([$row['phrase']], 'slimpdautocomplete', $term);
            $filterType = $filterTypeMapping[$row['type']];
            $entry = ['label' => $excerped[0], 'url' => $filterType === 'track' ? '/searchall/page/1/sort/relevance/desc?q=' . $row['phrase'] : '/library/' . $filterType . '/' . $row['itemid'], 'type' => $filterType, 'typelabel' => $app->ll->str($filterType), 'itemid' => $row['itemid']];
            switch ($filterType) {
                case 'artist':
                case 'label':
                    $entry['img'] = '/skin/default/img/icon-' . $filterType . '.png';
                    break;
                case 'album':
                case 'track':
                    $entry['img'] = '/image-50/' . $filterType . '/' . $row['itemid'];
                    break;
            }
            $result[] = $entry;
        }
    }
    #echo "<pre>" . print_r($result,1); die();
    #echo "<pre>" . print_r($rows,1); die();
    echo json_encode($result);
    exit;
})->name('autocomplete');
$app->get('/deliver/:item+', function ($item) use($app, $config) {
    $path = join(DS, $item);
    if (is_numeric($path)) {
        $track = \Slimpd\Track::getInstanceByAttributes(array('id' => (int) $path));
        $path = $track === NULL ? '' : $track->getRelativePath();
    }
    deliver($app->config['mpd']['alternative_musicdir'] . $path, $app);
    $app->stop();
});