From 5ed761ea71107ab6efe539b6f80bd2c987152299 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 19 Jan 2019 16:38:22 +0100 Subject: [PATCH] Rewrite the synchronization process (implement #348) --- app/Config/bootstrap.php | 2 +- app/Config/routes.php | 35 +- app/Console/Command/SonerezhShell.php | 6 +- app/Controller/Component/ScanComponent.php | 109 + app/Controller/SongsController.php | 10 +- app/Controller/SyncController.php | 391 + app/Lib/AudioFileManager/AudioFileManager.php | 213 + app/Lib/SongManager/SongManager.php | 175 - app/View/Elements/admin_navbar.ctp | 2 +- app/View/Songs/import.ctp | 150 - app/View/Sync/index.ctp | 277 + app/webroot/js/axios.js | 9 + app/webroot/js/vue.js | 10947 ++++++++++++++++ app/webroot/js/vue.min.js | 6 + 14 files changed, 11982 insertions(+), 350 deletions(-) create mode 100644 app/Controller/Component/ScanComponent.php create mode 100644 app/Controller/SyncController.php create mode 100644 app/Lib/AudioFileManager/AudioFileManager.php delete mode 100644 app/Lib/SongManager/SongManager.php delete mode 100755 app/View/Songs/import.ctp create mode 100644 app/View/Sync/index.ctp create mode 100644 app/webroot/js/axios.js create mode 100644 app/webroot/js/vue.js create mode 100644 app/webroot/js/vue.min.js diff --git a/app/Config/bootstrap.php b/app/Config/bootstrap.php index f348e53..6bc91b4 100755 --- a/app/Config/bootstrap.php +++ b/app/Config/bootstrap.php @@ -121,4 +121,4 @@ define('RESIZED_DIR', IMAGES.'resized'.DS); define('AVATARS_DIR', 'avatars'); define('DOCKER', false); -define('SYNC_BATCH_SIZE', 100); +define('SYNC_BATCH_SIZE', 50); diff --git a/app/Config/routes.php b/app/Config/routes.php index 91a9d9e..48a015f 100644 --- a/app/Config/routes.php +++ b/app/Config/routes.php @@ -24,36 +24,41 @@ * its action called 'display', and we pass a param to select the view file * to use (in this case, /app/View/Pages/home.ctp)... */ - Router::connect('/', array('controller' => 'songs', 'action' => 'index')); +Router::connect('/', array('controller' => 'songs', 'action' => 'index')); - Router::connect('/install', array('controller' => 'installers', 'action' => 'index')); - Router::connect('/docker-install', array('controller' => 'installers', 'action' => 'docker')); +Router::connect('/install', array('controller' => 'installers', 'action' => 'index')); +Router::connect('/docker-install', array('controller' => 'installers', 'action' => 'docker')); - Router::connect('/login', array('controller' => 'users', 'action' => 'login')); - Router::connect('/logout', array('controller' => 'users', 'action' => 'logout')); +Router::connect('/login', array('controller' => 'users', 'action' => 'login')); +Router::connect('/logout', array('controller' => 'users', 'action' => 'logout')); - Router::connect('/api/*', array('controller' => 'api')); +Router::connect('/api/*', array('controller' => 'api')); - Router::connect('/playlists/:action/:id', array('controller' => 'playlists'), array('pass' => array('id'))); - Router::connect('/playlists/add', array('controller' => 'playlists', 'action' => 'add')); - Router::connect('/playlists/*', array('controller' => 'playlists', 'action' => 'index')); +Router::connect('/playlists/:action/:id', array('controller' => 'playlists'), array('pass' => array('id'))); +Router::connect('/playlists/add', array('controller' => 'playlists', 'action' => 'add')); +Router::connect('/playlists/*', array('controller' => 'playlists', 'action' => 'index')); - Router::connect('/users', array('controller' => 'users', 'action' => 'index')); +Router::connect('/users', array('controller' => 'users', 'action' => 'index')); - Router::connect('/settings', array('controller' => 'settings', 'action' => 'index')); +Router::connect('/settings', array('controller' => 'settings', 'action' => 'index')); - Router::connect('/img/**', array('controller' => 'img', 'action' => 'index')); +Router::connect('/sync', array('controller' => 'sync', 'action' => 'index', '[method]' => 'GET')); +Router::connect('/sync', array('controller' => 'sync', 'action' => 'patchSync', '[method]' => 'PATCH')); +Router::connect('/sync', array('controller' => 'sync', 'action' => 'postSync', '[method]' => 'POST')); +Router::connect('/sync', array('controller' => 'sync', 'action' => 'deleteSync', '[method]' => 'DELETE')); - Router::connect('/:action', array('controller' => 'songs')); +Router::connect('/img/**', array('controller' => 'img', 'action' => 'index')); + +Router::connect('/:action', array('controller' => 'songs')); /** * Load all plugin routes. See the CakePlugin documentation on * how to customize the loading of plugin routes. */ - CakePlugin::routes(); +CakePlugin::routes(); /** * Load the CakePHP default routes. Only remove this if you do not want to use * the built-in default routes. */ - require CAKE . 'Config' . DS . 'routes.php'; +require CAKE . 'Config' . DS . 'routes.php'; diff --git a/app/Console/Command/SonerezhShell.php b/app/Console/Command/SonerezhShell.php index 8f24ff4..5e6f8de 100644 --- a/app/Console/Command/SonerezhShell.php +++ b/app/Console/Command/SonerezhShell.php @@ -2,7 +2,7 @@ App::uses('AppShell', 'Console/Command'); App::uses('Folder', 'Utility'); -App::uses('SongManager', 'SongManager'); +App::uses('AudioFileManager', 'AudioFileManager'); App::import('Vendor', 'Getid3/getid3'); @@ -113,8 +113,8 @@ public function import() { foreach ($to_import as $file) { pcntl_signal_dispatch(); - $song_manager = new SongManager($file); - $parse_result = $song_manager->parseMetadata(); + $song_manager = new AudioFileManager($file); + $parse_result = $song_manager->parse(); if ($parse_result['status'] != 'OK') { if ($parse_result['status'] == 'WARN') { diff --git a/app/Controller/Component/ScanComponent.php b/app/Controller/Component/ScanComponent.php new file mode 100644 index 0000000..3442cd3 --- /dev/null +++ b/app/Controller/Component/ScanComponent.php @@ -0,0 +1,109 @@ + array(), + 'to_import' => array(), + 'to_update' => array(), + 'to_remove' => array() + ); + $Track = ClassRegistry::init('Track'); + $Rootpath = ClassRegistry::init('Rootpath'); + $rootpaths = $Rootpath->find('list', array( + 'fields' => 'rootpath' + )); + + if (empty($rootpaths)) { + return $data; + } + + $imported = $Track->find('all', array( + 'fields' => array('id', 'source_path', 'updated') + )); + + $importedPaths = array(); + $importedUpdates = array(); + foreach ($imported as $value) { + $importedPaths[$value['Track']['source_path']] = $value['Track']['id']; + $importedUpdates[$value['Track']['id']] = $value['Track']['updated']; + } + + App::uses('Folder', 'Utility'); + foreach ($rootpaths as $rootpath) { + $folder = new Folder($rootpath); + $tree = $folder->tree(); + + foreach ($tree[0] as $directory) { + $directory = new Folder($directory); + + // Skip symlinks to avoid infinite loops + if (is_link($directory->path)) { + continue; + } + + $foundInThisDirectory = $directory->find('^.*\.(mp3|ogg|flac|aac)$'); + if (count($foundInThisDirectory) == 0) { + continue; + } + + foreach ($foundInThisDirectory as $key => $value) { + $suffix = Folder::isSlashTerm($directory->path) ? $value : DS . $value; + $sourcePath = $directory->path . $suffix; + $index = &$importedPaths[$sourcePath]; + + if ($index === null && $new) { + $data['to_import'][] = $sourcePath; + } elseif (filemtime($sourcePath) > strtotime($importedUpdates[$index]) && $outdated) { + $data['to_update'][] = $index; + $data['imported'][] = $sourcePath; + } else { + $data['imported'][] = $sourcePath; + } + + $countToImport = count($data['to_import']); + $countToUpdate = count($data['to_update']); + if ($batch && ($countToImport == $batch || $countToUpdate == $batch)) { + break 3; + } else { + continue; + } + } + } + } + + if ($orphans) { + $data['to_remove'] = []; + foreach ($importedPaths as $path => $id) { + if ( + !isset(array_flip($data['to_import'])[$path]) && + !isset(array_flip($data['imported'])[$path]) + ) { + $data['to_remove'][] = $id; + } + } + } + + return $data; + } +} \ No newline at end of file diff --git a/app/Controller/SongsController.php b/app/Controller/SongsController.php index 1f6b6e8..6300ae0 100755 --- a/app/Controller/SongsController.php +++ b/app/Controller/SongsController.php @@ -28,7 +28,7 @@ public function beforeFilter() { */ public function import() { App::uses('Folder', 'Utility'); - App::uses('SongManager', 'SongManager'); + App::uses('AudioFileManager', 'AudioFileManager'); $this->loadModel('Setting'); $this->Setting->contain('Rootpath'); @@ -124,8 +124,8 @@ public function import() { break; } - $song_manager = new SongManager($file); - $parse_result = $song_manager->parseMetadata(); + $song_manager = new AudioFileManager($file); + $parse_result = $song_manager->parse(); $this->Song->create(); if (!$this->Song->save($parse_result['data'])) { @@ -147,8 +147,8 @@ public function import() { break; } - $song_manager = new SongManager($file); - $parse_result = $song_manager->parseMetadata(); + $song_manager = new AudioFileManager($file); + $parse_result = $song_manager->parse(); // Get the song id and enrich the array $result = $this->Song->find('first', array( diff --git a/app/Controller/SyncController.php b/app/Controller/SyncController.php new file mode 100644 index 0000000..842992a --- /dev/null +++ b/app/Controller/SyncController.php @@ -0,0 +1,391 @@ +Auth->allow(); + $this->Security->csrfCheck = false; + $this->Security->validatePost = false; + } + + /** + * Get statistics about the synchronization between the Sonerezh's database + * and the filesystem. + */ + public function index() + { + if ($this->request->is('ajax') && $this->request->header('X-Powered-By') == 'Axios') { + $this->viewClass = 'Json'; + $this->cleanNotImportedTracks(); // Remove all the previous failed imports + $scan = $this->Scan->scan($new = true, $orphans = true, $outdated = true, $batch = 0); + $data = array( + 'to_import' => count($scan['to_import']), + 'to_update' => count($scan['to_update']), + 'to_remove' => count($scan['to_remove']) + ); + $this->set(compact('data')); + $this->set('_serialize', 'data'); + } + } + + /** + * Updates records of the `tracks` table. They are edited because their + * related file on the filesystem has been modified since the last import. + * Up to 250 files are processed per request. + */ + public function patchSync() + { + $this->viewClass = 'Json'; + $res = array(); + + if (Cache::read('import')) { + $this->response->statusCode(503); + $res['errors'][] = __('The import process is already running via another client or the CLI.'); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + return; + } else { + Cache::Write('import', true); + } + + $scan = $this->Scan->scan($new = false, $orphans = false, $outdated = true); + + if (empty($scan['to_update'])) { + $this->response->statusCode(204); + $this->set(compact('res')); + $this->set('_serialize', 'data'); + Cache::delete('import'); + return; + } + + App::uses('AudioFileManager', 'AudioFileManager'); + $this->loadModel('Album'); + $this->loadModel('Band'); + $this->loadModel('Track'); + + // The purpose of the two variables below is to avoid useless calls to + // the database to know if a band or an album already exists. + $bands_buffer = array(); + $albums_buffer = array(); + $original_tracks = $this->Track->find('all', array( + 'conditions' => array('id' => $scan['to_update']) + )); + + foreach ($original_tracks as $original_track) { + $audio = new AudioFileManager($original_track['Track']['source_path']); + $result = $audio->parse(); + + if ($result['status'] != 0) { + $res['errors'][] = $result['status_msg']; + continue; + } else { + $metadata = $result['data']; + } + + $band_id = &$bands_buffer[$metadata['Band']['name']]; + if ($band_id === null) { + $band = $this->Band->find('first', array( + 'fields' => array('id', 'name'), + 'conditions' => array('name' => $metadata['Band']['name']) + )); + + if (empty($band)) { + $this->Band->create(); + $band = $this->Band->save($metadata['Band']); + if (empty($band)) { + $bands_buffer[$band['Band']['name']] = $band['Band']['id']; + $band_id = $band['Band']['id']; + } else { + $res['errors'][] = array( + 'error' => __('Unable to save band "%s".', h($metadata['Band']['name'])), + 'record' => $metadata['Band'], + 'validation_errors' => $this->Band->validationErrors + ); + } + } else { + $bands_buffer[$band['Band']['name']] = $band['Band']['id']; + $band_id = $band['Band']['id']; + } + } + + $metadata['Album']['band_id'] = $band_id; + $album_id = &$albums_buffer[$metadata['Album']['name']]; + if ($album_id === null) { + $album = $this->Album->find('first', array( + 'fields' => array('id', 'name'), + 'conditions' => array('name' => $metadata['Album']['name']) + )); + + if (empty($album)) { + $this->Album->create(); + $album = $this->Album->save($metadata['Album']); + if (!empty($album)) { + $albums_buffer[$album['Album']['name']] = $album['Album']['id']; + $album_id = $album['Album']['id']; + } else { + $res['errors'][] = array( + 'error' => __('Unable to save album "%s".', h($metadata['Album']['name'])), + 'record' => $metadata['Album'], + 'validation_errors' => $this->Album->validationErrors + ); + } + } else { + $albums_buffer[$album['Album']['name']] = $album['Album']['id']; + $album_id = $album['Album']['id']; + } + } + + $metadata['Track']['album_id'] = $album_id; + $metadata['Track'] = array_merge($original_track['Track'], $metadata['Track']); + $this->Track->create(); + if ($this->Track->save($metadata['Track'])){ + $res['updated'][] = $metadata['Track']['title']; + } else { + // The path is recorded into the database even if the first + // attempt failed. But it is marked as "not imported". + $this->Track->save(array( + 'imported' => false, + 'source_path' => $metadata['Track']['source_path'] + )); + $res['errors'][] = array( + 'error' => __('Unable to save track "%s".', h($metadata['Track']['source_path'])), + 'record' => $metadata['Track'], + 'validation_errors' => $this->Track->validationErrors + ); + } + + $this->Album->clear(); + $this->Band->clear(); + $this->Track->clear(); + } + + Cache::delete('import'); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + } + + /** + * Imports audio files metadata into Sonerezh's database. + * Up to 250 files are processed per request. + */ + public function postSync() + { + $this->viewClass = 'Json'; + $res = array(); + + if (Cache::read('import')) { + $this->response->statusCode(503); + $res['errors'][] = __('The import process is already running via another client or the CLI.'); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + return; + } else { + Cache::Write('import', true); + } + + $scan = $this->Scan->scan($new = true, $orphans = false, $outdated = false); + + if (empty($scan['to_import'])) { + $this->response->statusCode(204); + $this->set(compact('res')); + $this->set('_serialize', 'data'); + Cache::delete('import'); + return; + } + + App::uses('AudioFileManager', 'AudioFileManager'); + $this->loadModel('Album'); + $this->loadModel('Band'); + $this->loadModel('Track'); + + // The purpose of the two variables below is to avoid useless calls to + // the database to know if a band or an album already exists. + $bands_buffer = array(); + $albums_buffer = array(); + + foreach ($scan['to_import'] as $path) { + $audio = new AudioFileManager($path); + $result = $audio->parse(); + + if ($result['status'] != 0) { + // The path is recorded into the database even if the parsing + // attempt failed, to avoid infinite import loop. But it is + // marked as "not imported". + $this->Track->save(array( + 'imported' => false, + 'source_path' => $path + )); + $res['errors'][$path] = $result['status_msg']; + $this->Track->clear(); + continue; + } else { + $metadata = $result['data']; + } + + $band_id = &$bands_buffer[$metadata['Band']['name']]; + if ($band_id === null) { + $band = $this->Band->find('first', array( + 'fields' => array('id', 'name'), + 'conditions' => array('name' => $metadata['Band']['name']) + )); + + if (empty($band)) { + $this->Band->create(); + $band = $this->Band->save($metadata['Band']); + if (!empty($band)) { + $bands_buffer[$band['Band']['name']] = $band['Band']['id']; + $band_id = $band['Band']['id']; + } else { + $res['errors'][] = array( + 'error' => __('Unable to save band "%s".', h($metadata['Band']['name'])), + 'record' => $metadata['Band'], + 'validation_errors' => $this->Band->validationErrors, + ); + } + } else { + $bands_buffer[$band['Band']['name']] = $band['Band']['id']; + $band_id = $band['Band']['id']; + } + } + + $metadata['Album']['band_id'] = $band_id; + + $album_id = &$albums_buffer[$metadata['Album']['name']]; + if ($album_id === null) { + $album = $this->Album->find('first', array( + 'fields' => array('id', 'name'), + 'conditions' => array('name' => $metadata['Album']['name']) + )); + + if (empty($album)) { + $this->Album->create(); + $album = $this->Album->save($metadata['Album']); + if (!empty($album)) { + $albums_buffer[$album['Album']['name']] = $album['Album']['id']; + $album_id = $album['Album']['id']; + } else { + $res['errors'][] = array( + 'error' => __('Unable to save album "%s".', h($metadata['Album']['name'])), + 'record' => $metadata['Album'], + 'validation_errors' => $this->Album->validationErrors + ); + } + } else { + $albums_buffer[$album['Album']['name']] = $album['Album']['id']; + $album_id = $album['Album']['id']; + } + } + + $metadata['Track']['album_id'] = $album_id; + $this->Track->create(); + if ($this->Track->save($metadata['Track'])){ + $res['imported'][] = $metadata['Track']['title']; + } else { + // The path is recorded into the database even if the first + // attempt failed. But it is marked as "not imported". + $this->Track->save(array( + 'imported' => false, + 'source_path' => $metadata['Track']['source_path'] + )); + $res['errors'][] = array( + 'error' => __('Unable to save track "%s".', h($metadata['Track']['source_path'])), + 'record' => $metadata['Track'], + 'validation_errors' => $this->Track->validationErrors + ); + } + + $this->Album->clear(); + $this->Band->clear(); + $this->Track->clear(); + } + + Cache::delete('import'); + $this->response->statusCode(201); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + } + + public function deleteSync() + { + $this->viewClass = 'Json'; + $res = array(); + + if (Cache::read('import')) { + $this->response->statusCode(503); + $res['errors'][] = __('The import process is already running via another client or the CLI.'); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + return; + } else { + Cache::Write('import', true); + } + + $this->loadModel('Track'); + try { + $scan = $this->Scan->scan($new = true, $orphans = true, $outdated = false); + } catch (Exception $exception) { + $res['errors'][] = array( + 'error' => $exception->getMessage() + ); + return; + } + + if (empty($scan['to_remove'])) { + $this->cleanOrphanDatabaseRecords(); + $this->response->statusCode(204); + $this->set(compact('res')); + $this->set('_serialize', 'data'); + Cache::delete('import'); + return; + } + + $this->loadModel('Track'); + $deletion = $this->Track->deleteAll(array( + 'id' => $scan['to_remove'] + ), false); + + if (! $deletion) { + $res['errors'][] = array( + 'error' => __('Unexpected error occurred while deleting data.') + ); + } + + Cache::delete('import'); + $this->response->statusCode(202); + $this->set(compact('res')); + $this->set('_serialize', 'res'); + } + + private function cleanNotImportedTracks() + { + $this->loadModel('Track'); + $this->Track->deleteAll(array('imported' => false)); + } + + private function cleanOrphanDatabaseRecords() + { + $this->loadModel('Track'); + $db = $this->Track->getDataSource(); + if ($db->config['datasource'] == 'Database/Mysql') { + $queries = array( + 'DELETE FROM albums LEFT JOIN tracks ON albums.id = tracks.album_id WHERE tracks.album_id IS NULL', + 'DELETE FROM bands LEFT JOIN albums ON bands.id = albums.band_id WHERE albums.band_id IS NULL' + ); + } else {$this->loadModel('Track'); + $queries = array( + 'DELETE FROM albums WHERE NOT EXISTS (SELECT 1 FROM tracks WHERE tracks.album_id = albums.id)', + 'DELETE FROM bands WHERE NOT EXISTS (SELECT 1 FROM albums WHERE albums.band_id = bands.id)' + ); + } + + foreach ($queries as $query) { + $db->fetchAll($query); + } + } +} \ No newline at end of file diff --git a/app/Lib/AudioFileManager/AudioFileManager.php b/app/Lib/AudioFileManager/AudioFileManager.php new file mode 100644 index 0000000..bfed85c --- /dev/null +++ b/app/Lib/AudioFileManager/AudioFileManager.php @@ -0,0 +1,213 @@ +file = new File($song); + $getID3 = new getID3(); + $this->raw_data = $getID3->analyze(($this->file->path)); + getid3_lib::CopyTagsToComments($this->raw_data); + } + + public function parse() + { + $result = array( + 'status' => 0, // 0, or 1 (SUCCESS or FAILURE) + 'status-msg' => '', // Debug message + 'data' => array() // The data ($metadata array below) + ); + + if (empty($this->raw_data['comments'])) { + $result['status'] = 1; + $result['status_msg'] = __('Metadata are unreadable or empty. Skipping.'); + return $result; + } + + // Order matters here! + $this->fetchBand(); + $this->fetchAlbum(); + $this->fetchTrack(); + + $metadata = array( + 'Band' => $this->band, + 'Album' => $this->album, + 'Track' => $this->track + ); + + $result['data'] = $metadata; + return $result; + } + + private function fetchAlbum () + { + if (empty($this->band)) { + $this->fetchBand(); + } + + if (!empty($this->raw_data['comments']['album'])) { + $this->album['name'] = end($this->raw_data['comments']['album']); + } else { + $this->album['name'] = 'Unknown album'; + } + + $this->album['cover'] = $this->fetchCover(); + $this->album['year'] = $this->fetchYear(); + } + + private function fetchBand () + { + if (!empty($this->raw_data['comments']['band'])) { // MP3 tags + $this->band['name'] = end($this->raw_data['comments']['band']); + } elseif (!empty($this->raw_data['comments']['ensemble'])) { // OGG tags + $this->band['name'] = end($this->raw_data['comments']['ensemble']); + } elseif (!empty($this->raw_data['comments']['albumartist'])) { // OGG or FLAC tags + $this->band['name'] = end($this->raw_data['comments']['albumartist']); + } elseif (!empty($this->raw_data['comments']['album artist'])) {// OGG or FLAC Tags + $this->band['name'] = end($this->raw_data['comments']['album artist']); + } elseif (!empty($this->raw_data['comments']['artist'])) { + $this->band['name'] = end($this->raw_data['comments']['artist']); + } else { + $this->band['name'] = 'Unknown'; + } + } + + + private function fetchCover () + { + if (empty($this->band) || empty($this->album)) { + return null; + } + + if (!file_exists(IMAGES . THUMBNAILS_DIR)) { + new Folder(IMAGES . THUMBNAILS_DIR, true, 0755); + } + + if (!empty($this->raw_data['comments']['picture'])) { + if (!empty(end($this->raw_data['comments']['picture'])['image_mime'])) { + $mime = preg_split('/\//', end($this->raw_data['comments']['picture'])['image_mime']); + $extension = $mime[1]; + } else { + $extension = 'jpg'; + } + + $cover = $this->fetchCoverName($extension); + $path = new File(IMAGES . THUMBNAILS_DIR . DS . $cover); + + if ($path->exists() || $path->write(end($this->raw_data['comments']['picture'])['data'])) { + return $cover; + } else { + return null; + } + } else { // Fallback to the filesystem if the cover was not in the metadata + $pattern = '^(folder|cover|front.*|albumart_.*_large)\.(jpg|jpeg|png)$'; + $covers = $this->file->Folder->find($pattern); + + if (!empty($covers)) { + $img_source_path = $this->file->Folder->addPathElement( + $this->file->Folder->path, + $covers[0] + ); + $img = new File($img_source_path); + $extension = $img->info()['extension']; + $cover = $this->fetchCoverName($extension); + $path = new File( IMAGES . THUMBNAILS_DIR . DS . $cover); + + if ($path->exists() || $img->copy($path->path)) { + return $cover; + } else { + return null; + } + } + } + return null; + } + + private function fetchCoverName($extension) + { + return md5($this->band['name'] . $this->album['name']) . '.' . $extension; + } + + private function fetchTrack() + { + $this->track = array( + 'source_path' => $this->file->path, + 'year' => $this->fetchYear() + ); + + if (!empty($this->raw_data['comments']['title'])) { + $this->track['title'] = end($this->raw_data['comments']['title']); + } elseif (!empty($this->raw_data['filename'])) { + $this->track['title'] = $this->raw_data['filename']; + } else { + $this->track['title'] = $this->file->name(); + } + + if (!empty($this->raw_data['comments']['artist'])) { + $this->track['artist'] = end($this->raw_data['comments']['artist']); + } else { + $this->track['artist'] = null; + } + + if (!empty($this->raw_data['playtime_string'])) { + $this->track['playtime'] = $this->raw_data['playtime_string']; + } + + if (!empty($this->raw_data['comments']['track'])) { // MP3 Tags + $this->track['track_number'] = (string)end($this->raw_data['comments']['track']); + } elseif (!empty($this->raw_data['comments']['track_number'])) { // MP3 Tags + // Some tags look like '1/10' + $track_number = explode('/', (string)end($this->raw_data['comments']['track_number'])); + $this->track['track_number'] = intval($track_number[0]); + if (!empty($track_number[1])) { + $this->track['max_track_number'] = intval($track_number[1]); + } + } elseif(!empty($this->raw_data['comments']['tracknumber'])){ // OGG Tags + $this->track['track_number'] = intval(end($this->raw_data['comments']['tracknumber'])); + } + + if (!empty($this->raw_data['comments']['part_of_a_set'])) { // MP3 Tags + $part_of_a_set = explode('/', (string)end($this->raw_data['comments']['part_of_a_set'])); + $this->track['disc_number'] = intval($part_of_a_set[0]); + if (!empty($part_of_a_set[1])) { + $this->track['max_disc_number'] = intval($part_of_a_set[1]); + } + } elseif (!empty($this->raw_data['comments']['discnumber'])) { // OGG Tags + $this->track['disc_number'] = intval(end($this->raw_data['comments']['discnumber'])); + if (!empty($this->raw_data['comments']['disctotal'])) { + $this->track['max_disc_number'] = intval(end($this->raw_data['comments']['disctotal'])); + } + } + + if (!empty($this->raw_data['comments']['genre'])) { + $this->track['genre'] = end($this->raw_data['comments']['genre']); + } + } + + private function fetchYear() + { + if (!empty($this->raw_data['comments']['year'])) { + $date = $this->raw_data['comments']['year']; + } elseif (!empty($this->raw_data['comments']['date'])) { + $date = $this->raw_data['comments']['date']; + } else { + $date = array(); + } + + if (!empty($date)) { + $strptime = strptime(end($date), "%Y"); + if ($strptime) { + return $strptime['tm_year'] + 1900; + } + } + + return null; + } +} diff --git a/app/Lib/SongManager/SongManager.php b/app/Lib/SongManager/SongManager.php deleted file mode 100644 index a7d04db..0000000 --- a/app/Lib/SongManager/SongManager.php +++ /dev/null @@ -1,175 +0,0 @@ -song = new File($song); - } - - function parseMetadata() { - $getID3 = new getID3(); - $file_infos = $getID3->analyze(($this->song->path)); - getid3_lib::CopyTagsToComments($file_infos); - - // Can be useful to add more debug in the future - $result = array( - 'status' => 'OK', // 'OK', 'WARN' or 'ERR' - 'message' => '', // Debug message - 'data' => array() // The data ($metadata array below) - ); - - if (!isset($file_infos['comments']) || empty($file_infos['comments'])) { - $result['status'] = 'WARN'; - $result['message'] = 'Metadata are unreadable or empty. Trying to import anyway...'; - } - - $metadata = array(); - - // Song title - if (!empty($file_infos['comments']['title'])) { - $metadata['title'] = end($file_infos['comments']['title']); - } elseif (!empty($file_infos['filename'])) { - $metadata['title'] = $file_infos['filename']; - } else { - $metadata['title'] = $this->song->name(); - } - - // Song artist - if (!empty($file_infos['comments']['artist'])) { - $metadata['artist'] = end($file_infos['comments']['artist']); - } else { - $metadata['artist'] = ''; - } - - // Song band - if (!empty($file_infos['comments']['band'])) { // MP3 Tag - $metadata['band'] = end($file_infos['comments']['band']); - } elseif (!empty($file_infos['comments']['ensemble'])) { // OGG Tag - $metadata['band'] = end($file_infos['comments']['ensemble']); - } elseif (!empty($file_infos['comments']['albumartist'])) { // OGG/FLAC Tag - $metadata['band'] = end($file_infos['comments']['albumartist']); - } elseif (!empty($file_infos['comments']['album artist'])) {// OGG/FLAC Tag - $metadata['band'] = end($file_infos['comments']['album artist']); - } else { - $metadata['band'] = $metadata['artist'] != '' ? $metadata['artist'] : 'Unknown Band'; - } - - // Song album - if (!empty($file_infos['comments']['album'])) { - $metadata['album'] = end($file_infos['comments']['album']); - } else { - $metadata['album'] = 'Unknown album'; - } - - // Song track number - if (!empty($file_infos['comments']['track'])) { // MP3 Tag - $metadata['track_number'] = (string)end($file_infos['comments']['track']); - } elseif (!empty($file_infos['comments']['track_number'])) { // MP3 Tag - // Some tags look like '1/10' - $track_number = explode('/', (string)end($file_infos['comments']['track_number'])); - $metadata['track_number'] = intval($track_number[0]); - } elseif(!empty($file_infos['comments']['tracknumber'])){ // OGG Tag - $metadata['track_number'] = end($file_infos['comments']['tracknumber']); - } - - // Song playtime - if (!empty($file_infos['playtime_string'])) { - $metadata['playtime'] = $file_infos['playtime_string']; - } - - // Song year - $date = false; - if (!empty($file_infos['comments']['year'])) { - $date = $file_infos['comments']['year']; - } elseif (!empty($file_infos['comments']['date'])) { - $date = $file_infos['comments']['date']; - } - - if ($date) { - $strptime = strptime(end($date), "%Y"); - if ($strptime) { - $metadata['year'] = $strptime['tm_year'] + 1900; - } - } - - // Song set - if (!empty($file_infos['comments']['part_of_a_set'])) { // MP3 Tag - $metadata['disc'] = end($file_infos['comments']['part_of_a_set']); - } elseif (!empty($file_infos['comments']['discnumber'])) { // OGG Tag - $metadata['disc'] = end($file_infos['comments']['discnumber']); - if (!empty($file_infos['comments']['disctotal'])) { - $metadata['disc'] .= '/' . end($file_infos['comments']['disctotal']); - } - } - - // Song genre - if (!empty($file_infos['comments']['genre'])) { - $metadata['genre'] = end($file_infos['comments']['genre']); - } - - // Song cover - if (!file_exists(IMAGES.THUMBNAILS_DIR)) { - new Folder(IMAGES.THUMBNAILS_DIR, true, 0755); - } - - if (!empty($file_infos['comments']['picture'])) { - $array_length = count($file_infos['comments']['picture']); - if (!empty($file_infos['comments']['picture'][$array_length - 1]['image_mime'])) { - $mime_type = preg_split('/\//', $file_infos['comments']['picture'][$array_length - 1]['image_mime']); - $cover_extension = $mime_type[1]; - } else { - $cover_extension = 'jpg'; - } - - $cover_name = md5($metadata['artist'].$metadata['album']) . '.' . $cover_extension; - $cover_path = new File(IMAGES.THUMBNAILS_DIR.DS.$cover_name); - - // IF the cover already exists - // OR the cover doesn't exist AND has been successfully written - if ( - file_exists(IMAGES.THUMBNAILS_DIR.DS.$cover_name) - || ( - !file_exists(IMAGES.THUMBNAILS_DIR.DS.$cover_name) - && $cover_path->write($file_infos['comments']['picture'][$array_length - 1]['data']) - ) - ) { - $metadata['cover'] = $cover_name; - } - - } else { - $cover_pattern = '^(folder|cover|front.*|albumart_.*_large)\.(jpg|jpeg|png)$'; - $covers = $this->song->Folder->find($cover_pattern); - - if (!empty($covers)) { - $cover_source_path = $this->song->Folder->addPathElement( - $this->song->Folder->path, - $covers[0] - ); - $cover_source = new File($cover_source_path); - $cover_info = $cover_source->info(); - $cover_extension = $cover_info['extension']; - $cover_name = md5($metadata['artist'].$metadata['album']) . '.' . $cover_extension; - - // IF the cover already exists - // OR the cover doesn't exist AND has been successfully copied - if ( - file_exists(IMAGES.THUMBNAILS_DIR.DS.$cover_name) - || ( - !file_exists(IMAGES.THUMBNAILS_DIR.DS.$cover_name) - && $cover_source->copy(IMAGES.THUMBNAILS_DIR.DS.$cover_name) - ) - ) { - $metadata['cover'] = $cover_name; - } - } - } - - $metadata['source_path'] = $this->song->path; - $result['data'] = $metadata; - return $result; - } -} diff --git a/app/View/Elements/admin_navbar.ctp b/app/View/Elements/admin_navbar.ctp index fead94e..3fd28dd 100644 --- a/app/View/Elements/admin_navbar.ctp +++ b/app/View/Elements/admin_navbar.ctp @@ -10,7 +10,7 @@