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("&", "&amp;", $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