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; }
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(); } }
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; } }
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(); }
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; }
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(); });