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\Repository\Model\Album; 28use Ampache\Module\Playback\Stream; 29use Ampache\Config\AmpConfig; 30use Ampache\Repository\Model\Art; 31use Ampache\Repository\Model\Artist; 32use Ampache\Repository\Model\Catalog; 33use Ampache\Repository\Model\Clip; 34use Ampache\Repository\AlbumRepositoryInterface; 35use Ampache\Repository\LiveStreamRepositoryInterface; 36use Ampache\Repository\SongRepositoryInterface; 37use DateTime; 38use DOMDocument; 39use Ampache\Repository\Model\Live_Stream; 40use Ampache\Repository\Model\Movie; 41use Ampache\Repository\Model\Personal_Video; 42use Ampache\Repository\Model\Playlist; 43use Ampache\Repository\Model\Podcast; 44use Ampache\Repository\Model\Podcast_Episode; 45use Ampache\Repository\Model\Search; 46use Ampache\Repository\Model\Song; 47use Ampache\Repository\Model\Tag; 48use Ampache\Repository\Model\TvShow; 49use Ampache\Repository\Model\TVShow_Episode; 50use Ampache\Repository\Model\TVShow_Season; 51use Ampache\Repository\Model\Video; 52use Exception; 53use XMLReader; 54 55/** 56 * UPnP Class 57 * 58 * This class wrap Ampache to UPnP API functions. 59 * These are all static calls. 60 * 61 * This class is a derived work from UMSP project (http://wiki.wdlxtv.com/UMSP). 62 * 63 */ 64class Upnp_Api 65{ 66 /** 67 * UPnP classes: 68 * object.item.audioItem 69 * object.item.imageItem 70 * object.item.videoItem 71 * object.item.playlistItem 72 * object.item.textItem 73 * object.container 74 */ 75 const SSDP_DEBUG = false; 76 77 /** 78 * @return string 79 */ 80 public static function get_uuidStr() 81 { 82 // Create uuid based on host 83 $key = 'ampache_' . AmpConfig::get('http_host'); 84 $hash = hash('md5', $key); 85 86 return substr($hash, 0, 8) . '-' . substr($hash, 8, 4) . '-' . substr($hash, 12, 4) . '-' . substr($hash, 16, 87 4) . '-' . substr($hash, 20); 88 } 89 90 /* ================================== Begin SSDP functions ================================== */ 91 92 /** 93 * @param string $buf 94 * @param integer $delay 95 * @param string $host 96 * @param integer $port 97 */ 98 private static function udpSend($buf, $delay = 15, $host = "239.255.255.250", $port = 1900) 99 { 100 usleep($delay * 1000); // we are supposed to delay before sending 101 $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); 102 // when broadcast, set broadcast socket option 103 if ($host == "239.255.255.250") { 104 socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1); 105 } 106 socket_sendto($socket, $buf, strlen((string) $buf), 0, $host, $port); 107 socket_close($socket); 108 } 109 110 /** 111 * @param integer $delay 112 * @param string $host 113 * @param integer $port 114 * @param string $prefix 115 * @param boolean $alive 116 */ 117 public static function sddpSend($delay = 15, $host = "239.255.255.250", $port = 1900, $prefix = "NT", $alive = true) 118 { 119 $strHeader = 'NOTIFY * HTTP/1.1' . "\r\n"; 120 $strHeader .= 'HOST: ' . $host . ':' . $port . "\r\n"; 121 $strHeader .= 'LOCATION: http://' . gethostbyname(AmpConfig::get('http_host')) . ':' . AmpConfig::get('http_port') . AmpConfig::get('raw_web_path') . '/upnp/MediaServerServiceDesc.php' . "\r\n"; 122 $strHeader .= 'SERVER: DLNADOC/1.50 UPnP/1.0 Ampache/' . AmpConfig::get('version') . "\r\n"; 123 $strHeader .= 'CACHE-CONTROL: max-age=1800' . "\r\n"; 124 //$strHeader .= 'NTS: ssdp:alive' . "\r\n"; 125 if ($alive) { 126 $strHeader .= 'NTS: ssdp:alive' . "\r\n"; 127 } else { 128 $strHeader .= 'NTS: ssdp:byebye' . "\r\n"; 129 $delay = 2; 130 } 131 $uuidStr = self::get_uuidStr(); 132 133 $rootDevice = $prefix . ': upnp:rootdevice' . "\r\n"; 134 $rootDevice .= 'USN: uuid:' . $uuidStr . '::upnp:rootdevice' . "\r\n" . "\r\n"; 135 $buf = $strHeader . $rootDevice; 136 self::udpSend($buf, $delay, $host, $port); 137 138 $uuid = $prefix . ': uuid:' . $uuidStr . "\r\n"; 139 $uuid .= 'USN: uuid:' . $uuidStr . "\r\n" . "\r\n"; 140 $buf = $strHeader . $uuid; 141 self::udpSend($buf, $delay, $host, $port); 142 143 $deviceType = $prefix . ': urn:schemas-upnp-org:device:MediaServer:1' . "\r\n"; 144 $deviceType .= 'USN: uuid:' . $uuidStr . '::urn:schemas-upnp-org:device:MediaServer:1' . "\r\n" . "\r\n"; 145 $buf = $strHeader . $deviceType; 146 self::udpSend($buf, $delay, $host, $port); 147 148 $serviceCM = $prefix . ': urn:schemas-upnp-org:service:ConnectionManager:1' . "\r\n"; 149 $serviceCM .= 'USN: uuid:' . $uuidStr . '::urn:schemas-upnp-org:service:ConnectionManager:1' . "\r\n" . "\r\n"; 150 $buf = $strHeader . $serviceCM; 151 self::udpSend($buf, $delay, $host, $port); 152 153 $serviceCD = $prefix . ': urn:schemas-upnp-org:service:ContentDirectory:1' . "\r\n"; 154 $serviceCD .= 'USN: uuid:' . $uuidStr . '::urn:schemas-upnp-org:service:ContentDirectory:1' . "\r\n" . "\r\n"; 155 $buf = $strHeader . $serviceCD; 156 self::udpSend($buf, $delay, $host, $port); 157 } 158 159 /** 160 * @param $delaytime 161 * @param $actst 162 * @param $address 163 * @throws Exception 164 */ 165 public static function sendResponse($delaytime, $actst, $address) 166 { 167 $response = 'HTTP/1.1 200 OK' . "\r\n"; 168 $response .= 'CACHE-CONTROL: max-age=1800' . "\r\n"; 169 $dt = new DateTime('UTC'); 170 $response .= 'DATE: ' . $dt->format('D, d M Y H:i:s \G\M\T') . "\r\n"; // RFC2616 date 171 $response .= 'EXT:' . "\r\n"; 172 // Note that quite a few devices are unable to resolve a URL into an IP address. Therefore we have to use a 173 // local IP address - resolve http_host into IP address 174 $response .= 'LOCATION: http://' . gethostbyname(AmpConfig::get('http_host')) . ':' . AmpConfig::get('http_port') . AmpConfig::get('raw_web_path') . '/upnp/MediaServerServiceDesc.php' . "\r\n"; 175 $response .= 'SERVER: DLNADOC/1.50 UPnP/1.0 Ampache/' . AmpConfig::get('version') . "\r\n"; 176 $response .= 'ST: ' . $actst . "\r\n"; 177 $response .= 'USN: ' . 'uuid:' . self::get_uuidStr() . '::' . $actst . "\r\n"; 178 $response .= "\r\n"; // gupnp-universal-cp cannot see us without this line. 179 180 if ($delaytime > 5) { 181 $delaytime = 5; 182 } 183 // Delay in ms 184 $delay = random_int(15, $delaytime * 1000); 185 186 $addr=explode(":", $address); 187 if (self::SSDP_DEBUG) { 188 debug_event(self::class, 'Sending response to: ' . $addr[0] . ':' . $addr[1] . PHP_EOL . $response, 5); 189 } 190 self::udpSend($response, $delay, $addr[0], (int) $addr[1]); 191 if (self::SSDP_DEBUG) { 192 // for timing 193 debug_event(self::class, '(Sent)', 5); 194 } 195 } 196 197 /** 198 * @param $unpacked 199 * @param $remote 200 */ 201 public static function notify_request($unpacked, $remote) 202 { 203 $headers = self::get_headers($unpacked); 204 $str = 'Notify ' . $remote . ' ' . $headers['nts'] . ' for ' . $headers['nt']; 205 // We don't do anything with notifications except log them to check rx working 206 if (self::SSDP_DEBUG) { 207 debug_event(self::class, $str, 5); 208 } 209 } 210 211 /** 212 * @param $data 213 * @return array 214 */ 215 public static function get_headers($data) 216 { 217 $lines = explode(PHP_EOL, $data); // split into lines 218 $keys = array(); 219 $values = array(); 220 foreach ($lines as $line) { 221 //$line = str_replace( ' ', '', $line ); 222 $line = preg_replace('/[\x00-\x1F\x7F]/', '', $line); 223 $tokens = explode(' ', $line); 224 //echo 'BARELINE:'.$line.'&'.count($tokens).PHP_EOL; 225 if (count($tokens) > 1) { 226 $tokens[0] = str_replace(':', '', $tokens[0]); // remove ':' and convert to keys lowercase for match 227 $tokens[0] = strtolower($tokens[0]); 228 array_push($keys, $tokens[0]); 229 $tokens[1] = str_replace("\"", '', $tokens[1]); 230 array_push($values, $tokens[1]); 231 } 232 } 233 234 return array_combine($keys, $values); 235 } 236 237 /** 238 * @param $data 239 * @param $address 240 * @throws Exception 241 */ 242 public static function discovery_request($data, $address) 243 { 244 // Process a discovery request. The response must be sent to the address specified by $remote 245 $headers = self::get_headers($data); 246 if (self::SSDP_DEBUG) { 247 debug_event(self::class, 'Discovery request from ' . $address, 5); 248 debug_event(self::class, 'HEADERS:' . var_export($headers, true), 5); 249 } 250 251 $new_usn = 'uuid:' . self::get_uuidStr(); 252 $actst = $headers['st']; 253 //echo 'DELAYTIME: [' . $headers['mx'] . ']' . PHP_EOL; 254 $delaytime = (int)($headers['mx']); 255 if ($headers['man'] == 'ssdp:discover') { 256 if ($headers['st'] == 'urn:schemas-upnp-org:device:MediaServer:1') { 257 self::sendResponse($delaytime, $actst, $address); 258 } elseif ($headers['st'] == 'urn:schemas-upnp-org:service:ConnectionManager:1') { 259 self::sendResponse($delaytime, $actst, $address); 260 } elseif ($headers['st'] == 'urn:schemas-upnp-org:service:ContentDirectory:1') { 261 self::sendResponse($delaytime, $actst, $address); 262 } elseif ($headers['st'] == 'upnp:rootdevice') { 263 self::sendResponse($delaytime, $actst, $address); 264 } elseif ($headers['st'] == $new_usn) { 265 self::sendResponse($delaytime, $actst, $address); 266 } elseif ($headers['st'] == 'ssdp:all') { 267 # echo 'discovery response for ssdp:all'; 268 self::sendResponse($delaytime, 'upnp:rootdevice', $address); 269 self::sendResponse($delaytime, $new_usn, $address); 270 self::sendResponse($delaytime, 'urn:schemas-upnp-org:device:MediaServer:1', $address); 271 self::sendResponse($delaytime, 'urn:schemas-upnp-org:service:ConnectionManager:1', $address); 272 self::sendResponse($delaytime, 'urn:schemas-upnp-org:service:ContentDirectory:1', $address); 273 # And one that MiniDLNA advertises 274 self::sendResponse($delaytime, 'urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1', $address); 275 } else { 276 if (self::SSDP_DEBUG) { 277 debug_event(self::class, 'ST header not for a service we provide [' . $actst . ']', 5); 278 } 279 } 280 } else { 281 if (self::SSDP_DEBUG) { 282 debug_event(self::class, 'M-SEARCH MAN header not understood [' . $headers['man'] . ']', 5); 283 } 284 } 285 } 286 287 /* ================================== End SSDP functions ================================== */ 288 289 /** 290 * @param $prmRequest 291 * @return array 292 */ 293 public static function parseUPnPRequest($prmRequest) 294 { 295 $retArr = array(); 296 $reader = new XMLReader(); 297 $result = XMLReader::XML($prmRequest); 298 if (!$result) { 299 debug_event(self::class, 'XML reader failed', 5); 300 } 301 302 while ($reader->read()) { 303 debug_event(self::class, $reader->localName . ' ' . (string) $reader->nodeType . ' ' . (string) XMLReader::ELEMENT . ' ' . (string) $reader->isEmptyElement, 5); 304 305 if (($reader->nodeType == XMLReader::ELEMENT)) { 306 switch ($reader->localName) { 307 case 'Browse': 308 $retArr['action'] = 'browse'; 309 break; 310 case 'Search': 311 $retArr['action'] = 'search'; 312 break; 313 case 'GetSortCapabilities': 314 $retArr['action'] = 'sortcapabilities'; 315 break; 316 case 'GetSearchCapabilities': 317 $retArr['action'] = 'searchcapabilities'; 318 break; 319 case 'GetSystemUpdateID': 320 $retArr['action'] = 'systemupdateID'; 321 break; 322 case 'ObjectID': 323 $reader->read(); 324 if ($reader->nodeType == XMLReader::TEXT) { 325 $retArr['objectid'] = $reader->value; 326 } // end if 327 break; 328 case 'BrowseFlag': 329 $reader->read(); 330 if ($reader->nodeType == XMLReader::TEXT) { 331 $retArr['browseflag'] = $reader->value; 332 } // end if 333 break; 334 case 'Filter': 335 $reader->read(); 336 if ($reader->nodeType == XMLReader::TEXT) { 337 $retArr['filter'] = $reader->value; 338 } // end if 339 break; 340 case 'StartingIndex': 341 $reader->read(); 342 if ($reader->nodeType == XMLReader::TEXT) { 343 $retArr['startingindex'] = $reader->value; 344 } // end if 345 break; 346 case 'RequestedCount': 347 $reader->read(); 348 if ($reader->nodeType == XMLReader::TEXT) { 349 $retArr['requestedcount'] = $reader->value; 350 } // end if 351 break; 352 case 'SearchCriteria': 353 $reader->read(); 354 if ($reader->nodeType == XMLReader::TEXT) { 355 $retArr['searchcriteria'] = $reader->value; 356 } // end if 357 break; 358 case 'SortCriteria': 359 $reader->read(); 360 if ($reader->nodeType == XMLReader::TEXT) { 361 $retArr['sortcriteria'] = $reader->value; 362 } // end if 363 break; 364 } // end switch 365 } // end if 366 } // end while 367 368 return $retArr; 369 } // end function 370 371 /** 372 * @param $filterValue 373 * @param $keyisRes 374 * @param $keytoCheck 375 * Checks whether key is in filter string, taking account of allowable filter wildcards and null strings 376 * @return bool 377 */ 378 public static function isinFilter($filterValue, $keyisRes, $keytoCheck) 379 { 380 if ($filterValue == null || $filterValue == '') { 381 return true; 382 } 383 if ($filterValue == "*") { 384 // genuine wildcard 385 386 return true; 387 } 388 if ($keyisRes) { 389 $testKey = 'res@' . $keytoCheck; 390 } else { 391 $testKey = $keytoCheck; 392 } 393 $filt = explode(',', $filterValue); // do exact word match rather than partial, which is what strpos does. 394 //debug_event(self::class, 'checking '.$testKey.' in '.var_export($filt, true), 5); 395 return in_array($testKey, $filt, true); // this is necessary, (rather than strpos) because "res" turns up in many keys, whose results may not be wanted 396 } 397 398 /** 399 * @param $prmItems 400 * @param $filterValue 401 * @return DOMDocument 402 */ 403 public static function createDIDL($prmItems, $filterValue) 404 { 405 $xmlDoc = new DOMDocument('1.0' /*, 'utf-8'*/); 406 $xmlDoc->formatOutput = true; // Note: other players don't seem to do this 407 // Create root element and add namespaces: 408 $ndDIDL = $xmlDoc->createElementNS('urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/', 'DIDL-Lite'); 409 $ndDIDL->setAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); 410 $ndDIDL->setAttribute('xmlns:upnp', 'urn:schemas-upnp-org:metadata-1-0/upnp/'); 411 $xmlDoc->appendChild($ndDIDL); 412 413 // Return empty DIDL if no items present: 414 if ((!isset($prmItems)) || (!is_array($prmItems))) { 415 return $xmlDoc; 416 } 417 418 // sometimes here comes only one single item, not an array. Convert it to array. (TODO - UGLY) 419 if ((count($prmItems) > 0) && (!is_array($prmItems[0]))) { 420 $prmItems = array($prmItems); 421 } 422 423 // Add each item in $prmItems array to $ndDIDL: 424 foreach ($prmItems as $item) { 425 if (!is_array($item)) { 426 debug_event(self::class, 'item is not array', 2); 427 debug_event(self::class, $item, 5); 428 continue; 429 } 430 431 if ($item['upnp:class'] == 'object.container' || 432 $item['upnp:class'] == 'object.container.album.musicAlbum' || 433 $item['upnp:class'] == 'object.container.person.musicArtist' || 434 $item['upnp:class'] == 'object.container.storageFolder') { 435 $ndItem = $xmlDoc->createElement('container'); 436 } else { 437 $ndItem = $xmlDoc->createElement('item'); 438 } 439 $useRes = false; 440 $ndRes = $xmlDoc->createElement('res'); 441 $ndRes_text = $xmlDoc->createTextNode($item['res']); 442 $ndRes->appendChild($ndRes_text); 443 444 /** 445 * Add each element / attribute in $item array to item node: 446 * Mini-substitution: & in value string needs to be double HTML escaped. 447 * saving DIDL as XML will do this once. 448 * Here we do it again (to match what MiniDLNA does) 449 */ 450 foreach ($item as $key => $value) { 451 // Handle attributes. Better solution? 452 switch ($key) { 453 case 'id': 454 $ndItem->setAttribute('id', $value); 455 break; 456 case 'parentID': 457 $ndItem->setAttribute('parentID', $value); 458 break; 459 case 'childCount': 460 $ndItem->setAttribute('childCount', $value); 461 break; 462 case 'restricted': 463 $ndItem->setAttribute('restricted', $value); 464 break; 465 case 'searchable': 466 $ndItem->SetAttribute('searchable', $value); 467 break; 468 case 'res': 469 break; 470 case 'duration': 471 if (self::isinFilter($filterValue, true, $key)) { 472 $ndRes->setAttribute('duration', $value); 473 $useRes = true; 474 } 475 break; 476 case 'size': 477 if (self::isinFilter($filterValue, true, $key)) { 478 $ndRes->setAttribute('size', $value); 479 $useRes = true; 480 } 481 break; 482 case 'bitrate': 483 if (self::isinFilter($filterValue, true, $key)) { 484 $ndRes->setAttribute('bitrate', $value); 485 $useRes = true; 486 } 487 break; 488 case 'protocolInfo': 489 if (self::isinFilter($filterValue, true, $key)) { 490 $ndRes->setAttribute('protocolInfo', $value); 491 $useRes = true; 492 } 493 break; 494 case 'resolution': 495 if (self::isinFilter($filterValue, true, $key)) { 496 $ndRes->setAttribute('resolution', $value); 497 $useRes = true; 498 } 499 break; 500 case 'colorDepth': 501 if (self::isinFilter($filterValue, true, $key)) { 502 $ndRes->setAttribute('colorDepth', $value); 503 $useRes = true; 504 } 505 break; 506 case 'sampleFrequency': 507 if (self::isinFilter($filterValue, true, $key)) { 508 $ndRes->setAttribute('sampleFrequency', $value); 509 $useRes = true; 510 } 511 break; 512 case 'nrAudioChannels': 513 if (self::isinFilter($filterValue, true, $key)) { 514 $ndRes->setAttribute('nrAudioChannels', $value); 515 $useRes = true; 516 } 517 break; 518 default: 519 if (self::isinFilter($filterValue, false, $key)) { 520 $ndTag = $xmlDoc->createElement($key); 521 $ndItem->appendChild($ndTag); 522 // check if string is already utf-8 encoded 523 $xvalue = str_replace("&", "&", $value); 524 $ndTag_text = $xmlDoc->createTextNode((mb_detect_encoding($xvalue, 'auto') == 'UTF-8')?$xvalue:utf8_encode($xvalue)); 525 $ndTag->appendChild($ndTag_text); 526 } 527 } 528 if ($useRes) { 529 $ndItem->appendChild($ndRes); 530 } 531 } 532 $ndDIDL->appendChild($ndItem); 533 } 534 535 return $xmlDoc; 536 } 537 538 /** 539 * @param $prmDIDL 540 * @param $prmNumRet 541 * @param $prmTotMatches 542 * @param string $prmResponseType 543 * @param string $prmUpdateID 544 * @return DOMDocument 545 */ 546 public static function createSOAPEnvelope( 547 $prmDIDL, 548 $prmNumRet, 549 $prmTotMatches, 550 $prmResponseType = 'u:BrowseResponse', 551 $prmUpdateID = '0' 552 ) { 553 /* 554 * $prmDIDL is DIDL XML string 555 * XML-Layout: 556 * 557 * -s:Envelope 558 * -s:Body 559 * -u:BrowseResponse 560 * Result (DIDL) 561 * NumberReturned 562 * TotalMatches 563 * UpdateID 564 */ 565 $doc = new DOMDocument('1.0', 'utf-8'); 566 $doc->formatOutput = true; 567 $ndEnvelope = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 's:Envelope'); 568 $ndEnvelope->setAttribute("s:encodingStyle", "http://schemas.xmlsoap.org/soap/encoding/"); 569 $doc->appendChild($ndEnvelope); 570 $ndBody = $doc->createElement('s:Body'); 571 $ndEnvelope->appendChild($ndBody); 572 $ndBrowseResp = $doc->createElementNS('urn:schemas-upnp-org:service:ContentDirectory:1', $prmResponseType); 573 $ndBody->appendChild($ndBrowseResp); 574 $ndResult = $doc->createElement('Result', $prmDIDL); 575 $ndBrowseResp->appendChild($ndResult); 576 $ndNumRet = $doc->createElement('NumberReturned', $prmNumRet); 577 $ndBrowseResp->appendChild($ndNumRet); 578 $ndTotMatches = $doc->createElement('TotalMatches', $prmTotMatches); 579 $ndBrowseResp->appendChild($ndTotMatches); 580 $ndUpdateID = $doc->createElement('UpdateID', $prmUpdateID); // seems to be ignored by the WDTVL 581 //$ndUpdateID = $doc->createElement('UpdateID', (string) bin2hex(random_bytes(20)); // seems to be ignored by the WDTVL 582 $ndBrowseResp->appendChild($ndUpdateID); 583 584 return $doc; 585 } 586 587 /** 588 * @param string $prmPath 589 * @return array|null 590 */ 591 public static function _musicMetadata($prmPath) 592 { 593 $root = 'amp://music'; 594 $pathreq = explode('/', $prmPath); 595 if ($pathreq[0] == '' && count($pathreq) > 0) { 596 array_shift($pathreq); 597 } 598 599 $meta = null; 600 $counts = Catalog::get_server_counts(0); 601 602 switch ($pathreq[0]) { 603 case 'artists': 604 switch (count($pathreq)) { 605 case 1: 606 $meta = array( 607 'id' => $root . '/artists', 608 'parentID' => $root, 609 'restricted' => '1', 610 'childCount' => $counts['artist'], 611 'dc:title' => T_('Artists'), 612 'upnp:class' => 'object.container', 613 ); 614 break; 615 case 2: 616 $artist = new Artist($pathreq[1]); 617 if ($artist->id) { 618 $artist->format(); 619 $meta = self::_itemArtist($artist, $root . '/artists'); 620 } 621 break; 622 } 623 break; 624 case 'albums': 625 switch (count($pathreq)) { 626 case 1: 627 $meta = array( 628 'id' => $root . '/albums', 629 'parentID' => $root, 630 'restricted' => '1', 631 'childCount' => $counts['album'], 632 'dc:title' => T_('Albums'), 633 'upnp:class' => 'object.container', 634 ); 635 break; 636 case 2: 637 $album = new Album($pathreq[1]); 638 if (isset($album->id)) { 639 $album->format(); 640 $meta = self::_itemAlbum($album, $root . '/albums'); 641 } 642 break; 643 } 644 break; 645 case 'songs': 646 switch (count($pathreq)) { 647 case 1: 648 $meta = array( 649 'id' => $root . '/songs', 650 'parentID' => $root, 651 'restricted' => '1', 652 'childCount' => $counts['song'], 653 'dc:title' => T_('Songs'), 654 'upnp:class' => 'object.container', 655 ); 656 break; 657 case 2: 658 $song = new Song($pathreq[1]); 659 if ($song->id) { 660 $song->format(); 661 $meta = self::_itemSong($song, $root . '/songs'); 662 } 663 break; 664 } 665 break; 666 case 'playlists': 667 switch (count($pathreq)) { 668 case 1: 669 $meta = array( 670 'id' => $root . '/playlists', 671 'parentID' => $root, 672 'restricted' => '1', 673 'childCount' => $counts['playlist'], 674 'dc:title' => T_('Playlists'), 675 'upnp:class' => 'object.container', 676 ); 677 break; 678 case 2: 679 $playlist = new Playlist($pathreq[1]); 680 if ($playlist->id) { 681 $playlist->format(); 682 $meta = self::_itemPlaylist($playlist, $root . '/playlists'); 683 } 684 break; 685 } 686 break; 687 case 'smartplaylists': 688 switch (count($pathreq)) { 689 case 1: 690 $meta = array( 691 'id' => $root . '/smartplaylists', 692 'parentID' => $root, 693 'restricted' => '1', 694 'childCount' => $counts['smartplaylist'], 695 'dc:title' => T_('Smart Playlists'), 696 'upnp:class' => 'object.container', 697 ); 698 break; 699 case 2: 700 $playlist = new Search($pathreq[1], 'song'); 701 if ($playlist->id) { 702 $playlist->format(); 703 $meta = self::_itemSmartPlaylist($playlist, $root . '/smartplaylists'); 704 } 705 break; 706 } 707 break; 708 case 'live_streams': 709 switch (count($pathreq)) { 710 case 1: 711 $meta = array( 712 'id' => $root . '/live_streams', 713 'parentID' => $root, 714 'restricted' => '1', 715 'childCount' => $counts['live_stream'], 716 'dc:title' => T_('Radio Stations'), 717 'upnp:class' => 'object.container', 718 ); 719 break; 720 case 2: 721 $radio = new Live_Stream($pathreq[1]); 722 if ($radio->id) { 723 $radio->format(); 724 $meta = self::_itemLiveStream($radio, $root . '/live_streams'); 725 } 726 break; 727 } 728 break; 729 case 'podcasts': 730 switch (count($pathreq)) { 731 case 1: 732 $meta = array( 733 'id' => $root . '/podcasts', 734 'parentID' => $root, 735 'restricted' => '1', 736 'childCount' => $counts['podcast'], 737 'dc:title' => T_('Podcasts'), 738 'upnp:class' => 'object.container', 739 ); 740 break; 741 case 2: 742 $podcast = new Podcast($pathreq[1]); 743 if ($podcast->id) { 744 $podcast->format(); 745 $meta = self::_itemPodcast($podcast, $root . '/podcasts'); 746 } 747 break; 748 case 3: 749 $episode = new Podcast_Episode($pathreq[2]); 750 if ($episode->id !== null) { 751 $episode->format(); 752 $meta = self::_itemPodcastEpisode($episode, $root . '/podcasts/' . $pathreq[1]); 753 } 754 break; 755 } 756 break; 757 default: 758 $meta = array( 759 'id' => $root, 760 'parentID' => '0', 761 'restricted' => '1', 762 'searchable' => '1', 763 'childCount' => '5', 764 'dc:title' => T_('Music'), 765 'upnp:class' => 'object.container', //.storageFolder', 766 'upnp:storageUsed' => '-1', 767 ); 768 break; 769 } 770 771 return $meta; 772 } 773 774 /** 775 * @param $items 776 * @param $start 777 * @param $count 778 * @return array 779 */ 780 public static function _slice($items, $start, $count) 781 { 782 $maxCount = count($items); 783 //debug_event(self::class, 'slice: ' . $maxCount . " " . $start . " " . $count, 5); 784 785 return array($maxCount, array_slice($items, $start, ($count == 0 ? $maxCount - $start : $count))); 786 } 787 788 /** 789 * @param $prmPath 790 * @param $prmQuery 791 * @param $start 792 * @param $count 793 * @return array 794 */ 795 public static function _musicChilds($prmPath, $prmQuery, $start, $count) 796 { 797 $mediaItems = array(); 798 $maxCount = 0; 799 $queryData = array(); 800 parse_str($prmQuery, $queryData); 801 802 debug_event(self::class, 'MusicChilds: [' . $prmPath . '] [' . $prmQuery . ']' . '[' . $start . '] [' . $count . ']', 5); 803 804 $parent = 'amp://music' . $prmPath; 805 $pathreq = explode('/', $prmPath); 806 if ($pathreq[0] == '' && count($pathreq) > 0) { 807 array_shift($pathreq); 808 } 809 debug_event(self::class, 'MusicChilds4: [' . $pathreq[0] . ']', 5); 810 $counts = Catalog::get_server_counts(0); 811 812 switch ($pathreq[0]) { 813 case 'artists': 814 switch (count($pathreq)) { 815 case 1: // Get artists list 816 $artists = Catalog::get_artists(null, $count, $start); 817 [$maxCount, $artists] = array($counts['artist'], $artists); 818 foreach ($artists as $artist) { 819 $artist->format(); 820 $mediaItems[] = self::_itemArtist($artist, $parent); 821 } 822 break; 823 case 2: // Get artist's albums list 824 $artist = new Artist($pathreq[1]); 825 if ($artist->id) { 826 $album_ids = static::getAlbumRepository()->getByArtist($artist->id); 827 [$maxCount, $album_ids] = self::_slice($album_ids, $start, $count); 828 foreach ($album_ids as $album_id) { 829 $album = new Album($album_id); 830 $album->format(); 831 $mediaItems[] = self::_itemAlbum($album, $parent); 832 } 833 } 834 break; 835 } 836 break; 837 case 'albums': 838 switch (count($pathreq)) { 839 case 1: // Get albums list 840 $album_ids = Catalog::get_albums($count, $start); 841 [$maxCount, $album_ids] = array($counts['album'], $album_ids); 842 foreach ($album_ids as $album_id) { 843 $album = new Album($album_id); 844 $album->format(); 845 $mediaItems[] = self::_itemAlbum($album, $parent); 846 } 847 break; 848 case 2: // Get album's songs list 849 $album = new Album($pathreq[1]); 850 if (isset($album->id)) { 851 $song_ids = static::getSongRepository()->getByAlbum($album->id); 852 [$maxCount, $song_ids] = self::_slice($song_ids, $start, $count); 853 foreach ($song_ids as $song_id) { 854 $song = new Song($song_id); 855 $song->format(); 856 $mediaItems[] = self::_itemSong($song, $parent); 857 } 858 } 859 break; 860 } 861 break; 862 case 'songs': 863 switch (count($pathreq)) { 864 case 1: // Get songs list 865 $catalogs = Catalog::get_catalogs(); 866 foreach ($catalogs as $catalog_id) { 867 $catalog = Catalog::create_from_id($catalog_id); 868 $songs = $catalog->get_songs(); 869 [$maxCount, $songs] = self::_slice($songs, $start, $count); 870 foreach ($songs as $song) { 871 $song->format(); 872 $mediaItems[] = self::_itemSong($song, $parent); 873 } 874 } 875 break; 876 } 877 break; 878 case 'playlists': 879 switch (count($pathreq)) { 880 case 1: // Get playlists list 881 $pl_ids = Playlist::get_playlists(); 882 [$maxCount, $pl_ids] = self::_slice($pl_ids, $start, $count); 883 foreach ($pl_ids as $pl_id) { 884 $playlist = new Playlist($pl_id); 885 $playlist->format(); 886 $mediaItems[] = self::_itemPlaylist($playlist, $parent); 887 } 888 break; 889 case 2: // Get playlist's songs list 890 $playlist = new Playlist($pathreq[1]); 891 if ($playlist->id) { 892 $items = $playlist->get_items(); 893 [$maxCount, $items] = self::_slice($items, $start, $count); 894 foreach ($items as $item) { 895 if ($item['object_type'] == 'song') { 896 $song = new Song($item['object_id']); 897 $song->format(); 898 $mediaItems[] = self::_itemSong($song, $parent); 899 } 900 } 901 } 902 break; 903 } 904 break; 905 case 'smartplaylists': 906 switch (count($pathreq)) { 907 case 1: // Get playlists list 908 $searches = Search::get_searches(); 909 [$maxCount, $searches] = self::_slice($searches, $start, $count); 910 foreach ($searches as $search) { 911 $playlist = new Search($search['id'], 'song'); 912 $mediaItems[] = self::_itemPlaylist($playlist, $parent); 913 } 914 break; 915 case 2: // Get playlist's songs list 916 $playlist = new Search($pathreq[1], 'song'); 917 if ($playlist->id) { 918 $items = $playlist->get_items(); 919 [$maxCount, $items] = self::_slice($items, $start, $count); 920 foreach ($items as $item) { 921 if ($item['object_type'] == 'song') { 922 $song = new Song($item['object_id']); 923 $song->format(); 924 $mediaItems[] = self::_itemSong($song, $parent); 925 } 926 } 927 } 928 break; 929 } 930 break; 931 case 'live_streams': 932 switch (count($pathreq)) { 933 case 1: // Get radios list 934 $radios = static::getLiveStreamRepository()->getAll(); 935 [$maxCount, $radios] = self::_slice($radios, $start, $count); 936 foreach ($radios as $radio_id) { 937 $radio = new Live_Stream($radio_id); 938 $radio->format(); 939 $mediaItems[] = self::_itemLiveStream($radio, $parent); 940 } 941 break; 942 } 943 break; 944 case 'podcasts': 945 switch (count($pathreq)) { 946 case 1: // Get podcasts list 947 $podcasts = Catalog::get_podcasts(); 948 [$maxCount, $podcasts] = self::_slice($podcasts, $start, $count); 949 foreach ($podcasts as $podcast) { 950 $podcast->format(); 951 $mediaItems[] = self::_itemPodcast($podcast, $parent); 952 } 953 break; 954 case 2: // Get podcast episodes list 955 $podcast = new Podcast($pathreq[1]); 956 if ($podcast->id) { 957 $episodes = $podcast->get_episodes(); 958 [$maxCount, $episodes] = self::_slice($episodes, $start, $count); 959 foreach ($episodes as $episode_id) { 960 $episode = new Podcast_Episode($episode_id); 961 $episode->format(); 962 $mediaItems[] = self::_itemPodcastEpisode($episode, $parent); 963 } 964 } 965 break; 966 } 967 break; 968 default: 969 $mediaItems[] = self::_musicMetadata('artists'); 970 $mediaItems[] = self::_musicMetadata('albums'); 971 $mediaItems[] = self::_musicMetadata('songs'); 972 $mediaItems[] = self::_musicMetadata('playlists'); 973 $mediaItems[] = self::_musicMetadata('smartplaylists'); 974 if (AmpConfig::get('live_stream')) { 975 $mediaItems[] = self::_musicMetadata('live_streams'); 976 } 977 if (AmpConfig::get('podcast')) { 978 $mediaItems[] = self::_musicMetadata('podcasts'); 979 } 980 [$maxCount, $mediaItems] = self::_slice($mediaItems, $start, $count); 981 break; 982 } 983 984 if ($maxCount == 0) { 985 $maxCount = count($mediaItems); 986 } 987 988 return array($maxCount, $mediaItems); 989 } 990 991 /** 992 * @param string $prmPath 993 * @return array|null 994 */ 995 public static function _videoMetadata($prmPath) 996 { 997 $root = 'amp://video'; 998 $pathreq = explode('/', $prmPath); 999 if ($pathreq[0] == '' && count($pathreq) > 0) { 1000 array_shift($pathreq); 1001 } 1002 1003 $meta = null; 1004 switch ($pathreq[0]) { 1005 case 'tvshows': 1006 switch (count($pathreq)) { 1007 case 1: 1008 $counts = count(Catalog::get_tvshows()); 1009 $meta = array( 1010 'id' => $root . '/tvshows', 1011 'parentID' => $root, 1012 'restricted' => '1', 1013 'childCount' => $counts, 1014 'dc:title' => T_('TV Shows'), 1015 'upnp:class' => 'object.container', 1016 ); 1017 break; 1018 case 2: 1019 $tvshow = new TvShow($pathreq[1]); 1020 if ($tvshow->id) { 1021 $tvshow->format(); 1022 $meta = self::_itemTVShow($tvshow, $root . '/tvshows'); 1023 } 1024 break; 1025 case 3: 1026 $season = new TVShow_Season($pathreq[2]); 1027 if ($season->id) { 1028 $season->format(); 1029 $meta = self::_itemTVShowSeason($season, $root . '/tvshows/' . $pathreq[1]); 1030 } 1031 break; 1032 case 4: 1033 $video = new TVShow_Episode($pathreq[3]); 1034 if ($video->id) { 1035 $video->format(); 1036 $meta = self::_itemVideo($video, $root . '/tvshows/' . $pathreq[1] . '/' . $pathreq[2]); 1037 } 1038 break; 1039 } 1040 break; 1041 case 'clips': 1042 switch (count($pathreq)) { 1043 case 1: 1044 $counts = Catalog::get_videos_count(null, 'clip'); 1045 $meta = array( 1046 'id' => $root . '/clips', 1047 'parentID' => $root, 1048 'restricted' => '1', 1049 'childCount' => $counts, 1050 'dc:title' => T_('Clips'), 1051 'upnp:class' => 'object.container', 1052 ); 1053 break; 1054 case 2: 1055 $video = new Clip($pathreq[1]); 1056 if ($video->id) { 1057 $video->format(); 1058 $meta = self::_itemVideo($video, $root . '/clips'); 1059 } 1060 break; 1061 } 1062 break; 1063 case 'movies': 1064 switch (count($pathreq)) { 1065 case 1: 1066 $counts = Catalog::get_videos_count(null, 'movie'); 1067 $meta = array( 1068 'id' => $root . '/movies', 1069 'parentID' => $root, 1070 'restricted' => '1', 1071 'childCount' => $counts, 1072 'dc:title' => T_('Movies'), 1073 'upnp:class' => 'object.container', 1074 ); 1075 break; 1076 case 2: 1077 $video = new Movie($pathreq[1]); 1078 if ($video->id) { 1079 $video->format(); 1080 $meta = self::_itemVideo($video, $root . '/movies'); 1081 } 1082 break; 1083 } 1084 break; 1085 case 'personal_videos': 1086 switch (count($pathreq)) { 1087 case 1: 1088 $counts = Catalog::get_videos_count(null, 'personal_video'); 1089 $meta = array( 1090 'id' => $root . '/personal_videos', 1091 'parentID' => $root, 1092 'restricted' => '1', 1093 'childCount' => $counts, 1094 'dc:title' => T_('Personal Videos'), 1095 'upnp:class' => 'object.container', 1096 ); 1097 break; 1098 case 2: 1099 $video = new Personal_Video($pathreq[1]); 1100 if ($video->id) { 1101 $video->format(); 1102 $meta = self::_itemVideo($video, $root . '/personal_videos'); 1103 } 1104 break; 1105 } 1106 break; 1107 default: 1108 $meta = array( 1109 'id' => $root, 1110 'parentID' => '0', 1111 'restricted' => '1', 1112 'searchable' => '1', 1113 'childCount' => '4', 1114 'dc:title' => T_('Video'), 1115 'upnp:class' => 'object.container', // .storageFolder', 1116 'upnp:storageUsed' => '-1', 1117 ); 1118 break; 1119 } 1120 1121 return $meta; 1122 } 1123 1124 /** 1125 * @param $prmPath 1126 * @param $prmQuery 1127 * @param $start 1128 * @param $count 1129 * @return array 1130 */ 1131 public static function _videoChilds($prmPath, $prmQuery, $start, $count) 1132 { 1133 $mediaItems = array(); 1134 $maxCount = 0; 1135 $queryData = array(); 1136 parse_str($prmQuery, $queryData); 1137 1138 $parent = 'amp://video' . $prmPath; 1139 $pathreq = explode('/', $prmPath); 1140 if ($pathreq[0] == '' && count($pathreq) > 0) { 1141 array_shift($pathreq); 1142 } 1143 1144 switch ($pathreq[0]) { 1145 case 'tvshows': 1146 switch (count($pathreq)) { 1147 case 1: // Get tvshow list 1148 $tvshows = Catalog::get_tvshows(); 1149 [$maxCount, $tvshows] = self::_slice($tvshows, $start, $count); 1150 foreach ($tvshows as $tvshow) { 1151 $tvshow->format(); 1152 $mediaItems[] = self::_itemTVShow($tvshow, $parent); 1153 } 1154 break; 1155 case 2: // Get season list 1156 $tvshow = new TvShow($pathreq[1]); 1157 if ($tvshow->id) { 1158 $season_ids = $tvshow->get_seasons(); 1159 [$maxCount, $season_ids] = self::_slice($season_ids, $start, $count); 1160 foreach ($season_ids as $season_id) { 1161 $season = new TVShow_Season($season_id); 1162 $season->format(); 1163 $mediaItems[] = self::_itemTVShowSeason($season, $parent); 1164 } 1165 } 1166 break; 1167 case 3: // Get episode list 1168 $season = new TVShow_Season($pathreq[2]); 1169 if ($season->id) { 1170 $episode_ids = $season->get_episodes(); 1171 [$maxCount, $episode_ids] = self::_slice($episode_ids, $start, $count); 1172 foreach ($episode_ids as $episode_id) { 1173 $video = new Video($episode_id); 1174 $video->format(); 1175 $mediaItems[] = self::_itemVideo($video, $parent); 1176 } 1177 } 1178 break; 1179 } 1180 break; 1181 case 'clips': 1182 switch (count($pathreq)) { 1183 case 1: // Get clips list 1184 $videos = Catalog::get_videos(null, 'clip'); 1185 [$maxCount, $videos] = self::_slice($videos, $start, $count); 1186 foreach ($videos as $video) { 1187 $video->format(); 1188 $mediaItems[] = self::_itemVideo($video, $parent); 1189 } 1190 break; 1191 } 1192 break; 1193 case 'movies': 1194 switch (count($pathreq)) { 1195 case 1: // Get clips list 1196 $videos = Catalog::get_videos(null, 'movie'); 1197 [$maxCount, $videos] = self::_slice($videos, $start, $count); 1198 foreach ($videos as $video) { 1199 $video->format(); 1200 $mediaItems[] = self::_itemVideo($video, $parent); 1201 } 1202 break; 1203 } 1204 break; 1205 case 'personal_videos': 1206 switch (count($pathreq)) { 1207 case 1: // Get clips list 1208 $videos = Catalog::get_videos(null, 'personal_video'); 1209 [$maxCount, $videos] = self::_slice($videos, $start, $count); 1210 foreach ($videos as $video) { 1211 $video->format(); 1212 $mediaItems[] = self::_itemVideo($video, $parent); 1213 } 1214 break; 1215 } 1216 break; 1217 default: 1218 $mediaItems[] = self::_videoMetadata('clips'); 1219 $mediaItems[] = self::_videoMetadata('tvshows'); 1220 $mediaItems[] = self::_videoMetadata('movies'); 1221 $mediaItems[] = self::_videoMetadata('personal_videos'); 1222 break; 1223 } 1224 1225 if ($maxCount == 0) { 1226 $maxCount = count($mediaItems); 1227 } 1228 1229 return array($maxCount, $mediaItems); 1230 } 1231 1232 /** 1233 * @param string $str 1234 * @return array 1235 */ 1236 private static function gettokens($str) 1237 { 1238 $tokens = array(); 1239 $nospacetokens = array(); 1240 // put the string into lowercase 1241 // $str = strtolower($str); 1242 1243 // make sure ( or ) get picked up as separate tokens 1244 $str = str_replace("(", " ( ", $str); 1245 $str = str_replace(")", " ) ", $str); 1246 1247 // get the actual tokens 1248 $actualtokens = explode(" ", $str); 1249 $actualsize = sizeof($actualtokens); 1250 1251 // trim spaces around tokens and discard those which have only spaces in them 1252 $index = 0; 1253 for ($i=0; $i < $actualsize; $i++) { 1254 $actualtokens[$i]=trim($actualtokens[$i]); 1255 if ($actualtokens[$i] != "") { 1256 $nospacetokens[$index++] = $actualtokens[$i]; 1257 } 1258 } 1259 1260 // now put together tokens which are actually one token e.g. upper hutt 1261 $onetoken = ""; 1262 $index = 0; 1263 $nospacesize = sizeof($nospacetokens); 1264 for ($i=0; $i < $nospacesize; $i++) { 1265 $token = $nospacetokens[$i]; 1266 switch ($token) { 1267 case "not": 1268 case "or": 1269 case "and": 1270 case "(": 1271 case ")": 1272 if ($onetoken != "") { 1273 $tokens[$index++] = $onetoken; 1274 $onetoken = ""; 1275 } 1276 $tokens[$index++] = $token; 1277 break; 1278 default: 1279 if ($onetoken == "") { 1280 $onetoken = $token; 1281 } else { 1282 $onetoken = $onetoken . " " . $token; 1283 } 1284 break; 1285 } 1286 } 1287 if ($onetoken != "") { 1288 $tokens[$index++] = $onetoken; 1289 } 1290 1291 return $tokens; 1292 } 1293 1294 /** 1295 * @param string $query 1296 * @param string $context 1297 * @return array 1298 */ 1299 private static function parse_upnp_search_term($query, $context) 1300 { 1301 //echo "Search term ", $query, "\n"; 1302 $tok = str_getcsv($query, ' '); 1303 //for ($i=0; $i<sizeof($tok); $i++) { 1304 // echo $i, $tok[$i]; 1305 // echo "\n"; 1306 //} 1307 debug_event(self::class, 'Token ' . var_export($tok, true), 5); 1308 1309 $term = array(); 1310 if (sizeof($tok) == 3) { 1311 // tuple, we understand 1312 switch ($tok[0]) { 1313 case 'dc:title': 1314 $term['ruletype'] = 'title'; 1315 break; 1316 case 'upnp:album': 1317 $term['ruletype'] = 'album'; 1318 break; 1319 case 'upnp:genre': 1320 $term['ruletype'] = 'tag'; 1321 break; 1322 case 'upnp:artist': // Artist is not implemented unformly through the database 1323 // If we're about to search the album table, we need to look 1324 // for album_artist instead of artist 1325 if ($context == 'album') { 1326 $term['ruletype'] = 'album_artist'; 1327 } else { 1328 $term['ruletype'] = 'artist'; 1329 } 1330 break; 1331 case 'upnp:author': 1332 $term['ruletype'] = 'author'; 1333 break; 1334 case 'upnp:author@role': 1335 $term['ruletype'] = $tok[2]; 1336 1337 return array(); 1338 default: 1339 return array(); 1340 } 1341 switch ($tok[1]) { 1342 case '=': 1343 $term['operator'] = 4; 1344 break; 1345 case 'contains': 1346 default: 1347 $term['operator'] = 0; 1348 break; 1349 } 1350 $term['input'] = $tok[2]; 1351 } 1352 1353 return $term; 1354 } 1355 1356 /** 1357 * Cannot be very precious about this as filtering capability ATM just relates to the kind of search we end up doing 1358 * @param $filter 1359 * @return string 1360 */ 1361 private static function parse_upnp_filter($filter) 1362 { 1363 // TODO patched out for now: creates problems in search results 1364 unset($filter); 1365 // NB filtering is handled in creation of the DIDL now 1366 //if( strpos( $filter, 'upnp:album' ) ){ 1367 // return 'album'; 1368 //} 1369 return 'song'; 1370 } 1371 1372 /** 1373 * @param $query 1374 * @param $type 1375 * @return array 1376 */ 1377 private static function parse_upnp_searchcriteria($query, $type) 1378 { 1379 // Transforms a upnp search query into an Ampache search query 1380 $upnp_translations = array( 1381 array('upnp:class = "object.container.album.musicAlbum"', 'album'), 1382 array('upnp:class derivedfrom "object.item.audioItem"', 'song'), 1383 array('upnp:class = "object.container.person.musicArtist"', 'artist'), 1384 array('upnp:class = "object.container.playlistContainer"', 'playlist'), 1385 array('upnp:class derivedfrom "object.container.playlistContainer"', 'playlist'), 1386 array('upnp:class = "object.container.genre.musicGenre"', 'tag'), 1387 array('@refID exists false', '') 1388 ); 1389 1390 $tokens = self::gettokens($query); 1391 $size = sizeof($tokens); 1392 // for ($i=0; $i<sizeof($tokens); $i++) { 1393 // echo $tokens[$i]."|"; 1394 // } 1395 // echo "\n"; 1396 1397 // Go through all the tokens and transform anything we recognize 1398 //If any translation goes to NUL then must remove previous token provided it is AND or OR 1399 for ($i=0; $i < $size; $i++) { 1400 for ($j=0; $j < 7; $j++) { 1401 if ($tokens[$i] == $upnp_translations[$j][0]) { 1402 $tokens[$i] = $upnp_translations[$j][1]; 1403 if ($upnp_translations[$j][1] == '' && $i > 1 && ($tokens[$i - 1] == "and" || $tokens[$i - 1] == "or")) { 1404 $tokens[$i - 1] = ''; 1405 } 1406 } 1407 } 1408 } 1409 //for ($i=0; $i<sizeof($tokens); $i++) { 1410 // echo $tokens[$i]."|"; 1411 //} 1412 // Start to construct the Ampache Search data array 1413 $data = array(); 1414 1415 // In some cases the first search term gives the type of search 1416 // Other types of device may specify the type of search implicitly by the type of filter 1417 // they supply after the search term. 1418 // Start with assuming a search type of "song" in the case where the first search term 1419 // is actually a term rather than a type 1420 if (str_word_count($tokens[0]) > 1) { 1421 // first token is not a type, need to work out one 1422 if ($type == '') { 1423 $data['type'] = 'song'; 1424 } else { 1425 $data['type'] = $type; 1426 } 1427 } else { 1428 $data['type'] = $tokens[0]; 1429 $tokens[0] = ''; 1430 } 1431 1432 // Construct the operator type. The first one is likely to be 'and' (if present), 1433 // and the remainder should be 'and' or 'or' 1434 // upnp allows all search terms to be and/or in any order. 1435 // Ampache's current search class can only handle terms being all of one type 1436 1437 $num_and = 0; 1438 $num_or = 0; 1439 $size = sizeof($tokens); 1440 for ($i=0; $i < $size; $i++) { 1441 if ($tokens[$i] == 'and') { 1442 $num_and++; 1443 $tokens[$i] = ''; 1444 } elseif ($tokens[$i] == 'or') { 1445 $num_or++; 1446 $tokens[$i] = ''; 1447 } elseif ($tokens[$i] == '(' || $tokens[$i] == ')') { 1448 $tokens[$i] = ''; 1449 } 1450 } 1451 // echo "\nNUM_AND ", $num_and; 1452 // echo "\nNUM_OR ", $num_or; 1453 // echo "\n"; 1454 1455 if ($num_and == 0 && $num_or == 0) { 1456 $data['operator'] = 'and'; 1457 } elseif ($num_and <= 1 && $num_or > 0) { 1458 $data['operator'] = 'or'; 1459 } elseif ($num_and > 0 && $num_or == 0) { 1460 $data['operator'] = 'and'; 1461 } else { 1462 $data['operator'] = 'error'; // Should really be an error operator/return 1463 1464 return $data; // go no further because we can't handle the combination of and and or 1465 } 1466 1467 $rule_num = 1; 1468 $size = sizeof($tokens); 1469 for ($i=0; $i < $size; $i++) { 1470 if ($tokens[$i] != '') { 1471 $rule = 'rule_' . (string) $rule_num; 1472 $term = self::parse_upnp_search_term($tokens[$i], $data['type']); 1473 if (!empty($term)) { 1474 $data[$rule] = $term['ruletype']; 1475 $data[$rule . '_operator'] = $term['operator']; 1476 $data[$rule . '_input'] = $term['input']; 1477 $rule_num++; 1478 } 1479 } 1480 } 1481 if ($rule_num == 1) { 1482 // Must be a wildcard search: no tuples detected. How to tell search class to search for something? 1483 // Insert search qualified on "ID > 0", which should call for everything 1484 $rule = 'rule_1'; 1485 $data[$rule] = 'id'; 1486 $data[$rule . '_operator'] = 'GT'; 1487 $data[$rule . '_input'] = '0'; 1488 } 1489 1490 return $data; 1491 } 1492 1493 /** 1494 * @param $criteria 1495 * @param $filter 1496 * @param $start 1497 * @param $count 1498 * @return array 1499 */ 1500 public static function _callSearch($criteria, $filter, $start, $count) 1501 { 1502 $mediaItems = array(); 1503 $maxCount = 0; 1504 $type = self::parse_upnp_filter($filter); 1505 $search_terms = self::parse_upnp_searchcriteria($criteria, $type); 1506 debug_event(self::class, 'Dumping $search_terms: ' . var_export($search_terms, true), 5); 1507 $ids = Search::run($search_terms); // return a list of IDs 1508 if (count($ids) == 0) { 1509 debug_event(self::class, 'Search returned no hits', 5); 1510 1511 return array(0, $mediaItems); 1512 } 1513 //debug_event(self::class, 'Dumping $search results: '.var_export( $ids, true ), 5); 1514 debug_event(self::class, ' ' . (string) count($ids) . ' ids looking for type ' . $search_terms['type'], 5); 1515 1516 switch ($search_terms['type']) { 1517 case 'artist': 1518 [$maxCount, $ids] = self::_slice($ids, $start, $count); 1519 foreach ($ids as $artist_id) { 1520 $artist = new Artist($artist_id); 1521 $artist->format(); 1522 $mediaItems[] = self::_itemArtist($artist, "amp://music/artists"); 1523 } 1524 break; 1525 case 'song': 1526 [$maxCount, $ids] = self::_slice($ids, $start, $count); 1527 foreach ($ids as $song_id) { 1528 $song = new Song($song_id); 1529 $song->format(); 1530 $mediaItems[] = self::_itemSong($song, $parent = 'amp://music/albums/' . (string) $song->album); 1531 } 1532 break; 1533 case 'album': 1534 [$maxCount, $ids] = self::_slice($ids, $start, $count); 1535 foreach ($ids as $album_id) { 1536 $album = new Album($album_id); 1537 $album->format(); 1538 //debug_event(self::class, $album->f_title, 5); 1539 $mediaItems[] = self::_itemAlbum($album, "amp://music/albums"); 1540 } 1541 break; 1542 case 'playlist': 1543 [$maxCount, $ids] = self::_slice($ids, $start, $count); 1544 foreach ($ids as $pl_id) { 1545 $playlist = new Playlist($pl_id); 1546 $playlist->format(); 1547 $mediaItems[] = self::_itemPlaylist($playlist, "amp://music/playlists"); 1548 } 1549 break; 1550 case 'tag': 1551 [$maxCount, $ids] = self::_slice($ids, $start, $count); 1552 foreach ($ids as $tag_id) { 1553 $tag = new Tag($tag_id); 1554 $tag->format(); 1555 $mediaItems[] = self::_itemTag($tag, "amp://music/tags"); 1556 } 1557 break; 1558 } 1559 if ($maxCount == 0) { 1560 $maxCount = count($mediaItems); 1561 } 1562 1563 return array($maxCount, $mediaItems); 1564 } 1565 1566 /** 1567 * @param $title 1568 * @return string|string[]|null 1569 */ 1570 private static function _replaceSpecialSymbols($title) 1571 { 1572 /* 1573 * replace non letter or digits 1574 * 17 Oct. patched this out because it's changing the titles of tracks so that 1575 * when the device comes to play and searches for songs belonging to the album, the 1576 * album is no longer found as a match 1577 */ 1578 //debug_event(self::class, 'replace <<< ' . $title, 5); 1579 //$title = preg_replace('~[^\\pL\d\.:\s\(\)\.\,\'\"]+~u', '-', $title); 1580 //debug_event(self::class, 'replace >>> ' . $title, 5); 1581 if ($title == "") { 1582 $title = '(no title)'; 1583 } 1584 1585 return $title; 1586 } 1587 1588 /** 1589 * @param Artist $artist 1590 * @param string $parent 1591 * @return array 1592 */ 1593 private static function _itemArtist($artist, $parent) 1594 { 1595 return array( 1596 'id' => 'amp://music/artists/' . $artist->id, 1597 'parentID' => $parent, 1598 'restricted' => 'false', 1599 'childCount' => $artist->albums, 1600 'dc:title' => self::_replaceSpecialSymbols($artist->f_name), 1601 //'upnp:class' => 'object.container.person.musicArtist', 1602 'upnp:class' => 'object.container', 1603 ); 1604 } 1605 1606 /** 1607 * @param Tag $tag 1608 * @param string $parent 1609 * @return array 1610 */ 1611 private static function _itemTag($tag, $parent) 1612 { 1613 return array( 1614 'id' => 'amp://music/tags/' . $tag->id, 1615 'parentID' => $parent, 1616 'restricted' => 'false', 1617 'childCount' => 1, 1618 'dc:title' => self::_replaceSpecialSymbols($tag->f_name), 1619 //'upnp:class' => 'object.container.person.musicArtist', 1620 'upnp:class' => 'object.container', 1621 ); 1622 } 1623 1624 /** 1625 * @param Album $album 1626 * @param string $parent 1627 * @return array 1628 */ 1629 private static function _itemAlbum($album, $parent) 1630 { 1631 $api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false; 1632 $art_url = Art::url($album->id, 'album', $api_session); 1633 1634 return array( 1635 'id' => 'amp://music/albums/' . $album->id, 1636 'parentID' => $parent, 1637 'restricted' => 'false', 1638 'childCount' => $album->song_count, 1639 'dc:title' => self::_replaceSpecialSymbols($album->f_title), 1640 'upnp:class' => 'object.container.album.musicAlbum', // object.container.album.musicAlbum 1641 //'upnp:class' => 'object.container', 1642 'upnp:albumArtist' => $album->album_artist, 1643 'upnp:albumArtURI' => $art_url, 1644 ); 1645 } 1646 1647 /** 1648 * @param $playlist 1649 * @param string $parent 1650 * @return array 1651 */ 1652 private static function _itemPlaylist($playlist, $parent) 1653 { 1654 return array( 1655 'id' => 'amp://music/playlists/' . $playlist->id, 1656 'parentID' => $parent, 1657 'restricted' => 'false', 1658 'childCount' => count($playlist->get_items()), 1659 'dc:title' => self::_replaceSpecialSymbols($playlist->name), 1660 'upnp:class' => 'object.container', // object.container.playlistContainer 1661 ); 1662 } 1663 1664 /** 1665 * @param Search $playlist 1666 * @param string $parent 1667 * @return array 1668 */ 1669 private static function _itemSmartPlaylist($playlist, $parent) 1670 { 1671 return array( 1672 'id' => 'amp://music/smartplaylists/' . $playlist->id, 1673 'parentID' => $parent, 1674 'restricted' => 'false', 1675 'childCount' => count($playlist->get_items()), 1676 'dc:title' => self::_replaceSpecialSymbols($playlist->name), 1677 'upnp:class' => 'object.container', 1678 ); 1679 } 1680 1681 /** 1682 * @param Song $song 1683 * @param string $parent 1684 * @return array 1685 */ 1686 public static function _itemSong($song, $parent) 1687 { 1688 $api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false; 1689 $art_url = Art::url($song->album, 'album', $api_session); 1690 1691 $fileTypesByExt = self::_getFileTypes(); 1692 $arrFileType = $fileTypesByExt[$song->type]; 1693 /** 1694 * Properties observed for MS media player include 1695 * GetSearchCapabilities 1696 * @id, @refID, 1697 * dc:title, dc:creator, dc:publisher, dc:language, dc:date, dc:description, 1698 * upnp:class, upnp:genre, upnp:artist, upnp:author, upnp:author@role, upnp:album, 1699 * upnp:originalTrackNumber, upnp:producer, upnp:rating,upnp:actor, upnp:director, upnp:toc, 1700 * upnp:userAnnotation, upnp:channelName, upnp:longDescription, upnp:programTitle 1701 * res@size, res@duration, res@protocolInfo, res@protection, 1702 * microsoft:userRatingInStars, microsoft:userEffectiveRatingInStars, microsoft:userRating, microsoft:userEffectiveRating, microsoft:serviceProvider, 1703 * microsoft:artistAlbumArtist, microsoft:artistPerformer, microsoft:artistConductor, microsoft:authorComposer, microsoft:authorOriginalLyricist, 1704 * microsoft:authorWriter 1705 */ 1706 return array( 1707 'id' => 'amp://music/songs/' . $song->id, 1708 'parentID' => $parent, 1709 'restricted' => 'false', // XXX 1710 'dc:title' => self::_replaceSpecialSymbols($song->f_title), 1711 'dc:date' => date("c", (int) $song->addition_time), 1712 'dc:creator' => self::_replaceSpecialSymbols($song->f_artist), 1713 'upnp:class' => (isset($arrFileType['class'])) ? $arrFileType['class'] : 'object.item.unknownItem', 1714 'upnp:albumArtURI' => $art_url, 1715 'upnp:artist' => self::_replaceSpecialSymbols($song->f_artist), 1716 'upnp:album' => self::_replaceSpecialSymbols($song->f_album), 1717 'upnp:genre' => Tag::get_display($song->tags, false, 'song'), 1718 'upnp:originalTrackNumber' => $song->track, 1719 'res' => $song->play_url('', 'api', true), // For upnp, use local 1720 'protocolInfo' => $arrFileType['mime'], 1721 'size' => $song->size, 1722 'duration' => $song->f_time_h . '.0', 1723 'bitrate' => $song->bitrate, 1724 'sampleFrequency' => $song->rate, 1725 'nrAudioChannels' => '2', // Just say its stereo as we don't have the real info 1726 'description' => self::_replaceSpecialSymbols($song->comment), 1727 ); 1728 } 1729 1730 /** 1731 * @param Live_Stream $radio 1732 * @param string $parent 1733 * @return array 1734 */ 1735 public static function _itemLiveStream($radio, $parent) 1736 { 1737 $api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false; 1738 $art_url = Art::url($radio->id, 'live_stream', $api_session); 1739 1740 $fileTypesByExt = self::_getFileTypes(); 1741 $arrFileType = $fileTypesByExt[$radio->codec]; 1742 1743 return array( 1744 'id' => 'amp://music/live_streams/' . $radio->id, 1745 'parentID' => $parent, 1746 'restricted' => 'false', 1747 'dc:title' => self::_replaceSpecialSymbols($radio->name), 1748 'upnp:class' => (isset($arrFileType['class'])) ? $arrFileType['class'] : 'object.item.unknownItem', 1749 'upnp:albumArtURI' => $art_url, 1750 1751 'res' => $radio->url, 1752 'protocolInfo' => $arrFileType['mime'] 1753 ); 1754 } 1755 1756 /** 1757 * @param $tvshow 1758 * @param string $parent 1759 * @return array 1760 */ 1761 private static function _itemTVShow($tvshow, $parent) 1762 { 1763 return array( 1764 'id' => 'amp://video/tvshows/' . $tvshow->id, 1765 'parentID' => $parent, 1766 'restricted' => '1', 1767 'childCount' => count($tvshow->get_seasons()), 1768 'dc:title' => self::_replaceSpecialSymbols($tvshow->f_name), 1769 'upnp:class' => 'object.container', 1770 ); 1771 } 1772 1773 /** 1774 * @param TVShow_Season $season 1775 * @param string $parent 1776 * @return array 1777 */ 1778 private static function _itemTVShowSeason($season, $parent) 1779 { 1780 return array( 1781 'id' => 'amp://video/tvshows/' . $season->tvshow . '/' . $season->id, 1782 'parentID' => $parent, 1783 'restricted' => '1', 1784 'childCount' => count($season->get_episodes()), 1785 'dc:title' => self::_replaceSpecialSymbols($season->f_name), 1786 'upnp:class' => 'object.container', 1787 ); 1788 } 1789 1790 /** 1791 * @param $video 1792 * @param string $parent 1793 * @return array 1794 */ 1795 private static function _itemVideo($video, $parent) 1796 { 1797 $api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false; 1798 $art_url = Art::url($video->id, 'video', $api_session); 1799 1800 $fileTypesByExt = self::_getFileTypes(); 1801 $arrFileType = $fileTypesByExt[$video->type]; 1802 1803 return array( 1804 'id' => $parent . '/' . $video->id, 1805 'parentID' => $parent, 1806 'restricted' => '1', 1807 'dc:title' => self::_replaceSpecialSymbols($video->f_title), 1808 'upnp:class' => (isset($arrFileType['class'])) ? $arrFileType['class'] : 'object.item.unknownItem', 1809 'upnp:albumArtURI' => $art_url, 1810 'upnp:genre' => Tag::get_display($video->tags, false, 'video'), 1811 1812 'res' => $video->play_url('', 'api'), 1813 'protocolInfo' => $arrFileType['mime'], 1814 'size' => $video->size, 1815 'duration' => $video->f_time_h . '.0', 1816 ); 1817 } 1818 1819 /** 1820 * @param $podcast 1821 * @param string $parent 1822 * @return array 1823 */ 1824 private static function _itemPodcast($podcast, $parent) 1825 { 1826 return array( 1827 'id' => 'amp://music/podcasts/' . $podcast->id, 1828 'parentID' => $parent, 1829 'restricted' => '1', 1830 'childCount' => count($podcast->get_episodes()), 1831 'dc:title' => self::_replaceSpecialSymbols($podcast->f_title), 1832 'upnp:class' => 'object.container', 1833 ); 1834 } 1835 1836 /** 1837 * @param Podcast_Episode $episode 1838 * @param string $parent 1839 * @return array 1840 */ 1841 private static function _itemPodcastEpisode($episode, $parent) 1842 { 1843 $api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false; 1844 $art_url = Art::url($episode->podcast, 'podcast', $api_session); 1845 1846 $fileTypesByExt = self::_getFileTypes(); 1847 $arrFileType = (!empty($episode->type)) ? $fileTypesByExt[$episode->type] : array(); 1848 1849 $ret = array( 1850 'id' => 'amp://music/podcasts/' . $episode->podcast . '/' . $episode->id, 1851 'parentID' => $parent, 1852 'restricted' => '1', 1853 'dc:title' => self::_replaceSpecialSymbols($episode->f_title), 1854 'upnp:album' => self::_replaceSpecialSymbols($episode->f_podcast), 1855 'upnp:class' => (isset($arrFileType['class'])) ? $arrFileType['class'] : 'object.item.unknownItem', 1856 'upnp:albumArtURI' => $art_url 1857 ); 1858 if (isset($arrFileType['mime'])) { 1859 $ret['res'] = $episode->play_url('', 'api'); 1860 $ret['protocolInfo'] = $arrFileType['mime']; 1861 $ret['size'] = $episode->size; 1862 $ret['duration'] = $episode->f_time_h . '.0'; 1863 } 1864 1865 return $ret; 1866 } 1867 1868 /** 1869 * @return array 1870 */ 1871 private static function _getFileTypes() 1872 { 1873 return array( 1874 'wav' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-wav:*',), 1875 'mpa' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/mpeg:*',), 1876 '.mp1' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/mpeg:*',), 1877 'mp3' => array('class' => 'object.item.audioItem.musicTrack', 'mime' => 'http-get:*:audio/mpeg:*',), 1878 'aiff' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-aiff:*',), 1879 'aif' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-aiff:*',), 1880 'wma' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-ms-wma:*',), 1881 'lpcm' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/lpcm:*',), 1882 'aac' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-aac:*',), 1883 'm4a' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-m4a:*',), 1884 'ac3' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-ac3:*',), 1885 'pcm' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/lpcm:*',), 1886 'flac' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/flac:*',), 1887 'ogg' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:application/ogg:*',), 1888 'mka' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-matroska:*',), 1889 'mp4a' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/x-m4a:*',), 1890 'mp2' => array('class' => 'object.item.audioItem', 'mime' => 'http-get:*:audio/mpeg:*',), 1891 'gif' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/gif:*',), 1892 'jpg' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/jpeg:*',), 1893 'jpe' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/jpeg:*',), 1894 'png' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/png:*',), 1895 'tiff' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/tiff:*',), 1896 'tif' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/tiff:*',), 1897 'jpeg' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/jpeg:*',), 1898 'bmp' => array('class' => 'object.item.imageItem', 'mime' => 'http-get:*:image/bmp:*',), 1899 'asf' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-ms-asf:*',), 1900 'wmv' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-ms-wmv:*',), 1901 'mpeg2' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2:*',), 1902 'avi' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-msvideo:*',), 1903 'divx' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-msvideo:*',), 1904 'mpg' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1905 'm1v' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1906 'm2v' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1907 'mp4' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mp4:*',), 1908 'mov' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/quicktime:*',), 1909 'vob' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/dvd:*',), 1910 'dvr-ms' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-ms-dvr:*',), 1911 'dat' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1912 'mpeg' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1913 'm1s' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg:*',), 1914 'm2p' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2:*',), 1915 'm2t' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1916 'm2ts' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1917 'mts' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1918 'ts' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1919 'tp' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1920 'trp' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1921 'm4t' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2ts:*',), 1922 'm4v' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/MP4V-ES:*',), 1923 'vbs' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2:*',), 1924 'mod' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mpeg2:*',), 1925 'mkv' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/x-matroska:*',), 1926 '3g2' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mp4:*',), 1927 '3gp' => array('class' => 'object.item.videoItem', 'mime' => 'http-get:*:video/mp4:*',), 1928 ); 1929 } 1930 1931 /** 1932 * @deprecated 1933 */ 1934 private static function getSongRepository(): SongRepositoryInterface 1935 { 1936 global $dic; 1937 1938 return $dic->get(SongRepositoryInterface::class); 1939 } 1940 1941 /** 1942 * @deprecated 1943 */ 1944 private static function getAlbumRepository(): AlbumRepositoryInterface 1945 { 1946 global $dic; 1947 1948 return $dic->get(AlbumRepositoryInterface::class); 1949 } 1950 1951 /** 1952 * @deprecated Inject by constructor 1953 */ 1954 private static function getLiveStreamRepository(): LiveStreamRepositoryInterface 1955 { 1956 global $dic; 1957 1958 return $dic->get(LiveStreamRepositoryInterface::class); 1959 } 1960} 1961