1<?php 2/* 3 * vim:set softtabstop=4 shiftwidth=4 expandtab: 4 * 5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later) 6 * Copyright 2001 - 2020 Ampache.org 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <https://www.gnu.org/licenses/>. 20 * 21 */ 22 23/* vim:set softtabstop=4 shiftwidth=4 expandtab: */ 24namespace Ampache\Module\Beets; 25 26use Ampache\Repository\Model\Album; 27use Ampache\Module\System\AmpError; 28use Ampache\Repository\Model\Metadata\Repository\Metadata; 29use Ampache\Repository\Model\Metadata\Repository\MetadataField; 30use Ampache\Repository\Model\library_item; 31use Ampache\Repository\Model\Media; 32use Ampache\Module\Util\Ui; 33use Ampache\Module\System\Dba; 34use Ampache\Repository\Model\Song; 35 36/** 37 * Catalog parent for local and remote beets catalog 38 * 39 * @author raziel 40 */ 41abstract class Catalog extends \Ampache\Repository\Model\Catalog 42{ 43 /** 44 * Added Songs counter 45 * @var integer 46 */ 47 protected $addedSongs = 0; 48 49 /** 50 * Verified Songs counter 51 * @var integer 52 */ 53 protected $verifiedSongs = 0; 54 55 /** 56 * Array of all songs 57 * @var array 58 */ 59 protected $songs = array(); 60 61 /** 62 * command which provides the list of all songs 63 * @var string $listCommand 64 */ 65 protected $listCommand; 66 67 /** 68 * Counter used for cleaning actions 69 */ 70 private int $cleanCounter = 0; 71 72 /** 73 * Constructor 74 * 75 * Catalog class constructor, pulls catalog information 76 * @param integer $catalog_id 77 */ 78 public function __construct($catalog_id = null) 79 { 80 // TODO: Basic constructor should be provided from parent 81 if ($catalog_id) { 82 $this->id = (int) $catalog_id; 83 $info = $this->get_info($catalog_id); 84 85 foreach ($info as $key => $value) { 86 $this->$key = $value; 87 } 88 } 89 } 90 91 /** 92 * 93 * @param Media $media 94 * @return Media 95 */ 96 public function prepare_media($media) 97 { 98 debug_event('beets_catalog', 'Play: Started remote stream - ' . $media->file, 5); 99 100 return $media; 101 } 102 103 /** 104 * 105 * @param string $prefix Prefix like add, updated, verify and clean 106 * @param integer $count song count 107 * @param array $song Song array 108 * @param boolean $ignoreTicker ignoring the ticker for the last update 109 */ 110 protected function updateUi($prefix, $count, $song = null, $ignoreTicker = false) 111 { 112 if ($ignoreTicker || Ui::check_ticker()) { 113 Ui::update_text($prefix . '_count_' . $this->id, $count); 114 if (isset($song)) { 115 Ui::update_text($prefix . '_dir_' . $this->id, scrub_out($this->getVirtualSongPath($song))); 116 } 117 } 118 } 119 120 /** 121 * Get the parser class like CliHandler or JsonHandler 122 */ 123 abstract protected function getParser(); 124 125 /** 126 * Adds new songs to the catalog 127 * @param array $options 128 */ 129 public function add_to_catalog($options = null) 130 { 131 if (!defined('SSE_OUTPUT')) { 132 require Ui::find_template('show_adds_catalog.inc.php'); 133 flush(); 134 } 135 set_time_limit(0); 136 if (!defined('SSE_OUTPUT')) { 137 Ui::show_box_top(T_('Running Beets Update')); 138 } 139 $parser = $this->getParser(); 140 $parser->setHandler($this, 'addSong'); 141 $parser->start($parser->getTimedCommand($this->listCommand, 'added', null)); 142 $this->updateUi('add', $this->addedSongs, null, true); 143 $this->update_last_add(); 144 145 if (!defined('SSE_OUTPUT')) { 146 Ui::show_box_bottom(); 147 } 148 } 149 150 /** 151 * Add $song to ampache if it isn't already 152 * @param array $song 153 */ 154 public function addSong($song) 155 { 156 $song['catalog'] = $this->id; 157 158 if ($this->checkSong($song)) { 159 debug_event('beets_catalog', 'Skipping existing song ' . $song['file'], 5); 160 } else { 161 $album_id = Album::check($song['catalog'], $song['album'], $song['year'], $song['disc'], $song['mbid'], $song['mb_releasegroupid'], $song['album_artist']); 162 $song['album_id'] = $album_id; 163 $songId = $this->insertSong($song); 164 if (Song::isCustomMetadataEnabled() && $songId) { 165 $songObj = new Song($songId); 166 $this->addMetadata($songObj, $song); 167 $this->updateUi('add', ++$this->addedSongs, $song); 168 } 169 } 170 } 171 172 /** 173 * @param library_item $libraryItem 174 * @param $metadata 175 */ 176 public function addMetadata(library_item $libraryItem, $metadata) 177 { 178 $tags = $this->getCleanMetadata($libraryItem, $metadata); 179 180 foreach ($tags as $tag => $value) { 181 $field = $libraryItem->getField($tag); 182 $libraryItem->addMetadata($field, $value); 183 } 184 } 185 186 /** 187 * Get rid of all tags found in the libraryItem 188 * @param library_item $libraryItem 189 * @param array $metadata 190 * @return array 191 */ 192 protected function getCleanMetadata(library_item $libraryItem, $metadata) 193 { 194 $tags = array_diff($metadata, get_object_vars($libraryItem)); 195 $keys = array_merge( 196 isset($libraryItem::$aliases) ? $libraryItem::$aliases : array(), 197 array_keys(get_object_vars($libraryItem)) 198 ); 199 foreach ($keys as $key) { 200 unset($tags[$key]); 201 } 202 203 return $tags; 204 } 205 206 /** 207 * Add the song to the DB 208 * @param array $song 209 * @return integer 210 */ 211 protected function insertSong($song) 212 { 213 $inserted = Song::insert($song); 214 if ($inserted) { 215 debug_event('beets_catalog', 'Adding song ' . $song['file'], 5, 'ampache-catalog'); 216 } else { 217 debug_event('beets_catalog', 'Insert failed for ' . $song['file'], 1); 218 /* HINT: filename (file path) */ 219 AmpError::add('general', T_('Unable to add Song - %s'), $song['file']); 220 echo AmpError::display('general'); 221 } 222 flush(); 223 224 return $inserted; 225 } 226 227 /** 228 * Verify songs. 229 * @return array 230 */ 231 public function verify_catalog_proc() 232 { 233 debug_event('beets_catalog', 'Verify: Starting on ' . $this->name, 5); 234 set_time_limit(0); 235 236 /* @var Handler $parser */ 237 $parser = $this->getParser(); 238 $parser->setHandler($this, 'verifySong'); 239 $parser->start($parser->getTimedCommand($this->listCommand, 'mtime', $this->last_update)); 240 $this->updateUi('verify', $this->verifiedSongs, null, true); 241 $this->update_last_update(); 242 243 return array('updated' => $this->verifiedSongs, 'total' => $this->verifiedSongs); 244 } 245 246 /** 247 * Verify and update a song 248 * @param array $beetsSong 249 */ 250 public function verifySong($beetsSong) 251 { 252 $song = new Song($this->getIdFromPath($beetsSong['file'])); 253 $beetsSong['album_id'] = $song->album; 254 255 if ($song->id) { 256 $song->update($beetsSong); 257 if (Song::isCustomMetadataEnabled()) { 258 $tags = $this->getCleanMetadata($song, $beetsSong); 259 $this->updateMetadata($song, $tags); 260 } 261 $this->updateUi('verify', ++$this->verifiedSongs, $beetsSong); 262 } 263 } 264 265 /** 266 * Cleans the Catalog. 267 * This way is a little fishy, but if we start beets for every single file, it may take horribly long. 268 * So first we get the difference between our and the beets database and then clean up the rest. 269 * @return integer 270 */ 271 public function clean_catalog_proc() 272 { 273 $parser = $this->getParser(); 274 $this->songs = $this->getAllSongfiles(); 275 $parser->setHandler($this, 'removeFromDeleteList'); 276 $parser->start($this->listCommand); 277 $count = count($this->songs); 278 if ($count > 0) { 279 $this->deleteSongs($this->songs); 280 } 281 if (Song::isCustomMetadataEnabled()) { 282 Metadata::garbage_collection(); 283 MetadataField::garbage_collection(); 284 } 285 $this->updateUi('clean', $this->cleanCounter, null, true); 286 287 return (int)$count; 288 } 289 290 /** 291 * move_catalog_proc 292 * This function updates the file path of the catalog to a new location (unsupported) 293 * @param string $new_path 294 * @return boolean 295 */ 296 public function move_catalog_proc($new_path) 297 { 298 return false; 299 } 300 301 /** 302 * @return boolean 303 */ 304 public function cache_catalog_proc() 305 { 306 return false; 307 } 308 309 /** 310 * Remove a song from the "to be deleted"-list if it was found. 311 * @param array $song 312 */ 313 public function removeFromDeleteList($song) 314 { 315 $key = array_search($song['file'], $this->songs, true); 316 $this->updateUi('clean', ++$this->cleanCounter, $song); 317 if ($key) { 318 unset($this->songs[$key]); 319 } 320 } 321 322 /** 323 * Delete Song from DB 324 * @param array $songs 325 */ 326 protected function deleteSongs($songs) 327 { 328 $ids = implode(',', array_keys($songs)); 329 $sql = "DELETE FROM `song` WHERE `id` IN ($ids)"; 330 Dba::write($sql); 331 } 332 333 /** 334 * 335 * @param string $path 336 * @return integer|boolean 337 */ 338 protected function getIdFromPath($path) 339 { 340 $sql = "SELECT `id` FROM `song` WHERE `file` = ?"; 341 $db_results = Dba::read($sql, array($path)); 342 343 $row = Dba::fetch_row($db_results); 344 345 return isset($row) ? $row[0] : false; 346 } 347 348 /** 349 * Get all songs from the DB into a array 350 * @return array array(id => file) 351 */ 352 public function getAllSongfiles() 353 { 354 $sql = "SELECT `id`, `file` FROM `song` WHERE `catalog` = ?"; 355 $db_results = Dba::read($sql, array($this->id)); 356 357 $files = array(); 358 while ($row = Dba::fetch_row($db_results)) { 359 $files[$row[0]] = $row[1]; 360 } 361 362 return $files; 363 } 364 365 /** 366 * Assembles a virtual Path. Mostly just to looks nice in the UI. 367 * @param array $song 368 * @return string 369 */ 370 protected function getVirtualSongPath($song) 371 { 372 return implode('/', array( 373 $song['artist'], 374 $song['album'], 375 $song['title'] 376 )); 377 } 378 379 /** 380 * get_description 381 * This returns the description of this catalog 382 */ 383 public function get_description() 384 { 385 return $this->description; 386 } 387 388 /** 389 * get_version 390 * This returns the current version 391 */ 392 public function get_version() 393 { 394 return $this->version; 395 } 396 397 /** 398 * get_type 399 * This returns the current catalog type 400 */ 401 public function get_type() 402 { 403 return $this->type; 404 } 405 406 /** 407 * Doesn't seems like we need this... 408 * @param string $file_path 409 */ 410 public function get_rel_path($file_path) 411 { 412 } 413 414 /** 415 * format 416 * 417 * This makes the object human-readable. 418 */ 419 public function format() 420 { 421 parent::format(); 422 } 423 424 /** 425 * @param $song 426 * @param $tags 427 */ 428 public function updateMetadata($song, $tags) 429 { 430 foreach ($tags as $tag => $value) { 431 $field = $song->getField($tag); 432 $song->updateOrInsertMetadata($field, $value); 433 } 434 } 435} 436