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