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 23declare(strict_types=0); 24 25namespace Ampache\Repository\Model; 26 27use Ampache\Config\AmpConfig; 28use Ampache\Module\System\Dba; 29use Ampache\Module\System\Session; 30use Ampache\Module\Util\ObjectTypeToClassNameMapper; 31use Ampache\Module\Util\Ui; 32use Ampache\Module\Util\VaInfo; 33use Ampache\Module\Api\Ajax; 34use Ampache\Module\Util\UtilityFactoryInterface; 35use Ampache\Module\Util\InterfaceImplementationChecker; 36use Ampache\Module\System\Core; 37use Ampache\Repository\SongRepositoryInterface; 38use Exception; 39use getID3; 40use PDOStatement; 41use Requests; 42use RuntimeException; 43 44/** 45 * This class handles the images / artwork in ampache 46 * This was initially in the album class, but was pulled out 47 * to be more general and potentially apply to albums, artists, movies etc 48 */ 49class Art extends database_object 50{ 51 protected const DB_TABLENAME = 'art'; 52 53 /** 54 * @var integer $id 55 */ 56 public $id; 57 /** 58 * @var string $type 59 */ 60 public $type; 61 /** 62 * @var integer $uid 63 */ 64 public $uid; // UID of the object not ID because it's not the ART.ID 65 /** 66 * @var string $raw 67 */ 68 public $raw; // Raw art data 69 /** 70 * @var string $raw_mime 71 */ 72 public $raw_mime; 73 /** 74 * @var string $kind 75 */ 76 public $kind; 77 78 /** 79 * @var string $thumb 80 */ 81 public $thumb; 82 /** 83 * @var string $thumb_mime 84 */ 85 public $thumb_mime; 86 87 /** 88 * Constructor 89 * Art constructor, takes the UID of the object and the 90 * object type. 91 * @param integer $uid 92 * @param string $type 93 * @param string $kind 94 */ 95 public function __construct($uid, $type = 'album', $kind = 'default') 96 { 97 if (Art::is_valid_type($type)) { 98 $this->type = $type; 99 $this->uid = (int)($uid); 100 $this->kind = $kind; 101 } 102 } // constructor 103 104 public function getId(): int 105 { 106 return (int) $this->id; 107 } 108 109 /** 110 * @param string $type 111 * @return boolean 112 */ 113 public static function is_valid_type($type) 114 { 115 return (InterfaceImplementationChecker::is_library_item($type) || $type == 'user'); 116 } 117 118 /** 119 * build_cache 120 * This attempts to reduce # of queries by asking for everything in the 121 * browse all at once and storing it in the cache, this can help if the 122 * db connection is the slow point 123 * @param integer[] $object_ids 124 * @param string $type 125 * @return boolean 126 */ 127 public static function build_cache($object_ids, $type = null) 128 { 129 if (empty($object_ids)) { 130 return false; 131 } 132 $idlist = '(' . implode(',', $object_ids) . ')'; 133 $sql = "SELECT `object_type`, `object_id`, `mime`, `size` FROM `image` WHERE `object_id` IN $idlist"; 134 if ($type !== null) { 135 $sql .= " AND `object_type` = '$type'"; 136 } 137 $db_results = Dba::read($sql); 138 139 while ($row = Dba::fetch_assoc($db_results)) { 140 parent::add_to_cache('art', $row['object_type'] . $row['object_id'] . $row['size'], $row); 141 } 142 143 return true; 144 } // build_cache 145 146 /** 147 * extension 148 * This returns the file extension for the currently loaded art 149 * @param string $mime 150 * @return string 151 */ 152 public static function extension($mime) 153 { 154 $data = explode("/", (string)$mime); 155 $extension = $data['1']; 156 157 if ($extension == 'jpeg') { 158 $extension = 'jpg'; 159 } 160 161 return (string)$extension; 162 } // extension 163 164 /** 165 * test_image 166 * Runs some sanity checks on the putative image 167 * @param string $source 168 * @return boolean 169 * @throws RuntimeException 170 */ 171 public static function test_image($source) 172 { 173 if (strlen((string) $source) < 10) { 174 debug_event(self::class, 'Invalid image passed', 1); 175 176 return false; 177 } 178 179 // Check image size doesn't exceed the limit 180 if (strlen((string) $source) > AmpConfig::get('max_upload_size')) { 181 debug_event(self::class, 'Image size (' . strlen((string) $source) . ') exceed the limit (' . AmpConfig::get('max_upload_size') . ').', 1); 182 183 return false; 184 } 185 186 $test = false; 187 $image = false; 188 // Check to make sure PHP:GD exists. If so, we can sanity check the image. 189 if (function_exists('ImageCreateFromString') && is_string($source)) { 190 $test = true; 191 $image = ImageCreateFromString($source); 192 if ($image == false || imagesx($image) < 5 || imagesy($image) < 5) { 193 debug_event(self::class, 'Image failed PHP-GD test', 1); 194 $test = false; 195 } 196 } 197 if ($test && $image != false) { 198 if (imagedestroy($image) === false) { 199 throw new RuntimeException('The image handle from source: ' . $source . ' could not be destroyed'); 200 } 201 } 202 203 return $test; 204 } // test_image 205 206 /** 207 * get 208 * This returns the art for our current object, this can 209 * look in the database and will return the thumb if it 210 * exists, if it doesn't depending on settings it will try 211 * to create it. 212 * @param boolean $raw 213 * @return string 214 */ 215 public function get($raw = false) 216 { 217 // Get the data either way 218 if (!$this->has_db_info()) { 219 return ''; 220 } 221 222 if ($raw || !$this->thumb) { 223 return $this->raw; 224 } else { 225 return $this->thumb; 226 } 227 } // get 228 229 /** 230 * has_db_info 231 * This pulls the information out from the database, depending 232 * on if we want to resize and if there is not a thumbnail go 233 * ahead and try to resize 234 * @return boolean 235 */ 236 public function has_db_info() 237 { 238 $sql = "SELECT `id`, `image`, `mime`, `size` FROM `image` WHERE `object_type` = ? AND `object_id` = ? AND `kind` = ?"; 239 $db_results = Dba::read($sql, array($this->type, $this->uid, $this->kind)); 240 241 while ($results = Dba::fetch_assoc($db_results)) { 242 if ($results['size'] == 'original') { 243 if (AmpConfig::get('album_art_store_disk')) { 244 $this->raw = self::read_from_dir($results['size'], $this->type, $this->uid, $this->kind, $results['mime']); 245 } else { 246 $this->raw = $results['image']; 247 } 248 $this->raw_mime = $results['mime']; 249 } elseif (AmpConfig::get('resize_images')) { 250 if (AmpConfig::get('album_art_store_disk')) { 251 $this->thumb = self::read_from_dir($results['size'], $this->type, $this->uid, $this->kind, $results['mime']); 252 } elseif ($results['size'] == '275x275') { 253 $this->thumb = $results['image']; 254 } 255 $this->raw_mime = $results['mime']; 256 } 257 $this->id = (int)$results['id']; 258 } 259 // If we get nothing return false 260 if (!$this->raw) { 261 return false; 262 } 263 264 // If there is no thumb and we want thumbs 265 if (!$this->thumb && AmpConfig::get('resize_images')) { 266 $size = array('width' => 275, 'height' => 275); 267 $data = $this->generate_thumb($this->raw, $size, $this->raw_mime); 268 // If it works save it! 269 if (!empty($data)) { 270 $this->save_thumb($data['thumb'], $data['thumb_mime'], $size); 271 $this->thumb = $data['thumb']; 272 $this->thumb_mime = $data['thumb_mime']; 273 } else { 274 debug_event(self::class, 'Art id {' . $this->id . '} Unable to generate thumbnail for ' . $this->type . ': ' . $this->uid, 1); 275 } 276 } // if no thumb, but art and we want to resize 277 278 return true; 279 } // has_db_info 280 281 /** 282 * This check if an object has an associated image in db. 283 * @param integer $object_id 284 * @param string $object_type 285 * @param string $kind 286 * @return boolean 287 */ 288 public static function has_db($object_id, $object_type, $kind = 'default') 289 { 290 $sql = "SELECT COUNT(`id`) AS `nb_img` FROM `image` WHERE `object_type` = ? AND `object_id` = ? AND `kind` = ?"; 291 $db_results = Dba::read($sql, array($object_type, $object_id, $kind)); 292 $nb_img = 0; 293 if ($results = Dba::fetch_assoc($db_results)) { 294 $nb_img = $results['nb_img']; 295 } 296 297 return ($nb_img > 0); 298 } 299 300 /** 301 * This insert art from url. 302 * @param string $url 303 */ 304 public function insert_url($url) 305 { 306 debug_event(self::class, 'Insert art from url ' . $url, 4); 307 $image = self::get_from_source(array('url' => $url), $this->type); 308 $rurl = pathinfo($url); 309 $mime = "image/" . $rurl['extension']; 310 $this->insert($image, $mime); 311 } 312 313 /** 314 * insert 315 * This takes the string representation of an image and inserts it into 316 * the database. You must also pass the mime type. 317 * @param string $source 318 * @param string $mime 319 * @return boolean 320 */ 321 public function insert($source, $mime = '') 322 { 323 // Disabled in demo mode cause people suck and upload porn 324 if (AmpConfig::get('demo_mode')) { 325 return false; 326 } 327 328 // Check to make sure we like this image 329 if (!self::test_image($source)) { 330 debug_event(self::class, 'Not inserting image for ' . $this->type . ' ' . $this->uid . ', invalid data passed', 1); 331 332 return false; 333 } 334 335 $dimensions = Core::image_dimensions($source); 336 $width = (int)($dimensions['width']); 337 $height = (int)($dimensions['height']); 338 $sizetext = 'original'; 339 340 if (!self::check_dimensions($dimensions)) { 341 return false; 342 } 343 344 // Default to image/jpeg if they don't pass anything 345 $mime = $mime ? $mime : 'image/jpeg'; 346 // Blow it away! 347 $this->reset(); 348 $current_picturetypeid = ($this->type == 'album') ? 3 : 8; 349 350 if (AmpConfig::get('write_tags', false)) { 351 $class_name = ObjectTypeToClassNameMapper::map($this->type); 352 $object = new $class_name($this->uid); 353 debug_event(__CLASS__, 'Inserting ' . $this->type . ' image' . $object->name . ' for song files.', 5); 354 if ($this->type === 'album') { 355 /** Use special treatment for albums */ 356 $songs = $this->getSongRepository()->getByAlbum($object->id); 357 } elseif ($this->type === 'artist') { 358 /** Use special treatment for artists */ 359 $songs = $this->getSongRepository()->getByArtist($object->id); 360 } 361 global $dic; 362 $utilityFactory = $dic->get(UtilityFactoryInterface::class); 363 364 foreach ($songs as $song_id) { 365 $song = new Song($song_id); 366 $song->format(); 367 $description = ($this->type == 'artist') ? $song->f_artist_full : $object->full_name; 368 $vainfo = $utilityFactory->createVaInfo( 369 $song->file 370 ); 371 372 $ndata = array(); 373 $data = $vainfo->read_id3(); 374 $fileformat = $data['fileformat']; 375 if ($fileformat == 'flac' || $fileformat == 'ogg') { 376 $apics = $data['flac']['PICTURE']; 377 } else { 378 $apics = $data['id3v2']['APIC']; 379 } 380 /* is the file flac or mp3? */ 381 $apic_typeid = ($fileformat == 'flac' || $fileformat == 'ogg') ? 'typeid' : 'picturetypeid'; 382 $apic_mimetype = ($fileformat == 'flac' || $fileformat == 'ogg') ? 'image_mime' : 'mime'; 383 $new_pic = array('data' => $source, 'mime' => $mime, 384 'picturetypeid' => $current_picturetypeid, 'description' => $description); 385 386 if (is_null($apics)) { 387 $ndata['attached_picture'][] = $new_pic; 388 } else { 389 switch (count($apics)) { 390 case 1: 391 $idx = $this->check_for_duplicate($apics, $ndata, $new_pic, $apic_typeid); 392 if (is_null($idx)) { 393 $ndata['attached_picture'][] = $new_pic; 394 $ndata['attached_picture'][] = array('data' => $apics[0]['data'], 'description' => $apics[0]['description'], 395 'mime' => $apics[0]['mime'], 'picturetypeid' => $apics[0]['picturetypeid']); 396 } 397 break; 398 case 2: 399 $idx = $this->check_for_duplicate($apics, $ndata, $new_pic, $apic_typeid); 400 /* If $idx is null, it means both images are of opposite types 401 * of the new image. Either image could be replaced to have 402 * one cover and one artist image. 403 */ 404 if (is_null($idx)) { 405 $ndata['attached_picture'][0] = $new_pic; 406 } else { 407 $apicsId = ($idx == 0) ? 1 : 0; 408 $ndata['attached_picture'][$apicsId] = array('data' => $apics[$apicsId]['data'], 'mime' => $apics[$apicsId][$apic_mimetype], 409 'picturetypeid' => $apics[$apicsId][$apic_typeid], 'description' => $apics[$apicsId]['description']); 410 } 411 412 break; 413 } 414 } 415 unset($apics); 416 $tags = ($fileformat == 'flac' || $fileformat == 'ogg') ? 'vorbiscomment' : 'id3v2'; 417 $ndata = array_merge($ndata, $vainfo->prepare_metadata_for_writing($data['tags'][$tags])); 418 $vainfo->write_id3($ndata); 419 } // foreach song 420 } // write_id3 421 422 if (AmpConfig::get('album_art_store_disk')) { 423 self::write_to_dir($source, $sizetext, $this->type, $this->uid, $this->kind, $mime); 424 $source = null; 425 } 426 // Insert it! 427 $sql = "INSERT INTO `image` (`image`, `mime`, `size`, `width`, `height`, `object_type`, `object_id`, `kind`) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; 428 Dba::write($sql, array($source, $mime, $sizetext, $width, $height, $this->type, $this->uid, $this->kind)); 429 430 return true; 431 } // insert 432 433 private function check_for_duplicate($apics, &$ndata, $new_pic, $apic_typeid) 434 { 435 $idx = null; 436 $cnt = count($apics); 437 for ($i=0; $i < $cnt; $i++) { 438 if ($new_pic['picturetypeid'] == $apics[$i][$apic_typeid]) { 439 $ndata['attached_picture'][$i]['description'] = $new_pic['description']; 440 $ndata['attached_picture'][$i]['data'] = $new_pic['data']; 441 $ndata['attached_picture'][$i]['mime'] = $new_pic['mime']; 442 $ndata['attached_picture'][$i]['picturetypeid'] = $new_pic['picturetypeid']; 443 $idx = $i; 444 break; 445 } 446 } 447 448 return $idx; 449 } 450 451 /** 452 * Prepares images to be written to file tag. 453 * @param array $pics 454 * @return array 455 */ 456 public static function prepare_pics($pics) 457 { 458 $ndata = array(); 459 $i = 0; 460 foreach ($pics as $pic) { 461 $ndata['attached_picture'][$i]['description'] = $pic['description']; 462 $ndata['attached_picture'][$i]['data'] = $pic['data']; 463 $ndata['attached_picture'][$i]['picturetypeid'] = $pic['picturetypeid']; 464 $ndata['attached_picture'][$i]['mime'] = $pic['mime']; 465 466 $i++; 467 } 468 469 return $ndata; 470 } 471 472 /** 473 * check_dimensions 474 * @param array $dimensions 475 * @return boolean 476 */ 477 public static function check_dimensions($dimensions) 478 { 479 $width = (int)($dimensions['width']); 480 $height = (int)($dimensions['height']); 481 482 if ($width > 0 && $height > 0) { 483 $minw = (AmpConfig::get('album_art_min_width')) ? AmpConfig::get('album_art_min_width') : 0; 484 $maxw = (AmpConfig::get('album_art_max_width')) ? AmpConfig::get('album_art_max_width') : 0; 485 $minh = (AmpConfig::get('album_art_min_height')) ? AmpConfig::get('album_art_min_height') : 0; 486 $maxh = (AmpConfig::get('album_art_max_height')) ? AmpConfig::get('album_art_max_height') : 0; 487 488 // minimum width is set and current width is too low 489 if ($minw > 0 && $width < $minw) { 490 debug_event(self::class, "Image width not in range (min=$minw, max=$maxw, current=$width).", 1); 491 492 return false; 493 } 494 // max width is set and current width is too high 495 if ($maxw > 0 && $width > $maxw) { 496 debug_event(self::class, "Image width not in range (min=$minw, max=$maxw, current=$width).", 1); 497 498 return false; 499 } 500 if ($minh > 0 && $height < $minh) { 501 debug_event(self::class, "Image height not in range (min=$minh, max=$maxh, current=$height).", 1); 502 503 return false; 504 } 505 if ($maxh > 0 && $height > $maxh) { 506 debug_event(self::class, "Image height not in range (min=$minh, max=$maxh, current=$height).", 1); 507 508 return false; 509 } 510 } 511 512 return true; 513 } 514 515 /** 516 * clear_image 517 * Clear the image column (if you have the image on disk) 518 */ 519 public static function clear_image($image_id) 520 { 521 $sql = "UPDATE `image` SET `image` = NULL WHERE `id` = ?"; 522 Dba::write($sql, array($image_id)); 523 } // clear_image 524 525 /** 526 * get_dir_on_disk 527 * @param string $type 528 * @param string $uid 529 * @param string $kind 530 * @param boolean $autocreate 531 * @return false|string 532 */ 533 public static function get_dir_on_disk($type, $uid, $kind = '', $autocreate = false) 534 { 535 $path = AmpConfig::get('local_metadata_dir'); 536 if (!$path) { 537 debug_event(self::class, 'local_metadata_dir setting is required to store art on disk.', 1); 538 539 return false; 540 } 541 542 // Correctly detect the slash we need to use here 543 if (strpos($path, '/') !== false) { 544 $slash_type = '/'; 545 } else { 546 $slash_type = '\\'; 547 } 548 549 $path .= $slash_type . $type; 550 if ($autocreate && !Core::is_readable($path)) { 551 mkdir($path); 552 } 553 554 $path .= $slash_type . $uid; 555 if ($autocreate && !Core::is_readable($path)) { 556 mkdir($path); 557 } 558 559 if (!empty($kind)) { 560 $path .= $slash_type . $kind; 561 if ($autocreate && !Core::is_readable($path)) { 562 mkdir($path); 563 } 564 } 565 $path .= $slash_type; 566 567 return $path; 568 } 569 570 /** 571 * write_to_dir 572 * @param string $source 573 * @param $sizetext 574 * @param string $type 575 * @param integer $uid 576 * @param $kind 577 * @return boolean 578 */ 579 private static function write_to_dir($source, $sizetext, $type, $uid, $kind, $mime) 580 { 581 $path = self::get_dir_on_disk($type, $uid, $kind, true); 582 if ($path === false) { 583 return false; 584 } 585 $path .= "art-" . $sizetext . "." . self::extension($mime); 586 if (Core::is_readable($path)) { 587 unlink($path); 588 } 589 $filepath = fopen($path, "wb"); 590 fwrite($filepath, $source); 591 fclose($filepath); 592 593 return true; 594 } 595 596 /** 597 * read_from_dir 598 * @param $sizetext 599 * @param string $type 600 * @param integer $uid 601 * @param string $kind 602 * @return string|null 603 */ 604 private static function read_from_dir($sizetext, $type, $uid, $kind, $mime) 605 { 606 $path = self::get_dir_on_disk($type, $uid, $kind); 607 if ($path === false) { 608 return null; 609 } 610 $path .= "art-" . $sizetext . '.' . self::extension($mime); 611 if (!Core::is_readable($path)) { 612 debug_event(self::class, 'Local image art ' . $path . ' cannot be read.', 1); 613 614 return null; 615 } 616 617 $image = ''; 618 $filepath = fopen($path, "rb"); 619 do { 620 $image .= fread($filepath, 2048); 621 } while (!feof($filepath)); 622 fclose($filepath); 623 624 return $image; 625 } 626 627 /** 628 * delete_from_dir 629 * @param string $type 630 * @param string $uid 631 * @param string $kind 632 */ 633 private static function delete_from_dir($type, $uid, $kind = '') 634 { 635 if ($type && $uid) { 636 $path = self::get_dir_on_disk($type, $uid, $kind); 637 if ($path !== false) { 638 self::delete_rec_dir(rtrim($path, '/')); 639 } 640 } 641 } 642 643 /** 644 * delete_rec_dir 645 * @param string $path 646 */ 647 private static function delete_rec_dir($path) 648 { 649 debug_event(self::class, 'Deleting ' . (string) $path . ' directory...', 5); 650 651 if (Core::is_readable($path)) { 652 foreach (scandir($path) as $file) { 653 if ('.' === $file || '..' === $file) { 654 continue; 655 } elseif (is_dir($path . '/' . $file)) { 656 self::delete_rec_dir(rtrim($path, '/') . '/' . $file); 657 } else { 658 unlink($path . '/' . $file); 659 } 660 } 661 rmdir($path); 662 } 663 } 664 665 /** 666 * reset 667 * This resets the art in the database 668 */ 669 public function reset() 670 { 671 if (AmpConfig::get('album_art_store_disk')) { 672 self::delete_from_dir($this->type, $this->uid, $this->kind); 673 } 674 $sql = "DELETE FROM `image` WHERE `object_id` = ? AND `object_type` = ? AND `kind` = ?"; 675 Dba::write($sql, array($this->uid, $this->type, $this->kind)); 676 } // reset 677 678 /** 679 * save_thumb 680 * This saves the thumbnail that we're passed 681 * @param string $source 682 * @param string $mime 683 * @param array $size 684 * @return boolean 685 */ 686 public function save_thumb($source, $mime, $size) 687 { 688 // Quick sanity check 689 if (!self::test_image($source)) { 690 debug_event(self::class, 'Not inserting thumbnail, invalid data passed', 1); 691 692 return false; 693 } 694 695 $width = $size['width']; 696 $height = $size['height']; 697 $sizetext = $width . 'x' . $height; 698 699 $sql = "DELETE FROM `image` WHERE `object_id` = ? AND `object_type` = ? AND `size` = ? AND `kind` = ?"; 700 Dba::write($sql, array($this->uid, $this->type, $sizetext, $this->kind)); 701 702 if (AmpConfig::get('album_art_store_disk')) { 703 self::write_to_dir($source, $sizetext, $this->type, $this->uid, $this->kind, $mime); 704 $source = null; 705 } 706 $sql = "INSERT INTO `image` (`image`, `mime`, `size`, `width`, `height`, `object_type`, `object_id`, `kind`) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; 707 Dba::write($sql, array($source, $mime, $sizetext, $width, $height, $this->type, $this->uid, $this->kind)); 708 709 return true; 710 } // save_thumb 711 712 /** 713 * get_thumb 714 * Returns the specified resized image. If the requested size doesn't 715 * already exist, create and cache it. 716 * @param array $size 717 * @return array 718 */ 719 public function get_thumb($size) 720 { 721 $sizetext = $size['width'] . 'x' . $size['height']; 722 $sql = "SELECT `image`, `mime` FROM `image` WHERE `size` = ? AND `object_type` = ? AND `object_id` = ? AND `kind` = ?"; 723 $db_results = Dba::read($sql, array($sizetext, $this->type, $this->uid, $this->kind)); 724 725 $results = Dba::fetch_assoc($db_results); 726 if (count($results)) { 727 if (AmpConfig::get('album_art_store_disk')) { 728 $image = self::read_from_dir($sizetext, $this->type, $this->uid, $this->kind, $results['mime']); 729 } else { 730 $image = $results['image']; 731 } 732 733 if ($image != null) { 734 return array( 735 'thumb' => (AmpConfig::get('album_art_store_disk')) 736 ? self::read_from_dir($sizetext, $this->type, $this->uid, $this->kind, $results['mime']) 737 : $results['image'], 738 'thumb_mime' => $results['mime'] 739 ); 740 } else { 741 debug_event(self::class, 'Thumb entry found in database but associated data cannot be found.', 3); 742 } 743 } 744 745 // If we didn't get a result try again 746 $results = array(); 747 if (!$this->raw && $this->thumb) { 748 $results = $this->generate_thumb($this->thumb, $size, $this->raw_mime); 749 } 750 if ($this->raw) { 751 $results = $this->generate_thumb($this->raw, $size, $this->raw_mime); 752 } 753 if (!empty($results)) { 754 $this->save_thumb($results['thumb'], $results['thumb_mime'], $size); 755 } 756 757 return $results; 758 } // get_thumb 759 760 /** 761 * generate_thumb 762 * Automatically resizes the image for thumbnail viewing. 763 * Only works on gif/jpg/png/bmp. Fails if PHP-GD isn't available 764 * or lacks support for the requested image type. 765 * @param string $image 766 * @param array $size 767 * @param string $mime 768 * @return array 769 */ 770 public function generate_thumb($image, $size, $mime) 771 { 772 $data = explode("/", (string) $mime); 773 $type = ((string) $data['1'] !== '') ? strtolower((string) $data['1']) : 'jpg'; 774 775 if (!self::test_image($image)) { 776 debug_event(self::class, 'Not trying to generate thumbnail, invalid data passed', 1); 777 778 return array(); 779 } 780 781 if (!function_exists('gd_info')) { 782 debug_event(self::class, 'PHP-GD Not found - unable to resize art', 1); 783 784 return array(); 785 } 786 787 // Check and make sure we can resize what you've asked us to 788 if (($type == 'jpg' || $type == 'jpeg' || $type == 'jpg?v=2') && !(imagetypes() & IMG_JPG)) { 789 debug_event(self::class, 'PHP-GD Does not support JPGs - unable to resize', 1); 790 791 return array(); 792 } 793 if ($type == 'png' && !imagetypes() & IMG_PNG) { 794 debug_event(self::class, 'PHP-GD Does not support PNGs - unable to resize', 1); 795 796 return array(); 797 } 798 if ($type == 'gif' && !imagetypes() & IMG_GIF) { 799 debug_event(self::class, 'PHP-GD Does not support GIFs - unable to resize', 1); 800 801 return array(); 802 } 803 if ($type == 'bmp' && !imagetypes() & IMG_WBMP) { 804 debug_event(self::class, 'PHP-GD Does not support BMPs - unable to resize', 1); 805 806 return array(); 807 } 808 809 $source = imagecreatefromstring($image); 810 811 if (!$source) { 812 debug_event(self::class, 'Failed to create Image from string - Source Image is damaged / malformed', 2); 813 814 return array(); 815 } 816 817 $source_size = array('height' => imagesy($source), 'width' => imagesx($source)); 818 819 // Create a new blank image of the correct size 820 $thumbnail = imagecreatetruecolor((int) $size['width'], (int) $size['height']); 821 822 if ($source_size['width'] > $source_size['height']) { 823 // landscape 824 $new_height = $size['height']; 825 $new_width = floor($source_size['width'] * ($new_height / $source_size['height'])); 826 $crop_x = ceil(($source_size['width'] - $source_size['height']) / 2); 827 $crop_y = 0; 828 } elseif ($source_size['height'] > $source_size['width']) { 829 // portrait 830 $new_width = $size['width']; 831 $new_height = floor($source_size['height'] * ($new_width / $source_size['width'])); 832 $crop_x = 0; 833 $crop_y = ceil(($source_size['height'] - $source_size['width']) / 3); // assuming most portrait images would have faces closer to the top 834 } else { 835 // square 836 $new_width = $size['width']; 837 $new_height = $size['height']; 838 $crop_x = 0; 839 $crop_y = 0; 840 } 841 842 if (!imagecopyresampled($thumbnail, $source, 0, 0, $crop_x, $crop_y, $new_width, $new_height, $source_size['width'], $source_size['height'])) { 843 debug_event(self::class, 'Unable to create resized image', 1); 844 imagedestroy($source); 845 imagedestroy($thumbnail); 846 847 return array(); 848 } 849 imagedestroy($source); 850 851 // Start output buffer 852 ob_start(); 853 854 // Generate the image to our OB 855 switch ($type) { 856 case 'jpg': 857 case 'jpeg': 858 case 'jpg?v=2': 859 case '(null)': 860 imagejpeg($thumbnail, null, 75); 861 $mime_type = image_type_to_mime_type(IMAGETYPE_JPEG); 862 break; 863 case 'gif': 864 imagegif($thumbnail); 865 $mime_type = image_type_to_mime_type(IMAGETYPE_GIF); 866 break; 867 // Turn bmps into pngs 868 case 'bmp': 869 case 'png': 870 imagepng($thumbnail); 871 $mime_type = image_type_to_mime_type(IMAGETYPE_PNG); 872 break; 873 default: 874 $mime_type = null; 875 } // resized 876 877 if ($mime_type === null) { 878 debug_event(self::class, 'Error: No mime type found using: ' . $mime, 2); 879 880 return array(); 881 } 882 883 $data = ob_get_contents(); 884 ob_end_clean(); 885 886 imagedestroy($thumbnail); 887 888 if (!strlen((string) $data)) { 889 debug_event(self::class, 'Unknown Error resizing art', 1); 890 891 return array(); 892 } 893 894 return array('thumb' => $data, 'thumb_mime' => $mime_type); 895 } // generate_thumb 896 897 /** 898 * get_from_source 899 * This gets an image for the album art from a source as 900 * defined in the passed array. Because we don't know where 901 * it's coming from we are a passed an array that can look like 902 * ['url'] = URL *** OPTIONAL *** 903 * ['file'] = FILENAME *** OPTIONAL *** 904 * ['raw'] = Actual Image data, already captured 905 * @param array $data 906 * @param string $type 907 * @return string 908 */ 909 public static function get_from_source($data, $type) 910 { 911 if (!isset($type)) { 912 $type = (AmpConfig::get('show_song_art')) ? 'song' : 'album'; 913 } 914 915 // Already have the data, this often comes from id3tags 916 if (isset($data['raw'])) { 917 return $data['raw']; 918 } 919 920 // If it came from the database 921 if (isset($data['db'])) { 922 $sql = "SELECT * FROM `image` WHERE `object_type` = ? AND `object_id` = ? AND `size`='original'"; 923 $db_results = Dba::read($sql, array($type, $data['db'])); 924 $row = Dba::fetch_assoc($db_results); 925 926 return $row['art']; 927 } // came from the db 928 929 // Check to see if it's a URL 930 if (filter_var($data['url'], FILTER_VALIDATE_URL)) { 931 debug_event(self::class, 'CHECKING URL ' . $data['url'], 2); 932 $options = array(); 933 try { 934 $options['timeout'] = 10; 935 Requests::register_autoloader(); 936 $request = Requests::get($data['url'], array(), Core::requests_options($options)); 937 $raw = $request->body; 938 } catch (Exception $error) { 939 debug_event(self::class, 'Error getting art: ' . $error->getMessage(), 2); 940 $raw = ''; 941 } 942 943 return $raw; 944 } 945 946 // Check to see if it's a FILE 947 if (isset($data['file'])) { 948 $handle = fopen($data['file'], 'rb'); 949 $image_data = (string)fread($handle, Core::get_filesize($data['file'])); 950 fclose($handle); 951 952 return $image_data; 953 } 954 955 // Check to see if it is embedded in id3 of a song 956 if (isset($data['song'])) { 957 // If we find a good one, stop looking 958 $getID3 = new getID3(); 959 $id3 = $getID3->analyze($data['song']); 960 961 if ($id3['format_name'] == "WMA") { 962 return $id3['asf']['extended_content_description_object']['content_descriptors']['13']['data']; 963 } elseif (isset($id3['id3v2']['APIC'])) { 964 // Foreach in case they have more then one 965 foreach ($id3['id3v2']['APIC'] as $image) { 966 return $image['data']; 967 } 968 } 969 } // if data song 970 971 return ''; 972 } // get_from_source 973 974 /** 975 * url 976 * This returns the constructed URL for the art in question 977 * @param integer $uid 978 * @param string $type 979 * @param string $sid 980 * @param integer|null $thumb 981 * @return string 982 */ 983 public static function url($uid, $type, $sid = null, $thumb = null) 984 { 985 if (!self::is_valid_type($type)) { 986 return null; 987 } 988 989 if (AmpConfig::get('use_auth') && AmpConfig::get('require_session')) { 990 $sid = $sid ? scrub_out($sid) : scrub_out(session_id()); 991 if ($sid == null) { 992 $sid = Session::create(array( 993 'type' => 'api' 994 )); 995 } 996 } 997 998 $key = $type . $uid; 999 1000 if (parent::is_cached('art', $key . '275x275') && AmpConfig::get('resize_images')) { 1001 $row = parent::get_from_cache('art', $key . '275x275'); 1002 $mime = $row['mime']; 1003 } 1004 if (parent::is_cached('art', $key . 'original')) { 1005 $row = parent::get_from_cache('art', $key . 'original'); 1006 $thumb_mime = $row['mime']; 1007 } 1008 if (!isset($mime) && !isset($thumb_mime)) { 1009 $sql = "SELECT `object_type`, `object_id`, `mime`, `size` FROM `image` WHERE `object_type` = ? AND `object_id` = ?"; 1010 $db_results = Dba::read($sql, array($type, $uid)); 1011 1012 while ($row = Dba::fetch_assoc($db_results)) { 1013 parent::add_to_cache('art', $key . $row['size'], $row); 1014 if ($row['size'] == 'original') { 1015 $mime = $row['mime']; 1016 } else { 1017 if ($row['size'] == '275x275' && AmpConfig::get('resize_images')) { 1018 $thumb_mime = $row['mime']; 1019 } 1020 } 1021 } 1022 } 1023 1024 $mime = isset($thumb_mime) ? $thumb_mime : (isset($mime) ? $mime : null); 1025 $extension = self::extension($mime); 1026 1027 if (AmpConfig::get('stream_beautiful_url')) { 1028 if (empty($extension)) { 1029 $extension = 'jpg'; 1030 } 1031 $url = AmpConfig::get('web_path') . '/play/art/' . $sid . '/' . scrub_out($type) . '/' . scrub_out($uid) . '/thumb'; 1032 if ($thumb !== null) { 1033 $url .= $thumb; 1034 } 1035 $url .= '.' . $extension; 1036 } else { 1037 $url = AmpConfig::get('web_path') . '/image.php?object_id=' . scrub_out($uid) . '&object_type=' . scrub_out($type) . '&auth=' . $sid; 1038 if ($thumb !== null) { 1039 $url .= '&thumb=' . $thumb; 1040 } 1041 if (!empty($extension)) { 1042 $name = 'art.' . $extension; 1043 $url .= '&name=' . $name; 1044 } 1045 } 1046 1047 return $url; 1048 } // url 1049 1050 /** 1051 * garbage_collection 1052 * This cleans up art that no longer has a corresponding object 1053 * @param string $object_type 1054 * @param integer $object_id 1055 */ 1056 public static function garbage_collection($object_type = null, $object_id = null) 1057 { 1058 $types = array( 1059 'album', 1060 'artist', 1061 'tvshow', 1062 'tvshow_season', 1063 'video', 1064 'user', 1065 'live_stream', 1066 'song' 1067 ); 1068 1069 if ($object_type !== null) { 1070 if (in_array($object_type, $types)) { 1071 if (AmpConfig::get('album_art_store_disk')) { 1072 self::delete_from_dir($object_type, $object_id); 1073 } 1074 $sql = "DELETE FROM `image` WHERE `object_type` = ? AND `object_id` = ?"; 1075 Dba::write($sql, array($object_type, $object_id)); 1076 } else { 1077 debug_event(self::class, 'Garbage collect on type `' . $object_type . '` is not supported.', 1); 1078 } 1079 } else { 1080 // iterate over our types and delete the images 1081 foreach ($types as $type) { 1082 if (AmpConfig::get('album_art_store_disk')) { 1083 $sql = "SELECT `image`.`object_id`, `image`.`object_type` FROM `image` LEFT JOIN `" . $type . "` ON `" . $type . "`.`id`=" . "`image`.`object_id` WHERE `object_type`='" . $type . "' AND `" . $type . "`.`id` IS NULL"; 1084 $db_results = Dba::read($sql); 1085 while ($row = Dba::fetch_row($db_results)) { 1086 self::delete_from_dir($row[1], $row[0]); 1087 } 1088 } 1089 $sql = "DELETE FROM `image` USING `image` LEFT JOIN `" . $type . "` ON `" . $type . "`.`id`=" . "`image`.`object_id` WHERE `object_type`='" . $type . "' AND `" . $type . "`.`id` IS NULL"; 1090 Dba::write($sql); 1091 } // foreach 1092 } 1093 } 1094 1095 /** 1096 * Migrate an object associate images to a new object 1097 * @param string $object_type 1098 * @param integer $old_object_id 1099 * @param integer $new_object_id 1100 * @return PDOStatement|boolean 1101 */ 1102 public static function migrate($object_type, $old_object_id, $new_object_id) 1103 { 1104 $sql = "UPDATE `image` SET `object_id` = ? WHERE `object_type` = ? AND `object_id` = ?"; 1105 1106 return Dba::write($sql, array($new_object_id, $object_type, $old_object_id)); 1107 } 1108 1109 /** 1110 * Duplicate an object associate images to a new object 1111 * @param string $object_type 1112 * @param integer $old_object_id 1113 * @param integer $new_object_id 1114 * @return PDOStatement|boolean 1115 */ 1116 public static function duplicate($object_type, $old_object_id, $new_object_id) 1117 { 1118 if (Art::has_db($new_object_id, $object_type) || $old_object_id == $new_object_id) { 1119 return false; 1120 } 1121 1122 debug_event(self::class, 'duplicate... type:' . $object_type . ' old_id:' . $old_object_id . ' new_id:' . $new_object_id, 5); 1123 if (AmpConfig::get('album_art_store_disk')) { 1124 $sql = "SELECT `size`, `kind`, `mime` FROM `image` WHERE `object_type` = ? AND `object_id` = ?"; 1125 $db_results = Dba::read($sql, array($object_type, $old_object_id)); 1126 while ($row = Dba::fetch_assoc($db_results)) { 1127 $image = self::read_from_dir($row['size'], $object_type, $old_object_id, $row['kind'], $row['mime']); 1128 if ($image !== null) { 1129 self::write_to_dir($image, $row['size'], $object_type, $new_object_id, $row['kind'], $row['mime']); 1130 } 1131 } 1132 } 1133 1134 $sql = "INSERT INTO `image` (`image`, `mime`, `size`, `object_type`, `object_id`, `kind`) SELECT `image`, `mime`, `size`, `object_type`, ? as `object_id`, `kind` FROM `image` WHERE `object_type` = ? AND `object_id` = ?"; 1135 1136 return Dba::write($sql, array($new_object_id, $object_type, $old_object_id)); 1137 } 1138 1139 /** 1140 * Gather metadata from plugin. 1141 * @param $plugin 1142 * @param string $type 1143 * @param array $options 1144 * @return array 1145 */ 1146 public static function gather_metadata_plugin($plugin, $type, $options) 1147 { 1148 $gtypes = array(); 1149 $media_info = array(); 1150 switch ($type) { 1151 case 'tvshow': 1152 case 'tvshow_season': 1153 case 'tvshow_episode': 1154 $gtypes[] = 'tvshow'; 1155 $media_info['tvshow'] = $options['tvshow']; 1156 $media_info['tvshow_season'] = $options['tvshow_season']; 1157 $media_info['tvshow_episode'] = $options['tvshow_episode']; 1158 break; 1159 case 'song': 1160 $media_info['mb_trackid'] = $options['mb_trackid']; 1161 $media_info['title'] = $options['title']; 1162 $media_info['artist'] = $options['artist']; 1163 $media_info['album'] = $options['album']; 1164 $gtypes[] = 'song'; 1165 break; 1166 case 'album': 1167 $media_info['mb_albumid'] = $options['mb_albumid']; 1168 $media_info['mb_albumid_group'] = $options['mb_albumid_group']; 1169 $media_info['artist'] = $options['artist']; 1170 $media_info['title'] = $options['album']; 1171 $gtypes[] = 'music'; 1172 $gtypes[] = 'album'; 1173 break; 1174 case 'artist': 1175 $media_info['mb_artistid'] = $options['mb_artistid']; 1176 $media_info['title'] = $options['artist']; 1177 $gtypes[] = 'music'; 1178 $gtypes[] = 'artist'; 1179 break; 1180 case 'movie': 1181 $gtypes[] = 'movie'; 1182 $media_info['title'] = $options['keyword']; 1183 break; 1184 } 1185 1186 $meta = $plugin->get_metadata($gtypes, $media_info); 1187 $images = array(); 1188 1189 if ($meta['art']) { 1190 $url = $meta['art']; 1191 $ures = pathinfo($url); 1192 $images[] = array('url' => $url, 'mime' => 'image/' . $ures['extension'], 'title' => $plugin->name); 1193 } 1194 if ($meta['tvshow_season_art']) { 1195 $url = $meta['tvshow_season_art']; 1196 $ures = pathinfo($url); 1197 $images[] = array('url' => $url, 'mime' => 'image/' . $ures['extension'], 'title' => $plugin->name); 1198 } 1199 if ($meta['tvshow_art']) { 1200 $url = $meta['tvshow_art']; 1201 $ures = pathinfo($url); 1202 $images[] = array('url' => $url, 'mime' => 'image/' . $ures['extension'], 'title' => $plugin->name); 1203 } 1204 1205 return $images; 1206 } 1207 1208 /** 1209 * Get thumb size from thumb type. 1210 * @param integer $thumb 1211 * @return array 1212 */ 1213 public static function get_thumb_size($thumb) 1214 { 1215 $size = array(); 1216 1217 switch ($thumb) { 1218 case 1: 1219 // This is used by the now_playing / browse stuff 1220 $size['height'] = 100; 1221 $size['width'] = 100; 1222 break; 1223 case 2: 1224 // live stream, artist pages 1225 $size['height'] = 128; 1226 $size['width'] = 128; 1227 break; 1228 case 22: 1229 $size['height'] = 256; 1230 $size['width'] = 256; 1231 break; 1232 case 32: 1233 // Single Album & Podcast pages 1234 $size['height'] = 384; 1235 $size['width'] = 384; 1236 break; 1237 case 3: 1238 // This is used by the embedded web player 1239 $size['height'] = 80; 1240 $size['width'] = 80; 1241 break; 1242 case 5: 1243 // Web Player size 1244 $size['height'] = 32; 1245 $size['width'] = 32; 1246 break; 1247 case 6: 1248 // Video browsing size 1249 $size['height'] = 150; 1250 $size['width'] = 100; 1251 break; 1252 case 34: 1253 // small 34x34 1254 $size['height'] = 34; 1255 $size['width'] = 34; 1256 break; 1257 case 64: 1258 // medium 64x64 1259 $size['height'] = 64; 1260 $size['width'] = 64; 1261 break; 1262 case 174: 1263 // large 174x174 1264 $size['height'] = 174; 1265 $size['width'] = 174; 1266 break; 1267 case 300: 1268 // extralarge, mega 300x300 1269 case 7: 1270 // Video page size 1271 $size['height'] = 300; 1272 $size['width'] = 200; 1273 break; 1274 case 8: 1275 // Video preview size 1276 $size['height'] = 200; 1277 $size['width'] = 470; 1278 break; 1279 case 9: 1280 // Video preview size 1281 $size['height'] = 84; 1282 $size['width'] = 150; // cel_cover max-width is 150px 1283 break; 1284 case 10: 1285 // Search preview size 1286 $size['height'] = 24; 1287 $size['width'] = 24; 1288 break; 1289 case 4: 1290 // Popup Web Player size 1291 case 11: 1292 // Large view browse size 1293 case 12: 1294 // Search preview size 1295 $size['height'] = 150; 1296 $size['width'] = 150; 1297 break; 1298 default: 1299 $size['height'] = 200; 1300 $size['width'] = 200; 1301 break; 1302 } 1303 1304 // For @2x output 1305 $size['height'] *= 2; 1306 $size['width'] *= 2; 1307 1308 return $size; 1309 } 1310 1311 /** 1312 * Display an item art. 1313 * @param string $object_type 1314 * @param integer $object_id 1315 * @param string $name 1316 * @param integer $thumb 1317 * @param string $link 1318 * @param boolean $show_default 1319 * @param string $kind 1320 * @return boolean 1321 */ 1322 public static function display( 1323 $object_type, 1324 $object_id, 1325 $name, 1326 $thumb, 1327 $link = null, 1328 $show_default = true, 1329 $kind = 'default' 1330 ) { 1331 if (!self::is_valid_type($object_type)) { 1332 return false; 1333 } 1334 1335 if (!$show_default) { 1336 // Don't show any image if not available 1337 if (!self::has_db($object_id, $object_type, $kind)) { 1338 return false; 1339 } 1340 } 1341 $size = self::get_thumb_size($thumb); 1342 $prettyPhoto = ($link === null); 1343 if ($link === null) { 1344 $link = AmpConfig::get('web_path') . "/image.php?object_id=" . $object_id . "&object_type=" . $object_type; 1345 if (AmpConfig::get('use_auth') && AmpConfig::get('require_session')) { 1346 $link .= "&auth=" . session_id(); 1347 } 1348 if ($kind != 'default') { 1349 $link .= '&kind=' . $kind; 1350 } 1351 } 1352 echo "<div class=\"item_art\">"; 1353 echo "<a href=\"" . $link . "\" title=\"" . $name . "\""; 1354 if ($prettyPhoto) { 1355 echo " rel=\"prettyPhoto\""; 1356 } 1357 echo ">"; 1358 $imgurl = AmpConfig::get('web_path') . "/image.php?object_id=" . $object_id . "&object_type=" . $object_type . "&thumb=" . $thumb; 1359 if ($kind != 'default') { 1360 $imgurl .= '&kind=' . $kind; 1361 } 1362 // This to keep browser cache feature but force a refresh in case image just changed 1363 if (Art::has_db($object_id, $object_type)) { 1364 $art = new Art($object_id, $object_type); 1365 if ($art->has_db_info()) { 1366 $imgurl .= '&fooid=' . $art->id; 1367 } 1368 } 1369 1370 // For @2x output 1371 $size['height'] /= 2; 1372 $size['width'] /= 2; 1373 1374 echo "<img src=\"" . $imgurl . "\" alt=\"" . $name . "\" height=\"" . $size['height'] . "\" width=\"" . $size['width'] . "\" />"; 1375 1376 // don't put the play icon on really large images. 1377 if ($size['height'] >= 150 && $size['height'] <= 300) { 1378 echo "<div class=\"item_art_play\">"; 1379 echo Ajax::text('?page=stream&action=directplay&object_type=' . $object_type . '&object_id=' . $object_id . '\' + getPagePlaySettings() + \'', 1380 '<span class="item_art_play_icon" title="' . T_('Play') . '" />', 1381 'directplay_art_' . $object_type . '_' . $object_id); 1382 echo "</div>"; 1383 } 1384 1385 if ($prettyPhoto) { 1386 $class_name = ObjectTypeToClassNameMapper::map($object_type); 1387 $libitem = new $class_name($object_id); 1388 echo "<div class=\"item_art_actions\">"; 1389 if (Core::get_global('user')->has_access(50) || (Core::get_global('user')->has_access(25) && Core::get_global('user')->id == $libitem->get_user_owner())) { 1390 echo "<a href=\"javascript:NavigateTo('" . AmpConfig::get('web_path') . "/arts.php?action=show_art_dlg&object_type=" . $object_type . "&object_id=" . $object_id . "&burl=' + getCurrentPage());\">"; 1391 echo Ui::get_icon('edit', T_('Edit/Find Art')); 1392 echo "</a>"; 1393 1394 echo "<a href=\"javascript:NavigateTo('" . AmpConfig::get('web_path') . "/arts.php?action=clear_art&object_type=" . $object_type . "&object_id=" . $object_id . "&burl=' + getCurrentPage());\" onclick=\"return confirm('" . T_('Do you really want to reset art?') . "');\">"; 1395 echo Ui::get_icon('delete', T_('Reset Art')); 1396 echo "</a>"; 1397 } 1398 echo "</div>"; 1399 } 1400 1401 echo "</a>\n"; 1402 echo "</div>"; 1403 1404 return true; 1405 } 1406 1407 /** 1408 * Get the object details for the art table 1409 * @return array 1410 */ 1411 public static function get_art_array() 1412 { 1413 $results = array(); 1414 $sql = "SELECT `id`, `object_id`, `object_type`, `size`, `mime` FROM `image` WHERE `image` IS NOT NULL;"; 1415 $db_results = Dba::read($sql); 1416 1417 while ($row = Dba::fetch_assoc($db_results)) { 1418 $results[] = $row; 1419 } 1420 1421 return $results; 1422 } 1423 1424 /** 1425 * Get the object details for the art table 1426 * @param array $data 1427 * @return string 1428 */ 1429 public static function get_raw_image($data) 1430 { 1431 $sql = "SELECT `image` FROM `image` WHERE `object_id` = ? AND `object_type` = ? AND `size` = ? AND `mime` = ?;"; 1432 $db_results = Dba::read($sql, $data); 1433 1434 while ($row = Dba::fetch_assoc($db_results)) { 1435 return (string)$row['image']; 1436 } 1437 } 1438 /** 1439 * @deprecated 1440 */ 1441 private function getSongRepository(): SongRepositoryInterface 1442 { 1443 global $dic; 1444 1445 return $dic->get(SongRepositoryInterface::class); 1446 } 1447} 1448