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