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\System\Session;
28use Ampache\Repository\Model\Album;
29use Ampache\Repository\Model\Random;
30use Ampache\Module\Authorization\Access;
31use Ampache\Module\Playback\Localplay\LocalPlay;
32use Ampache\Module\Playback\Stream;
33use Ampache\Module\Playback\Stream_Playlist;
34use Ampache\Module\Playback\Stream_Url;
35use Ampache\Module\Statistics\Stats;
36use Ampache\Module\User\PasswordGenerator;
37use Ampache\Module\User\PasswordGeneratorInterface;
38use Ampache\Module\Util\Mailer;
39use Ampache\Module\Util\Recommendation;
40use Ampache\Config\AmpConfig;
41use Ampache\Repository\Model\Art;
42use Ampache\Repository\Model\Artist;
43use Ampache\Repository\Model\Bookmark;
44use Ampache\Repository\Model\Catalog;
45use Ampache\Module\System\Core;
46use Ampache\Repository\AlbumRepositoryInterface;
47use Ampache\Repository\BookmarkRepositoryInterface;
48use Ampache\Repository\LiveStreamRepositoryInterface;
49use Ampache\Repository\Model\User_Playlist;
50use Ampache\Repository\PrivateMessageRepositoryInterface;
51use Ampache\Repository\SongRepositoryInterface;
52use Ampache\Repository\UserRepositoryInterface;
53use DOMDocument;
54use Ampache\Repository\Model\Playlist;
55use Ampache\Repository\Model\Podcast;
56use Ampache\Repository\Model\Podcast_Episode;
57use Ampache\Repository\Model\Preference;
58use Ampache\Repository\Model\Rating;
59use Requests;
60use Ampache\Repository\Model\Search;
61use Ampache\Repository\Model\Share;
62use SimpleXMLElement;
63use Ampache\Repository\Model\Song;
64use Ampache\Repository\Model\Tag;
65use Ampache\Repository\Model\User;
66use Ampache\Repository\Model\Userflag;
67
68/**
69 * Subsonic Class
70 *
71 * This class wrap Ampache to Subsonic API functions. See http://www.subsonic.org/pages/api.jsp
72 * These are all static calls.
73 *
74 * @SuppressWarnings("unused")
75 */
76class Subsonic_Api
77{
78
79    /**
80     * check_parameter
81     * @param array $input
82     * @param string $parameter
83     * @param boolean $addheader
84     * @return boolean|mixed
85     */
86    public static function check_parameter($input, $parameter, $addheader = false)
87    {
88        if (empty($input[$parameter])) {
89            ob_end_clean();
90            if ($addheader) {
91                self::setHeader($input['f']);
92            }
93            self::apiOutput($input,
94                Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, '', 'check_parameter'));
95
96            return false;
97        }
98
99        return $input[$parameter];
100    }
101
102    /**
103     * @param $password
104     * @return string
105     */
106    public static function decrypt_password($password)
107    {
108        // Decode hex-encoded password
109        $encpwd = strpos($password, "enc:");
110        if ($encpwd !== false) {
111            $hex    = substr($password, 4);
112            $decpwd = '';
113            for ($count = 0; $count < strlen((string)$hex); $count += 2) {
114                $decpwd .= chr((int)hexdec(substr($hex, $count, 2)));
115            }
116            $password = $decpwd;
117        }
118
119        return $password;
120    }
121
122    /**
123     * @param $curl
124     * @param $data
125     * @return integer
126     */
127    public static function output_body($curl, $data)
128    {
129        unset($curl);
130        echo $data;
131        ob_flush();
132
133        return strlen((string)$data);
134    }
135
136    /**
137     * @param $curl
138     * @param $header
139     * @return integer
140     */
141    public static function output_header($curl, $header)
142    {
143        $rheader = trim((string)$header);
144        $rhpart  = explode(':', $rheader);
145        if (!empty($rheader) && count($rhpart) > 1) {
146            if ($rhpart[0] != "Transfer-Encoding") {
147                header($rheader);
148            }
149        } else {
150            if (substr($header, 0, 5) === "HTTP/") {
151                // if $header starts with HTTP/ assume it's the status line
152                http_response_code(curl_getinfo($curl, CURLINFO_HTTP_CODE));
153            }
154        }
155
156        return strlen((string)$header);
157    }
158
159    /**
160     * follow_stream
161     * @param string $url
162     */
163    public static function follow_stream($url)
164    {
165        set_time_limit(0);
166        ob_end_clean();
167        header("Access-Control-Allow-Origin: *");
168        if (function_exists('curl_version')) {
169            // Here, we use curl from the Ampache server to download data from
170            // the Ampache server, which can be a bit counter-intuitive.
171            // We use the curl `writefunction` and `headerfunction` callbacks
172            // to write the fetched data back to the open stream from the
173            // client.
174            $headers      = apache_request_headers();
175            $reqheaders   = array();
176            $reqheaders[] = "User-Agent: " . $headers['User-Agent'];
177            if (isset($headers['Range'])) {
178                $reqheaders[] = "Range: " . $headers['Range'];
179            }
180            // Curl support, we stream transparently to avoid redirect. Redirect can fail on few clients
181            debug_event(self::class, 'Stream proxy: ' . $url, 5);
182            $curl = curl_init($url);
183            if ($curl) {
184                curl_setopt_array($curl, array(
185                    CURLOPT_FAILONERROR => true,
186                    CURLOPT_HTTPHEADER => $reqheaders,
187                    CURLOPT_HEADER => false,
188                    CURLOPT_RETURNTRANSFER => false,
189                    CURLOPT_FOLLOWLOCATION => true,
190                    CURLOPT_WRITEFUNCTION => array('Ampache\Module\Api\Subsonic_Api', 'output_body'),
191                    CURLOPT_HEADERFUNCTION => array('Ampache\Module\Api\Subsonic_Api', 'output_header'),
192                    // Ignore invalid certificate
193                    // Default trusted chain is crap anyway and currently no custom CA option
194                    CURLOPT_SSL_VERIFYPEER => false,
195                    CURLOPT_SSL_VERIFYHOST => false,
196                    CURLOPT_TIMEOUT => 0
197                ));
198                if (curl_exec($curl) === false) {
199                    debug_event(self::class, 'Stream error: ' . curl_error($curl), 1);
200                }
201                curl_close($curl);
202            }
203        } else {
204            // Stream media using http redirect if no curl support
205            // Bug fix for android clients looking for /rest/ in destination url
206            // Warning: external catalogs will not work!
207            $url = str_replace('/play/', '/rest/fake/', $url);
208            header("Location: " . $url);
209        }
210    }
211
212    /**
213     * @param $filetype
214     */
215    public static function setHeader($filetype)
216    {
217        if (strtolower((string)$filetype) == "json") {
218            header("Content-type: application/json; charset=" . AmpConfig::get('site_charset'));
219            Subsonic_Xml_Data::$enable_json_checks = true;
220        } elseif (strtolower((string)$filetype) == "jsonp") {
221            header("Content-type: text/javascript; charset=" . AmpConfig::get('site_charset'));
222            Subsonic_Xml_Data::$enable_json_checks = true;
223        } else {
224            header("Content-type: text/xml; charset=" . AmpConfig::get('site_charset'));
225        }
226        header("Access-Control-Allow-Origin: *");
227    }
228
229    /**
230     * apiOutput
231     * @param array $input
232     * @param SimpleXMLElement $xml
233     * @param array $alwaysArray
234     */
235
236    public static function apiOutput($input, $xml, $alwaysArray = array('musicFolder', 'channel', 'artist', 'child', 'song', 'album', 'share', 'entry'))
237    {
238        $format   = ($input['f']) ? strtolower((string) $input['f']) : 'xml';
239        $callback = $input['callback'];
240        self::apiOutput2($format, $xml, $callback, $alwaysArray);
241    }
242
243    /**
244     * apiOutput2
245     * @param string $format
246     * @param SimpleXMLElement $xml
247     * @param string $callback
248     * @param array $alwaysArray
249     */
250    public static function apiOutput2($format, $xml, $callback = '', $alwaysArray = array('musicFolder', 'channel', 'artist', 'child', 'song', 'album', 'share', 'entry'))
251    {
252        $conf = array('alwaysArray' => $alwaysArray);
253        if ($format == "json") {
254            echo json_encode(self::xml2json($xml, $conf), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
255
256            return;
257        }
258        if ($format == "jsonp") {
259            echo $callback . '(' . json_encode(self::xml2json($xml, $conf), JSON_PRETTY_PRINT) . ')';
260
261            return;
262        }
263        $xmlstr = $xml->asXml();
264        // clean illegal XML characters.
265        $clean_xml = preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', '_', $xmlstr);
266        $dom       = new DOMDocument();
267        $dom->loadXML($clean_xml, LIBXML_PARSEHUGE);
268        $dom->formatOutput = true;
269        $output            = $dom->saveXML();
270        // saving xml can fail
271        if (!$output) {
272            $output = "<subsonic-response status=\"failed\" version=\"1.13.0\"><error code=\"0\" message=\"Error creating response.\"/></subsonic-response>";
273        }
274        echo $output;
275    }
276
277    /**
278     * xml2json
279     * [based from http://outlandish.com/blog/xml-to-json/]
280     * Because we cannot use only json_encode to respect JSON Subsonic API
281     * @param SimpleXMLElement $xml
282     * @param array $input_options
283     * @return array
284     */
285    private static function xml2json($xml, $input_options = array())
286    {
287        $defaults = array(
288            'namespaceSeparator' => ' :', // you may want this to be something other than a colon
289            'attributePrefix' => '', // to distinguish between attributes and nodes with the same name
290            'alwaysArray' => array('musicFolder', 'channel', 'artist', 'child', 'song', 'album', 'share'), // array of xml tag names which should always become arrays
291            'alwaysDouble' => array('averageRating'),
292            'alwaysInteger' => array('albumCount', 'audioTrackId', 'bitRate', 'bookmarkPosition', 'code',
293                                     'count', 'current', 'currentIndex', 'discNumber', 'duration', 'folder',
294                                     'lastModified', 'maxBitRate', 'minutesAgo', 'offset', 'originalHeight',
295                                     'originalWidth', 'playCount', 'playerId', 'position', 'size', 'songCount',
296                                     'time', 'totalHits', 'track', 'userRating', 'visitCount', 'year'), // array of xml tag names which should always become integers
297            'autoArray' => true, // only create arrays for tags which appear more than once
298            'textContent' => 'value', // key used for the text content of elements
299            'autoText' => true, // skip textContent key if node has no attributes or child nodes
300            'keySearch' => false, // optional search and replace on tag and attribute names
301            'keyReplace' => false, // replace values for above search values (as passed to str_replace())
302            'boolean' => true // replace true and false string with boolean values
303        );
304        $options        = array_merge($defaults, $input_options);
305        $namespaces     = $xml->getDocNamespaces();
306        $namespaces[''] = null; // add base (empty) namespace
307        // get attributes from all namespaces
308        $attributesArray = array();
309        foreach ($namespaces as $prefix => $namespace) {
310            foreach ($xml->attributes($namespace) as $attributeName => $attribute) {
311                // replace characters in attribute name
312                if ($options['keySearch']) {
313                    $attributeName = str_replace($options['keySearch'], $options['keyReplace'], $attributeName);
314                }
315                $attributeKey = $options['attributePrefix'] . ($prefix ? $prefix . $options['namespaceSeparator'] : '') . $attributeName;
316                $strattr      = trim((string)$attribute);
317                if ($options['boolean'] && ($strattr == "true" || $strattr == "false")) {
318                    $vattr = ($strattr == "true");
319                } else {
320                    $vattr = $strattr;
321                    if (in_array($attributeName, $options['alwaysInteger'])) {
322                        $vattr = (int) $strattr;
323                    }
324                    if (in_array($attributeName, $options['alwaysDouble'])) {
325                        $vattr = (double) $strattr;
326                    }
327                }
328                $attributesArray[$attributeKey] = $vattr;
329            }
330        }
331
332        // these children must be in an array.
333        $forceArray = array('channel', 'share');
334        // get child nodes from all namespaces
335        $tagsArray = array();
336        foreach ($namespaces as $prefix => $namespace) {
337            foreach ($xml->children($namespace) as $childXml) {
338                // recurse into child nodes
339                $childArray = self::xml2json($childXml, $options);
340                foreach ($childArray as $childTagName => $childProperties) {
341                    // replace characters in tag name
342                    if ($options['keySearch']) {
343                        $childTagName = str_replace($options['keySearch'], $options['keyReplace'], $childTagName);
344                    }
345                    // add namespace prefix, if any
346                    if ($prefix) {
347                        $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName;
348                    }
349
350                    if (!isset($tagsArray[$childTagName])) {
351                        // plain strings aren't countable/nested
352                        if (!is_string($childProperties)) {
353                            // only entry with this key
354                            if (count($childProperties) === 0) {
355                                $tagsArray[$childTagName] = (object)$childProperties;
356                            } elseif (self::has_Nested_Array($childProperties) && !in_array($childTagName, $forceArray)) {
357                                $tagsArray[$childTagName] = (object)$childProperties;
358                            } else {
359                                // test if tags of this type should always be arrays, no matter the element count
360                                $tagsArray[$childTagName] = in_array($childTagName,
361                                    $options['alwaysArray']) || !$options['autoArray'] ? array($childProperties) : $childProperties;
362                            }
363                        } else {
364                            // test if tags of this type should always be arrays, no matter the element count
365                            $tagsArray[$childTagName] = in_array($childTagName,
366                                $options['alwaysArray']) || !$options['autoArray'] ? array($childProperties) : $childProperties;
367                        }
368                    } elseif (is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName]) === range(0,
369                            count($tagsArray[$childTagName]) - 1)) {
370                        //key already exists and is integer indexed array
371                        $tagsArray[$childTagName][] = $childProperties;
372                    } else {
373                        //key exists so convert to integer indexed array with previous value in position 0
374                        $tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties);
375                    }
376                }
377            } // REPLACING list($childTagName, $childProperties) = each($childArray);
378        }
379
380        // get text content of node
381        $textContentArray = array();
382        $plainText        = (string)$xml;
383        if ($plainText !== '') {
384            $textContentArray[$options['textContent']] = $plainText;
385        }
386
387        // stick it all together
388        $propertiesArray = !$options['autoText'] || !empty($attributesArray) || !empty($tagsArray) || ($plainText === '') ? array_merge($attributesArray,
389            $tagsArray, $textContentArray) : $plainText;
390
391        if (isset($propertiesArray['xmlns'])) {
392            unset($propertiesArray['xmlns']);
393        }
394
395        // return node as array
396        return array(
397            $xml->getName() => $propertiesArray
398        );
399    }
400
401    /**
402     * has_Nested_Array
403     * Used for xml2json to detect a sub-array
404     * @param $properties
405     * @return boolean
406     */
407    private static function has_Nested_Array($properties)
408    {
409        foreach ($properties as $property) {
410            if (is_array($property)) {
411                return true;
412            }
413        }
414
415        return false;
416    }
417
418    /**
419     * ping
420     * Simple server ping to test connectivity with the server.
421     * Takes no parameter.
422     * @param array $input
423     */
424    public static function ping($input)
425    {
426        // Don't check client API version here. Some client give version 0.0.0 for ping command
427
428        self::apiOutput($input, Subsonic_Xml_Data::createSuccessResponse('ping'));
429    }
430
431    /**
432     * getLicense
433     * Get details about the software license. Always return a valid default license.
434     * Takes no parameter.
435     * @param array $input
436     */
437    public static function getlicense($input)
438    {
439        $response = Subsonic_Xml_Data::createSuccessResponse('getlicense');
440        Subsonic_Xml_Data::addLicense($response);
441        self::apiOutput($input, $response);
442    }
443
444    /**
445     * getMusicFolders
446     * Get all configured top-level music folders (= Ampache catalogs).
447     * Takes no parameter.
448     * @param array $input
449     */
450    public static function getmusicfolders($input)
451    {
452        $username = $input['u'];
453        $user     = User::get_from_username((string)$username);
454        $catalogs = Catalog::get_catalogs('music', $user->id);
455        $response = Subsonic_Xml_Data::createSuccessResponse('getmusicfolders');
456        Subsonic_Xml_Data::addMusicFolders($response, $catalogs);
457        self::apiOutput($input, $response);
458    }
459
460    /**
461     * getIndexes
462     * Get an indexed structure of all artists.
463     * Takes optional musicFolderId and optional ifModifiedSince in parameters.
464     * @param array $input
465     */
466    public static function getindexes($input)
467    {
468        set_time_limit(300);
469
470        $username         = self::check_parameter($input, 'u');
471        $user             = User::get_from_username((string)$username);
472        $musicFolderId    = $input['musicFolderId'];
473        $ifModifiedSince  = $input['ifModifiedSince'];
474
475        $catalogs = array();
476        if (!empty($musicFolderId) && $musicFolderId != '-1') {
477            $catalogs[] = $musicFolderId;
478        } else {
479            $catalogs = Catalog::get_catalogs('', $user->id);
480        }
481
482        $lastmodified = 0;
483        $fcatalogs    = array();
484
485        foreach ($catalogs as $catalogid) {
486            $clastmodified = 0;
487            $catalog       = Catalog::create_from_id($catalogid);
488
489            if ($catalog->last_update > $clastmodified) {
490                $clastmodified = $catalog->last_update;
491            }
492            if ($catalog->last_add > $clastmodified) {
493                $clastmodified = $catalog->last_add;
494            }
495            if ($catalog->last_clean > $clastmodified) {
496                $clastmodified = $catalog->last_clean;
497            }
498
499            if ($clastmodified > $lastmodified) {
500                $lastmodified = $clastmodified;
501            }
502            if (!empty($ifModifiedSince) && $clastmodified > ($ifModifiedSince / 1000)) {
503                $fcatalogs[] = $catalogid;
504            }
505        }
506        if (empty($ifModifiedSince)) {
507            $fcatalogs = $catalogs;
508        }
509
510        $response = Subsonic_Xml_Data::createSuccessResponse('getindexes');
511        if (count($fcatalogs) > 0) {
512            $artists = Catalog::get_artist_arrays($fcatalogs);
513            Subsonic_Xml_Data::addArtistsIndexes($response, $artists, $lastmodified, $fcatalogs);
514        }
515        self::apiOutput($input, $response);
516    }
517
518    /**
519     * getMusicDirectory
520     * Get a list of all files in a music directory.
521     * Takes the directory id in parameters.
522     * @param array $input
523     */
524    public static function getmusicdirectory($input)
525    {
526        $object_id = self::check_parameter($input, 'id');
527        $response  = Subsonic_Xml_Data::createSuccessResponse('getmusicdirectory');
528        if (Subsonic_Xml_Data::isArtist($object_id)) {
529            Subsonic_Xml_Data::addArtistDirectory($response, $object_id);
530        } elseif (Subsonic_Xml_Data::isAlbum($object_id)) {
531            Subsonic_Xml_Data::addAlbumDirectory($response, $object_id);
532        }
533        self::apiOutput($input, $response);
534    }
535
536    /**
537     * getGenres
538     * Get all genres.
539     * Takes no parameter.
540     * @param array $input
541     */
542    public static function getgenres($input)
543    {
544        $response = Subsonic_Xml_Data::createSuccessResponse('getgenres');
545        Subsonic_Xml_Data::addGenres($response, Tag::get_tags('song'));
546        self::apiOutput($input, $response);
547    }
548
549    /**
550     * getArtists
551     * Get all artists.
552     * @param array $input
553     */
554    public static function getartists($input)
555    {
556        $musicFolderId = $input['musicFolderId'];
557        $catalogs      = array();
558        if (!empty($musicFolderId) && $musicFolderId != '-1') {
559            $catalogs[] = $musicFolderId;
560        }
561        $response = Subsonic_Xml_Data::createSuccessResponse('getartists');
562        $artists  = Artist::get_id_arrays($catalogs);
563        Subsonic_Xml_Data::addArtistsRoot($response, $artists);
564        self::apiOutput($input, $response);
565    }
566
567    /**
568     * getArtist
569     * Get details for an artist, including a list of albums.
570     * Takes the artist id in parameter.
571     * @param array $input
572     */
573    public static function getartist($input)
574    {
575        $artistid = self::check_parameter($input, 'id');
576
577        $artist = new Artist(Subsonic_Xml_Data::getAmpacheId($artistid));
578        if (empty($artist->name)) {
579            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, "Artist not found.",
580                'getartist');
581        } else {
582            $response = Subsonic_Xml_Data::createSuccessResponse('getartist');
583            Subsonic_Xml_Data::addArtist($response, $artist, true, true);
584        }
585        self::apiOutput($input, $response, array('album'));
586    }
587
588    /**
589     * getAlbum
590     * Get details for an album, including a list of songs.
591     * Takes the album id in parameter.
592     * @param array $input
593     */
594    public static function getalbum($input)
595    {
596        $albumid = self::check_parameter($input, 'id');
597        $album   = new Album(Subsonic_Xml_Data::getAmpacheId($albumid));
598        if (!isset($album->id)) {
599            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, "Album not found.", 'getalbum');
600        } else {
601            $response = Subsonic_Xml_Data::createSuccessResponse('getalbum');
602            Subsonic_Xml_Data::addAlbum($response, $album, true);
603        }
604
605        self::apiOutput($input, $response, array('song'));
606    }
607
608    /**
609     * getVideos
610     * Get all videos.
611     * Takes no parameter.
612     * @param array $input
613     */
614    public static function getvideos($input)
615    {
616        $response = Subsonic_Xml_Data::createSuccessResponse('getvideos');
617        $videos   = Catalog::get_videos();
618        Subsonic_Xml_Data::addVideos($response, $videos);
619        self::apiOutput($input, $response);
620    }
621
622    /**
623     * _albumList
624     * @param array $input
625     * @param string $type
626     * @return array|false
627     */
628    private static function _albumList($input, $type)
629    {
630        $size          = $input['size'] ? (int)$input['size'] : 10;
631        $offset        = $input['offset'] ? (int)$input['offset'] : 0;
632        $musicFolderId = $input['musicFolderId'] ? (int)$input['musicFolderId'] : 0;
633
634        // Get albums from all catalogs by default Catalog filter is not supported for all request types for now.
635        $catalogs = null;
636        if ($musicFolderId > 0) {
637            $catalogs   = array();
638            $catalogs[] = $musicFolderId;
639        }
640        $albums = false;
641        switch ($type) {
642            case "random":
643                $username = self::check_parameter($input, 'u');
644                $user     = User::get_from_username((string)$username);
645                $albums   = static::getAlbumRepository()->getRandom(
646                    $user->id,
647                    $size
648                );
649                break;
650            case "newest":
651                $username = self::check_parameter($input, 'u');
652                $user     = User::get_from_username((string)$username);
653                $albums   = Stats::get_newest("album", $size, $offset, $musicFolderId, $user->id);
654                break;
655            case "highest":
656                $username = self::check_parameter($input, 'u');
657                $user     = User::get_from_username((string)$username);
658                $albums   = Rating::get_highest("album", $size, $offset, $user->id);
659                break;
660            case "frequent":
661                $albums = Stats::get_top("album", $size, 0, $offset);
662                break;
663            case "recent":
664                $albums = Stats::get_recent("album", $size, $offset);
665                break;
666            case "starred":
667                $albums   = Userflag::get_latest('album', 0, $size, $offset);
668                break;
669            case "alphabeticalByName":
670                $albums = Catalog::get_albums($size, $offset, $catalogs);
671                break;
672            case "alphabeticalByArtist":
673                $albums = Catalog::get_albums_by_artist($size, $offset, $catalogs);
674                break;
675            case "byYear":
676                $fromYear = $input['fromYear'] < $input['toYear'] ? $input['fromYear'] : $input['toYear'];
677                $toYear   = $input['toYear'] > $input['fromYear'] ? $input['toYear'] : $input['fromYear'];
678
679                if ($fromYear || $toYear) {
680                    $search = Search::year_search($fromYear, $toYear, $size, $offset);
681                    $albums = Search::run($search);
682                }
683                break;
684            case "byGenre":
685                $genre  = self::check_parameter($input, 'genre');
686                $tag_id = Tag::tag_exists($genre);
687                if ($tag_id > 0) {
688                    $albums = Tag::get_tag_objects('album', $tag_id, $size, $offset);
689                }
690                break;
691            default:
692                $albums = false;
693        }
694
695        return $albums;
696    }
697
698    /**
699     * getAlbumList
700     * Get a list of random, newest, highest rated etc. albums.
701     * Takes the list type with optional size and offset in parameters.
702     * @param array $input
703     * @param string $elementName
704     */
705    public static function getalbumlist($input, $elementName = "albumList")
706    {
707        $type     = self::check_parameter($input, 'type');
708        $response = Subsonic_Xml_Data::createSuccessResponse('getalbumlist');
709        if ($type) {
710            $errorOccured = false;
711            $albums       = self::_albumList($input, (string)$type);
712            if ($albums === false) {
713                $response     = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_GENERIC, "Invalid list type: " . scrub_out((string)$type), $elementName);
714                $errorOccured = true;
715            }
716            if (!$errorOccured) {
717                Subsonic_Xml_Data::addAlbumList($response, $albums, $elementName);
718            }
719        }
720        self::apiOutput($input, $response);
721    }
722
723    /**
724     * getAlbumList2
725     * See getAlbumList.
726     * @param array $input
727     */
728    public static function getalbumlist2($input)
729    {
730        self::getAlbumList($input, "albumList2");
731    }
732
733    /**
734     * getRandomSongs
735     * Get random songs matching the given criteria.
736     * Takes the optional size, genre, fromYear, toYear and music folder id in parameters.
737     * @param array $input
738     */
739    public static function getrandomsongs($input)
740    {
741        $size = $input['size'];
742        if (!$size) {
743            $size = 10;
744        }
745
746        $username      = self::check_parameter($input, 'u');
747        $genre         = $input['genre'];
748        $fromYear      = $input['fromYear'];
749        $toYear        = $input['toYear'];
750        $musicFolderId = $input['musicFolderId'];
751
752        $search           = array();
753        $search['limit']  = $size;
754        $search['random'] = $size;
755        $search['type']   = "song";
756        $count            = 0;
757        if ($genre) {
758            $search['rule_' . $count . '_input']    = $genre;
759            $search['rule_' . $count . '_operator'] = 0;
760            $search['rule_' . $count . '']          = "tag";
761            ++$count;
762        }
763        if ($fromYear) {
764            $search['rule_' . $count . '_input']    = $fromYear;
765            $search['rule_' . $count . '_operator'] = 0;
766            $search['rule_' . $count . '']          = "year";
767            ++$count;
768        }
769        if ($toYear) {
770            $search['rule_' . $count . '_input']    = $toYear;
771            $search['rule_' . $count . '_operator'] = 1;
772            $search['rule_' . $count . '']          = "year";
773            ++$count;
774        }
775        if ($musicFolderId) {
776            if (Subsonic_Xml_Data::isArtist($musicFolderId)) {
777                $artist   = new Artist(Subsonic_Xml_Data::getAmpacheId($musicFolderId));
778                $finput   = $artist->f_name;
779                $operator = 4;
780                $ftype    = "artist";
781            } else {
782                if (Subsonic_Xml_Data::isAlbum($musicFolderId)) {
783                    $album    = new Album(Subsonic_Xml_Data::getAmpacheId($musicFolderId));
784                    $finput   = $album->f_name;
785                    $operator = 4;
786                    $ftype    = "artist";
787                } else {
788                    $finput   = (int)($musicFolderId);
789                    $operator = 0;
790                    $ftype    = "catalog";
791                }
792            }
793            $search['rule_' . $count . '_input']    = $finput;
794            $search['rule_' . $count . '_operator'] = $operator;
795            $search['rule_' . $count . '']          = $ftype;
796            ++$count;
797        }
798        $user = User::get_from_username((string)$username);
799        if ($count > 0) {
800            $songs = Random::advanced('song', $search);
801        } else {
802            $songs = Random::get_default($size, $user->id);
803        }
804
805        $response = Subsonic_Xml_Data::createSuccessResponse('getrandomsongs');
806        Subsonic_Xml_Data::addRandomSongs($response, $songs);
807        self::apiOutput($input, $response);
808    }
809
810    /**
811     * getSong
812     * Get details for a song
813     * Takes the song id in parameter.
814     * @param array $input
815     */
816    public static function getsong($input)
817    {
818        $songid   = self::check_parameter($input, 'id');
819        $response = Subsonic_Xml_Data::createSuccessResponse('getsong');
820        $song     = Subsonic_Xml_Data::getAmpacheId($songid);
821        Subsonic_Xml_Data::addSong($response, $song);
822        self::apiOutput($input, $response, array());
823    }
824
825    /**
826     * getTopSongs
827     * Get most popular songs for a given artist.
828     * Takes the genre with optional count and offset in parameters.
829     * @param array $input
830     */
831    public static function gettopsongs($input)
832    {
833        $artist = self::check_parameter($input, 'artist');
834        $count  = (int)$input['count'];
835        $songs  = array();
836        if ($count < 1) {
837            $count = 50;
838        }
839        if ($artist) {
840            $songs = static::getSongRepository()->getTopSongsByArtist(
841                Artist::get_from_name(urldecode((string)$artist)),
842                $count
843            );
844        }
845        $response = Subsonic_Xml_Data::createSuccessResponse('gettopsongs');
846        Subsonic_Xml_Data::addTopSongs($response, $songs);
847        self::apiOutput($input, $response);
848    }
849
850    /**
851     * getSongsByGenre
852     * Get songs in a given genre.
853     * Takes the genre with optional count and offset in parameters.
854     * @param array $input
855     */
856    public static function getsongsbygenre($input)
857    {
858        $genre  = self::check_parameter($input, 'genre');
859        $count  = $input['count'];
860        $offset = $input['offset'];
861
862        $tag = Tag::construct_from_name($genre);
863        if ($tag->id) {
864            $songs = Tag::get_tag_objects("song", $tag->id, $count, $offset);
865        } else {
866            $songs = array();
867        }
868        $response = Subsonic_Xml_Data::createSuccessResponse('getsongsbygenre');
869        Subsonic_Xml_Data::addSongsByGenre($response, $songs);
870        self::apiOutput($input, $response);
871    }
872
873    /**
874     * getNowPlaying
875     * Get what is currently being played by all users.
876     * Takes no parameter.
877     * @param array $input
878     */
879    public static function getnowplaying($input)
880    {
881        $data     = Stream::get_now_playing();
882        $response = Subsonic_Xml_Data::createSuccessResponse('getnowplaying');
883        Subsonic_Xml_Data::addNowPlaying($response, $data);
884        self::apiOutput($input, $response);
885    }
886
887    /**
888     * search2
889     * Get albums, artists and songs matching the given criteria.
890     * Takes query with optional artist count, artist offset, album count, album offset, song count and song offset in parameters.
891     * @param array $input
892     * @param string $elementName
893     */
894    public static function search2($input, $elementName = "searchResult2")
895    {
896        $query    = self::check_parameter($input, 'query');
897        $artists  = array();
898        $albums   = array();
899        $songs    = array();
900        $operator = 0;
901
902        if (strlen((string)$query) > 1) {
903            if (substr((string)$query, -1) == "*") {
904                $query    = substr((string)$query, 0, -1);
905                $operator = 2; // Start with
906            }
907        }
908
909        $artistCount  = isset($input['artistCount']) ? $input['artistCount'] : 20;
910        $artistOffset = $input['artistOffset'];
911        $albumCount   = isset($input['albumCount']) ? $input['albumCount'] : 20;
912        $albumOffset  = $input['albumOffset'];
913        $songCount    = isset($input['songCount']) ? $input['songCount'] : 20;
914        $songOffset   = $input['songOffset'];
915
916        $sartist          = array();
917        $sartist['limit'] = $artistCount;
918        if ($artistOffset) {
919            $sartist['offset'] = $artistOffset;
920        }
921        $sartist['rule_1_input']    = $query;
922        $sartist['rule_1_operator'] = $operator;
923        $sartist['rule_1']          = "name";
924        $sartist['type']            = "artist";
925        if ($artistCount > 0) {
926            $artists = Search::run($sartist);
927        }
928
929        $salbum          = array();
930        $salbum['limit'] = $albumCount;
931        if ($albumOffset) {
932            $salbum['offset'] = $albumOffset;
933        }
934        $salbum['rule_1_input']    = $query;
935        $salbum['rule_1_operator'] = $operator;
936        $salbum['rule_1']          = "title";
937        $salbum['type']            = "album";
938        if ($albumCount > 0) {
939            $albums = Search::run($salbum);
940        }
941
942        $ssong          = array();
943        $ssong['limit'] = $songCount;
944        if ($songOffset) {
945            $ssong['offset'] = $songOffset;
946        }
947        $ssong['rule_1_input']    = $query;
948        $ssong['rule_1_operator'] = $operator;
949        $ssong['rule_1']          = "anywhere";
950        $ssong['type']            = "song";
951        if ($songCount > 0) {
952            $songs = Search::run($ssong);
953        }
954
955        $response = Subsonic_Xml_Data::createSuccessResponse('search2');
956        Subsonic_Xml_Data::addSearchResult($response, $artists, $albums, $songs, $elementName);
957        self::apiOutput($input, $response);
958    }
959
960    /**
961     * search3
962     * See search2.
963     * @param array $input
964     */
965    public static function search3($input)
966    {
967        self::search2($input, "searchResult3");
968    }
969
970    /**
971     * getPlaylists
972     * Get all playlists a user is allowed to play.
973     * Takes optional user in parameter.
974     * @param array $input
975     */
976    public static function getplaylists($input)
977    {
978        $response = Subsonic_Xml_Data::createSuccessResponse('getplaylists');
979        $username = $input['username'] ?: $input['u'];
980        $user     = User::get_from_username((string)$username);
981
982        // Don't allow playlist listing for another user
983        Subsonic_Xml_Data::addPlaylists($response, Playlist::get_playlists($user->id), Playlist::get_smartlists($user->id));
984        self::apiOutput($input, $response);
985    }
986
987    /**
988     * getPlaylist
989     * Get the list of files in a saved playlist.
990     * Takes the playlist id in parameters.
991     * @param array $input
992     */
993    public static function getplaylist($input)
994    {
995        $playlistid = self::check_parameter($input, 'id');
996
997        $response = Subsonic_Xml_Data::createSuccessResponse('getplaylist');
998        if (Subsonic_Xml_Data::isSmartPlaylist($playlistid)) {
999            $playlist = new Search(Subsonic_Xml_Data::getAmpacheId($playlistid), 'song');
1000            Subsonic_Xml_Data::addSmartPlaylist($response, $playlist, true);
1001        } else {
1002            $playlist = new Playlist(Subsonic_Xml_Data::getAmpacheId($playlistid));
1003            Subsonic_Xml_Data::addPlaylist($response, $playlist, true);
1004        }
1005        self::apiOutput($input, $response);
1006    }
1007
1008    /**
1009     * createPlaylist
1010     * Create (or updates) a playlist.
1011     * Takes playlist id in parameter if updating, name in parameter if creating and a list of song id for the playlist.
1012     * @param array $input
1013     */
1014    public static function createplaylist($input)
1015    {
1016        $playlistId = $input['playlistId'];
1017        $name       = $input['name'];
1018        $songIdList = array();
1019        if (is_array($input['songId'])) {
1020            $songIdList = $input['songId'];
1021        } elseif (is_string($input['songId'])) {
1022            $songIdList = explode(',', $input['songId']);
1023        }
1024
1025        if ($playlistId) {
1026            self::_updatePlaylist($playlistId, $name, $songIdList, array(), true, true);
1027            $response = Subsonic_Xml_Data::createSuccessResponse('createplaylist');
1028        } else {
1029            if (!empty($name)) {
1030                $playlistId = Playlist::create($name, 'private');
1031                if (count($songIdList) > 0) {
1032                    self::_updatePlaylist($playlistId, "", $songIdList, array(), true, true);
1033                }
1034                $response = Subsonic_Xml_Data::createSuccessResponse('createplaylist');
1035            } else {
1036                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, '',
1037                    'createplaylist');
1038            }
1039        }
1040        self::apiOutput($input, $response);
1041    }
1042
1043    /**
1044     * @param $playlist_id
1045     * @param string $name
1046     * @param array $songsIdToAdd
1047     * @param array $songIndexToRemove
1048     * @param boolean $public
1049     * @param boolean $clearFirst
1050     */
1051    private static function _updatePlaylist(
1052        $playlist_id,
1053        $name,
1054        $songsIdToAdd = array(),
1055        $songIndexToRemove = array(),
1056        $public = true,
1057        $clearFirst = false
1058    ) {
1059        $playlist           = new Playlist(Subsonic_Xml_Data::getAmpacheId($playlist_id));
1060        $songsIdToAdd_count = count($songsIdToAdd);
1061        $newdata            = array();
1062        $newdata['name']    = (!empty($name)) ? $name : $playlist->name;
1063        $newdata['pl_type'] = ($public) ? "public" : "private";
1064        $playlist->update($newdata);
1065        if ($clearFirst) {
1066            $playlist->delete_all();
1067        }
1068
1069        if ($songsIdToAdd_count > 0) {
1070            for ($i = 0; $i < $songsIdToAdd_count; ++$i) {
1071                $songsIdToAdd[$i] = Subsonic_Xml_Data::getAmpacheId($songsIdToAdd[$i]);
1072            }
1073            $playlist->add_songs($songsIdToAdd);
1074        }
1075        if (count($songIndexToRemove) > 0) {
1076            $playlist->regenerate_track_numbers(); // make sure track indexes are in order
1077            rsort($songIndexToRemove);
1078            foreach ($songIndexToRemove as $track) {
1079                $playlist->delete_track_number(((int)$track + 1));
1080            }
1081            $playlist->set_items();
1082            $playlist->regenerate_track_numbers(); // reorder now that the tracks are removed
1083        }
1084    }
1085
1086    /**
1087     * updatePlaylist
1088     * Update a playlist.
1089     * Takes playlist id in parameter with optional name, comment, public level and a list of song id to add/remove.
1090     * @param array $input
1091     */
1092    public static function updateplaylist($input)
1093    {
1094        $playlistId = self::check_parameter($input, 'playlistId');
1095        $name       = $input['name'];
1096        $public     = ($input['public'] === "true");
1097
1098        if (!Subsonic_Xml_Data::isSmartPlaylist($playlistId)) {
1099            $songIdToAdd = array();
1100            if (is_array($input['songIdToAdd'])) {
1101                $songIdToAdd = $input['songIdToAdd'];
1102            } elseif (is_string($input['songIdToAdd'])) {
1103                $songIdToAdd = explode(',', $input['songIdToAdd']);
1104            }
1105            $songIndexToRemove = array();
1106            if (is_array($input['songIndexToRemove'])) {
1107                $songIndexToRemove = $input['songIndexToRemove'];
1108            } elseif (is_string($input['songIndexToRemove'])) {
1109                $songIndexToRemove = explode(',', $input['songIndexToRemove']);
1110            }
1111            self::_updatePlaylist(Subsonic_Xml_Data::getAmpacheId($playlistId), $name, $songIdToAdd, $songIndexToRemove,
1112                $public);
1113
1114            $response = Subsonic_Xml_Data::createSuccessResponse('updateplaylist');
1115        } else {
1116            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED,
1117                'Cannot edit a smart playlist.', 'updateplaylist');
1118        }
1119        self::apiOutput($input, $response);
1120    }
1121
1122    /**
1123     * deletePlaylist
1124     * Delete a saved playlist.
1125     * Takes playlist id in parameter.
1126     * @param array $input
1127     */
1128    public static function deleteplaylist($input)
1129    {
1130        $playlistId = self::check_parameter($input, 'id');
1131
1132        if (Subsonic_Xml_Data::isSmartPlaylist($playlistId)) {
1133            $playlist = new Search(Subsonic_Xml_Data::getAmpacheId($playlistId), 'song');
1134        } else {
1135            $playlist = new Playlist(Subsonic_Xml_Data::getAmpacheId($playlistId));
1136        }
1137        $playlist->delete();
1138
1139        $response = Subsonic_Xml_Data::createSuccessResponse('deleteplaylist');
1140        self::apiOutput($input, $response);
1141    }
1142
1143    /**
1144     * stream
1145     * Streams a given media file.
1146     * Takes the file id in parameter with optional max bit rate, file format, time offset, size and estimate content length option.
1147     * @param array $input
1148     */
1149    public static function stream($input)
1150    {
1151        $fileid = self::check_parameter($input, 'id', true);
1152
1153        $maxBitRate    = $input['maxBitRate'];
1154        $format        = $input['format']; // mp3, flv or raw
1155        $timeOffset    = $input['timeOffset'];
1156        $contentLength = $input['estimateContentLength']; // Force content-length guessing if transcode
1157        $user_id       = User::get_from_username($input['u'])->id;
1158
1159        $params = '&client=' . rawurlencode($input['c']);
1160        if ($contentLength == 'true') {
1161            $params .= '&content_length=required';
1162        }
1163        if ($format && $format != "raw") {
1164            $params .= '&transcode_to=' . $format;
1165        }
1166        if ((int)$maxBitRate > 0) {
1167            $params .= '&bitrate=' . $maxBitRate;
1168        }
1169        if ($timeOffset) {
1170            $params .= '&frame=' . $timeOffset;
1171        }
1172        if (AmpConfig::get('subsonic_stream_scrobble') == 'false') {
1173            $params .= '&cache=1';
1174        }
1175
1176        $url = '';
1177        if (Subsonic_Xml_Data::isSong($fileid)) {
1178            $object = new Song(Subsonic_Xml_Data::getAmpacheId($fileid));
1179            $url    = $object->play_url($params, 'api', function_exists('curl_version'), $user_id);
1180        } elseif (Subsonic_Xml_Data::isPodcastEp($fileid)) {
1181            $object = new Podcast_Episode(Subsonic_Xml_Data::getAmpacheId($fileid));
1182            $url    = $object->play_url($params, 'api', function_exists('curl_version'), $user_id);
1183        }
1184
1185        // return an error on missing files
1186        if (empty($url)) {
1187            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'download');
1188            self::apiOutput($input, $response);
1189
1190            return;
1191        }
1192        self::follow_stream($url);
1193    }
1194
1195    /**
1196     * download
1197     * Downloads a given media file.
1198     * Takes the file id in parameter.
1199     * @param array $input
1200     */
1201    public static function download($input)
1202    {
1203        $fileid  = self::check_parameter($input, 'id', true);
1204        $user_id = User::get_from_username($input['u'])->id;
1205        $params  = '&action=download' . '&client=' . rawurlencode($input['c']);
1206        $url     = '';
1207        if (Subsonic_Xml_Data::isSong($fileid)) {
1208            $object = new Song(Subsonic_Xml_Data::getAmpacheId($fileid));
1209            $url    = $object->play_url($params, 'api', function_exists('curl_version'), $user_id);
1210        } elseif (Subsonic_Xml_Data::isPodcastEp($fileid)) {
1211            $object = new Podcast_Episode(Subsonic_Xml_Data::getAmpacheId($fileid));
1212            $url    = $object->play_url($params, 'api', function_exists('curl_version'), $user_id);
1213        }
1214        // return an error on missing files
1215        if (empty($url)) {
1216            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'download');
1217            self::apiOutput($input, $response);
1218
1219            return;
1220        }
1221        self::follow_stream($url);
1222    }
1223
1224    /**
1225     * hls
1226     * Create an HLS playlist.
1227     * Takes the file id in parameter with optional max bit rate.
1228     * @param array $input
1229     */
1230    public static function hls($input)
1231    {
1232        $fileid = self::check_parameter($input, 'id', true);
1233
1234        $bitRate = $input['bitRate'];
1235
1236        $media                = array();
1237        if (Subsonic_Xml_Data::isSong($fileid)) {
1238            $media['object_type'] = 'song';
1239        } elseif (Subsonic_Xml_Data::isVideo($fileid)) {
1240            $media['object_type'] = 'video';
1241        } else {
1242            self::apiOutput(
1243                $input,
1244                Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND,
1245                                               'Invalid id',
1246                                               'hls'));
1247        }
1248        $media['object_id']   = Subsonic_Xml_Data::getAmpacheId($fileid);
1249
1250        $medias            = array();
1251        $medias[]          = $media;
1252        $stream            = new Stream_Playlist();
1253        $additional_params = '';
1254        if ($bitRate) {
1255            $additional_params .= '&bitrate=' . $bitRate;
1256        }
1257        //$additional_params .= '&transcode_to=ts';
1258        $stream->add($medias, $additional_params);
1259
1260        // vlc won't work if we use application/vnd.apple.mpegurl, but works fine with this. this is
1261        // also an allowed header by the standard
1262        header('Content-Type: audio/mpegurl;');
1263        $stream->create_m3u();
1264    }
1265
1266    /**
1267     * getCoverArt
1268     * Get a cover art image.
1269     * Takes the cover art id in parameter.
1270     * @param array $input
1271     */
1272    public static function getcoverart($input)
1273    {
1274        $sub_id = str_replace('al-', '', self::check_parameter($input, 'id'));
1275        $sub_id = str_replace('ar-', '', $sub_id);
1276        $sub_id = str_replace('pl-', '', $sub_id);
1277        $sub_id = str_replace('pod-', '', $sub_id);
1278        // sometimes we're sent a full art url...
1279        preg_match('/\/artist\/([0-9]*)\//', $sub_id, $matches);
1280        if (!empty($matches)) {
1281            $sub_id = (string)(100000000 + (int)$matches[1]);
1282        }
1283        $size   = $input['size'];
1284        $type   = Subsonic_Xml_Data::getAmpacheType($sub_id);
1285        if ($type == "") {
1286            self::setHeader($input['f']);
1287            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, "Media not found.", 'getcoverart');
1288            self::apiOutput($input, $response);
1289
1290            return;
1291        }
1292
1293        $art = null;
1294
1295        if ($type == 'artist') {
1296            $art = new Art(Subsonic_Xml_Data::getAmpacheId($sub_id), "artist");
1297        }
1298        if ($type == 'album') {
1299            $art = new Art(Subsonic_Xml_Data::getAmpacheId($sub_id), "album");
1300        }
1301        if (($type == 'song')) {
1302            $art = new Art(Subsonic_Xml_Data::getAmpacheId($sub_id), "song");
1303            if ($art != null && $art->id == null) {
1304                // in most cases the song doesn't have a picture, but the album does
1305                $song          = new Song(Subsonic_Xml_Data::getAmpacheId(Subsonic_Xml_Data::getAmpacheId($sub_id)));
1306                $show_song_art = AmpConfig::get('show_song_art', false);
1307                $has_art       = Art::has_db($song->id, 'song');
1308                $art_object    = ($show_song_art && $has_art) ? $song->id : $song->album;
1309                $art_type      = ($show_song_art && $has_art) ? 'song' : 'album';
1310                $art           = new Art($art_object, $art_type);
1311            }
1312        }
1313        if (($type == 'podcast')) {
1314            $art = new Art(Subsonic_Xml_Data::getAmpacheId($sub_id), "podcast");
1315        }
1316        if ($type == 'search' || $type == 'playlist') {
1317            $listitems = array();
1318            // playlists and smartlists
1319            if (($type == 'search')) {
1320                $playlist  = new Search(Subsonic_Xml_Data::getAmpacheId($sub_id));
1321                $listitems = $playlist->get_items();
1322            } elseif (($type == 'playlist')) {
1323                $playlist  = new Playlist(Subsonic_Xml_Data::getAmpacheId($sub_id));
1324                $listitems = $playlist->get_items();
1325            }
1326            $item = (!empty($listitems)) ? $listitems[array_rand($listitems)] : array();
1327            $art  = (!empty($item)) ? new Art($item['object_id'], $item['object_type']) : null;
1328            if ($art != null && $art->id == null) {
1329                $song = new Song($item['object_id']);
1330                $art  = new Art($song->album, "album");
1331            }
1332        }
1333        if (!$art || $art->get() == '') {
1334            self::setHeader($input['f']);
1335            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, "Media not found.", 'getcoverart');
1336            self::apiOutput($input, $response);
1337
1338            return;
1339        }
1340        // we have the art so lets show it
1341        header("Access-Control-Allow-Origin: *");
1342        if ($size && AmpConfig::get('resize_images')) {
1343            $dim           = array();
1344            $dim['width']  = $size;
1345            $dim['height'] = $size;
1346            $thumb         = $art->get_thumb($dim);
1347            if (!empty($thumb)) {
1348                header('Content-type: ' . $thumb['thumb_mime']);
1349                header('Content-Length: ' . strlen((string) $thumb['thumb']));
1350                echo $thumb['thumb'];
1351
1352                return;
1353            }
1354        }
1355        $image = $art->get(true);
1356        header('Content-type: ' . $art->raw_mime);
1357        header('Content-Length: ' . strlen((string) $image));
1358        echo $image;
1359    }
1360
1361    /**
1362     * setRating
1363     * Sets the rating for a music file.
1364     * Takes the file id and rating in parameters.
1365     * @param array $input
1366     */
1367    public static function setrating($input)
1368    {
1369        $object_id = self::check_parameter($input, 'id');
1370        $rating    = $input['rating'];
1371
1372        $robj = null;
1373        if (Subsonic_Xml_Data::isArtist($object_id)) {
1374            $robj = new Rating(Subsonic_Xml_Data::getAmpacheId($object_id), "artist");
1375        } else {
1376            if (Subsonic_Xml_Data::isAlbum($object_id)) {
1377                $robj = new Rating(Subsonic_Xml_Data::getAmpacheId($object_id), "album");
1378            } else {
1379                if (Subsonic_Xml_Data::isSong($object_id)) {
1380                    $robj = new Rating(Subsonic_Xml_Data::getAmpacheId($object_id), "song");
1381                }
1382            }
1383        }
1384
1385        if ($robj != null) {
1386            $robj->set_rating($rating);
1387
1388            $response = Subsonic_Xml_Data::createSuccessResponse('setrating');
1389        } else {
1390            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, "Media not found.",
1391                'setrating');
1392        }
1393
1394        self::apiOutput($input, $response);
1395    }
1396
1397    /**
1398     * getStarred
1399     * Get starred songs, albums and artists.
1400     * Takes no parameter.
1401     * Not supported.
1402     * @param array $input
1403     * @param string $elementName
1404     */
1405    public static function getstarred($input, $elementName = "starred")
1406    {
1407        $user_id = User::get_from_username($input['u'])->id;
1408
1409        $response = Subsonic_Xml_Data::createSuccessResponse('getstarred');
1410        Subsonic_Xml_Data::addStarred($response, Userflag::get_latest('artist', $user_id, 10000),
1411            Userflag::get_latest('album', $user_id, 10000), Userflag::get_latest('song', $user_id, 10000),
1412            $elementName);
1413        self::apiOutput($input, $response);
1414    }
1415
1416    /**
1417     * getStarred2
1418     * See getStarred.
1419     * @param array $input
1420     */
1421    public static function getstarred2($input)
1422    {
1423        self::getStarred($input, "starred2");
1424    }
1425
1426    /**
1427     * star
1428     * Attaches a star to a song, album or artist.
1429     * Takes the optional file id, album id or artist id in parameters.
1430     * Not supported.
1431     * @param array $input
1432     */
1433    public static function star($input)
1434    {
1435        self::_setStar($input, true);
1436    }
1437
1438    /**
1439     * unstar
1440     * Removes the star from a song, album or artist.
1441     * Takes the optional file id, album id or artist id in parameters.
1442     * Not supported.
1443     * @param array $input
1444     */
1445    public static function unstar($input)
1446    {
1447        self::_setStar($input, false);
1448    }
1449
1450    /**
1451     * @param array $input
1452     * @param boolean $star
1453     */
1454    private static function _setStar($input, $star)
1455    {
1456        $object_id = $input['id'];
1457        $albumId   = $input['albumId'];
1458        $artistId  = $input['artistId'];
1459
1460        // Normalize all in one array
1461        $ids = array();
1462
1463        $response = Subsonic_Xml_Data::createSuccessResponse('_setStar');
1464        if ($object_id) {
1465            if (!is_array($object_id)) {
1466                $object_id = array($object_id);
1467            }
1468            foreach ($object_id as $item) {
1469                $aid = Subsonic_Xml_Data::getAmpacheId($item);
1470                if (Subsonic_Xml_Data::isArtist($item)) {
1471                    $type = 'artist';
1472                } else {
1473                    if (Subsonic_Xml_Data::isAlbum($item)) {
1474                        $type = 'album';
1475                    } else {
1476                        if (Subsonic_Xml_Data::isSong($item)) {
1477                            $type = 'song';
1478                        } else {
1479                            $type = "";
1480                        }
1481                    }
1482                }
1483                $ids[] = array('id' => $aid, 'type' => $type);
1484            }
1485        } else {
1486            if ($albumId) {
1487                if (!is_array($albumId)) {
1488                    $albumId = array($albumId);
1489                }
1490                foreach ($albumId as $album) {
1491                    $aid   = Subsonic_Xml_Data::getAmpacheId($album);
1492                    $ids[] = array('id' => $aid, 'type' => 'album');
1493                }
1494            } else {
1495                if ($artistId) {
1496                    if (!is_array($artistId)) {
1497                        $artistId = array($artistId);
1498                    }
1499                    foreach ($artistId as $artist) {
1500                        $aid   = Subsonic_Xml_Data::getAmpacheId($artist);
1501                        $ids[] = array('id' => $aid, 'type' => 'artist');
1502                    }
1503                } else {
1504                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM,
1505                        'Missing parameter', '_setStar');
1506                }
1507            }
1508        }
1509
1510        foreach ($ids as $object_id) {
1511            $flag = new Userflag($object_id['id'], $object_id['type']);
1512            $flag->set_flag($star);
1513        }
1514        self::apiOutput($input, $response);
1515    }
1516
1517    /**
1518     * getUser
1519     * Get details about a given user.
1520     * Takes the username in parameter.
1521     * Not supported.
1522     * @param array $input
1523     */
1524    public static function getuser($input)
1525    {
1526        $username = self::check_parameter($input, 'username');
1527        $myuser   = User::get_from_username($input['u']);
1528
1529        if ($myuser->access >= 100 || $myuser->username == $username) {
1530            $response = Subsonic_Xml_Data::createSuccessResponse('getuser');
1531            if ($myuser->username == $username) {
1532                $user = $myuser;
1533            } else {
1534                $user = User::get_from_username((string)$username);
1535            }
1536            Subsonic_Xml_Data::addUser($response, $user);
1537        } else {
1538            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED,
1539                $input['u'] . ' is not authorized to get details for other users.', 'getuser');
1540        }
1541        self::apiOutput($input, $response);
1542    }
1543
1544    /**
1545     * getUsers
1546     * Get details about a given user.
1547     * Takes no parameter.
1548     * Not supported.
1549     * @param array $input
1550     */
1551    public static function getusers($input)
1552    {
1553        $myuser = User::get_from_username($input['u']);
1554        if ($myuser->access >= 100) {
1555            $response = Subsonic_Xml_Data::createSuccessResponse('getusers');
1556            $users    = static::getUserRepository()->getValid();
1557            Subsonic_Xml_Data::addUsers($response, $users);
1558        } else {
1559            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED,
1560                $input['u'] . ' is not authorized to get details for other users.', 'getusers');
1561        }
1562        self::apiOutput($input, $response);
1563    }
1564
1565    /**
1566     * getAvatar
1567     * Return the user avatar in bytes.
1568     * @param array $input
1569     */
1570    public static function getavatar($input)
1571    {
1572        $username = self::check_parameter($input, 'username');
1573        $myuser   = User::get_from_username($input['u']);
1574
1575        $response = null;
1576        if ($myuser->access >= 100 || $myuser->username == $username) {
1577            if ($myuser->username == $username) {
1578                $user = $myuser;
1579            } else {
1580                $user = User::get_from_username((string)$username);
1581            }
1582
1583            if ($user !== null) {
1584                // Get Session key
1585                $avatar = $user->get_avatar(true, $input);
1586                if (isset($avatar['url']) && !empty($avatar['url'])) {
1587                    $request = Requests::get($avatar['url'], array(), Core::requests_options());
1588                    header("Content-Type: " . $request->headers['Content-Type']);
1589                    echo $request->body;
1590                }
1591            } else {
1592                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'getavatar');
1593            }
1594        } else {
1595            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED,
1596                $input['u'] . ' is not authorized to get avatar for other users.', 'getavatar');
1597        }
1598
1599        if ($response != null) {
1600            self::apiOutput($input, $response);
1601        }
1602    }
1603
1604    /**
1605     * getInternetRadioStations
1606     * Get all internet radio stations
1607     * Takes no parameter.
1608     * @param array $input
1609     */
1610    public static function getinternetradiostations($input)
1611    {
1612        $response = Subsonic_Xml_Data::createSuccessResponse('getinternetradiostations');
1613        $radios   = static::getLiveStreamRepository()->getAll();
1614        Subsonic_Xml_Data::addRadios($response, $radios);
1615        self::apiOutput($input, $response);
1616    }
1617
1618    /**
1619     * getShares
1620     * Get information about shared media this user is allowed to manage.
1621     * Takes no parameter.
1622     * @param array $input
1623     */
1624    public static function getshares($input)
1625    {
1626        $user     = User::get_from_username($input['u']);
1627        $response = Subsonic_Xml_Data::createSuccessResponse('getshares');
1628        $shares   = Share::get_share_list($user);
1629        Subsonic_Xml_Data::addShares($response, $shares);
1630        self::apiOutput($input, $response);
1631    }
1632
1633    /**
1634     * createShare
1635     * Create a public url that can be used by anyone to stream media.
1636     * Takes the file id with optional description and expires parameters.
1637     * @param array $input
1638     */
1639    public static function createshare($input)
1640    {
1641        $libitem_id  = self::check_parameter($input, 'id');
1642        $description = $input['description'];
1643        if (AmpConfig::get('share')) {
1644            $expire_days = (isset($input['expires']))
1645                ? (int) $input['expires']
1646                : Share::get_expiry($input['expires']);
1647            $object_type = null;
1648            if (is_array($libitem_id) && Subsonic_Xml_Data::isSong($libitem_id[0])) {
1649                $song_id     = Subsonic_Xml_Data::getAmpacheId($libitem_id[0]);
1650                $tmp_song    = new Song($song_id);
1651                $object_id   = Subsonic_Xml_Data::getAmpacheId($tmp_song->album);
1652                $object_type = 'album';
1653            } else {
1654                $object_id = Subsonic_Xml_Data::getAmpacheId($libitem_id);
1655                if (Subsonic_Xml_Data::isAlbum($libitem_id)) {
1656                    $object_type = 'album';
1657                }
1658                if (Subsonic_Xml_Data::isSong($libitem_id)) {
1659                    $object_type = 'song';
1660                }
1661                if (Subsonic_Xml_Data::isPlaylist($libitem_id)) {
1662                    $object_type = 'playlist';
1663                }
1664            }
1665            debug_event(self::class, 'createShare: sharing ' . $object_type . ' ' . $object_id, 4);
1666
1667            if (!empty($object_type) && !empty($object_id)) {
1668                // @todo remove after refactoring
1669                global $dic;
1670                $passwordGenerator = $dic->get(PasswordGeneratorInterface::class);
1671
1672                $response = Subsonic_Xml_Data::createSuccessResponse('createshare');
1673                $shares   = array();
1674                $shares[] = Share::create_share(
1675                    $object_type,
1676                    $object_id,
1677                    true,
1678                    Access::check_function('download'),
1679                    $expire_days,
1680                    $passwordGenerator->generate(PasswordGenerator::DEFAULT_LENGTH),
1681                    0,
1682                    $description
1683                );
1684                Subsonic_Xml_Data::addShares($response, $shares);
1685            } else {
1686                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'createshare');
1687            }
1688        } else {
1689            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'createshare');
1690        }
1691        self::apiOutput($input, $response);
1692    }
1693
1694    /**
1695     * deleteShare
1696     * Delete an existing share.
1697     * Takes the share id to delete in parameters.
1698     * @param array $input
1699     */
1700    public static function deleteshare($input)
1701    {
1702        $username = self::check_parameter($input, 'u');
1703        $user     = User::get_from_username((string)$username);
1704        $id       = self::check_parameter($input, 'id');
1705        if (AmpConfig::get('share')) {
1706            if (Share::delete_share($id, $user)) {
1707                $response = Subsonic_Xml_Data::createSuccessResponse('deleteshare');
1708            } else {
1709                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'deleteshare');
1710            }
1711        } else {
1712            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'deleteshare');
1713        }
1714        self::apiOutput($input, $response);
1715    }
1716
1717    /**
1718     * updateShare
1719     * Update the description and/or expiration date for an existing share.
1720     * Takes the share id to update with optional description and expires parameters.
1721     * Not supported.
1722     * @param array $input
1723     */
1724    public static function updateshare($input)
1725    {
1726        $username    = self::check_parameter($input, 'u');
1727        $share_id    = self::check_parameter($input, 'id');
1728        $user        = User::get_from_username((string)$username);
1729        $description = $input['description'];
1730
1731        if (AmpConfig::get('share')) {
1732            $share = new Share(Subsonic_Xml_Data::getAmpacheId($share_id));
1733            if ($share->id > 0) {
1734                $expires = $share->expire_days;
1735                if (isset($input['expires'])) {
1736                    // Parse as a string to work on 32-bit computers
1737                    $expires = $input['expires'];
1738                    if (strlen((string)$expires) > 3) {
1739                        $expires = (int)(substr($expires, 0, -3));
1740                    }
1741                    if ($expires > 0) {
1742                        $expires = ($expires - $share->creation_date) / 86400;
1743                        $expires = ceil($expires);
1744                    }
1745                }
1746
1747                $data = array(
1748                    'max_counter' => $share->max_counter,
1749                    'expire' => $expires,
1750                    'allow_stream' => $share->allow_stream,
1751                    'allow_download' => $share->allow_download,
1752                    'description' => $description ?: $share->description,
1753                );
1754                if ($share->update($data, $user)) {
1755                    $response = Subsonic_Xml_Data::createSuccessResponse('updateshare');
1756                } else {
1757                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
1758                        'updateshare');
1759                }
1760            } else {
1761                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'updateshare');
1762            }
1763        } else {
1764            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'updateshare');
1765        }
1766
1767        self::apiOutput($input, $response);
1768    }
1769
1770    /**
1771     * createUser
1772     * Create a new user.
1773     * Takes the username, password and email with optional roles in parameters.
1774     * @param array $input
1775     */
1776    public static function createuser($input)
1777    {
1778        $username     = self::check_parameter($input, 'username');
1779        $password     = self::check_parameter($input, 'password');
1780        $email        = urldecode((string)self::check_parameter($input, 'email'));
1781        $adminRole    = ($input['adminRole'] == 'true');
1782        $downloadRole = ($input['downloadRole'] == 'true');
1783        $uploadRole   = ($input['uploadRole'] == 'true');
1784        $coverArtRole = ($input['coverArtRole'] == 'true');
1785        $shareRole    = ($input['shareRole'] == 'true');
1786        //$ldapAuthenticated = $input['ldapAuthenticated'];
1787        //$settingsRole = $input['settingsRole'];
1788        //$streamRole = $input['streamRole'];
1789        //$jukeboxRole = $input['jukeboxRole'];
1790        //$playlistRole = $input['playlistRole'];
1791        //$commentRole = $input['commentRole'];
1792        //$podcastRole = $input['podcastRole'];
1793        if ($email) {
1794            $email = urldecode($email);
1795        }
1796
1797        if (Access::check('interface', 100)) {
1798            $access = 25;
1799            if ($coverArtRole) {
1800                $access = 75;
1801            }
1802            if ($adminRole) {
1803                $access = 100;
1804            }
1805            $password = self::decrypt_password($password);
1806            $user_id  = User::create($username, $username, $email, null, $password, $access);
1807            if ($user_id > 0) {
1808                if ($downloadRole) {
1809                    Preference::update('download', $user_id, 1);
1810                }
1811                if ($uploadRole) {
1812                    Preference::update('allow_upload', $user_id, 1);
1813                }
1814                if ($shareRole) {
1815                    Preference::update('share', $user_id, 1);
1816                }
1817                $response = Subsonic_Xml_Data::createSuccessResponse('createuser');
1818            } else {
1819                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'createuser');
1820            }
1821        } else {
1822            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'createuser');
1823        }
1824
1825        self::apiOutput($input, $response);
1826    }
1827
1828    /**
1829     * updateUser
1830     * Update an existing user.
1831     * Takes the username with optional parameters.
1832     * @param array $input
1833     */
1834    public static function updateuser($input)
1835    {
1836        $username = self::check_parameter($input, 'username');
1837        $password = $input['password'];
1838        $email    = urldecode($input['email']);
1839        //$ldapAuthenticated = $input['ldapAuthenticated'];
1840        $adminRole    = ($input['adminRole'] == 'true');
1841        $downloadRole = ($input['downloadRole'] == 'true');
1842        $uploadRole   = ($input['uploadRole'] == 'true');
1843        $coverArtRole = ($input['coverArtRole'] == 'true');
1844        $shareRole    = ($input['shareRole'] == 'true');
1845        //$musicfolderid = $input['musicFolderId'];
1846        $maxbitrate = $input['maxBitRate'];
1847
1848        if (Access::check('interface', 100)) {
1849            $access = 25;
1850            if ($coverArtRole) {
1851                $access = 75;
1852            }
1853            if ($adminRole) {
1854                $access = 100;
1855            }
1856            // identify the user to modify
1857            $user    = User::get_from_username((string)$username);
1858            $user_id = $user->id;
1859
1860            if ($user_id > 0) {
1861                // update access level
1862                $user->update_access($access);
1863                // update password
1864                if ($password && !AmpConfig::get('simple_user_mode')) {
1865                    $password = self::decrypt_password($password);
1866                    $user->update_password($password);
1867                }
1868                // update e-mail
1869                if (Mailer::validate_address($email)) {
1870                    $user->update_email($email);
1871                }
1872                // set preferences
1873                if ($downloadRole) {
1874                    Preference::update('download', $user_id, 1);
1875                }
1876                if ($uploadRole) {
1877                    Preference::update('allow_upload', $user_id, 1);
1878                }
1879                if ($shareRole) {
1880                    Preference::update('share', $user_id, 1);
1881                }
1882                if ((int)$maxbitrate > 0) {
1883                    Preference::update('transcode_bitrate', $user_id, $maxbitrate);
1884                }
1885                $response = Subsonic_Xml_Data::createSuccessResponse('updateuser');
1886            } else {
1887                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'updateuser');
1888            }
1889        } else {
1890            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'updateuser');
1891        }
1892
1893        self::apiOutput($input, $response);
1894    }
1895
1896    /**
1897     * deleteUser
1898     * Delete an existing user.
1899     * Takes the username in parameter.
1900     * @param array $input
1901     */
1902    public static function deleteuser($input)
1903    {
1904        $username = self::check_parameter($input, 'username');
1905        if (Access::check('interface', 100)) {
1906            $user = User::get_from_username((string)$username);
1907            if ($user->id) {
1908                $user->delete();
1909                $response = Subsonic_Xml_Data::createSuccessResponse('deleteuser');
1910            } else {
1911                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'deleteuser');
1912            }
1913        } else {
1914            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'deleteuser');
1915        }
1916
1917        self::apiOutput($input, $response);
1918    }
1919
1920    /**
1921     * change password
1922     * Change the password of an existing user.
1923     * Takes the username with new password in parameters.
1924     * @param array $input
1925     */
1926    public static function changepassword($input)
1927    {
1928        $username = self::check_parameter($input, 'username');
1929        $inp_pass = self::check_parameter($input, 'password');
1930        $password = self::decrypt_password($inp_pass);
1931        $myuser   = User::get_from_username($input['u']);
1932
1933        if ($myuser->username == $username || Access::check('interface', 100)) {
1934            $user = User::get_from_username((string) $username);
1935            if ($user->id && !AmpConfig::get('simple_user_mode')) {
1936                $user->update_password($password);
1937                $response = Subsonic_Xml_Data::createSuccessResponse('changepassword');
1938            } else {
1939                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '',
1940                    'changepassword');
1941            }
1942        } else {
1943            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'changepassword');
1944        }
1945        self::apiOutput($input, $response);
1946    }
1947
1948    /**
1949     * jukeboxControl
1950     * Control the jukebox.
1951     * Takes the action with optional index, offset, song id and volume gain in parameters.
1952     * Not supported.
1953     * @param array $input
1954     */
1955    public static function jukeboxcontrol($input)
1956    {
1957        $action = self::check_parameter($input, 'action');
1958        $id     = $input['id'];
1959        $gain   = $input['gain'];
1960
1961        $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'jukeboxcontrol');
1962        debug_event(__CLASS__, 'Using Localplay controller: ' . AmpConfig::get('localplay_controller'), 5);
1963        $localplay = new LocalPlay(AmpConfig::get('localplay_controller'));
1964
1965        if ($localplay->connect()) {
1966            $ret = false;
1967            switch ($_REQUEST['action']) {
1968                case 'get':
1969                case 'status':
1970                    $ret = true;
1971                    break;
1972                case 'start':
1973                    $ret = $localplay->play();
1974                    break;
1975                case 'stop':
1976                    $ret = $localplay->stop();
1977                    break;
1978                case 'skip':
1979                    if (isset($input['index'])) {
1980                        if ($localplay->skip($input['index'])) {
1981                            $ret = $localplay->play();
1982                        }
1983                    } elseif (isset($input['offset'])) {
1984                        debug_event(self::class, 'Skip with offset is not supported on JukeboxControl.', 5);
1985                    } else {
1986                        $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, '',
1987                            'jukeboxcontrol');
1988                    }
1989                    break;
1990                case 'set':
1991                    $localplay->delete_all();
1992                    // Intentional break fall-through
1993                case 'add':
1994                    $user = User::get_from_username($input['u']);
1995                    if ($id) {
1996                        if (!is_array($id)) {
1997                            $rid   = array();
1998                            $rid[] = $id;
1999                            $id    = $rid;
2000                        }
2001
2002                        foreach ($id as $song_id) {
2003                            $url = null;
2004
2005                            if (Subsonic_Xml_Data::isSong($song_id)) {
2006                                $media = new Song(Subsonic_Xml_Data::getAmpacheId($song_id));
2007                                $url   = $media->play_url('&client=' . $localplay->type, 'api', function_exists('curl_version'), $user->id);
2008                            }
2009
2010                            if ($url !== null) {
2011                                debug_event(self::class, 'Adding ' . $url, 5);
2012                                $stream        = array();
2013                                $stream['url'] = $url;
2014                                $ret           = $localplay->add_url(new Stream_Url($stream));
2015                            }
2016                        }
2017                    }
2018                    break;
2019                case 'clear':
2020                    $ret = $localplay->delete_all();
2021                    break;
2022                case 'remove':
2023                    if (isset($input['index'])) {
2024                        $ret = $localplay->delete_track($input['index']);
2025                    } else {
2026                        $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, '',
2027                            'jukeboxcontrol');
2028                    }
2029                    break;
2030                case 'shuffle':
2031                    $ret = $localplay->random(true);
2032                    break;
2033                case 'setGain':
2034                    $ret = $localplay->volume_set($gain * 100);
2035                    break;
2036            }
2037
2038            if ($ret) {
2039                $response = Subsonic_Xml_Data::createSuccessResponse('jukeboxcontrol');
2040                if ($action == 'get') {
2041                    Subsonic_Xml_Data::addJukeboxPlaylist($response, $localplay);
2042                } else {
2043                    Subsonic_Xml_Data::createJukeboxStatus($response, $localplay);
2044                }
2045            }
2046        }
2047
2048        self::apiOutput($input, $response);
2049    }
2050
2051    /**
2052     * scrobble
2053     * Scrobbles a given music file on last.fm.
2054     * Takes the file id with optional time and submission parameters.
2055     * @param array $input
2056     */
2057    public static function scrobble($input)
2058    {
2059        $object_ids = self::check_parameter($input, 'id');
2060        $submission = ($input['submission'] === 'true' || $input['submission'] === '1');
2061        $username   = (string) $input['u'];
2062        $client     = (string) $input['c'];
2063        $user       = User::get_from_username($username);
2064
2065        if (!is_array($object_ids)) {
2066            $rid        = array();
2067            $rid[]      = $object_ids;
2068            $object_ids = $rid;
2069        }
2070        $user_data = User::get_user_data($user->id, 'playqueue_time');
2071        $now_time  = time();
2072        // don't scrobble after setting the play queue too quickly
2073        if ($user_data['playqueue_time'] < ($now_time - 2)) {
2074            foreach ($object_ids as $subsonic_id) {
2075                $time     = isset($input['time']) ? (int)$input['time'] / 1000 : $now_time;
2076                $previous = Stats::get_last_play($user->id, $client, $time);
2077                $media    = Subsonic_Xml_Data::getAmpacheObject($subsonic_id);
2078                $type     = Subsonic_Xml_Data::getAmpacheType($subsonic_id);
2079                $media->format();
2080
2081                // long pauses might cause your now_playing to hide
2082                Stream::garbage_collection();
2083                Stream::insert_now_playing((int) $media->id, (int) $user->id, ((int)$media->time), $username, $type, ((int)$time));
2084                // submission is true: go to scrobble plugins (Plugin::get_plugins('save_mediaplay'))
2085                if ($submission && get_class($media) == Song::class && ($previous['object_id'] != $media->id) && (($time - $previous['time']) > 5)) {
2086                    // stream has finished
2087                    debug_event(self::class, $user->username . ' scrobbled: {' . $media->id . '} at ' . $time, 5);
2088                    User::save_mediaplay($user, $media);
2089                }
2090                // Submission is false and not a repeat. let repeats go though to saveplayqueue
2091                if ((!$submission) && $media->id && ($previous['object_id'] != $media->id) && (($time - $previous['time']) > 5)) {
2092                    $media->set_played($user->id, $client, array(), $time);
2093                }
2094            }
2095        }
2096
2097        $response = Subsonic_Xml_Data::createSuccessResponse('scrobble');
2098        self::apiOutput($input, $response);
2099    }
2100
2101    /**
2102     * getLyrics
2103     * Searches and returns lyrics for a given song.
2104     * Takes the optional artist and title in parameters.
2105     * @param array $input
2106     */
2107    public static function getlyrics($input)
2108    {
2109        $artist = $input['artist'];
2110        $title  = $input['title'];
2111
2112        if (!$artist && !$title) {
2113            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_MISSINGPARAM, '', 'getlyrics');
2114        } else {
2115            $search           = array();
2116            $search['limit']  = 1;
2117            $search['offset'] = 0;
2118            $search['type']   = "song";
2119
2120            $count = 0;
2121            if ($artist) {
2122                $search['rule_' . $count . '_input']    = $artist;
2123                $search['rule_' . $count . '_operator'] = 4;
2124                $search['rule_' . $count . '']          = "artist";
2125                ++$count;
2126            }
2127            if ($title) {
2128                $search['rule_' . $count . '_input']    = $title;
2129                $search['rule_' . $count . '_operator'] = 4;
2130                $search['rule_' . $count . '']          = "title";
2131            }
2132
2133            $songs    = Search::run($search);
2134            $response = Subsonic_Xml_Data::createSuccessResponse('getlyrics');
2135            if (count($songs) > 0) {
2136                Subsonic_Xml_Data::addLyrics($response, $artist, $title, $songs[0]);
2137            }
2138        }
2139
2140        self::apiOutput($input, $response);
2141    }
2142
2143    /**
2144     * getArtistInfo
2145     * Returns artist info with biography, image URLs and similar artists, using data from last.fm.
2146     * Takes artist id in parameter with optional similar artist count and if not present similar artist should be returned.
2147     * @param array $input
2148     * @param string $child
2149     */
2150    public static function getartistinfo($input, $child = "artistInfo")
2151    {
2152        $id                = self::check_parameter($input, 'id');
2153        $count             = $input['count'] ?: 20;
2154        $includeNotPresent = ($input['includeNotPresent'] === "true");
2155
2156        if (Subsonic_Xml_Data::isArtist($id)) {
2157            $artist_id = Subsonic_Xml_Data::getAmpacheId($id);
2158            $info      = Recommendation::get_artist_info($artist_id);
2159            $similars  = Recommendation::get_artists_like($artist_id, $count, !$includeNotPresent);
2160            $response  = Subsonic_Xml_Data::createSuccessResponse('getartistinfo');
2161            Subsonic_Xml_Data::addArtistInfo($response, $info, $similars, $child);
2162        } else {
2163            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'getartistinfo');
2164        }
2165
2166        self::apiOutput($input, $response);
2167    }
2168
2169    /**
2170     * getArtistInfo2
2171     * See getArtistInfo.
2172     * @param array $input
2173     */
2174    public static function getartistinfo2($input)
2175    {
2176        self::getartistinfo($input, 'artistInfo2');
2177    }
2178
2179    /**
2180     * getSimilarSongs
2181     * Returns a random collection of songs from the given artist and similar artists, using data from last.fm. Typically used for artist radio features.
2182     * Takes song/album/artist id in parameter with optional similar songs count.
2183     * @param array $input
2184     * @param string $child
2185     */
2186    public static function getsimilarsongs($input, $child = "similarSongs")
2187    {
2188        if (!AmpConfig::get('show_similar')) {
2189            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND,
2190                "Show similar must be enabled", 'getsimilarsongs');
2191            self::apiOutput($input, $response);
2192
2193            return;
2194        }
2195
2196        $id    = self::check_parameter($input, 'id');
2197        $count = $input['count'] ?: 50;
2198
2199        $songs = array();
2200        if (Subsonic_Xml_Data::isArtist($id)) {
2201            $similars = Recommendation::get_artists_like(Subsonic_Xml_Data::getAmpacheId($id));
2202            if (!empty($similars)) {
2203                debug_event(self::class, 'Found: ' . count($similars) . ' similar artists', 4);
2204                foreach ($similars as $similar) {
2205                    debug_event(self::class, $similar['name'] . ' (id=' . $similar['id'] . ')', 5);
2206                    if ($similar['id']) {
2207                        $artist = new Artist($similar['id']);
2208                        // get the songs in a random order for even more chaos
2209                        $artist_songs = static::getSongRepository()->getRandomByArtist($artist);
2210                        foreach ($artist_songs as $song) {
2211                            $songs[] = array('id' => $song);
2212                        }
2213                    }
2214                }
2215            }
2216            // randomize and slice
2217            shuffle($songs);
2218            $songs = array_slice($songs, 0, $count);
2219        //} elseif (Ampache\Module\Api\Subsonic_Xml_Data::isAlbum($id)) {
2220            //    // TODO: support similar songs for albums
2221        } elseif (Subsonic_Xml_Data::isSong($id)) {
2222            $songs = Recommendation::get_songs_like(Subsonic_Xml_Data::getAmpacheId($id), $count);
2223        }
2224
2225        if (count($songs) == 0) {
2226            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'getsimilarsongs');
2227        } else {
2228            $response = Subsonic_Xml_Data::createSuccessResponse('getsimilarsongs');
2229            Subsonic_Xml_Data::addSimilarSongs($response, $songs, $child);
2230        }
2231
2232        self::apiOutput($input, $response);
2233    }
2234
2235    /**
2236     * getSimilarSongs2
2237     * See getSimilarSongs.
2238     * @param array $input
2239     */
2240    public static function getsimilarsongs2($input)
2241    {
2242        self::getsimilarsongs($input, "similarSongs2");
2243    }
2244
2245    /**
2246     * getPodcasts
2247     * Get all podcast channels.
2248     * Takes the optional includeEpisodes and channel id in parameters
2249     * @param array $input
2250     */
2251    public static function getpodcasts($input)
2252    {
2253        $podcast_id      = $input['id'];
2254        $includeEpisodes = !isset($input['includeEpisodes']) || $input['includeEpisodes'] === "true";
2255
2256        if (AmpConfig::get('podcast')) {
2257            if ($podcast_id) {
2258                $podcast = new Podcast(Subsonic_Xml_Data::getAmpacheId($podcast_id));
2259                if ($podcast->id) {
2260                    $response = Subsonic_Xml_Data::createSuccessResponse('getpodcasts');
2261                    Subsonic_Xml_Data::addPodcasts($response, array($podcast), $includeEpisodes);
2262                } else {
2263                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '',
2264                        'getpodcasts');
2265                }
2266            } else {
2267                $podcasts = Catalog::get_podcasts();
2268                $response = Subsonic_Xml_Data::createSuccessResponse('getpodcasts');
2269                Subsonic_Xml_Data::addPodcasts($response, $podcasts, $includeEpisodes);
2270            }
2271        } else {
2272            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'getpodcasts');
2273        }
2274        self::apiOutput($input, $response);
2275    }
2276
2277    /**
2278     * getNewestPodcasts
2279     * Get the most recently published podcast episodes.
2280     * Takes the optional count in parameters
2281     * @param array $input
2282     */
2283    public static function getnewestpodcasts($input)
2284    {
2285        $count = $input['count'] ?: AmpConfig::get('podcast_new_download');
2286
2287        if (AmpConfig::get('podcast')) {
2288            $response = Subsonic_Xml_Data::createSuccessResponse('getnewestpodcasts');
2289            $episodes = Catalog::get_newest_podcasts($count);
2290            Subsonic_Xml_Data::addNewestPodcastEpisodes($response, $episodes);
2291        } else {
2292            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'getnewestpodcasts');
2293        }
2294        self::apiOutput($input, $response);
2295    }
2296
2297    /**
2298     * refreshPodcasts
2299     * Request the server to check for new podcast episodes.
2300     * Takes no parameters.
2301     * @param array $input
2302     */
2303    public static function refreshpodcasts($input)
2304    {
2305        if (AmpConfig::get('podcast') && Access::check('interface', 75)) {
2306            $podcasts = Catalog::get_podcasts();
2307            foreach ($podcasts as $podcast) {
2308                $podcast->sync_episodes(true);
2309            }
2310            $response = Subsonic_Xml_Data::createSuccessResponse('refreshpodcasts');
2311        } else {
2312            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '', 'refreshpodcasts');
2313        }
2314        self::apiOutput($input, $response);
2315    }
2316
2317    /**
2318     * createPodcastChannel
2319     * Add a new podcast channel.
2320     * Takes the podcast url in parameter.
2321     * @param array $input
2322     */
2323    public static function createpodcastchannel($input)
2324    {
2325        $url      = self::check_parameter($input, 'url');
2326        $username = self::check_parameter($input, 'u');
2327        $user     = User::get_from_username((string)$username);
2328
2329        if (AmpConfig::get('podcast') && Access::check('interface', 75)) {
2330            $catalogs = Catalog::get_catalogs('podcast', $user->id);
2331            if (count($catalogs) > 0) {
2332                $data            = array();
2333                $data['feed']    = $url;
2334                $data['catalog'] = $catalogs[0];
2335                if (Podcast::create($data)) {
2336                    $response = Subsonic_Xml_Data::createSuccessResponse('createpodcastchannel');
2337                } else {
2338                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_GENERIC, '',
2339                        'createpodcastchannel');
2340                }
2341            } else {
2342                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
2343                    'createpodcastchannel');
2344            }
2345        } else {
2346            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
2347                'createpodcastchannel');
2348        }
2349        self::apiOutput($input, $response);
2350    }
2351
2352    /**
2353     * deletePodcastChannel
2354     * Delete an existing podcast channel
2355     * Takes the podcast id in parameter.
2356     * @param array $input
2357     */
2358    public static function deletepodcastchannel($input)
2359    {
2360        $podcast_id = (int)self::check_parameter($input, 'id');
2361
2362        if (AmpConfig::get('podcast') && Access::check('interface', 75)) {
2363            $podcast = new Podcast(Subsonic_Xml_Data::getAmpacheId($podcast_id));
2364            if ($podcast->id) {
2365                if ($podcast->remove()) {
2366                    $response = Subsonic_Xml_Data::createSuccessResponse('deletepodcastchannel');
2367                } else {
2368                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_GENERIC, '',
2369                        'deletepodcastchannel');
2370                }
2371            } else {
2372                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '',
2373                    'deletepodcastchannel');
2374            }
2375        } else {
2376            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
2377                'deletepodcastchannel');
2378        }
2379        self::apiOutput($input, $response);
2380    }
2381
2382    /**
2383     * deletePodcastEpisode
2384     * Delete a podcast episode
2385     * Takes the podcast episode id in parameter.
2386     * @param array $input
2387     */
2388    public static function deletepodcastepisode($input)
2389    {
2390        $id = self::check_parameter($input, 'id');
2391
2392        if (AmpConfig::get('podcast') && Access::check('interface', 75)) {
2393            $episode = new Podcast_Episode(Subsonic_Xml_Data::getAmpacheId($id));
2394            if ($episode->id !== null) {
2395                if ($episode->remove()) {
2396                    $response = Subsonic_Xml_Data::createSuccessResponse('deletepodcastepisode');
2397                } else {
2398                    $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_GENERIC, '',
2399                        'deletepodcastepisode');
2400                }
2401            } else {
2402                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '',
2403                    'deletepodcastepisode');
2404            }
2405        } else {
2406            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
2407                'deletepodcastepisode');
2408        }
2409        self::apiOutput($input, $response);
2410    }
2411
2412    /**
2413     * downloadPodcastEpisode
2414     * Request the server to download a podcast episode
2415     * Takes the podcast episode id in parameter.
2416     * @param array $input
2417     */
2418    public static function downloadpodcastepisode($input)
2419    {
2420        $id = self::check_parameter($input, 'id');
2421
2422        if (AmpConfig::get('podcast') && Access::check('interface', 75)) {
2423            $episode = new Podcast_Episode(Subsonic_Xml_Data::getAmpacheId($id));
2424            if ($episode->id !== null) {
2425                $episode->gather();
2426                $response = Subsonic_Xml_Data::createSuccessResponse('downloadpodcastepisode');
2427            } else {
2428                $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '',
2429                    'downloadpodcastepisode');
2430            }
2431        } else {
2432            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_UNAUTHORIZED, '',
2433                'downloadpodcastepisode');
2434        }
2435        self::apiOutput($input, $response);
2436    }
2437
2438    /**
2439     * getBookmarks
2440     * Get all user bookmarks.
2441     * Takes no parameter.
2442     * Not supported.
2443     * @param array $input
2444     */
2445    public static function getbookmarks($input)
2446    {
2447        $user_id   = User::get_from_username($input['u'])->getId();
2448        $response  = Subsonic_Xml_Data::createSuccessResponse('getbookmarks');
2449        $bookmarks = [];
2450
2451        foreach (static::getBookmarkRepository()->getBookmarks($user_id) as $bookmarkId) {
2452            $bookmarks[] = new Bookmark($bookmarkId);
2453        }
2454
2455        Subsonic_Xml_Data::addBookmarks($response, $bookmarks);
2456        self::apiOutput($input, $response, array('bookmark'));
2457    }
2458
2459    /**
2460     * createBookmark
2461     * Creates or updates a bookmark.
2462     * Takes the file id and position with optional comment in parameters.
2463     * Not supported.
2464     * @param array $input
2465     */
2466    public static function createbookmark($input)
2467    {
2468        $object_id = self::check_parameter($input, 'id');
2469        $position  = self::check_parameter($input, 'position');
2470        $comment   = $input['comment'];
2471        $type      = Subsonic_Xml_Data::getAmpacheType($object_id);
2472
2473        if (!empty($type)) {
2474            $bookmark = new Bookmark(Subsonic_Xml_Data::getAmpacheId($object_id), $type);
2475            if ($bookmark->id) {
2476                static::getBookmarkRepository()->update($bookmark->getId(), (int) $position);
2477            } else {
2478                Bookmark::create(
2479                    [
2480                        'object_id' => Subsonic_Xml_Data::getAmpacheId($object_id),
2481                        'object_type' => $type,
2482                        'comment' => $comment,
2483                        'position' => $position
2484                    ],
2485                    Core::get_global('user')->id,
2486                    time()
2487                );
2488            }
2489            $response = Subsonic_Xml_Data::createSuccessResponse('createbookmark');
2490        } else {
2491            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'createbookmark');
2492        }
2493        self::apiOutput($input, $response);
2494    }
2495
2496    /**
2497     * deleteBookmark
2498     * Delete an existing bookmark.
2499     * Takes the file id in parameter.
2500     * Not supported.
2501     * @param array $input
2502     */
2503    public static function deletebookmark($input)
2504    {
2505        $id   = self::check_parameter($input, 'id');
2506        $type = Subsonic_Xml_Data::getAmpacheType($id);
2507
2508        $bookmark = new Bookmark(Subsonic_Xml_Data::getAmpacheId($id), $type);
2509        if ($bookmark->id) {
2510            static::getBookmarkRepository()->delete($bookmark->getId());
2511            $response = Subsonic_Xml_Data::createSuccessResponse('deletebookmark');
2512        } else {
2513            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'deletebookmark');
2514        }
2515        self::apiOutput($input, $response);
2516    }
2517
2518    /**
2519     * getChatMessages
2520     * Get the current chat messages.
2521     * Takes no parameter.
2522     * Not supported.
2523     * @param array $input
2524     */
2525    public static function getchatmessages($input)
2526    {
2527        $since                    = (int) $input['since'];
2528        $privateMessageRepository = static::getPrivateMessageRepository();
2529
2530        $privateMessageRepository->cleanChatMessages();
2531
2532        $messages = $privateMessageRepository->getChatMessages($since);
2533
2534        $response = Subsonic_Xml_Data::createSuccessResponse('getchatmessages');
2535        Subsonic_Xml_Data::addMessages($response, $messages);
2536        self::apiOutput($input, $response);
2537    }
2538
2539    /**
2540     * addChatMessages
2541     * Add a message to the chat.
2542     * Takes the message in parameter.
2543     * Not supported.
2544     * @param array $input
2545     */
2546    public static function addchatmessage($input)
2547    {
2548        $message = self::check_parameter($input, 'message');
2549
2550        $message = trim(
2551            strip_tags(
2552                filter_var(
2553                    $message,
2554                    FILTER_SANITIZE_STRING,
2555                    FILTER_FLAG_NO_ENCODE_QUOTES
2556                )
2557            )
2558        );
2559
2560        $user_id = User::get_from_username($input['u'])->getId();
2561        if (static::getPrivateMessageRepository()->sendChatMessage($message, $user_id) !== null) {
2562            $response = Subsonic_Xml_Data::createSuccessResponse('addchatmessage');
2563        } else {
2564            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'addChatMessage');
2565        }
2566        self::apiOutput($input, $response);
2567    }
2568
2569    /**
2570     * savePlayQueue
2571     * Save the state of the play queue for the authenticated user.
2572     * Takes multiple song id in parameter with optional current id playing song and position.
2573     * @param array $input
2574     */
2575    public static function saveplayqueue($input)
2576    {
2577        $current = (int)$input['current'];
2578        $media   = Subsonic_Xml_Data::getAmpacheObject($current);
2579        if ($media->id) {
2580            $response  = Subsonic_Xml_Data::createSuccessResponse('saveplayqueue');
2581            $position  = (int)((int)$input['position'] / 1000);
2582            $username  = (string) $input['u'];
2583            $client    = (string) $input['c'];
2584            $user_id   = User::get_from_username($username)->id;
2585            $user_data = User::get_user_data($user_id, 'playqueue_time');
2586            $time      = time();
2587            // wait a few seconds before smashing out play times
2588            if ($user_data['playqueue_time'] < ($time - 2)) {
2589                $previous = Stats::get_last_play($user_id, $client);
2590                $type     = Subsonic_Xml_Data::getAmpacheType($current);
2591                // long pauses might cause your now_playing to hide
2592                Stream::garbage_collection();
2593                Stream::insert_now_playing((int)$media->id, (int)$user_id, ((int)$media->time - $position), $username, $type, ($time - $position));
2594
2595                if ($previous['object_id'] == $media->id) {
2596                    $time_diff = $time - $previous['date'];
2597                    $old_play  = $time_diff > $media->time * 5;
2598                    // shift the start time if it's an old play or has been pause/played
2599                    if ($position >= 1 || $old_play) {
2600                        Stats::shift_last_play($user_id, $client, $previous['date'], ($time - $position));
2601                    }
2602                    // track has just started. repeated plays aren't called by scrobble so make sure we call this too
2603                    if (($position < 1 && $time_diff > 5) && !$old_play) {
2604                        $media->set_played((int)$user_id, $client, array(), $time);
2605                    }
2606                }
2607                $playQueue = new User_Playlist($user_id);
2608                $sub_ids   = (is_array($input['id']))
2609                    ? $input['id']
2610                    : array($input['id']);
2611                $playlist  = Subsonic_Xml_Data::getAmpacheIdArrays($sub_ids);
2612                $playQueue->set_items($playlist, $type, $media->id, $position, $time, $client);
2613            }
2614        } else {
2615            $response = Subsonic_Xml_Data::createError(Subsonic_Xml_Data::SSERROR_DATA_NOTFOUND, '', 'saveplayqueue');
2616        }
2617
2618        self::apiOutput($input, $response);
2619    }
2620
2621    /**
2622     * getPlayQueue
2623     * Returns the state of the play queue for the authenticated user.
2624     * Takes no additional parameters
2625     * @param array $input
2626     */
2627    public static function getplayqueue($input)
2628    {
2629        $username = (string) $input['u'];
2630        $client   = (string) $input['c'];
2631        $user_id  = User::get_from_username($username)->id;
2632        $response = Subsonic_Xml_Data::createSuccessResponse('getplayqueue');
2633        User::set_user_data($user_id, 'playqueue_time', time());
2634        User::set_user_data($user_id, 'playqueue_client', $client);
2635
2636        Subsonic_Xml_Data::addPlayQueue($response, $user_id, $username);
2637        self::apiOutput($input, $response);
2638    }
2639
2640    /**
2641     * @deprecated Inject by constructor
2642     */
2643    private static function getAlbumRepository(): AlbumRepositoryInterface
2644    {
2645        global $dic;
2646
2647        return $dic->get(AlbumRepositoryInterface::class);
2648    }
2649
2650    /**
2651     * @deprecated Inject by constructor
2652     */
2653    private static function getSongRepository(): SongRepositoryInterface
2654    {
2655        global $dic;
2656
2657        return $dic->get(SongRepositoryInterface::class);
2658    }
2659
2660    /**
2661     * @deprecated Inject by constructor
2662     */
2663    private static function getLiveStreamRepository(): LiveStreamRepositoryInterface
2664    {
2665        global $dic;
2666
2667        return $dic->get(LiveStreamRepositoryInterface::class);
2668    }
2669
2670    /**
2671     * @deprecated inject dependency
2672     */
2673    private static function getUserRepository(): UserRepositoryInterface
2674    {
2675        global $dic;
2676
2677        return $dic->get(UserRepositoryInterface::class);
2678    }
2679
2680    /**
2681     * @deprecated inject dependency
2682     */
2683    private static function getBookmarkRepository(): BookmarkRepositoryInterface
2684    {
2685        global $dic;
2686
2687        return $dic->get(BookmarkRepositoryInterface::class);
2688    }
2689
2690    /**
2691     * @deprecated inject dependency
2692     */
2693    private static function getPrivateMessageRepository(): PrivateMessageRepositoryInterface
2694    {
2695        global $dic;
2696
2697        return $dic->get(PrivateMessageRepositoryInterface::class);
2698    }
2699}
2700