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
23namespace Ampache\Module\Playback\Localplay\HttpQ;
24
25use Ampache\Config\AmpConfig;
26use Ampache\Repository\Model\Democratic;
27use Ampache\Module\Playback\Localplay\localplay_controller;
28use Ampache\Repository\Model\Preference;
29use Ampache\Repository\Model\Song;
30use Ampache\Module\Playback\Stream_Url;
31use Ampache\Module\System\Core;
32use Ampache\Module\System\Dba;
33use Ampache\Module\Util\ObjectTypeToClassNameMapper;
34use PDOStatement;
35
36/**
37 * AmpacheHttpq Class
38 *
39 * This is the class for the httpQ Localplay method to remotely control
40 * a WinAmp Instance
41 *
42 */
43class AmpacheHttpq extends localplay_controller
44{
45    /* Variables */
46    private $version     = '000002';
47    private $description = "Controls an httpQ instance, requires Ampache's httpQ version";
48
49    /* Constructed variables */
50    private $_httpq;
51
52    /**
53     * get_description
54     * This returns the description of this Localplay method
55     */
56    public function get_description()
57    {
58        return $this->description;
59    } // get_description
60
61    /**
62     * get_version
63     * This returns the current version
64     */
65    public function get_version()
66    {
67        return $this->version;
68    } // get_version
69
70    /**
71     * is_installed
72     * This returns true or false if this controller is installed
73     */
74    public function is_installed()
75    {
76        $sql        = "SHOW TABLES LIKE 'localplay_httpq'";
77        $db_results = Dba::read($sql);
78
79        return (Dba::num_rows($db_results) > 0);
80    } // is_installed
81
82    /**
83     * install
84     * This function installs the controller
85     */
86    public function install()
87    {
88        $collation = (AmpConfig::get('database_collation', 'utf8mb4_unicode_ci'));
89        $charset   = (AmpConfig::get('database_charset', 'utf8mb4'));
90        $engine    = ($charset == 'utf8mb4') ? 'InnoDB' : 'MYISAM';
91
92        $sql = "CREATE TABLE `localplay_httpq` (`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, `password` VARCHAR(255) COLLATE $collation NOT NULL, `access` SMALLINT(4) UNSIGNED NOT NULL DEFAULT '0') ENGINE = $engine DEFAULT CHARSET=$charset COLLATE=$collation";
93        Dba::query($sql);
94
95        // Add an internal preference for the users current active instance
96        Preference::insert('httpq_active', T_('HTTPQ Active Instance'), 0, 25, 'integer', 'internal', 'httpq');
97
98        return true;
99    } // install
100
101    /**
102     * uninstall
103     * This removes the Localplay controller
104     */
105    public function uninstall()
106    {
107        $sql = "DROP TABLE `localplay_httpq`";
108        Dba::write($sql);
109
110        // Remove the pref we added for this
111        Preference::delete('httpq_active');
112
113        return true;
114    } // uninstall
115
116    /**
117     * add_instance
118     * This takes keyed data and inserts a new httpQ instance
119     * @param array $data
120     * @return PDOStatement|boolean
121     */
122    public function add_instance($data)
123    {
124        $name     = Dba::escape($data['name']);
125        $host     = Dba::escape($data['host']);
126        $port     = Dba::escape($data['port']);
127        $password = Dba::escape($data['password']);
128        $user_id  = Dba::escape(Core::get_global('user')->id);
129
130        $sql = "INSERT INTO `localplay_httpq` (`name`, `host`, `port`, `password`, `owner`) VALUES ('$name', '$host', '$port', '$password', '$user_id')";
131
132        return Dba::write($sql);
133    } // add_instance
134
135    /**
136     * delete_instance
137     * This takes a UID and deletes the instance in question
138     * @param $uid
139     * @return boolean
140     */
141    public function delete_instance($uid)
142    {
143        $uid = Dba::escape($uid);
144        $sql = "DELETE FROM `localplay_httpq` WHERE `id`='$uid'";
145        Dba::write($sql);
146
147        return true;
148    } // delete_instance
149
150    /**
151     * get_instances
152     * This returns a keyed array of the instance information with
153     * [UID]=>[NAME]
154     */
155    public function get_instances()
156    {
157        $sql        = "SELECT * FROM `localplay_httpq` ORDER BY `name`";
158
159        $db_results = Dba::read($sql);
160        $results    = array();
161        while ($row = Dba::fetch_assoc($db_results)) {
162            $results[$row['id']] = $row['name'];
163        }
164
165        return $results;
166    } // get_instances
167
168    /**
169     * update_instance
170     * This takes an ID and an array of data and updates the instance specified
171     * @param $uid
172     * @param array $data
173     * @return boolean
174     */
175    public function update_instance($uid, $data)
176    {
177        $uid  = Dba::escape($uid);
178        $port = Dba::escape($data['port']);
179        $host = Dba::escape($data['host']);
180        $name = Dba::escape($data['name']);
181        $pass = Dba::escape($data['password']);
182
183        $sql = "UPDATE `localplay_httpq` SET `host`='$host', `port`='$port', `name`='$name', `password`='$pass' WHERE `id`='$uid'";
184        Dba::write($sql);
185
186        return true;
187    } // update_instance
188
189    /**
190     * instance_fields
191     * This returns a keyed array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the
192     * fields so that we can on-the-fly generate a form
193     */
194    public function instance_fields()
195    {
196        $fields['name']     = array('description' => T_('Instance Name'), 'type' => 'text');
197        $fields['host']     = array('description' => T_('Hostname'), 'type' => 'text');
198        $fields['port']     = array('description' => T_('Port'), 'type' => 'number');
199        $fields['password'] = array('description' => T_('Password'), 'type' => 'password');
200
201        return $fields;
202    } // instance_fields
203
204    /**
205     * get_instance
206     * This returns a single instance and all its variables
207     * @param string $instance
208     * @return array
209     */
210    public function get_instance($instance = '')
211    {
212        $instance   = is_numeric($instance) ? (int) $instance : (int) AmpConfig::get('httpq_active', 0);
213        $sql        = ($instance > 1) ? "SELECT * FROM `localplay_httpq` WHERE `id` = ?" : "SELECT * FROM `localplay_httpq`";
214        $db_results = Dba::query($sql, array($instance));
215
216        return Dba::fetch_assoc($db_results);
217    } // get_instance
218
219    /**
220     * set_active_instance
221     * This sets the specified instance as the 'active' one
222     * @param $uid
223     * @param string $user_id
224     * @return boolean
225     */
226    public function set_active_instance($uid, $user_id = '')
227    {
228        // Not an admin? bubkiss!
229        if (!Core::get_global('user')->has_access('100')) {
230            $user_id = Core::get_global('user')->id;
231        }
232
233        $user_id = $user_id ?: Core::get_global('user')->id;
234
235        Preference::update('httpq_active', $user_id, $uid);
236        AmpConfig::set('httpq_active', $uid, true);
237
238        return true;
239    } // set_active_instance
240
241    /**
242     * get_active_instance
243     * This returns the UID of the current active instance
244     * false if none are active
245     */
246    public function get_active_instance()
247    {
248    } // get_active_instance
249
250    /**
251     * add_url
252     * This is the new hotness
253     * @param Stream_Url $url
254     * @return boolean
255     */
256    public function add_url(Stream_Url $url)
257    {
258        if ($this->_httpq->add($url->title, $url->url) === null) {
259            debug_event('httpq.controller', 'add_url failed to add ' . (string)$url->url, 1);
260
261            return false;
262        }
263
264        return true;
265    }
266
267    /**
268     * delete_track
269     * This must take an ID (as returned by our get function)
270     * and delete it from httpQ
271     * @param integer $object_id
272     * @return boolean
273     */
274    public function delete_track($object_id)
275    {
276        if ($this->_httpq->delete_pos($object_id) === null) {
277            debug_event('httpq.controller', 'Unable to delete ' . $object_id . ' from httpQ', 1);
278
279            return false;
280        }
281
282        return true;
283    } // delete_track
284
285    /**
286     * clear_playlist
287     */
288    public function clear_playlist()
289    {
290        if ($this->_httpq->clear() === null) {
291            return false;
292        }
293
294        // If the clear worked we should stop it!
295        $this->stop();
296
297        return true;
298    } // clear_playlist
299
300    /**
301     * play
302     * This just tells httpQ to start playing, it does not
303     * take any arguments
304     */
305    public function play()
306    {
307        // A play when it's already playing causes a track restart, so double check its state
308        if ($this->_httpq->state() == 'play') {
309            return true;
310        }
311
312        if ($this->_httpq->play() === null) {
313            return false;
314        }
315
316        return true;
317    } // play
318
319    /**
320     * stop
321     * This just tells httpQ to stop playing, it does not take
322     * any arguments
323     */
324    public function stop()
325    {
326        if ($this->_httpq->stop() === null) {
327            return false;
328        }
329
330        return true;
331    } // stop
332
333    /**
334     * skip
335     * This tells httpQ to skip to the specified song
336     * @param $song
337     * @return boolean
338     */
339    public function skip($song)
340    {
341        if ($this->_httpq->skip($song) === null) {
342            return false;
343        }
344
345        return true;
346    } // skip
347
348    /**
349     * This tells httpQ to increase the volume by WinAmps default amount
350     */
351    public function volume_up()
352    {
353        return $this->_httpq->volume_up();
354    } // volume_up
355
356    /**
357     * This tells httpQ to decrease the volume by Winamp's default amount
358     */
359    public function volume_down()
360    {
361        return $this->_httpq->volume_down();
362    } // volume_down
363
364    /**
365     * next
366     * This just tells httpQ to skip to the next song
367     */
368    public function next()
369    {
370        if ($this->_httpq->next() === null) {
371            return false;
372        }
373
374        return true;
375    } // next
376
377    /**
378     * prev
379     * This just tells httpQ to skip to the prev song
380     */
381    public function prev()
382    {
383        if ($this->_httpq->prev() === null) {
384            return false;
385        }
386
387        return true;
388    } // prev
389
390    /**
391     * pause
392     * This tells httpQ to pause the current song
393     */
394    public function pause()
395    {
396        if ($this->_httpq->pause() === null) {
397            return false;
398        }
399
400        return true;
401    } // pause
402
403    /**
404     * volume
405     * This tells httpQ to set the volume to the specified amount this
406     * is 0-100
407     * @param $volume
408     * @return boolean
409     */
410    public function volume($volume)
411    {
412        return $this->_httpq->set_volume($volume);
413    } // volume
414
415    /**
416     * repeat
417     * This tells httpQ to set the repeating the playlist (i.e. loop) to
418     * either on or off
419     * @param $state
420     * @return boolean
421     */
422    public function repeat($state)
423    {
424        if ($this->_httpq->repeat($state) === null) {
425            return false;
426        }
427
428        return true;
429    } // repeat
430
431    /**
432     * random
433     * This tells httpQ to turn on or off the playing of songs from the
434     * playlist in random order
435     * @param $onoff
436     * @return boolean
437     */
438    public function random($onoff)
439    {
440        if ($this->_httpq->random($onoff) === null) {
441            return false;
442        }
443
444        return true;
445    } // random
446
447    /**
448     * get
449     * This functions returns an array containing information about
450     * The songs that httpQ currently has in its playlist. This must be
451     * done in a standardized fashion
452     */
453    public function get()
454    {
455        /* Get the Current Playlist */
456        $list = $this->_httpq->get_tracks();
457
458        if (!$list) {
459            return array();
460        }
461
462        $songs   = explode("::", $list);
463        $results = array();
464
465        foreach ($songs as $key => $entry) {
466            $data = array();
467
468            /* Required Elements */
469            $data['id']  = $key;
470            $data['raw'] = $entry;
471
472            $url_data = $this->parse_url($entry);
473            switch ($url_data['primary_key']) {
474                case 'oid':
475                    $data['oid'] = $url_data['oid'];
476                    $song        = new Song($data['oid']);
477                    $song->format();
478                    $data['name'] = $song->f_title . ' - ' . $song->f_album . ' - ' . $song->f_artist;
479                    $data['link'] = $song->f_link;
480                    break;
481                case 'demo_id':
482                    $democratic   = new Democratic($url_data['demo_id']);
483                    $data['name'] = T_('Democratic') . ' - ' . $democratic->name;
484                    $data['link'] = '';
485                    break;
486                case 'random':
487                    $data['name'] = T_('Random') . ' - ' . scrub_out(ucfirst($url_data['type']));
488                    $data['link'] = '';
489                    break;
490                default:
491                    // If we don't know it, look up by filename
492                    $filename = Dba::escape($entry['file']);
493                    $sql      = "SELECT `id`, 'song' AS `type` FROM `song` WHERE `file` LIKE '%$filename' UNION ALL SELECT `id`, 'live_stream' AS `type` FROM `live_stream` WHERE `url`='$filename' ";
494
495                    $db_results = Dba::read($sql);
496                    if ($row = Dba::fetch_assoc($db_results)) {
497                        $class_name = ObjectTypeToClassNameMapper::map($row['type']);
498                        $media      = new $class_name($row['id']);
499                        $media->format();
500                        switch ($row['type']) {
501                            case 'song':
502                                $data['name'] = $media->f_title . ' - ' . $media->f_album . ' - ' . $media->f_artist;
503                                $data['link'] = $media->f_link;
504                                break;
505                            case 'live_stream':
506                                $frequency    = $media->frequency ? '[' . $media->frequency . ']' : '';
507                                $site_url     = $media->site_url ? '(' . $media->site_url . ')' : '';
508                                $data['name'] = "$media->name $frequency $site_url";
509                                $data['link'] = $media->site_url;
510                                break;
511                        } // end switch on type
512                    } else {
513                        $data['name'] = basename($data['raw']);
514                        $data['link'] = basename($data['raw']);
515                    }
516
517                    break;
518            } // end switch on primary key type
519
520            $data['track'] = $key + 1;
521
522            $results[] = $data;
523        } // foreach playlist items
524
525        return $results;
526    } // get
527
528    /**
529     * status
530     * This returns bool/int values for features, loop, repeat and any other features
531     * That this Localplay method supports. required function
532     * @return array
533     */
534    public function status()
535    {
536        /* Construct the Array */
537        $array['state']  = $this->_httpq->state();
538        $array['volume'] = $this->_httpq->get_volume();
539        $array['repeat'] = $this->_httpq->get_repeat();
540        $array['random'] = $this->_httpq->get_random();
541        $array['track']  = $this->_httpq->get_now_playing();
542        $url_data        = $this->parse_url($array['track']);
543
544        if (isset($url_data['oid'])) {
545            $song                  = new Song($url_data['oid']);
546            $array['track_title']  = $song->title;
547            $array['track_artist'] = $song->get_artist_name();
548            $array['track_album']  = $song->get_album_name();
549        } else {
550            $array['track_title'] = basename($array['track']);
551        }
552
553        return $array;
554    } // status
555
556    /**
557     * connect
558     * This functions creates the connection to httpQ and returns
559     * a boolean value for the status, to save time this handle
560     * is stored in this class
561     */
562    public function connect()
563    {
564        $options      = self::get_instance();
565        $this->_httpq = new HttpQPlayer($options['host'], $options['password'], $options['port']);
566
567        // Test our connection by retrieving the version
568        if ($this->_httpq->version() !== null) {
569            return true;
570        }
571
572        return false;
573    } // connect
574}
575