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\Module\Api;
26
27use Ampache\Module\Authorization\Access;
28use Ampache\Repository\Model\Album;
29use Ampache\Repository\Model\Bookmark;
30use Ampache\Repository\Model\Podcast;
31use Ampache\Module\Playback\Localplay\LocalPlay;
32use Ampache\Module\Util\InterfaceImplementationChecker;
33use Ampache\Config\AmpConfig;
34use Ampache\Repository\Model\Art;
35use Ampache\Repository\Model\Artist;
36use Ampache\Repository\Model\Catalog;
37use Ampache\Module\System\Dba;
38use Ampache\Repository\Model\Live_Stream;
39use Ampache\Repository\Model\Playlist;
40use Ampache\Repository\Model\Podcast_Episode;
41use Ampache\Repository\Model\Preference;
42use Ampache\Repository\Model\PrivateMsg;
43use Ampache\Repository\Model\Rating;
44use Ampache\Repository\Model\Search;
45use Ampache\Repository\Model\Share;
46use Ampache\Repository\AlbumRepositoryInterface;
47use Ampache\Repository\Model\User_Playlist;
48use Ampache\Repository\SongRepositoryInterface;
49use SimpleXMLElement;
50use Ampache\Repository\Model\Song;
51use Ampache\Repository\Model\Tag;
52use Ampache\Repository\Model\User;
53use Ampache\Repository\Model\Userflag;
54use Ampache\Repository\Model\Video;
55
56/**
57 * XML_Data Class
58 *
59 * This class takes care of all of the xml document stuff in Ampache these
60 * are all static calls
61 *
62 */
63class Subsonic_Xml_Data
64{
65    const API_VERSION = "1.13.0";
66
67    const SSERROR_GENERIC               = 0;
68    const SSERROR_MISSINGPARAM          = 10;
69    const SSERROR_APIVERSION_CLIENT     = 20;
70    const SSERROR_APIVERSION_SERVER     = 30;
71    const SSERROR_BADAUTH               = 40;
72    const SSERROR_TOKENAUTHNOTSUPPORTED = 41;
73    const SSERROR_UNAUTHORIZED          = 50;
74    const SSERROR_TRIAL                 = 60;
75    const SSERROR_DATA_NOTFOUND         = 70;
76
77    // Ampache doesn't have a global unique id but each items are unique per category. We use id pattern to identify item category.
78    const AMPACHEID_ARTIST    = 100000000;
79    const AMPACHEID_ALBUM     = 200000000;
80    const AMPACHEID_SONG      = 300000000;
81    const AMPACHEID_SMARTPL   = 400000000;
82    const AMPACHEID_VIDEO     = 500000000;
83    const AMPACHEID_PODCAST   = 600000000;
84    const AMPACHEID_PODCASTEP = 700000000;
85    const AMPACHEID_PLAYLIST  = 800000000;
86
87    public static $enable_json_checks = false;
88
89    /**
90     * @param $artistid
91     * @return integer
92     */
93    public static function getArtistId($artistid)
94    {
95        return $artistid + self::AMPACHEID_ARTIST;
96    }
97
98    /**
99     * @param $albumid
100     * @return integer
101     */
102    public static function getAlbumId($albumid)
103    {
104        return $albumid + self::AMPACHEID_ALBUM;
105    }
106
107    /**
108     * @param $songid
109     * @return integer
110     */
111    public static function getSongId($songid)
112    {
113        return $songid + self::AMPACHEID_SONG;
114    }
115
116    /**
117     * @param integer $videoid
118     * @return integer
119     */
120    public static function getVideoId($videoid)
121    {
122        return $videoid + Subsonic_Xml_Data::AMPACHEID_VIDEO;
123    }
124
125    /**
126     * @param integer $plistid
127     * @return integer
128     */
129    public static function getSmartPlId($plistid)
130    {
131        return $plistid + self::AMPACHEID_SMARTPL;
132    }
133
134    /**
135     * @param integer $podcastid
136     * @return integer
137     */
138    public static function getPodcastId($podcastid)
139    {
140        return $podcastid + self::AMPACHEID_PODCAST;
141    }
142
143    /**
144     * @param integer $episode_id
145     * @return integer
146     */
147    public static function getPodcastEpId($episode_id)
148    {
149        return $episode_id + self::AMPACHEID_PODCASTEP;
150    }
151
152    /**
153     * @param integer $plist_id
154     * @return integer
155     */
156    public static function getPlaylistId($plist_id)
157    {
158        return $plist_id + self::AMPACHEID_PLAYLIST;
159    }
160
161    /**
162     * cleanId
163     * @param string $object_id
164     * @return integer
165     */
166    private static function cleanId($object_id)
167    {
168        // Remove all al-, ar-, ... prefixes
169        $tpos = strpos((string)$object_id, "-");
170        if ($tpos !== false) {
171            $object_id = substr((string) $object_id, $tpos + 1);
172        }
173
174        return (int) $object_id;
175    }
176
177    /**
178     * getAmpacheId
179     * @param string $object_id
180     * @return integer
181     */
182    public static function getAmpacheId($object_id)
183    {
184        return (self::cleanId($object_id) % self::AMPACHEID_ARTIST);
185    }
186
187    /**
188     * getAmpacheIds
189     * @param array $object_ids
190     * @return array
191     */
192    public static function getAmpacheIds($object_ids)
193    {
194        $ampids = array();
195        foreach ($object_ids as $object_id) {
196            $ampids[] = self::getAmpacheId($object_id);
197        }
198
199        return $ampids;
200    }
201
202    /**
203     * getAmpacheIdArrays
204     * @param array $object_ids
205     * @return array
206     */
207    public static function getAmpacheIdArrays($object_ids)
208    {
209        $ampidarrays = array();
210        foreach ($object_ids as $object_id) {
211            $ampidarrays[] = array(
212                'object_id' => self::getAmpacheId($object_id),
213                'object_type' => self::getAmpacheType($object_id)
214            );
215        }
216
217        return $ampidarrays;
218    }
219
220    /**
221     * @param string $artist_id
222     * @return boolean
223     */
224    public static function isArtist($artist_id)
225    {
226        return (self::cleanId($artist_id) >= self::AMPACHEID_ARTIST && $artist_id < self::AMPACHEID_ALBUM);
227    }
228
229    /**
230     * @param string $album_id
231     * @return boolean
232     */
233    public static function isAlbum($album_id)
234    {
235        return (self::cleanId($album_id) >= self::AMPACHEID_ALBUM && $album_id < self::AMPACHEID_SONG);
236    }
237
238    /**
239     * @param string $song_id
240     * @return boolean
241     */
242    public static function isSong($song_id)
243    {
244        return (self::cleanId($song_id) >= self::AMPACHEID_SONG && $song_id < self::AMPACHEID_SMARTPL);
245    }
246
247    /**
248     * @param string $plist_id
249     * @return boolean
250     */
251    public static function isSmartPlaylist($plist_id)
252    {
253        return (self::cleanId($plist_id) >= self::AMPACHEID_SMARTPL && $plist_id < self::AMPACHEID_VIDEO);
254    }
255
256    /**
257     * @param string $video_id
258     * @return boolean
259     */
260    public static function isVideo($video_id)
261    {
262        $video_id = self::cleanId($video_id);
263
264        return (self::cleanId($video_id) >= self::AMPACHEID_VIDEO && $video_id < self::AMPACHEID_PODCAST);
265    }
266
267    /**
268     * @param string $podcast_id
269     * @return boolean
270     */
271    public static function isPodcast($podcast_id)
272    {
273        return (self::cleanId($podcast_id) >= self::AMPACHEID_PODCAST && $podcast_id < self::AMPACHEID_PODCASTEP);
274    }
275
276    /**
277     * @param string $episode_id
278     * @return boolean
279     */
280    public static function isPodcastEp($episode_id)
281    {
282        return (self::cleanId($episode_id) >= self::AMPACHEID_PODCASTEP && $episode_id < self::AMPACHEID_PLAYLIST);
283    }
284
285    /**
286     * @param string $plistid
287     * @return boolean
288     */
289    public static function isPlaylist($plistid)
290    {
291        return (self::cleanId($plistid) >= self::AMPACHEID_PLAYLIST);
292    }
293
294    /**
295     * getAmpacheType
296     * @param string $object_id
297     * @return string
298     */
299    public static function getAmpacheType($object_id)
300    {
301        if (self::isArtist($object_id)) {
302            return "artist";
303        } elseif (self::isAlbum($object_id)) {
304            return "album";
305        } elseif (self::isSong($object_id)) {
306            return "song";
307        } elseif (self::isSmartPlaylist($object_id)) {
308            return "search";
309        } elseif (self::isVideo($object_id)) {
310            return "video";
311        } elseif (self::isPodcast($object_id)) {
312            return "podcast";
313        } elseif (self::isPodcastEp($object_id)) {
314            return "podcast_episode";
315        } elseif (self::isPlaylist($object_id)) {
316            return "playlist";
317        }
318
319        return "";
320    }
321
322    /**
323     * createFailedResponse
324     * @param string $function
325     * @return SimpleXMLElement
326     */
327    public static function createFailedResponse($function = '')
328    {
329        $version  = self::API_VERSION;
330        $response = self::createResponse($version, 'failed');
331        debug_event(self::class, 'API fail in function ' . $function . '-' . $version, 3);
332
333        return $response;
334    }
335
336    /**
337     * createSuccessResponse
338     * @param string $function
339     * @return SimpleXMLElement
340     */
341    public static function createSuccessResponse($function = '')
342    {
343        $version  = self::API_VERSION;
344        $response = self::createResponse($version);
345        debug_event(self::class, 'API success in function ' . $function . '-' . $version, 5);
346
347        return $response;
348    }
349
350    /**
351     * createResponse
352     * @param string $version
353     * @param string $status
354     * @return SimpleXMLElement
355     */
356    public static function createResponse($version, $status = 'ok')
357    {
358        $response = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><subsonic-response/>');
359        $response->addAttribute('xmlns', 'http://subsonic.org/restapi');
360        //       $response->addAttribute('type', 'ampache');
361        $response->addAttribute('status', (string)$status);
362        $response->addAttribute('version', (string)$version);
363
364        return $response;
365    }
366
367    /**
368     * createError
369     * @param $code
370     * @param string $message
371     * @param string $function
372     * @return SimpleXMLElement
373     */
374    public static function createError($code, $message, $function = '')
375    {
376        $response = self::createFailedResponse($function);
377        self::setError($response, $code, $message);
378
379        return $response;
380    }
381
382    /**
383     * setError
384     * Set error information.
385     *
386     * @param SimpleXMLElement $xml Parent node
387     * @param integer $code Error code
388     * @param string $message Error message
389     */
390    public static function setError($xml, $code, $message = '')
391    {
392        $xerr = $xml->addChild('error');
393        $xerr->addAttribute('code', (string)$code);
394
395        if (empty($message)) {
396            switch ($code) {
397                case self::SSERROR_GENERIC:
398                    $message = "A generic error.";
399                    break;
400                case self::SSERROR_MISSINGPARAM:
401                    $message = "Required parameter is missing.";
402                    break;
403                case self::SSERROR_APIVERSION_CLIENT:
404                    $message = "Incompatible Subsonic REST protocol version. Client must upgrade.";
405                    break;
406                case self::SSERROR_APIVERSION_SERVER:
407                    $message = "Incompatible Subsonic REST protocol version. Server must upgrade.";
408                    break;
409                case self::SSERROR_BADAUTH:
410                    $message = "Wrong username or password.";
411                    break;
412                case self::SSERROR_TOKENAUTHNOTSUPPORTED:
413                    $message = "Token authentication not supported.";
414                    break;
415                case self::SSERROR_UNAUTHORIZED:
416                    $message = "User is not authorized for the given operation.";
417                    break;
418                case self::SSERROR_TRIAL:
419                    $message = "The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium. Visit subsonic.org for details.";
420                    break;
421                case self::SSERROR_DATA_NOTFOUND:
422                    $message = "The requested data was not found.";
423                    break;
424            }
425        }
426
427        $xerr->addAttribute('message', (string)$message);
428    }
429
430    /**
431     * addLicense
432     * @param SimpleXMLElement $xml
433     */
434    public static function addLicense($xml)
435    {
436        $xlic = $xml->addChild('license');
437        $xlic->addAttribute('valid', 'true');
438        $xlic->addAttribute('email', 'webmaster@ampache.org');
439        $xlic->addAttribute('key', 'ABC123DEF');
440        $xlic->addAttribute('date', '2009-09-03T14:46:43');
441    }
442
443    /**
444     * addMusicFolders
445     * @param SimpleXMLElement $xml
446     * @param integer[] $catalogs
447     */
448    public static function addMusicFolders($xml, $catalogs)
449    {
450        $xfolders = $xml->addChild('musicFolders');
451        foreach ($catalogs as $folderid) {
452            $catalog = Catalog::create_from_id($folderid);
453            $xfolder = $xfolders->addChild('musicFolder');
454            $xfolder->addAttribute('id', (string)$folderid);
455            $xfolder->addAttribute('name', (string)$catalog->name);
456        }
457    }
458
459    /**
460     * addIgnoredArticles
461     * @param SimpleXMLElement $xml
462     */
463    private static function addIgnoredArticles($xml)
464    {
465        $ignoredArticles = AmpConfig::get('catalog_prefix_pattern');
466        if (!empty($ignoredArticles)) {
467            $ignoredArticles = str_replace("|", " ", $ignoredArticles);
468            $xml->addAttribute('ignoredArticles', (string)$ignoredArticles);
469        }
470    }
471
472    /**
473     * addArtistsIndexes
474     * @param SimpleXMLElement $xml
475     * @param array $artists
476     * @param $lastModified
477     * @param array $catalogs
478     */
479    public static function addArtistsIndexes($xml, $artists, $lastModified, $catalogs)
480    {
481        $xindexes = $xml->addChild('indexes');
482        $xindexes->addAttribute('lastModified', number_format($lastModified * 1000, 0, '.', ''));
483        self::addIgnoredArticles($xindexes);
484        foreach ($catalogs as $folderid) {
485            $catalog = Catalog::create_from_id($folderid);
486            $xfolder = $xindexes->addChild('shortcut');
487            $xfolder->addAttribute('id', (string)$folderid);
488            $xfolder->addAttribute('name', (string)$catalog->name);
489        }
490        self::addArtistArrays($xindexes, $artists);
491    }
492
493    /**
494     * addArtistsRoot
495     * @param SimpleXMLElement $xml
496     * @param array $artists
497     */
498    public static function addArtistsRoot($xml, $artists)
499    {
500        $xartists = $xml->addChild('artists');
501        self::addIgnoredArticles($xartists);
502        self::addArtistArrays($xartists, $artists);
503    }
504
505    /**
506     * addArtists
507     * @param SimpleXMLElement $xml
508     * @param Artist[] $artists
509     * @param boolean $extra
510     * @param boolean $albumsSet
511     */
512    public static function addArtists($xml, $artists, $extra = false, $albumsSet = false)
513    {
514        $xlastcat     = null;
515        $sharpartists = array();
516        $xlastletter  = '';
517        foreach ($artists as $artist) {
518            if (strlen((string)$artist->name) > 0) {
519                $letter = strtoupper((string)$artist->name[0]);
520                if ($letter == "X" || $letter == "Y" || $letter == "Z") {
521                    $letter = "X-Z";
522                } else {
523                    if (!preg_match("/^[A-W]$/", $letter)) {
524                        $sharpartists[] = $artist;
525                        continue;
526                    }
527                }
528
529                if ($letter != $xlastletter) {
530                    $xlastletter = $letter;
531                    $xlastcat    = $xml->addChild('index');
532                    $xlastcat->addAttribute('name', (string)$xlastletter);
533                }
534            }
535
536            if ($xlastcat != null) {
537                self::addArtist($xlastcat, $artist, $extra, false, $albumsSet);
538            }
539        }
540
541        // Always add # index at the end
542        if (count($sharpartists) > 0) {
543            $xsharpcat = $xml->addChild('index');
544            $xsharpcat->addAttribute('name', '#');
545
546            foreach ($sharpartists as $artist) {
547                self::addArtist($xsharpcat, $artist, $extra, false, $albumsSet);
548            }
549        }
550    }
551
552    /**
553     * addArtist
554     * @param SimpleXMLElement $xml
555     * @param Artist $artist
556     * @param boolean $extra
557     * @param boolean $albums
558     * @param boolean $albumsSet
559     */
560    public static function addArtist($xml, $artist, $extra = false, $albums = false, $albumsSet = false)
561    {
562        $artist->format();
563        $xartist = $xml->addChild('artist');
564        $xartist->addAttribute('id', (string)self::getArtistId($artist->id));
565        $xartist->addAttribute('name', (string)self::checkName($artist->f_name));
566        $allalbums = array();
567        if (($extra && !$albumsSet) || $albums) {
568            $allalbums = static::getAlbumRepository()->getByArtist($artist->id);
569        }
570
571        if ($extra) {
572            $xartist->addAttribute('coverArt', 'ar-' . (string)self::getArtistId($artist->id));
573            if ($albumsSet) {
574                $xartist->addAttribute('albumCount', (string)$artist->albums);
575            } else {
576                $xartist->addAttribute('albumCount', (string)count($allalbums));
577            }
578        }
579        if ($albums) {
580            foreach ($allalbums as $albumid) {
581                $album = new Album($albumid);
582                self::addAlbum($xartist, $album);
583            }
584        }
585    }
586
587    /**
588     * addArtistArrays
589     * @param SimpleXMLElement $xml
590     * @param array $artists
591     */
592    public static function addArtistArrays($xml, $artists)
593    {
594        $xlastcat     = null;
595        $sharpartists = array();
596        $xlastletter  = '';
597        foreach ($artists as $artist) {
598            if (strlen((string)$artist['name']) > 0) {
599                $letter = strtoupper((string)$artist['name'][0]);
600                if ($letter == "X" || $letter == "Y" || $letter == "Z") {
601                    $letter = "X-Z";
602                } else {
603                    if (!preg_match("/^[A-W]$/", $letter)) {
604                        $sharpartists[] = $artist;
605                        continue;
606                    }
607                }
608
609                if ($letter != $xlastletter) {
610                    $xlastletter = $letter;
611                    $xlastcat    = $xml->addChild('index');
612                    $xlastcat->addAttribute('name', (string)$xlastletter);
613                }
614            }
615
616            if ($xlastcat != null) {
617                self::addArtistArray($xlastcat, $artist);
618            }
619        }
620
621        // Always add # index at the end
622        if (count($sharpartists) > 0) {
623            $xsharpcat = $xml->addChild('index');
624            $xsharpcat->addAttribute('name', '#');
625
626            foreach ($sharpartists as $artist) {
627                self::addArtistArray($xsharpcat, $artist);
628            }
629        }
630    }
631
632    /**
633     * addArtistArray
634     * @param SimpleXMLElement $xml
635     * @param array $artist
636     */
637    public static function addArtistArray($xml, $artist)
638    {
639        $sub_id  = (string)self::getArtistId($artist['id']);
640        $xartist = $xml->addChild('artist');
641        $xartist->addAttribute('id', $sub_id);
642        $xartist->addAttribute('parent', $artist['catalog_id']);
643        $xartist->addAttribute('name', (string)self::checkName($artist['f_name']));
644
645        if (isset($artist['album_count'])) {
646            $xartist->addAttribute('coverArt', 'ar-' . $sub_id);
647            $xartist->addAttribute('albumCount', (string)$artist['album_count']);
648        }
649    }
650
651    /**
652     * addAlbumList
653     * @param SimpleXMLElement $xml
654     * @param $albums
655     * @param string $elementName
656     */
657    public static function addAlbumList($xml, $albums, $elementName = "albumList")
658    {
659        $xlist = $xml->addChild(htmlspecialchars($elementName));
660        foreach ($albums as $albumid) {
661            $album = new Album($albumid);
662            self::addAlbum($xlist, $album);
663        }
664    }
665
666    /**
667     * addAlbum
668     * @param SimpleXMLElement $xml
669     * @param Album $album
670     * @param boolean $songs
671     * @param string $elementName
672     */
673    public static function addAlbum($xml, $album, $songs = false, $elementName = "album")
674    {
675        $album->format();
676        $xalbum = $xml->addChild(htmlspecialchars($elementName));
677        $xalbum->addAttribute('id', (string)self::getAlbumId($album->id));
678        $xalbum->addAttribute('parent', (string) self::getArtistId($album->album_artist));
679        $xalbum->addAttribute('album', (string)self::checkName($album->f_name));
680        $xalbum->addAttribute('title', (string)self::checkName($album->f_title));
681        $xalbum->addAttribute('name', (string)self::checkName($album->f_name));
682        $xalbum->addAttribute('isDir', 'true');
683        $xalbum->addAttribute('discNumber', (string)$album->disk);
684
685        $xalbum->addAttribute('coverArt', 'al-' . self::getAlbumId($album->id));
686        $xalbum->addAttribute('songCount', (string) $album->song_count);
687        $xalbum->addAttribute('created', date("c", (int)$album->addition_time));
688        $xalbum->addAttribute('duration', (string) $album->total_duration);
689        $xalbum->addAttribute('artistId', (string) self::getArtistId($album->album_artist));
690        $xalbum->addAttribute('artist', (string) self::checkName($album->f_album_artist_name));
691        // original year (fall back to regular year)
692        $original_year = AmpConfig::get('use_original_year');
693        $year          = ($original_year && $album->original_year)
694            ? $album->original_year
695            : $album->year;
696        if ($year > 0) {
697            $xalbum->addAttribute('year', (string)$year);
698        }
699        if (count($album->tags) > 0) {
700            $tag_values = array_values($album->tags);
701            $tag        = array_shift($tag_values);
702            $xalbum->addAttribute('genre', (string)$tag['name']);
703        }
704
705        $rating      = new Rating($album->id, "album");
706        $user_rating = ($rating->get_user_rating() ?: 0);
707        if ($user_rating > 0) {
708            $xalbum->addAttribute('userRating', (string)ceil($user_rating));
709        }
710        $avg_rating = $rating->get_average_rating();
711        if ($avg_rating > 0) {
712            $xalbum->addAttribute('averageRating', (string)$avg_rating);
713        }
714
715        self::setIfStarred($xalbum, 'album', $album->id);
716
717        if ($songs) {
718            $disc_ids = $album->get_group_disks_ids();
719            foreach ($disc_ids as $discid) {
720                $disc     = new Album($discid);
721                $allsongs = static::getSongRepository()->getByAlbum($disc->id);
722                foreach ($allsongs as $songid) {
723                    self::addSong($xalbum, $songid);
724                }
725            }
726        }
727    }
728
729    /**
730     * addSong
731     * @param SimpleXMLElement $xml
732     * @param integer $songId
733     * @param string $elementName
734     * @return SimpleXMLElement
735     */
736    public static function addSong($xml, $songId, $elementName = 'song')
737    {
738        $songData    = self::getSongData($songId);
739        $albumData   = self::getAlbumData($songData['album']);
740        $artistData  = self::getArtistData($songData['artist']);
741        $catalogData = self::getCatalogData($songData['catalog'], $songData['file']);
742        //$catalog_path = rtrim((string) $catalogData[0], "/");
743
744        return self::createSong($xml, $songData, $albumData, $artistData, $catalogData, $elementName);
745    }
746
747    /**
748     * getSongData
749     * @param integer $songId
750     * @return array
751     */
752    public static function getSongData($songId)
753    {
754        $sql        = 'SELECT `song`.`id`, `song`.`file`, `song`.`catalog`, `song`.`album`, `album`.`album_artist` AS `albumartist`, `song`.`year`, `song`.`artist`, `song`.`title`, `song`.`bitrate`, `song`.`rate`, `song`.`mode`, `song`.`size`, `song`.`time`, `song`.`track`, `song`.`played`, `song`.`enabled`, `song`.`update_time`, `song`.`mbid`, `song`.`addition_time`, `song`.`license`, `song`.`composer`, `song`.`user_upload`, `song`.`total_count`, `album`.`mbid` AS `album_mbid`, `artist`.`mbid` AS `artist_mbid`, `album_artist`.`mbid` AS `albumartist_mbid` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` LEFT JOIN `artist` AS `album_artist` ON `album_artist`.`id` = `album`.`album_artist` WHERE `song`.`id` = ?';
755        $db_results = Dba::read($sql, array($songId));
756
757        $results = Dba::fetch_assoc($db_results);
758        if (isset($results['id'])) {
759            if (AmpConfig::get('show_played_times')) {
760                $results['object_cnt'] = (int) $results['total_count'];
761            }
762        }
763        $extension       = pathinfo((string)$results['file'], PATHINFO_EXTENSION);
764        $results['type'] = strtolower((string)$extension);
765        $results['mime'] = Song::type_to_mime($results['type']);
766
767        return $results;
768    }
769
770    /**
771     * getAlbumData
772     * @param integer $albumId
773     * @return array
774     */
775    public static function getAlbumData($albumId)
776    {
777        $sql        = "SELECT * FROM `album` WHERE `id`='$albumId'";
778        $db_results = Dba::read($sql);
779
780        if (!$db_results) {
781            return array();
782        }
783        $row           = Dba::fetch_assoc($db_results);
784        $row['f_name'] = trim(trim((string)$row['prefix']) . ' ' . trim((string)$row['name']));
785
786        return $row;
787    }
788
789    /**
790     * getArtistData
791     * @param integer $artistId
792     * @return array
793     */
794    public static function getArtistData($artistId)
795    {
796        $sql        = "SELECT * FROM `artist` WHERE `id`='$artistId'";
797        $db_results = Dba::read($sql);
798
799        if (!$db_results) {
800            return array();
801        }
802
803        $row           = Dba::fetch_assoc($db_results);
804        $row['f_name'] = trim(trim((string)$row['prefix']) . ' ' . trim((string)$row['name']));
805
806        return $row;
807    }
808
809    /**
810     * getCatalogData
811     * @param integer $catalogId
812     * @param string $file_Path
813     * @return array
814     */
815    public static function getCatalogData($catalogId, $file_Path)
816    {
817        $results     = array();
818        $sqllook     = 'SELECT `catalog_type` FROM `catalog` WHERE `id` = ?';
819        $db_results  = Dba::read($sqllook, [$catalogId]);
820        $resultcheck = Dba::fetch_assoc($db_results);
821        if (!empty($resultcheck)) {
822            $sql             = 'SELECT `path` FROM `catalog_' . $resultcheck['catalog_type'] . '` WHERE `catalog_id` = ?';
823            $db_results      = Dba::read($sql, [$catalogId]);
824            $result          = Dba::fetch_assoc($db_results);
825            $catalog_path    = rtrim((string)$result['path'], "/");
826            $results['path'] = str_replace($catalog_path . "/", "", $file_Path);
827        }
828
829        return $results;
830    }
831
832    /**
833     * createSong
834     * @param SimpleXMLElement $xml
835     * @param $songData
836     * @param $albumData
837     * @param $artistData
838     * @param $catalogData
839     * @param string $elementName
840     * @return SimpleXMLElement
841     */
842    public static function createSong(
843        $xml,
844        $songData,
845        $albumData,
846        $artistData,
847        $catalogData,
848        $elementName = 'song'
849    ) {
850        // Don't create entries for disabled songs
851        if (!$songData['enabled']) {
852            return null;
853        }
854
855        $xsong = $xml->addChild(htmlspecialchars($elementName));
856        $xsong->addAttribute('id', (string)self::getSongId($songData['id']));
857        $xsong->addAttribute('parent', (string)self::getAlbumId($songData['album']));
858        //$xsong->addAttribute('created', );
859        $xsong->addAttribute('title', (string)self::checkName($songData['title']));
860        $xsong->addAttribute('isDir', 'false');
861        $xsong->addAttribute('isVideo', 'false');
862        $xsong->addAttribute('type', 'music');
863        // $album = new Album(songData->album);
864        $xsong->addAttribute('albumId', (string)self::getAlbumId($albumData['id']));
865        $xsong->addAttribute('album', (string)self::checkName($albumData['f_name']));
866        // $artist = new Artist($song->artist);
867        // $artist->format();
868        $xsong->addAttribute('artistId', (string) self::getArtistId($songData['artist']));
869        $xsong->addAttribute('artist', (string) self::checkName($artistData['f_name']));
870        $art_object = (AmpConfig::get('show_song_art') && Art::has_db($songData['id'], 'song')) ? self::getSongId($songData['id']) : self::getAlbumId($albumData['id']);
871        $xsong->addAttribute('coverArt', (string) $art_object);
872        $xsong->addAttribute('duration', (string) $songData['time']);
873        $xsong->addAttribute('bitRate', (string) ((int) ($songData['bitrate'] / 1000)));
874        // <!-- Added in 1.14.0 -->
875        // $xsong->addAttribute('playCount', (string)$songData['object_cnt']);
876        $rating      = new Rating($songData['id'], "song");
877        $user_rating = ($rating->get_user_rating() ?: 0);
878        if ($user_rating > 0) {
879            $xsong->addAttribute('userRating', (string)ceil($user_rating));
880        }
881        $avg_rating = $rating->get_average_rating();
882        if ($avg_rating > 0) {
883            $xsong->addAttribute('averageRating', (string)$avg_rating);
884        }
885        self::setIfStarred($xsong, 'song', $songData['id']);
886        if ($songData['track'] > 0) {
887            $xsong->addAttribute('track', (string)$songData['track']);
888        }
889        if ($songData['year'] > 0) {
890            $xsong->addAttribute('year', (string)$songData['year']);
891        }
892        $tags = Tag::get_object_tags('song', (int) $songData['id']);
893        if (count($tags) > 0) {
894            $xsong->addAttribute('genre', (string)$tags[0]['name']);
895        }
896        $xsong->addAttribute('size', (string)$songData['size']);
897        if (Album::sanitize_disk($albumData['disk']) > 0) {
898            $xsong->addAttribute('discNumber', (string)Album::sanitize_disk($albumData['disk']));
899        }
900        $xsong->addAttribute('suffix', (string)$songData['type']);
901        $xsong->addAttribute('contentType', (string)$songData['mime']);
902        // Return a file path relative to the catalog root path
903        $xsong->addAttribute('path', (string)$catalogData['path']);
904
905        // Set transcoding information if required
906        $transcode_cfg = AmpConfig::get('transcode');
907        $valid_types   = Song::get_stream_types_for_type($songData['type'], 'api');
908        if ($transcode_cfg == 'always' || ($transcode_cfg != 'never' && !in_array('native', $valid_types))) {
909            // $transcode_settings = Song::get_transcode_settings_for_media(null, null, 'api', 'song');
910            $transcode_type = AmpConfig::get('encode_player_api_target', 'mp3');
911            $xsong->addAttribute('transcodedSuffix', (string)$transcode_type);
912            $xsong->addAttribute('transcodedContentType', Song::type_to_mime($transcode_type));
913        }
914
915        return $xsong;
916    }
917
918    /**
919     * checkName
920     * This to fix xml=>json which can result to wrong type parsing
921     * @param string $name
922     * @return string|null
923     */
924    private static function checkName($name)
925    {
926        // Ensure to have always a string type
927        if (self::$enable_json_checks && !empty($name)) {
928            if (is_numeric($name)) {
929                // Add space character to fail numeric test
930                $name = $name .= " ";
931            }
932        }
933
934        return html_entity_decode($name, ENT_NOQUOTES, 'UTF-8');
935    }
936
937    /**
938     * getAmpacheObject
939     * Return the Ampache media object
940     * @param integer $object_id
941     * @return Song|Video|Podcast_Episode|null
942     */
943    public static function getAmpacheObject($object_id)
944    {
945        if (Subsonic_Xml_Data::isSong($object_id)) {
946            return new Song(Subsonic_Xml_Data::getAmpacheId($object_id));
947        }
948        if (Subsonic_Xml_Data::isVideo($object_id)) {
949            return new Video(Subsonic_Xml_Data::getAmpacheId($object_id));
950        }
951        if (Subsonic_Xml_Data::isPodcastEp($object_id)) {
952            return new Podcast_Episode(Subsonic_Xml_Data::getAmpacheId($object_id));
953        }
954
955        return null;
956    } // getAmpacheObject
957
958    /**
959     * addArtistDirectory for subsonic artist id
960     * @param SimpleXMLElement $xml
961     * @param string $artist_id
962     */
963    public static function addArtistDirectory($xml, $artist_id)
964    {
965        $amp_id = self::getAmpacheId($artist_id);
966        $data   = Artist::get_id_array($amp_id);
967        $xdir   = $xml->addChild('directory');
968        $xdir->addAttribute('id', (string)$artist_id);
969        $xdir->addAttribute('parent', (string)Catalog::get_catalog_map('artist', $artist_id));
970        $xdir->addAttribute('name', (string)$data['f_name']);
971        $allalbums = static::getAlbumRepository()->getByArtist($amp_id);
972        foreach ($allalbums as $album_id) {
973            $album = new Album($album_id);
974            self::addAlbum($xdir, $album, false, "child");
975        }
976    }
977
978    /**
979     * addAlbumDirectory for subsonic album id
980     * @param SimpleXMLElement $xml
981     * @param string $album_id
982     */
983    public static function addAlbumDirectory($xml, $album_id)
984    {
985        $album = new Album(self::getAmpacheId($album_id));
986        $album->format();
987        $xdir = $xml->addChild('directory');
988        $xdir->addAttribute('id', (string)$album_id);
989        if ($album->album_artist) {
990            $xdir->addAttribute('parent', (string)self::getArtistId($album->album_artist));
991        } else {
992            $xdir->addAttribute('parent', (string)$album->catalog);
993        }
994        $xdir->addAttribute('name', (string)self::checkName($album->f_title));
995
996        $disc_ids  = $album->get_group_disks_ids();
997        $media_ids = static::getAlbumRepository()->getSongsGrouped($disc_ids);
998        foreach ($media_ids as $song_id) {
999            self::addSong($xdir, $song_id, "child");
1000        }
1001    }
1002
1003    /**
1004     * addGenres
1005     * @param SimpleXMLElement $xml
1006     * @param $tags
1007     */
1008    public static function addGenres($xml, $tags)
1009    {
1010        $xgenres = $xml->addChild('genres');
1011
1012        foreach ($tags as $tag) {
1013            $otag   = new Tag($tag['id']);
1014            $xgenre = $xgenres->addChild('genre', htmlspecialchars($otag->name));
1015            $counts = $otag->count();
1016            $xgenre->addAttribute('songCount', (string) $counts['song'] ?: 0);
1017            $xgenre->addAttribute('albumCount', (string) $counts['album'] ?: 0);
1018        }
1019    }
1020
1021    /**
1022     * addVideos
1023     * @param SimpleXMLElement $xml
1024     * @param Video[] $videos
1025     */
1026    public static function addVideos($xml, $videos)
1027    {
1028        $xvideos = $xml->addChild('videos');
1029        foreach ($videos as $video) {
1030            $video->format();
1031            self::addVideo($xvideos, $video);
1032        }
1033    }
1034
1035    /**
1036     * addVideo
1037     * @param SimpleXMLElement $xml
1038     * @param Video $video
1039     * @param string $elementName
1040     */
1041    public static function addVideo($xml, $video, $elementName = 'video')
1042    {
1043        $xvideo = $xml->addChild(htmlspecialchars($elementName));
1044        $xvideo->addAttribute('id', (string)self::getVideoId($video->id));
1045        $xvideo->addAttribute('title', (string)$video->f_full_title);
1046        $xvideo->addAttribute('isDir', 'false');
1047        $xvideo->addAttribute('coverArt', (string)self::getVideoId($video->id));
1048        $xvideo->addAttribute('isVideo', 'true');
1049        $xvideo->addAttribute('type', 'video');
1050        $xvideo->addAttribute('duration', (string)$video->time);
1051        if ($video->year > 0) {
1052            $xvideo->addAttribute('year', (string)$video->year);
1053        }
1054        $tags = Tag::get_object_tags('video', (int)$video->id);
1055        if (count($tags) > 0) {
1056            $xvideo->addAttribute('genre', (string)$tags[0]['name']);
1057        }
1058        $xvideo->addAttribute('size', (string)$video->size);
1059        $xvideo->addAttribute('suffix', (string)$video->type);
1060        $xvideo->addAttribute('contentType', (string)$video->mime);
1061        // Create a clean fake path instead of song real file path to have better offline mode storage on Subsonic clients
1062        $path = basename($video->file);
1063        $xvideo->addAttribute('path', (string)$path);
1064
1065        self::setIfStarred($xvideo, 'video', $video->id);
1066        // Set transcoding information if required
1067        $transcode_cfg = AmpConfig::get('transcode');
1068        $valid_types   = Song::get_stream_types_for_type($video->type, 'api');
1069        if ($transcode_cfg == 'always' || ($transcode_cfg != 'never' && !in_array('native', $valid_types))) {
1070            $transcode_settings = $video->get_transcode_settings(null, 'api');
1071            if (!empty($transcode_settings)) {
1072                $transcode_type = $transcode_settings['format'];
1073                $xvideo->addAttribute('transcodedSuffix', (string)$transcode_type);
1074                $xvideo->addAttribute('transcodedContentType', Video::type_to_mime($transcode_type));
1075            }
1076        }
1077    }
1078
1079    /**
1080     * addPlaylists
1081     * @param SimpleXMLElement $xml
1082     * @param $playlists
1083     * @param array $smartplaylists
1084     */
1085    public static function addPlaylists($xml, $playlists, $smartplaylists = array())
1086    {
1087        $xplaylists = $xml->addChild('playlists');
1088        foreach ($playlists as $plistid) {
1089            $playlist = new Playlist($plistid);
1090            self::addPlaylist($xplaylists, $playlist);
1091        }
1092        foreach ($smartplaylists as $splistid) {
1093            $smartplaylist = new Search((int)str_replace('smart_', '', (string)$splistid), 'song');
1094            self::addSmartPlaylist($xplaylists, $smartplaylist);
1095        }
1096    }
1097
1098    /**
1099     * addPlaylist
1100     * @param SimpleXMLElement $xml
1101     * @param Playlist $playlist
1102     * @param boolean $songs
1103     */
1104    public static function addPlaylist($xml, $playlist, $songs = false)
1105    {
1106        $playlist_id = (string)self::getPlaylistId($playlist->id);
1107        $songcount   = $playlist->get_media_count('song');
1108        $duration    = ($songcount > 0) ? $playlist->get_total_duration() : 0;
1109        $xplaylist   = $xml->addChild('playlist');
1110        $xplaylist->addAttribute('id', $playlist_id);
1111        $xplaylist->addAttribute('name', (string)self::checkName($playlist->get_fullname()));
1112        $xplaylist->addAttribute('owner', (string)$playlist->username);
1113        $xplaylist->addAttribute('public', ($playlist->type != "private") ? "true" : "false");
1114        $xplaylist->addAttribute('created', date("c", (int)$playlist->date));
1115        $xplaylist->addAttribute('changed', date("c", (int)$playlist->last_update));
1116        $xplaylist->addAttribute('songCount', (string)$songcount);
1117        $xplaylist->addAttribute('duration', (string)$duration);
1118        $xplaylist->addAttribute('coverArt', $playlist_id);
1119
1120        if ($songs) {
1121            $allsongs = $playlist->get_songs();
1122            foreach ($allsongs as $songId) {
1123                self::addSong($xplaylist, $songId, "entry");
1124            }
1125        }
1126    }
1127
1128    /**
1129     * addPlayQueue
1130     * current="133" position="45000" username="admin" changed="2015-02-18T15:22:22.825Z" changedBy="android"
1131     * @param SimpleXMLElement $xml
1132     * @param int $user_id
1133     * @param string $username
1134     */
1135    public static function addPlayQueue($xml, $user_id, $username)
1136    {
1137        $PlayQueue = new User_Playlist($user_id);
1138        $items     = $PlayQueue->get_items();
1139        if (!empty($items)) {
1140            $current    = $PlayQueue->get_current_object();
1141            $changed    = User::get_user_data($user_id, 'playqueue_date')['playqueue_date'];
1142            $changedBy  = User::get_user_data($user_id, 'playqueue_client')['playqueue_date'];
1143            $xplayqueue = $xml->addChild('playQueue');
1144            $xplayqueue->addAttribute('current', self::getSongId($current['object_id']));
1145            $xplayqueue->addAttribute('position', (string)$current['current_time']);
1146            $xplayqueue->addAttribute('username', (string)$username);
1147            $xplayqueue->addAttribute('changed', date("c", (int)$changed));
1148            $xplayqueue->addAttribute('changedBy', (string)$changedBy);
1149
1150            if ($items) {
1151                foreach ($items as $row) {
1152                    self::addSong($xplayqueue, (int)$row['object_id'], "entry");
1153                }
1154            }
1155        }
1156    }
1157
1158    /**
1159     * addSmartPlaylist
1160     * @param SimpleXMLElement $xml
1161     * @param Search $playlist
1162     * @param boolean $songs
1163     */
1164    public static function addSmartPlaylist($xml, $playlist, $songs = false)
1165    {
1166        $playlist_id = (string) self::getSmartPlId($playlist->id);
1167        $xplaylist   = $xml->addChild('playlist');
1168        debug_event(self::class, 'addsmartplaylist ' . $playlist->id, 5);
1169        $xplaylist->addAttribute('id', $playlist_id);
1170        $xplaylist->addAttribute('name', (string) self::checkName($playlist->get_fullname()));
1171        $xplaylist->addAttribute('owner', (string)$playlist->username);
1172        $xplaylist->addAttribute('public', ($playlist->type != "private") ? "true" : "false");
1173        $xplaylist->addAttribute('created', date("c", (int)$playlist->date));
1174        $xplaylist->addAttribute('changed', date("c", time()));
1175
1176        if ($songs) {
1177            $allitems = $playlist->get_items();
1178            $xplaylist->addAttribute('songCount', (string)count($allitems));
1179            $duration = (count($allitems) > 0) ? Search::get_total_duration($allitems) : 0;
1180            $xplaylist->addAttribute('duration', (string)$duration);
1181            $xplaylist->addAttribute('coverArt', $playlist_id);
1182            foreach ($allitems as $item) {
1183                self::addSong($xplaylist, (int)$item['object_id'], "entry");
1184            }
1185        } else {
1186            $xplaylist->addAttribute('songCount', (string)$playlist->last_count);
1187            $xplaylist->addAttribute('duration', (string)$playlist->last_duration);
1188            $xplaylist->addAttribute('coverArt', $playlist_id);
1189        }
1190    }
1191
1192    /**
1193     * addRandomSongs
1194     * @param SimpleXMLElement $xml
1195     * @param array $songs
1196     */
1197    public static function addRandomSongs($xml, $songs)
1198    {
1199        $xsongs = $xml->addChild('randomSongs');
1200        foreach ($songs as $songid) {
1201            self::addSong($xsongs, $songid);
1202        }
1203    }
1204
1205    /**
1206     * addSongsByGenre
1207     * @param SimpleXMLElement $xml
1208     * @param array $songs
1209     */
1210    public static function addSongsByGenre($xml, $songs)
1211    {
1212        $xsongs = $xml->addChild('songsByGenre');
1213        foreach ($songs as $songid) {
1214            self::addSong($xsongs, $songid);
1215        }
1216    }
1217
1218    /**
1219     * addTopSongs
1220     * @param SimpleXMLElement $xml
1221     * @param array $songs
1222     */
1223    public static function addTopSongs($xml, $songs)
1224    {
1225        $xsongs = $xml->addChild('topSongs');
1226        foreach ($songs as $songid) {
1227            self::addSong($xsongs, $songid);
1228        }
1229    }
1230
1231    /**
1232     * addNowPlaying
1233     * @param SimpleXMLElement $xml
1234     * @param array $data
1235     */
1236    public static function addNowPlaying($xml, $data)
1237    {
1238        $xplaynow = $xml->addChild('nowPlaying');
1239        foreach ($data as $d) {
1240            $track = self::addSong($xplaynow, $d['media']->getId(), "entry");
1241            if ($track !== null) {
1242                $track->addAttribute('username', (string)$d['client']->username);
1243                $track->addAttribute('minutesAgo',
1244                    (string)(abs((time() - ($d['expire'] - $d['media']->time)) / 60)));
1245                $track->addAttribute('playerId', (string)$d['agent']);
1246            }
1247        }
1248    }
1249
1250    /**
1251     * addSearchResult
1252     * @param SimpleXMLElement $xml
1253     * @param array $artists
1254     * @param array $albums
1255     * @param array $songs
1256     * @param string $elementName
1257     */
1258    public static function addSearchResult($xml, $artists, $albums, $songs, $elementName = "searchResult2")
1259    {
1260        $xresult = $xml->addChild(htmlspecialchars($elementName));
1261        foreach ($artists as $artistid) {
1262            $artist = new Artist($artistid);
1263            self::addArtist($xresult, $artist);
1264        }
1265        foreach ($albums as $albumid) {
1266            $album = new Album($albumid);
1267            self::addAlbum($xresult, $album);
1268        }
1269        foreach ($songs as $songid) {
1270            self::addSong($xresult, $songid);
1271        }
1272    }
1273
1274    /**
1275     * setIfStarred
1276     * @param SimpleXMLElement $xml
1277     * @param string $objectType
1278     * @param integer $object_id
1279     */
1280    private static function setIfStarred($xml, $objectType, $object_id)
1281    {
1282        if (InterfaceImplementationChecker::is_library_item($objectType)) {
1283            if (AmpConfig::get('userflags')) {
1284                $starred = new Userflag($object_id, $objectType);
1285                if ($res = $starred->get_flag(null, true)) {
1286                    $xml->addAttribute('starred', date("Y-m-d\TH:i:s\Z", (int)$res[1]));
1287                }
1288            }
1289        }
1290    }
1291
1292    /**
1293     * addStarred
1294     * @param SimpleXMLElement $xml
1295     * @param array $artists
1296     * @param array $albums
1297     * @param array $songs
1298     * @param string $elementName
1299     */
1300    public static function addStarred($xml, $artists, $albums, $songs, $elementName = "starred")
1301    {
1302        $xstarred = $xml->addChild(htmlspecialchars($elementName));
1303
1304        foreach ($artists as $artistid) {
1305            $artist = new Artist($artistid);
1306            self::addArtist($xstarred, $artist);
1307        }
1308
1309        foreach ($albums as $albumid) {
1310            $album = new Album($albumid);
1311            self::addAlbum($xstarred, $album);
1312        }
1313
1314        foreach ($songs as $songid) {
1315            self::addSong($xstarred, $songid);
1316        }
1317    }
1318
1319    /**
1320     * addUser
1321     * @param SimpleXMLElement $xml
1322     * @param User $user
1323     */
1324    public static function addUser($xml, $user)
1325    {
1326        $xuser = $xml->addChild('user');
1327        $xuser->addAttribute('username', (string)$user->username);
1328        $xuser->addAttribute('email', (string)$user->email);
1329        $xuser->addAttribute('scrobblingEnabled', 'true');
1330        $isManager = ($user->access >= 75);
1331        $isAdmin   = ($user->access >= 100);
1332        $xuser->addAttribute('adminRole', $isAdmin ? 'true' : 'false');
1333        $xuser->addAttribute('settingsRole', 'true');
1334        $xuser->addAttribute('downloadRole', Preference::get_by_user($user->id, 'download') ? 'true' : 'false');
1335        $xuser->addAttribute('playlistRole', 'true');
1336        $xuser->addAttribute('coverArtRole', $isManager ? 'true' : 'false');
1337        $xuser->addAttribute('commentRole', (AmpConfig::get('social')) ? 'true' : 'false');
1338        $xuser->addAttribute('podcastRole', (AmpConfig::get('podcast')) ? 'true' : 'false');
1339        $xuser->addAttribute('streamRole', 'true');
1340        $xuser->addAttribute('jukeboxRole', (AmpConfig::get('allow_localplay_playback') && AmpConfig::get('localplay_controller') && Access::check('localplay', 5)) ? 'true' : 'false');
1341        $xuser->addAttribute('shareRole', Preference::get_by_user($user->id, 'share') ? 'true' : 'false');
1342    }
1343
1344    /**
1345     * addUsers
1346     * @param SimpleXMLElement $xml
1347     * @param array $users
1348     */
1349    public static function addUsers($xml, $users)
1350    {
1351        $xusers = $xml->addChild('users');
1352        foreach ($users as $userid) {
1353            $user = new User($userid);
1354            self::addUser($xusers, $user);
1355        }
1356    }
1357
1358    /**
1359     * addRadio
1360     * @param SimpleXMLElement $xml
1361     * @param Live_Stream $radio
1362     */
1363    public static function addRadio($xml, $radio)
1364    {
1365        $xradio = $xml->addChild('internetRadioStation ');
1366        $xradio->addAttribute('id', (string)$radio->id);
1367        $xradio->addAttribute('name', (string)self::checkName($radio->name));
1368        $xradio->addAttribute('streamUrl', (string)$radio->url);
1369        $xradio->addAttribute('homePageUrl', (string)$radio->site_url);
1370    }
1371
1372    /**
1373     * addRadios
1374     * @param SimpleXMLElement $xml
1375     * @param $radios
1376     */
1377    public static function addRadios($xml, $radios)
1378    {
1379        $xradios = $xml->addChild('internetRadioStations');
1380        foreach ($radios as $radioid) {
1381            $radio = new Live_Stream($radioid);
1382            self::addRadio($xradios, $radio);
1383        }
1384    }
1385
1386    /**
1387     * addShare
1388     * @param SimpleXMLElement $xml
1389     * @param Share $share
1390     */
1391    public static function addShare($xml, $share)
1392    {
1393        $xshare = $xml->addChild('share');
1394        $xshare->addAttribute('id', (string)$share->id);
1395        $xshare->addAttribute('url', (string)$share->public_url);
1396        $xshare->addAttribute('description', (string)$share->description);
1397        $user = new User($share->user);
1398        $xshare->addAttribute('username', (string)$user->username);
1399        $xshare->addAttribute('created', date("c", (int)$share->creation_date));
1400        if ($share->lastvisit_date > 0) {
1401            $xshare->addAttribute('lastVisited', date("c", (int)$share->lastvisit_date));
1402        }
1403        if ($share->expire_days > 0) {
1404            $xshare->addAttribute('expires', date("c", (int)$share->creation_date + ($share->expire_days * 86400)));
1405        }
1406        $xshare->addAttribute('visitCount', (string)$share->counter);
1407
1408        if ($share->object_type == 'song') {
1409            self::addSong($xshare, $share->object_id, "entry");
1410        } elseif ($share->object_type == 'playlist') {
1411            $playlist = new Playlist($share->object_id);
1412            $songs    = $playlist->get_songs();
1413            foreach ($songs as $songid) {
1414                self::addSong($xshare, $songid, "entry");
1415            }
1416        } elseif ($share->object_type == 'album') {
1417            $songs = static::getSongRepository()->getByAlbum($share->object_id);
1418            foreach ($songs as $songid) {
1419                self::addSong($xshare, $songid, "entry");
1420            }
1421        }
1422    }
1423
1424    /**
1425     * addShares
1426     * @param SimpleXMLElement $xml
1427     * @param array $shares
1428     */
1429    public static function addShares($xml, $shares)
1430    {
1431        $xshares = $xml->addChild('shares');
1432        foreach ($shares as $share_id) {
1433            $share = new Share($share_id);
1434            // Don't add share with max counter already reached
1435            if ($share->max_counter == 0 || $share->counter < $share->max_counter) {
1436                self::addShare($xshares, $share);
1437            }
1438        }
1439    }
1440
1441    /**
1442     * addJukeboxPlaylist
1443     * @param SimpleXMLElement $xml
1444     * @param LocalPlay $localplay
1445     */
1446    public static function addJukeboxPlaylist($xml, LocalPlay $localplay)
1447    {
1448        $xjbox  = self::createJukeboxStatus($xml, $localplay, 'jukeboxPlaylist');
1449        $tracks = $localplay->get();
1450        foreach ($tracks as $track) {
1451            if ($track['oid']) {
1452                self::addSong($xjbox, (int)$track['oid'], 'entry');
1453            }
1454        }
1455    }
1456
1457    /**
1458     * createJukeboxStatus
1459     * @param SimpleXMLElement $xml
1460     * @param LocalPlay $localplay
1461     * @param string $elementName
1462     * @return SimpleXMLElement
1463     */
1464    public static function createJukeboxStatus($xml, LocalPlay $localplay, $elementName = 'jukeboxStatus')
1465    {
1466        $xjbox  = $xml->addChild(htmlspecialchars($elementName));
1467        $status = $localplay->status();
1468        $xjbox->addAttribute('currentIndex', 0); // Not supported
1469        $xjbox->addAttribute('playing', ($status['state'] == 'play') ? 'true' : 'false');
1470        $xjbox->addAttribute('gain', (string)$status['volume']);
1471        $xjbox->addAttribute('position', 0); // Not supported
1472
1473        return $xjbox;
1474    }
1475
1476    /**
1477     * addLyrics
1478     * @param SimpleXMLElement $xml
1479     * @param $artist
1480     * @param $title
1481     * @param $song_id
1482     */
1483    public static function addLyrics($xml, $artist, $title, $song_id)
1484    {
1485        $song = new Song($song_id);
1486        $song->fill_ext_info('lyrics');
1487        $lyrics = $song->get_lyrics();
1488
1489        if (!empty($lyrics) && $lyrics['text']) {
1490            $text    = preg_replace('/\<br(\s*)?\/?\>/i', "\n", $lyrics['text']);
1491            $text    = str_replace("\r", '', (string)$text);
1492            $xlyrics = $xml->addChild('lyrics', htmlspecialchars($text));
1493            if ($artist) {
1494                $xlyrics->addAttribute('artist', (string)$artist);
1495            }
1496            if ($title) {
1497                $xlyrics->addAttribute('title', (string)$title);
1498            }
1499        }
1500    }
1501
1502    /**
1503     * addArtistInfo
1504     * @param SimpleXMLElement $xml
1505     * @param array $info
1506     * @param array $similars
1507     * @param string $child
1508     */
1509    public static function addArtistInfo($xml, $info, $similars, $child)
1510    {
1511        $artist = new Artist($info['id']);
1512
1513        $xartist = $xml->addChild(htmlspecialchars($child));
1514        $xartist->addChild('biography', htmlspecialchars(trim((string)$info['summary'])));
1515        $xartist->addChild('musicBrainzId', $artist->mbid);
1516        //$xartist->addChild('lastFmUrl', "");
1517        $xartist->addChild('smallImageUrl', htmlentities($info['smallphoto']));
1518        $xartist->addChild('mediumImageUrl', htmlentities($info['mediumphoto']));
1519        $xartist->addChild('largeImageUrl', htmlentities($info['largephoto']));
1520
1521        foreach ($similars as $similar) {
1522            $xsimilar = $xartist->addChild('similarArtist');
1523            $xsimilar->addAttribute('id', ($similar['id'] !== null ? self::getArtistId($similar['id']) : "-1"));
1524            $xsimilar->addAttribute('name', (string)self::checkName($similar['name']));
1525        }
1526    }
1527
1528    /**
1529     * addSimilarSongs
1530     * @param SimpleXMLElement $xml
1531     * @param array $similar_songs
1532     * @param string $child
1533     */
1534    public static function addSimilarSongs($xml, $similar_songs, $child)
1535    {
1536        $xsimilar = $xml->addChild(htmlspecialchars($child));
1537        foreach ($similar_songs as $similar_song) {
1538            if ($similar_song['id'] !== null) {
1539                self::addSong($xsimilar, $similar_song['id']);
1540            }
1541        }
1542    }
1543
1544    /**
1545     * addPodcasts
1546     * @param SimpleXMLElement $xml
1547     * @param Podcast[] $podcasts
1548     * @param boolean $includeEpisodes
1549     */
1550    public static function addPodcasts($xml, $podcasts, $includeEpisodes = true)
1551    {
1552        $xpodcasts = $xml->addChild('podcasts');
1553        foreach ($podcasts as $podcast) {
1554            $podcast->format();
1555            $xchannel = $xpodcasts->addChild('channel');
1556            $xchannel->addAttribute('id', (string)self::getPodcastId($podcast->id));
1557            $xchannel->addAttribute('url', (string)$podcast->feed);
1558            $xchannel->addAttribute('title', (string)self::checkName($podcast->f_title));
1559            $xchannel->addAttribute('description', (string)$podcast->f_description);
1560            if (Art::has_db($podcast->id, 'podcast')) {
1561                $xchannel->addAttribute('coverArt', 'pod-' . self::getPodcastId($podcast->id));
1562            }
1563            $xchannel->addAttribute('status', 'completed');
1564            if ($includeEpisodes) {
1565                $episodes = $podcast->get_episodes();
1566                foreach ($episodes as $episode_id) {
1567                    $episode = new Podcast_Episode($episode_id);
1568                    self::addPodcastEpisode($xchannel, $episode);
1569                }
1570            }
1571        }
1572    }
1573
1574    /**
1575     * addPodcastEpisode
1576     * @param SimpleXMLElement $xml
1577     * @param Podcast_Episode $episode
1578     * @param string $elementName
1579     */
1580    private static function addPodcastEpisode($xml, $episode, $elementName = 'episode')
1581    {
1582        $episode->format();
1583        $xepisode = $xml->addChild(htmlspecialchars($elementName));
1584        $xepisode->addAttribute('id', (string)self::getPodcastEpId($episode->id));
1585        $xepisode->addAttribute('channelId', (string)self::getPodcastId($episode->podcast));
1586        $xepisode->addAttribute('title', (string)self::checkName($episode->f_title));
1587        $xepisode->addAttribute('album', (string)$episode->f_podcast);
1588        $xepisode->addAttribute('description', (string)self::checkName($episode->f_description));
1589        $xepisode->addAttribute('duration', (string)$episode->time);
1590        $xepisode->addAttribute('genre', "Podcast");
1591        $xepisode->addAttribute('isDir', "false");
1592        $xepisode->addAttribute('publishDate', $episode->f_pubdate);
1593        $xepisode->addAttribute('status', (string)$episode->state);
1594        $xepisode->addAttribute('parent', (string)self::getPodcastId($episode->podcast));
1595        if (Art::has_db($episode->podcast, 'podcast')) {
1596            $xepisode->addAttribute('coverArt', (string)self::getPodcastId($episode->podcast));
1597        }
1598
1599        self::setIfStarred($xepisode, 'podcast_episode', $episode->id);
1600
1601        if ($episode->file) {
1602            $xepisode->addAttribute('streamId', (string)self::getPodcastEpId($episode->id));
1603            $xepisode->addAttribute('size', (string)$episode->size);
1604            $xepisode->addAttribute('suffix', (string)$episode->type);
1605            $xepisode->addAttribute('contentType', (string)$episode->mime);
1606            // Create a clean fake path instead of song real file path to have better offline mode storage on Subsonic clients
1607            $path = basename($episode->file);
1608            $xepisode->addAttribute('path', (string)$path);
1609        }
1610    }
1611
1612    /**
1613     * addNewestPodcastEpisodes
1614     * @param SimpleXMLElement $xml
1615     * @param Podcast_Episode[] $episodes
1616     */
1617    public static function addNewestPodcastEpisodes($xml, $episodes)
1618    {
1619        $xpodcasts = $xml->addChild('newestPodcasts');
1620        foreach ($episodes as $episode) {
1621            $episode->format();
1622            self::addPodcastEpisode($xpodcasts, $episode);
1623        }
1624    }
1625
1626    /**
1627     * addBookmarks
1628     * @param SimpleXMLElement $xml
1629     * @param Bookmark[] $bookmarks
1630     */
1631    public static function addBookmarks($xml, $bookmarks)
1632    {
1633        $xbookmarks = $xml->addChild('bookmarks');
1634        foreach ($bookmarks as $bookmark) {
1635            self::addBookmark($xbookmarks, $bookmark);
1636        }
1637    }
1638
1639    /**
1640     * addBookmark
1641     * @param SimpleXMLElement $xml
1642     * @param Bookmark $bookmark
1643     */
1644    private static function addBookmark($xml, $bookmark)
1645    {
1646        $xbookmark = $xml->addChild('bookmark');
1647        $xbookmark->addAttribute('position', (string)$bookmark->position);
1648        $xbookmark->addAttribute('username', (string)$bookmark->getUserName());
1649        $xbookmark->addAttribute('comment', (string)$bookmark->comment);
1650        $xbookmark->addAttribute('created', date("c", (int)$bookmark->creation_date));
1651        $xbookmark->addAttribute('changed', date("c", (int)$bookmark->update_date));
1652        if ($bookmark->object_type == "song") {
1653            $song = new Song($bookmark->object_id);
1654            self::addSong($xbookmark, $song->id, 'entry');
1655        } elseif ($bookmark->object_type == "video") {
1656            self::addVideo($xbookmark, new Video($bookmark->object_id), 'entry');
1657        } elseif ($bookmark->object_type == "podcast_episode") {
1658            self::addPodcastEpisode($xbookmark, new Podcast_Episode($bookmark->object_id), 'entry');
1659        }
1660    }
1661
1662    /**
1663     * addMessages
1664     * @param SimpleXMLElement $xml
1665     * @param integer[] $messages
1666     */
1667    public static function addMessages($xml, $messages)
1668    {
1669        $xmessages = $xml->addChild('chatMessages');
1670        if (empty($messages)) {
1671            return;
1672        }
1673        foreach ($messages as $message) {
1674            $chat = new PrivateMsg($message);
1675            self::addMessage($xmessages, $chat);
1676        }
1677    }
1678
1679    /**
1680     * addMessage
1681     * @param SimpleXMLElement $xml
1682     * @param PrivateMsg $message
1683     */
1684    private static function addMessage($xml, $message)
1685    {
1686        $user      = new User($message->getSenderUserId());
1687        $xbookmark = $xml->addChild('chatMessage');
1688        if ($user->fullname_public) {
1689            $xbookmark->addAttribute('username', (string)$user->fullname);
1690        } else {
1691            $xbookmark->addAttribute('username', (string)$user->username);
1692        }
1693        $xbookmark->addAttribute('time', (string)($message->getCreationDate() * 1000));
1694        $xbookmark->addAttribute('message', (string)$message->getMessage());
1695    }
1696
1697    /**
1698     * @deprecated
1699     */
1700    private static function getSongRepository(): SongRepositoryInterface
1701    {
1702        global $dic;
1703
1704        return $dic->get(SongRepositoryInterface::class);
1705    }
1706
1707    /**
1708     * @deprecated
1709     */
1710    private static function getAlbumRepository(): AlbumRepositoryInterface
1711    {
1712        global $dic;
1713
1714        return $dic->get(AlbumRepositoryInterface::class);
1715    }
1716}
1717