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
23/* vim:set softtabstop=4 shiftwidth=4 expandtab: */
24
25namespace Ampache\Module\Playback\Localplay\Xbmc;
26
27use Ampache\Config\AmpConfig;
28use Ampache\Module\Playback\Localplay\localplay_controller;
29use Ampache\Repository\Model\Preference;
30use Ampache\Repository\Model\Song;
31use Ampache\Module\Playback\Stream_Url;
32use Ampache\Module\System\Core;
33use Ampache\Module\System\Dba;
34use PDOStatement;
35use XBMC_RPC_ConnectionException;
36use XBMC_RPC_Exception;
37use XBMC_RPC_HTTPClient;
38
39/**
40 * This is the class for the XBMC Localplay method to remote control
41 * a XBMC Instance
42 */
43class AmpacheXbmc extends localplay_controller
44{
45    /* Variables */
46    private $version     = '000001';
47    private $description = 'Controls a XBMC instance';
48
49    /* Constructed variables */
50    private $_xbmc;
51    // Always use player 0 for now
52    private $_playerId = 0;
53    // Always use playlist 0 for now
54    private $_playlistId = 0;
55
56    /**
57     * get_description
58     * This returns the description of this Localplay method
59     */
60    public function get_description()
61    {
62        return $this->description;
63    } // get_description
64
65    /**
66     * get_version
67     * This returns the current version
68     */
69    public function get_version()
70    {
71        return $this->version;
72    } // get_version
73
74    /**
75     * is_installed
76     * This returns true or false if xbmc controller is installed
77     */
78    public function is_installed()
79    {
80        $sql        = "SHOW TABLES LIKE 'localplay_xbmc'";
81        $db_results = Dba::query($sql);
82
83        return (Dba::num_rows($db_results) > 0);
84    } // is_installed
85
86    /**
87     * install
88     * This function installs the XBMC Localplay controller
89     */
90    public function install()
91    {
92        $collation = (AmpConfig::get('database_collation', 'utf8mb4_unicode_ci'));
93        $charset   = (AmpConfig::get('database_charset', 'utf8mb4'));
94        $engine    = ($charset == 'utf8mb4') ? 'InnoDB' : 'MYISAM';
95
96        $sql = "CREATE TABLE `localplay_xbmc` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(128) COLLATE $collation NOT NULL, `owner` INT(11) NOT NULL, `host` VARCHAR(255) COLLATE $collation NOT NULL, `port` INT(11) UNSIGNED NOT NULL, `user` VARCHAR(255) COLLATE $collation NOT NULL, `pass` VARCHAR(255) COLLATE $collation NOT NULL) ENGINE = $engine DEFAULT CHARSET=$charset COLLATE=$collation";
97        Dba::query($sql);
98
99        // Add an internal preference for the users current active instance
100        Preference::insert('xbmc_active', T_('XBMC Active Instance'), 0, 25, 'integer', 'internal', 'xbmc');
101
102        return true;
103    } // install
104
105    /**
106     * uninstall
107     * This removes the Localplay controller
108     */
109    public function uninstall()
110    {
111        $sql = "DROP TABLE `localplay_xbmc`";
112        Dba::query($sql);
113
114        // Remove the pref we added for this
115        Preference::delete('xbmc_active');
116
117        return true;
118    } // uninstall
119
120    /**
121     * add_instance
122     * This takes key'd data and inserts a new xbmc instance
123     * @param array $data
124     * @return PDOStatement|boolean
125     */
126    public function add_instance($data)
127    {
128        $sql = "INSERT INTO `localplay_xbmc` (`name`, `host`, `port`, `user`, `pass`, `owner`) VALUES (?, ?, ?, ?, ?, ?)";
129
130        return Dba::query($sql, array(
131            $data['name'],
132            $data['host'],
133            $data['port'],
134            $data['user'],
135            $data['pass'],
136            Core::get_global('user')->id
137        ));
138    } // add_instance
139
140    /**
141     * delete_instance
142     * This takes a UID and deletes the instance in question
143     * @param $uid
144     * @return boolean
145     */
146    public function delete_instance($uid)
147    {
148        $sql = "DELETE FROM `localplay_xbmc` WHERE `id` = ?";
149        Dba::query($sql, array($uid));
150
151        return true;
152    } // delete_instance
153
154    /**
155     * get_instances
156     * This returns a key'd array of the instance information with
157     * [UID]=>[NAME]
158     */
159    public function get_instances()
160    {
161        $sql        = "SELECT * FROM `localplay_xbmc` ORDER BY `name`";
162        $db_results = Dba::query($sql);
163        $results    = array();
164
165        while ($row = Dba::fetch_assoc($db_results)) {
166            $results[$row['id']] = $row['name'];
167        }
168
169        return $results;
170    } // get_instances
171
172    /**
173     * update_instance
174     * This takes an ID and an array of data and updates the instance specified
175     * @param $uid
176     * @param array $data
177     * @return boolean
178     */
179    public function update_instance($uid, $data)
180    {
181        $sql = "UPDATE `localplay_xbmc` SET `host` = ?, `port` = ?, `name` = ?, `user` = ?, `pass` = ? WHERE `id` = ?";
182        Dba::query($sql, array($data['host'], $data['port'], $data['name'], $data['user'], $data['pass'], $uid));
183
184        return true;
185    } // update_instance
186
187    /**
188     * instance_fields
189     * This returns a key'd array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the
190     * fields so that we can on-the-fly generate a form
191     */
192    public function instance_fields()
193    {
194        $fields['name'] = array('description' => T_('Instance Name'), 'type' => 'text');
195        $fields['host'] = array('description' => T_('Hostname'), 'type' => 'text');
196        $fields['port'] = array('description' => T_('Port'), 'type' => 'number');
197        $fields['user'] = array('description' => T_('Username'), 'type' => 'text');
198        $fields['pass'] = array('description' => T_('Password'), 'type' => 'password');
199
200        return $fields;
201    } // instance_fields
202
203    /**
204     * get_instance
205     * This returns a single instance and all it's variables
206     * @param string $instance
207     * @return array
208     */
209    public function get_instance($instance = '')
210    {
211        $instance   = is_numeric($instance) ? (int) $instance : (int) AmpConfig::get('xbmc_active', 0);
212        $sql        = ($instance > 1) ? "SELECT * FROM `localplay_xbmc` WHERE `id` = ?" : "SELECT * FROM `localplay_xbmc`";
213        $db_results = Dba::query($sql, array($instance));
214
215        return Dba::fetch_assoc($db_results);
216    } // get_instance
217
218    /**
219     * set_active_instance
220     * This sets the specified instance as the 'active' one
221     * @param $uid
222     * @param string $user_id
223     * @return boolean
224     */
225    public function set_active_instance($uid, $user_id = '')
226    {
227        // Not an admin? bubkiss!
228        if (!Core::get_global('user')->has_access('100')) {
229            $user_id = Core::get_global('user')->id;
230        }
231
232        $user_id = $user_id ?: Core::get_global('user')->id;
233
234        Preference::update('xbmc_active', $user_id, $uid);
235        AmpConfig::set('xbmc_active', $uid, true);
236
237        return true;
238    } // set_active_instance
239
240    /**
241     * get_active_instance
242     * This returns the UID of the current active instance
243     * false if none are active
244     */
245    public function get_active_instance()
246    {
247    } // get_active_instance
248
249    /**
250     * @param Stream_Url $url
251     * @return boolean
252     */
253    public function add_url(Stream_Url $url)
254    {
255        if (!$this->_xbmc) {
256            return false;
257        }
258
259        try {
260            $this->_xbmc->Playlist->Add(array(
261                'playlistid' => $this->_playlistId,
262                'item' => array('file' => $url->url)
263            ));
264
265            return true;
266        } catch (XBMC_RPC_Exception $ex) {
267            debug_event(self::class, 'add_url failed: ' . $ex->getMessage(), 1);
268
269            return false;
270        }
271    }
272
273    /**
274     * delete_track
275     * Delete a track from the xbmc playlist
276     * @param $object_id
277     * @return boolean
278     */
279    public function delete_track($object_id)
280    {
281        if (!$this->_xbmc) {
282            return false;
283        }
284
285        try {
286            $this->_xbmc->Playlist->Remove(array(
287                'playlistid' => $this->_playlistId,
288                'position' => $object_id
289            ));
290
291            return true;
292        } catch (XBMC_RPC_Exception $ex) {
293            debug_event(self::class, 'delete_track failed: ' . $ex->getMessage(), 1);
294
295            return false;
296        }
297    } // delete_track
298
299    /**
300     * clear_playlist
301     * This deletes the entire xbmc playlist.
302     */
303    public function clear_playlist()
304    {
305        if (!$this->_xbmc) {
306            return false;
307        }
308
309        try {
310            $this->_xbmc->Playlist->Clear(array(
311                'playlistid' => $this->_playlistId
312            ));
313
314            return true;
315        } catch (XBMC_RPC_Exception $ex) {
316            debug_event(self::class, 'clear_playlist failed: ' . $ex->getMessage(), 1);
317
318            return false;
319        }
320    } // clear_playlist
321
322    /**
323     * play
324     * This just tells xbmc to start playing, it does not
325     * take any arguments
326     */
327    public function play()
328    {
329        if (!$this->_xbmc) {
330            return false;
331        }
332
333        try {
334            // XBMC requires to load a playlist to play. We don't know if this play is after a new playlist or after pause
335            // So we get current status
336            $status = $this->status();
337            if ($status['state'] == 'stop') {
338                $this->_xbmc->Player->Open(array(
339                    'item' => array('playlistid' => $this->_playlistId)
340                ));
341            } else {
342                $this->_xbmc->Player->PlayPause(array(
343                    'playerid' => $this->_playlistId,
344                    'play' => true
345                ));
346            }
347
348            return true;
349        } catch (XBMC_RPC_Exception $ex) {
350            debug_event(self::class, 'play failed: ' . $ex->getMessage(), 1);
351
352            return false;
353        }
354    } // play
355
356    /**
357     * pause
358     * This tells XBMC to pause the current song
359     */
360    public function pause()
361    {
362        if (!$this->_xbmc) {
363            return false;
364        }
365
366        try {
367            $this->_xbmc->Player->PlayPause(array(
368                'playerid' => $this->_playerId,
369                'play' => false
370            ));
371
372            return true;
373        } catch (XBMC_RPC_Exception $ex) {
374            debug_event(self::class, 'pause failed, is the player started? ' . $ex->getMessage(), 1);
375
376            return false;
377        }
378    } // pause
379
380    /**
381     * stop
382     * This just tells XBMC to stop playing, it does not take
383     * any arguments
384     */
385    public function stop()
386    {
387        if (!$this->_xbmc) {
388            return false;
389        }
390
391        try {
392            $this->_xbmc->Player->Stop(array(
393                'playerid' => $this->_playerId
394            ));
395
396            return true;
397        } catch (XBMC_RPC_Exception $ex) {
398            debug_event(self::class, 'stop failed, is the player started? ' . $ex->getMessage(), 1);
399
400            return false;
401        }
402    } // stop
403
404    /**
405     * skip
406     * This tells XBMC to skip to the specified song
407     * @param $song
408     * @return boolean
409     */
410    public function skip($song)
411    {
412        if (!$this->_xbmc) {
413            return false;
414        }
415
416        try {
417            $this->_xbmc->Player->GoTo(array(
418                'playerid' => $this->_playerId,
419                'to' => $song
420            ));
421
422            return true;
423        } catch (XBMC_RPC_Exception $ex) {
424            debug_event(self::class, 'skip failed, is the player started?: ' . $ex->getMessage(), 1);
425
426            return false;
427        }
428    } // skip
429
430    /**
431     * This tells XBMC to increase the volume
432     */
433    public function volume_up()
434    {
435        if (!$this->_xbmc) {
436            return false;
437        }
438
439        try {
440            $this->_xbmc->Application->SetVolume(array(
441                'volume' => 'increment'
442            ));
443
444            return true;
445        } catch (XBMC_RPC_Exception $ex) {
446            debug_event(self::class, 'volume_up failed: ' . $ex->getMessage(), 1);
447
448            return false;
449        }
450    } // volume_up
451
452    /**
453     * This tells XBMC to decrease the volume
454     */
455    public function volume_down()
456    {
457        if (!$this->_xbmc) {
458            return false;
459        }
460
461        try {
462            $this->_xbmc->Application->SetVolume(array(
463                'volume' => 'decrement'
464            ));
465
466            return true;
467        } catch (XBMC_RPC_Exception $ex) {
468            debug_event(self::class, 'volume_down failed: ' . $ex->getMessage(), 1);
469
470            return false;
471        }
472    } // volume_down
473
474    /**
475     * next
476     * This just tells xbmc to skip to the next song
477     */
478    public function next()
479    {
480        if (!$this->_xbmc) {
481            return false;
482        }
483
484        try {
485            $this->_xbmc->Player->GoTo(array(
486                'playerid' => $this->_playerId,
487                'to' => 'next'
488            ));
489
490            return true;
491        } catch (XBMC_RPC_Exception $ex) {
492            debug_event(self::class, 'next failed, is the player started? ' . $ex->getMessage(), 1);
493
494            return false;
495        }
496    } // next
497
498    /**
499     * prev
500     * This just tells xbmc to skip to the prev song
501     */
502    public function prev()
503    {
504        if (!$this->_xbmc) {
505            return false;
506        }
507
508        try {
509            $this->_xbmc->Player->GoTo(array(
510                'playerid' => $this->_playerId,
511                'to' => 'previous'
512            ));
513
514            return true;
515        } catch (XBMC_RPC_Exception $ex) {
516            debug_event(self::class, 'prev failed, is the player started? ' . $ex->getMessage(), 1);
517
518            return false;
519        }
520    } // prev
521
522    /**
523     * volume
524     * This tells XBMC to set the volume to the specified amount
525     * @param $volume
526     * @return boolean
527     */
528    public function volume($volume)
529    {
530        if (!$this->_xbmc) {
531            return false;
532        }
533
534        try {
535            $this->_xbmc->Application->SetVolume(array(
536                'volume' => $volume
537            ));
538
539            return true;
540        } catch (XBMC_RPC_Exception $ex) {
541            debug_event(self::class, 'volume failed: ' . $ex->getMessage(), 1);
542
543            return false;
544        }
545    } // volume
546
547    /**
548     * repeat
549     * This tells XBMC to set the repeating the playlist (i.e. loop) to either on or off
550     * @param $state
551     * @return boolean
552     */
553    public function repeat($state)
554    {
555        if (!$this->_xbmc) {
556            return false;
557        }
558
559        try {
560            $this->_xbmc->Player->SetRepeat(array(
561                'playerid' => $this->_playerId,
562                'repeat' => ($state ? 'all' : 'off')
563            ));
564
565            return true;
566        } catch (XBMC_RPC_Exception $ex) {
567            debug_event(self::class, 'repeat failed, is the player started? ' . $ex->getMessage(), 1);
568
569            return false;
570        }
571    } // repeat
572
573    /**
574     * random
575     * This tells XBMC to turn on or off the playing of songs from the playlist in random order
576     * @param $onoff
577     * @return boolean
578     */
579    public function random($onoff)
580    {
581        if (!$this->_xbmc) {
582            return false;
583        }
584
585        try {
586            $this->_xbmc->Player->SetShuffle(array(
587                'playerid' => $this->_playerId,
588                'shuffle' => $onoff
589            ));
590
591            return true;
592        } catch (XBMC_RPC_Exception $ex) {
593            debug_event(self::class, 'random failed, is the player started? ' . $ex->getMessage(), 1);
594
595            return false;
596        }
597    } // random
598
599    /**
600     * get
601     * This functions returns an array containing information about
602     * The songs that XBMC currently has in it's playlist. This must be
603     * done in a standardized fashion
604     */
605    public function get()
606    {
607        if (!$this->_xbmc) {
608            return false;
609        }
610
611        $results = array();
612
613        try {
614            $playlist = $this->_xbmc->Playlist->GetItems(array(
615                'playlistid' => $this->_playlistId,
616                'properties' => array('file')
617            ));
618
619            for ($i = $playlist['limits']['start']; $i < $playlist['limits']['end']; ++$i) {
620                $item = $playlist['items'][$i];
621
622                $data          = array();
623                $data['link']  = $item['file'];
624                $data['id']    = $i;
625                $data['track'] = $i + 1;
626
627                $url_data = $this->parse_url($data['link']);
628                if ($url_data != null) {
629                    $data['oid'] = $url_data['oid'];
630                    $song        = new Song($data['oid']);
631                    if ($song != null) {
632                        $data['name'] = $song->get_artist_name() . ' - ' . $song->title;
633                    }
634                }
635                if (!$data['name']) {
636                    $data['name'] = $item['label'];
637                }
638                $results[] = $data;
639            }
640        } catch (XBMC_RPC_Exception $ex) {
641            debug_event(self::class, 'get failed: ' . $ex->getMessage(), 1);
642        }
643
644        return $results;
645    } // get
646
647    /**
648     * status
649     * This returns bool/int values for features, loop, repeat and any other features
650     * that this Localplay method supports.
651     * This works as in requesting the xbmc properties
652     * @return array
653     */
654    public function status()
655    {
656        $array = array();
657        if (!$this->_xbmc) {
658            return $array;
659        }
660
661        try {
662            $appprop = $this->_xbmc->Application->GetProperties(array(
663                'properties' => array('volume')
664            ));
665            $array['volume'] = (int)($appprop['volume']);
666
667            try {
668                $currentplay = $this->_xbmc->Player->GetItem(array(
669                    'playerid' => $this->_playerId,
670                    'properties' => array('file')
671                ));
672                // We assume it's playing. No pause detection support.
673                $array['state'] = 'play';
674
675                $playprop = $this->_xbmc->Player->GetProperties(array(
676                    'playerid' => $this->_playerId,
677                    'properties' => array('repeat', 'shuffled')
678                ));
679                $array['repeat'] = ($playprop['repeat'] != "off");
680                $array['random'] = (strtolower($playprop['shuffled']) == 1);
681                $array['track']  = $currentplay['file'];
682
683                $url_data = $this->parse_url($array['track']);
684                $song     = new Song($url_data['oid']);
685                if ($song->title || $song->get_artist_name() || $song->get_album_name()) {
686                    $array['track_title']  = $song->title;
687                    $array['track_artist'] = $song->get_artist_name();
688                    $array['track_album']  = $song->get_album_name();
689                }
690            } catch (XBMC_RPC_Exception $ex) {
691                debug_event(self::class, 'get current item failed, player probably stopped. ' . $ex->getMessage(), 1);
692                $array['state'] = 'stop';
693            }
694        } catch (XBMC_RPC_Exception $ex) {
695            debug_event(self::class, 'status failed: ' . $ex->getMessage(), 1);
696        }
697
698        return $array;
699    } // status
700
701    /**
702     * connect
703     * This functions creates the connection to XBMC and returns
704     * a boolean value for the status, to save time this handle
705     * is stored in this class
706     */
707    public function connect()
708    {
709        $options = self::get_instance();
710        try {
711            debug_event(self::class, 'Trying to connect xbmc instance ' . $options['host'] . ':' . $options['port'] . '.', 5);
712            $this->_xbmc = new XBMC_RPC_HTTPClient($options);
713            debug_event(self::class, 'Connected.', 5);
714
715            return true;
716        } catch (XBMC_RPC_ConnectionException $ex) {
717            debug_event(self::class, 'xbmc connection failed: ' . $ex->getMessage(), 1);
718
719            return false;
720        }
721    } // connect
722}
723