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
23namespace Ampache\Module\Catalog;
24
25use Ampache\Config\AmpConfig;
26use Ampache\Module\Util\UtilityFactoryInterface;
27use Ampache\Repository\Model\Art;
28use Ampache\Repository\Model\Catalog;
29use Ampache\Repository\Model\Media;
30use Ampache\Repository\Model\Podcast_Episode;
31use Ampache\Repository\Model\Song;
32use Ampache\Repository\Model\Song_Preview;
33use Ampache\Repository\Model\Video;
34use Ampache\Module\System\AmpError;
35use Ampache\Module\System\Dba;
36use Ampache\Module\Util\Ui;
37use Ampache\Module\Util\VaInfo;
38use Exception;
39use Kunnu\Dropbox\DropboxApp;
40use Kunnu\Dropbox\Dropbox;
41use Kunnu\Dropbox\DropboxFile;
42use Kunnu\Dropbox\Exceptions\DropboxClientException;
43use ReflectionException;
44
45/**
46 * This class handles all actual work in regards to remote Dropbox catalogs.
47 */
48class Catalog_dropbox extends Catalog
49{
50    private $version     = '000002';
51    private $type        = 'dropbox';
52    private $description = 'Dropbox Remote Catalog';
53
54    /**
55     * get_description
56     * This returns the description of this catalog
57     */
58    public function get_description()
59    {
60        return $this->description;
61    } // get_description
62
63    /**
64     * get_version
65     * This returns the current version
66     */
67    public function get_version()
68    {
69        return $this->version;
70    } // get_version
71
72    /**
73     * get_type
74     * This returns the current catalog type
75     */
76    public function get_type()
77    {
78        return $this->type;
79    } // get_type
80
81    /**
82     * get_create_help
83     * This returns hints on catalog creation
84     */
85    public function get_create_help()
86    {
87        return "<ul><li>" . T_("Go to https://www.dropbox.com/developers/apps/create") . "</li><li>" . T_("Select 'Dropbox API app'") . "</li><li>" . T_("Select 'Full Dropbox'") . "</li><li>" . T_("Give a name to your application and create it") . "</li><li>" . T_("Click the 'Generate' button to create an Access Token") . "</li><li>" . T_("Copy your App key and App secret and Access Token into the following fields.") . "</li></ul>";
88    } // get_create_help
89
90    /**
91     * is_installed
92     * This returns true or false if remote catalog is installed
93     */
94    public function is_installed()
95    {
96        $sql        = "SHOW TABLES LIKE 'catalog_dropbox'";
97        $db_results = Dba::query($sql);
98
99        return (Dba::num_rows($db_results) > 0);
100    } // is_installed
101
102    /**
103     * install
104     * This function installs the remote catalog
105     */
106    public function install()
107    {
108        $collation = (AmpConfig::get('database_collation', 'utf8mb4_unicode_ci'));
109        $charset   = (AmpConfig::get('database_charset', 'utf8mb4'));
110        $engine    = ($charset == 'utf8mb4') ? 'InnoDB' : 'MYISAM';
111
112        $sql = "CREATE TABLE `catalog_dropbox` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `apikey` VARCHAR(255) COLLATE $collation NOT NULL, `secret` VARCHAR(255) COLLATE $collation NOT NULL, `path` VARCHAR(255) COLLATE $collation NOT NULL, `authtoken` VARCHAR(255) COLLATE $collation NOT NULL, `getchunk` TINYINT(1) NOT NULL, `catalog_id` INT(11) NOT NULL) ENGINE = $engine DEFAULT CHARSET=$charset COLLATE=$collation";
113        Dba::query($sql);
114
115        return true;
116    } // install
117
118    /**
119     * @return array
120     */
121    public function catalog_fields()
122    {
123        $fields = array();
124
125        $fields['apikey']    = array('description' => T_('API key'), 'type' => 'text');
126        $fields['secret']    = array('description' => T_('Secret'), 'type' => 'password');
127        $fields['authtoken'] = array('description' => T_('Access Token'), 'type' => 'text');
128        $fields['path']      = array('description' => T_('Path'), 'type' => 'text', 'value' => '/');
129        $fields['getchunk']  = array(
130            'description' => T_('Get chunked files on analyze'),
131            'type' => 'checkbox',
132            'value' => true
133        );
134
135        return $fields;
136    }
137
138    /**
139     * @return boolean
140     */
141    public function isReady()
142    {
143        return (!empty($this->authtoken));
144    }
145
146    public function show_ready_process()
147    {
148        // $this->showAuthToken();
149    }
150
151    public function perform_ready()
152    {
153        // $this->authcode = $_REQUEST['authcode'];
154        // $this->completeAuthToken();
155    }
156
157    public $apikey;
158    public $secret;
159    public $path;
160    public $authtoken;
161    public $getchunk;
162
163    /**
164     * Constructor
165     *
166     * Catalog class constructor, pulls catalog information
167     * @param integer $catalog_id
168     */
169    public function __construct($catalog_id = null)
170    {
171        if ($catalog_id) {
172            $this->id = (int)$catalog_id;
173            $info     = $this->get_info($catalog_id);
174
175            foreach ($info as $key => $value) {
176                $this->$key = $value;
177            }
178        }
179    }
180
181    /**
182     * create_type
183     *
184     * This creates a new catalog type entry for a catalog
185     * It checks to make sure its parameters is not already used before creating
186     * the catalog.
187     * @param $catalog_id
188     * @param array $data
189     * @return boolean
190     */
191    public static function create_type($catalog_id, $data)
192    {
193        $apikey    = trim($data['apikey']);
194        $secret    = trim($data['secret']);
195        $authtoken = trim($data['authtoken']);
196        $path      = $data['path'];
197        $getchunk  = $data['getchunk'];
198
199        if (!strlen($apikey) || !strlen($secret) || !strlen($authtoken)) {
200            AmpError::add('general', T_('Error: API Key, Secret and Access Token Required for Dropbox Catalogs'));
201
202            return false;
203        }
204        try {
205            $app = new DropboxApp($apikey, $secret, $authtoken);
206        } catch (DropboxClientException $e) {
207            AmpError::add('general', T_('Invalid "API key", "secret", or "access token": ' . $e->getMessage()));
208
209            return false;
210        }
211        $dropbox = new Dropbox($app);
212
213        try {
214            $listFolderContents = $dropbox->listFolder($path);
215        } catch (DropboxClientException $e) {
216            AmpError::add('general', T_('Invalid "dropbox-path": ' . $e->getMessage()));
217            $listFolderContents = null;
218
219            return false;
220        }
221
222        // Make sure this catalog isn't already in use by an existing catalog
223        $sql        = 'SELECT `id` FROM `catalog_dropbox` WHERE `apikey` = ?';
224        $db_results = Dba::read($sql, array($apikey));
225
226        if (Dba::num_rows($db_results)) {
227            debug_event('dropbox.catalog', 'Cannot add catalog with duplicate key ' . $apikey, 1);
228            AmpError::add('general', sprintf(T_('Error: Catalog with %s already exists'), $apikey));
229
230            return false;
231        }
232
233        $sql = 'INSERT INTO `catalog_dropbox` (`apikey`, `secret`, `authtoken`, `path`, `getchunk`, `catalog_id`) VALUES (?, ?, ?, ?, ?, ?)';
234        Dba::write($sql, array($apikey, $secret, $authtoken, $path, ($getchunk ? 1 : 0), $catalog_id));
235
236        return true;
237    }
238
239    /**
240     * add_to_catalog
241     * this function adds new files to an
242     * existing catalog
243     * @param array $options
244     * @return boolean
245     */
246    public function add_to_catalog($options = null)
247    {
248        // Prevent the script from timing out
249        set_time_limit(0);
250
251        if ($options != null) {
252            $this->authcode = $options['authcode'];
253        }
254
255        if (!defined('SSE_OUTPUT')) {
256            Ui::show_box_top(T_('Running Dropbox Remote Update') . '. . .');
257        }
258        $this->update_remote_catalog();
259        if (!defined('SSE_OUTPUT')) {
260            Ui::show_box_bottom();
261        }
262
263        return true;
264    } // add_to_catalog
265
266    /**
267     * update_remote_catalog
268     *
269     * Pulls the data from a remote catalog and adds any missing songs to the
270     * database.
271     */
272    public function update_remote_catalog()
273    {
274        $app         = new DropboxApp($this->apikey, $this->secret, $this->authtoken);
275        $dropbox     = new Dropbox($app);
276        $this->count = 0;
277        $this->add_files($dropbox, $this->path);
278        /* Update the Catalog last_add */
279        $this->update_last_add();
280
281        Ui::update_text('', sprintf(T_('Catalog Update Finished.  Total Media: [%s]'), $this->count));
282
283        return true;
284    }
285
286    /**
287     * add_files
288     *
289     * Recurses through directories and pulls out all media files
290     * @param $dropbox
291     * @param $path
292     */
293    public function add_files($dropbox, $path)
294    {
295        debug_event('dropbox.catalog', "List contents for " . $path, 5);
296        $listFolderContents = $dropbox->listFolder($path, ['recursive' => true]);
297
298        // Fetch items on the first page
299        $items = $listFolderContents->getItems();
300        foreach ($items as $item) {
301            if ($item->getDataProperty('.tag') == "file") {
302                $subpath = $item->getDataProperty('path_display');
303                $this->add_file($dropbox, $subpath);
304            }
305        }
306
307        // Dropbox lists items in pages so you need to set your current
308        // position then re-fetch the list from that cursor position.
309        if ($listFolderContents->hasMoreItems()) {
310            do {
311                $cursor             = $listFolderContents->getCursor();
312                $listFolderContinue = $dropbox->listFolderContinue($cursor);
313                $remainingItems     = $listFolderContinue->getItems();
314                foreach ($remainingItems as $item) {
315                    if ($item->getDataProperty('.tag') == "file") {
316                        $subpath = $item->getDataProperty('path_display');
317                        $this->add_file($dropbox, $subpath);
318                    }
319                }
320            } while ($listFolderContinue->hasMoreItems() == true);
321        }
322    }
323
324    /**
325     * @param $dropbox
326     * @param $path
327     * @return boolean
328     */
329    public function add_file($dropbox, $path)
330    {
331        $file     = $dropbox->getMetadata($path, ["include_media_info" => true, "include_deleted" => true]);
332        $filesize = $file->getDataProperty('size');
333        if ($filesize > 0) {
334            $is_audio_file = Catalog::is_audio_file($path);
335            $is_video_file = Catalog::is_video_file($path);
336
337            if ($is_audio_file) {
338                if (count($this->get_gather_types('music')) > 0) {
339                    $this->insert_song($dropbox, $path);
340                } else {
341                    debug_event('dropbox.catalog', "read " . $path . " ignored, bad media type for this catalog.", 5);
342
343                    return false;
344                }
345            } else {
346                if (count($this->get_gather_types('video')) > 0) {
347                    if ($is_video_file) {
348                        $this->insert_video($dropbox, $path);
349                    } else {
350                        debug_event('dropbox.catalog',
351                            "read " . $path . " ignored, bad media type for this video catalog.", 5);
352
353                        return false;
354                    }
355                }
356            }
357        } else {
358            debug_event('dropbox.catalog', "read " . $path . " ignored, 0 bytes", 5);
359        }
360
361        return true;
362    }
363
364    /**
365     * _insert_local_song
366     *
367     * Insert a song that isn't already in the database.
368     * @param $dropbox
369     * @param $path
370     * @return boolean
371     * @throws DropboxClientException|Exception
372     */
373    private function insert_song($dropbox, $path)
374    {
375        if ($this->check_remote_file($path)) {
376            debug_event('dropbox_catalog', 'Skipping existing song ' . $path, 5);
377        } else {
378            $readfile = true;
379            $meta     = $dropbox->getMetadata($path);
380            $outfile  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $meta->getName();
381
382            // Download File
383            $this->download($dropbox, $path, -1, $outfile);
384
385            $vainfo = $this->getUtilityFactory()->createVaInfo(
386                $outfile,
387                $this->get_gather_types('music'),
388                '',
389                '',
390                $this->sort_pattern,
391                $this->rename_pattern,
392                $readfile
393            );
394            $vainfo->get_info();
395
396            $key     = VaInfo::get_tag_type($vainfo->tags);
397            $results = VaInfo::clean_tag_info($vainfo->tags, $key, $outfile);
398            // Set the remote path
399            $results['file']    = $path;
400            $results['catalog'] = $this->id;
401
402            // Set the remote path
403            if (!empty($results['artist']) && !empty($results['album'])) {
404                $this->count++;
405                $results['file'] = $outfile;
406                $song_id         = Song::insert($results);
407                if ($song_id) {
408                    parent::gather_art([$song_id]);
409                }
410                $results['file'] = $path;
411                $sql             = "UPDATE `song` SET `file` = ? WHERE `id` = ?";
412                Dba::write($sql, array($results['file'], $song_id));
413            } else {
414                debug_event('dropbox.catalog',
415                    $results['file'] . " ignored because it is an orphan songs. Please check your catalog patterns.",
416                    5);
417            }
418            unlink($outfile);
419
420            return true;
421        }
422
423        return false;
424    }
425
426    /**
427     * insert_local_video
428     * This inserts a video file into the video file table the tag
429     * information we can get is super sketchy so it's kind of a crap shoot
430     * here
431     * @param $dropbox
432     * @param $path
433     * @return integer
434     * @throws DropboxClientException|Exception
435     */
436    public function insert_video($dropbox, $path)
437    {
438        if ($this->check_remote_file($path)) {
439            debug_event('dropbox_catalog', 'Skipping existing song ' . $path, 5);
440        } else {
441            /* Create the vainfo object and get info */
442            $readfile = true;
443            $meta     = $dropbox->getMetadata($path);
444            $outfile  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $meta->getName();
445
446            // Download File
447            $res = $this->download($dropbox, $path, 40960, $outfile);
448
449            if ($res) {
450                $gtypes = $this->get_gather_types('video');
451
452                $vainfo = $this->getUtilityFactory()->createVaInfo(
453                    $outfile,
454                    $gtypes,
455                    '',
456                    '',
457                    $this->sort_pattern,
458                    $this->rename_pattern,
459                    $readfile
460                );
461                $vainfo->get_info();
462
463                $tag_name           = VaInfo::get_tag_type($vainfo->tags, 'metadata_order_video');
464                $results            = VaInfo::clean_tag_info($vainfo->tags, $tag_name, $outfile);
465                $results['catalog'] = $this->id;
466
467                $results['file'] = $outfile;
468                $video_id        = Video::insert($results, $gtypes, []);
469                if ($results['art']) {
470                    $art = new Art($video_id, 'video');
471                    $art->insert_url($results['art']);
472
473                    if (AmpConfig::get('generate_video_preview')) {
474                        Video::generate_preview($video_id);
475                    }
476                } else {
477                    $this->videos_to_gather[] = $video_id;
478                }
479                $results['file'] = $path;
480                $sql             = "UPDATE `video` SET `file` = ? WHERE `id` = ?";
481                Dba::write($sql, array($results['file'], $video_id));
482
483                return $video_id;
484            } else {
485                debug_event('dropbox.catalog', 'failed to download file', 5, 'ampache-catalog');
486            }
487        } // insert_video
488
489        return 0;
490    }
491
492    /**
493     * @param $dropbox
494     * @param $path
495     * @param $maxlen
496     * @param $dropboxFile
497     * @return boolean
498     * @throws DropboxClientException
499     */
500    public function download($dropbox, $path, $maxlen, $dropboxFile = null)
501    {
502        // Path cannot be null
503        if (is_null($path)) {
504            throw new DropboxClientException("Path cannot be null.");
505        }
506
507        // Make Dropbox File if target is specified
508        $dropboxFile = $dropboxFile ? $dropbox->makeDropboxFile($dropboxFile, $maxlen, null,
509            DropboxFile::MODE_WRITE) : null;
510
511        // Download File
512        $response = $dropbox->postToContent('/files/download', ['path' => $path], null, $dropboxFile);
513        if ($response->getHttpStatusCode() == 200) {
514            return true;
515        }
516
517        return false;
518    }
519
520    /**
521     * @return array
522     * @throws ReflectionException
523     */
524    public function verify_catalog_proc()
525    {
526        $updated = array('total' => 0, 'updated' => 0);
527
528        set_time_limit(0);
529
530        $utilityFactory = $this->getUtilityFactory();
531        $app            = new DropboxApp($this->apikey, $this->secret, $this->authtoken);
532        $dropbox        = new Dropbox($app);
533        try {
534            $sql        = 'SELECT `id`, `file`, `title` FROM `song` WHERE `catalog` = ?';
535            $db_results = Dba::read($sql, array($this->id));
536            while ($row = Dba::fetch_assoc($db_results)) {
537                $updated['total']++;
538                debug_event('dropbox.catalog', 'Starting verify on ' . $row['file'] . '(' . $row['id'] . ')', 5,
539                    'ampache-catalog');
540                $path     = $row['file'];
541                $readfile = true;
542                $filesize = 40960;
543                $meta     = $dropbox->getMetadata($path);
544                $outfile  = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $meta->getName();
545
546                $res = $this->download($dropbox, $path, $filesize, $outfile);
547                if ($res) {
548                    debug_event('dropbox.catalog', 'updating song', 5, 'ampache-catalog');
549                    $song = new Song($row['id']);
550
551                    $vainfo = $utilityFactory->createVaInfo(
552                        $outfile,
553                        $this->get_gather_types('music'),
554                        '',
555                        '',
556                        $this->sort_pattern,
557                        $this->rename_pattern,
558                        $readfile
559                    );
560                    $vainfo->forceSize($filesize);
561                    $vainfo->get_info();
562
563                    $key     = VaInfo::get_tag_type($vainfo->tags);
564                    $results = VaInfo::clean_tag_info($vainfo->tags, $key, $outfile);
565                    // Must compare to original path, not temporary location.
566                    $results['file'] = $path;
567                    $info            = ($song->id) ? self::update_song_from_tags($results, $song) : array();
568                    if ($info['change']) {
569                        Ui::update_text('', sprintf(T_('Updated song: "%s"'), $row['title']));
570                        $updated['updated']++;
571                    } else {
572                        Ui::update_text('', sprintf(T_('Song up to date: "%s"'), $row['title']));
573                    }
574                } else {
575                    debug_event('dropbox.catalog', 'removing song', 5, 'ampache-catalog');
576                    Ui::update_text('', sprintf(T_('Removing song: "%s"'), $row['title']));
577                    Dba::write('DELETE FROM `song` WHERE `id` = ?', array($row['id']));
578                }
579            }
580
581            $this->update_last_update();
582        } catch (DropboxClientException $e) {
583            AmpError::add('general', T_('Invalid "API key", "secret", or "access token": ' . $e->getMessage()));
584        }
585
586        return $updated;
587    }
588
589    /**
590     * clean_catalog_proc
591     *
592     * Removes songs that no longer exist.
593     */
594    public function clean_catalog_proc()
595    {
596        $dead    = 0;
597        $app     = new DropboxApp($this->apikey, $this->secret, $this->authtoken);
598        $dropbox = new Dropbox($app);
599
600        $sql        = 'SELECT `id`, `file` FROM `song` WHERE `catalog` = ?';
601        $db_results = Dba::read($sql, array($this->id));
602        while ($row = Dba::fetch_assoc($db_results)) {
603            debug_event('dropbox.catalog', 'Starting clean on ' . $row['file'] . '(' . $row['id'] . ')', 5,
604                'ampache-catalog');
605            $file = $row['file'];
606            try {
607                $metadata = $dropbox->getMetadata($file, ["include_deleted" => true]);
608            } catch (DropboxClientException $e) {
609                if ($e->getCode() == 409) {
610                    $dead++;
611                    Dba::write('DELETE FROM `song` WHERE `id` = ?', array($row['id']));
612                } else {
613                    AmpError::add('general', T_('API Error: cannot connect to Dropbox.'));
614                }
615            }
616        }
617        $this->update_last_clean();
618
619        return $dead;
620    }
621
622    /**
623     * move_catalog_proc
624     * This function updates the file path of the catalog to a new location (unsupported)
625     * @param string $new_path
626     * @return boolean
627     */
628    public function move_catalog_proc($new_path)
629    {
630        return false;
631    }
632
633    /**
634     * @return boolean
635     */
636    public function cache_catalog_proc()
637    {
638        return false;
639    }
640
641    /**
642     * check_remote_song
643     *
644     * checks to see if a remote song exists in the database or not
645     * if it find a song it returns the UID
646     * @param $file
647     * @return boolean|mixed
648     */
649    public function check_remote_file($file)
650    {
651        $is_audio_file = Catalog::is_audio_file($file);
652        if ($is_audio_file) {
653            $sql = 'SELECT `id` FROM `song` WHERE `file` = ?';
654        } else {
655            $sql = 'SELECT `id` FROM `video` WHERE `file` = ?';
656        }
657        $db_results = Dba::read($sql, array($file));
658        if ($results = Dba::fetch_assoc($db_results)) {
659            return $results['id'];
660        }
661
662        return false;
663    }
664
665    /**
666     * @param $file
667     * @return string
668     */
669    public function get_virtual_path($file)
670    {
671        return $this->apikey . '|' . $file;
672    }
673
674    /**
675     * @param string $file_path
676     * @return false|string
677     */
678    public function get_rel_path($file_path)
679    {
680        $path = strpos($file_path, "|");
681        if ($path !== false) {
682            $path++;
683        }
684
685        return substr($file_path, $path);
686    }
687
688    /**
689     * format
690     *
691     * This makes the object human-readable.
692     */
693    public function format()
694    {
695        parent::format();
696        $this->f_info      = $this->apikey;
697        $this->f_full_info = $this->apikey;
698    }
699
700    /**
701     * @param Podcast_Episode|Song|Song_Preview|Video $media
702     * @return Media|Podcast_Episode|Song|Song_Preview|Video|null
703     */
704    public function prepare_media($media)
705    {
706        $app     = new DropboxApp($this->apikey, $this->secret, $this->authtoken);
707        $dropbox = new Dropbox($app);
708        try {
709            set_time_limit(0);
710            $meta = $dropbox->getMetadata($media->file);
711
712            $outfile = sys_get_temp_dir() . "/" . $meta->getName();
713
714            // Download File
715            $this->download($dropbox, $media->file, null, $outfile);
716            $media->file = $outfile;
717            // Generate browser class for sending headers
718            fclose($outfile);
719        } catch (DropboxClientException $e) {
720            debug_event('dropbox.catalog', 'File not found on Dropbox: ' . $media->file, 5);
721        }
722
723        return $media;
724    }
725
726    /**
727     * gather_art
728     *
729     * This runs through all of the albums and finds art for them
730     * This runs through all of the needs art albums and tries
731     * to find the art for them from the mp3s
732     * @param integer[]|null $songs
733     * @param integer[]|null $videos
734     * @return boolean
735     * @throws DropboxClientException
736     */
737    public function gather_art($songs = null, $videos = null)
738    {
739
740        // Make sure they've actually got methods
741        $art_order = AmpConfig::get('art_order');
742        if (!count($art_order)) {
743            debug_event('dropbox.catalog', 'art_order not set, Catalog::gather_art aborting', 3);
744
745            return true;
746        }
747        $app     = new DropboxApp($this->apikey, $this->secret, $this->authtoken);
748        $dropbox = new Dropbox($app);
749        $songs   = $this->get_songs();
750
751        // Prevent the script from timing out
752        set_time_limit(0);
753
754        $search_count = 0;
755        $searches     = array();
756        if ($songs == null) {
757            $searches['album']  = $this->get_album_ids();
758            $searches['artist'] = $this->get_artist_ids();
759        } else {
760            $searches['album']  = array();
761            $searches['artist'] = array();
762            foreach ($songs as $song) {
763                if ($song->id) {
764                    $meta    = $dropbox->getMetadata($song->file);
765                    $outfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $meta->getName();
766
767                    // Download File
768                    $res = $this->download($dropbox, $song->file, 40960, $outfile);
769                    if ($res) {
770                        $sql = "UPDATE `song` SET `file` = ? WHERE `id` = ?";
771                        Dba::write($sql, array($outfile, $song->id));
772                        parent::gather_art([$song->id]);
773                        $sql = "UPDATE `song` SET `file` = ? WHERE `id` = ?";
774                        Dba::write($sql, array($song->file, $song->id));
775                        $search_count++;
776                        if (Ui::check_ticker()) {
777                            Ui::update_text('count_art_' . $this->id, $search_count);
778                        }
779                    }
780                }
781            }
782        }
783
784        // One last time for good measure
785        Ui::update_text('count_art_' . $this->id, $search_count);
786
787        return true;
788    }
789
790    /**
791     * @deprecated Inject by constructor
792     */
793    private function getUtilityFactory(): UtilityFactoryInterface
794    {
795        global $dic;
796
797        return $dic->get(UtilityFactoryInterface::class);
798    }
799}
800