1/*
2 * Copyright (C) 2016-2019
3 *      Jean-Luc Barriere <jlbarriere68@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18import QtQuick 2.9
19import NosonApp 1.0
20
21/*
22 * This file should *only* manage the media playing and the relevant settings
23 * It should therefore only access ZonePlayer, trackQueue and Settings
24 * Anything else within the app should use Connections to listen for changes
25 */
26
27
28Item {
29    id: player
30    objectName: "controller"
31    property alias trackQueue: trackQueueLoader.item
32    property alias renderingModel: renderingModelLoader.item
33    property bool connected: false
34    property string zoneId: ""
35    property string zoneName: ""
36    property string zoneShortName: ""
37    property string controllerURI: ""
38    property string currentMetaAlbum: ""
39    property string currentMetaArt: ""
40    property string currentMetaArtist: ""
41    property string currentMetaSource: ""
42    property string currentMetaTitle: ""
43    property string currentMetaURITitle: ""
44    property int currentProtocol: -1
45    property int currentIndex: -1
46    property int currentCount: 0
47    property string playbackState: ""
48    readonly property bool isPlaying: (playbackState === "PLAYING")
49    property int trackPosition: 0
50    property int trackDuration: 0
51    property int volumeMaster: 0
52    property int bass: 0
53    property int treble: 0
54    property bool repeat: false
55    property bool shuffle: false
56    property bool mute: false
57    property bool outputFixed: false
58    property int renderingControlCount: 0
59    property bool sleepTimerEnabled: false
60    property bool nightmodeEnabled: false
61    property bool loudnessEnabled: false
62    property var covers: []
63
64    property string queueInfo: queueOverviewString()
65
66    signal stopped()
67    signal sourceChanged()
68    signal currentPositionChanged(int position, int duration)
69    signal renderingControlChanged() // see function refreshRendering
70
71    onCurrentCountChanged: queueInfo = queueOverviewString()
72    onCurrentIndexChanged: queueInfo = queueOverviewString()
73
74    function connectZonePlayer(zonePlayer) {
75        connected = false; // force signal
76        var zp = zone.handle;
77        zp.disableMPRIS2();
78        zone.handle = zonePlayer;
79        zone.handle.enableMPRIS2();
80        zone.pid = zonePlayer.pid;
81
82        // execute all handlers to signal the changes
83        player.handleZPConnectedChanged();
84        player.handleZPSourceChanged();
85        player.handleZPRenderingCountChanged();
86        player.handleZPRenderingGroupChanged();
87        player.handleZPRenderingChanged();
88        player.handleZPPlayModeChanged();
89        player.handleZPPlaybackStateChanged();
90
91        // release the old zone player
92        AllZonesModel.releasePlayer(zp);
93    }
94
95    function coordinatorName() {
96        return zone.handle.coordinatorName();
97    }
98
99    function queueOverviewString() {
100        var str = "";
101        if (!connected)
102            str = "- / -";
103        else if (currentIndex < 0)
104            str = "- / " + currentCount;
105        else
106            str = (currentIndex + 1) + " / " + currentCount;
107        return str;
108    }
109
110    function ping(onFinished) {
111        if (connected) {
112            var future = zone.handle.tryPing();
113            future.onFinished.connect(onFinished);
114            return future.start();
115        } else {
116            onFinished(false);
117        }
118    }
119
120    function stop(onFinished) {
121        var future = zone.handle.tryStop();
122        future.onFinished.connect(onFinished);
123        return future.start();
124    }
125
126    function play(onFinished) {
127        var future = zone.handle.tryPlay();
128        future.onFinished.connect(onFinished);
129        return future.start();
130    }
131
132    function pause(onFinished) {
133        var future = zone.handle.tryPause();
134        future.onFinished.connect(onFinished);
135        return future.start();
136    }
137
138    function toggle(onFinished) {
139        if (isPlaying)
140            return pause(onFinished);
141        else
142            return play(onFinished);
143    }
144
145    function playSource(modelItem, onFinished) {
146        var future = zone.handle.tryPlaySource(modelItem.payload);
147        // hack: run animation now without waiting event
148        future.finished.connect(function(result) {
149            if (!result)
150                handleZPPlaybackStateChanged();
151            onFinished(result);
152        });
153        if (future.start()) {
154            player.playbackState = "TRANSITIONING";
155            return true;
156        }
157        return false;
158    }
159
160    function previousSong(onFinished) {
161        var future = zone.handle.tryPrevious();
162        future.onFinished.connect(onFinished);
163        return future.start();
164    }
165
166    function nextSong(onFinished) {
167        var future = zone.handle.tryNext();
168        future.onFinished.connect(onFinished);
169        return future.start();
170    }
171
172    function toggleRepeat(onFinished) {
173        var future = zone.handle.tryToggleRepeat();
174        future.onFinished.connect(onFinished);
175        return future.start(false);
176    }
177
178    function toggleShuffle(onFinished) {
179        var future = zone.handle.tryToggleShuffle();
180        future.onFinished.connect(onFinished);
181        return future.start(false);
182    }
183
184    function seek(position, onFinished) {
185        if (player.canSeekInStream()) {
186            var future = zone.handle.trySeekTime(Math.floor(position / 1000));
187            future.onFinished.connect(onFinished);
188            return future.start();
189        } else {
190            onFinished(false);
191        }
192    }
193
194    function setVolumeGroupForFake(volume) {
195        return zone.handle.setVolumeGroup(volume, true);
196    }
197
198    function setVolumeForFake(uuid, volume) {
199        return zone.handle.setVolume(uuid, volume, true);
200    }
201
202    function setVolumeGroup(volume, onFinished) {
203        var future = zone.handle.trySetVolumeGroup(volume);
204        future.onFinished.connect(onFinished);
205        return future.start(false);
206    }
207
208    function setVolume(uuid, volume, onFinished) {
209        var future = zone.handle.trySetVolume(uuid, volume);
210        future.onFinished.connect(onFinished);
211        return future.start(false);
212    }
213
214    function setBass(value, onFinished) {
215        var future = zone.handle.trySetBass(value)
216        future.onFinished.connect(onFinished);
217        return future.start(false);
218    }
219
220    function setTreble(value, onFinished) {
221        var future = zone.handle.trySetTreble(value)
222        future.onFinished.connect(onFinished);
223        return future.start(false);
224    }
225
226    function toggleMuteGroup(onFinished) {
227        var future = zone.handle.tryToggleMute();
228        future.onFinished.connect(onFinished);
229        return future.start(false);
230    }
231
232    function toggleMute(uuid, onFinished) {
233        var future = zone.handle.tryToggleMute(uuid);
234        future.onFinished.connect(onFinished);
235        return future.start(false);
236    }
237
238    function toggleNightmode(onFinished) {
239        var future = zone.handle.tryToggleNightmode();
240        future.onFinished.connect(onFinished);
241        return future.start(false);
242    }
243
244    function toggleLoudness(onFinished) {
245        var future = zone.handle.tryToggleLoudness();
246        future.onFinished.connect(onFinished);
247        return future.start(false);
248    }
249
250    function playQueue(start, onFinished) {
251        var future = zone.handle.tryPlayQueue(start);
252        future.onFinished.connect(onFinished);
253        return future.start();
254    }
255
256    function seekTrack(nr, onFinished) {
257        var future = zone.handle.trySeekTrack(nr);
258        future.onFinished.connect(onFinished);
259        return future.start();
260    }
261
262    function addItemToQueue(modelItem, nr, onFinished) {
263        var future = zone.handle.tryAddItemToQueue(modelItem.payload, nr);
264        future.onFinished.connect(onFinished);
265        return future.start();
266    }
267
268    function makeFilePictureLocalURL(filePath) {
269        return zone.handle.makeFilePictureLocalURL(filePath);
270    }
271
272    function makeFileStreamItem(filePath, codec, title, album, author, duration, hasArt) {
273        return zone.handle.makeFileStreamItem(filePath, codec, title, album, author, duration, hasArt);
274    }
275
276    function addMultipleItemsToQueue(modelItemList, onFinished) {
277        var payloads = [];
278        for (var i = 0; i < modelItemList.length; ++i)
279            payloads.push(modelItemList[i].payload);
280        var future = zone.handle.tryAddMultipleItemsToQueue(payloads);
281        future.onFinished.connect(onFinished);
282        return future.start();
283    }
284
285    function removeAllTracksFromQueue(onFinished) {
286        var future = zone.handle.tryRemoveAllTracksFromQueue();
287        future.onFinished.connect(onFinished);
288        return future.start();
289    }
290
291    function removeTrackFromQueue(modelItem, onFinished) {
292        var future = zone.handle.tryRemoveTrackFromQueue(modelItem.id, trackQueue.model.containerUpdateID());
293        future.onFinished.connect(onFinished);
294        return future.start();
295    }
296
297    function reorderTrackInQueue(nrFrom, nrTo, onFinished) {
298        var future = zone.handle.tryReorderTrackInQueue(nrFrom, nrTo, trackQueue.model.containerUpdateID());
299        future.onFinished.connect(onFinished);
300        return future.start();
301    }
302
303    function saveQueue(title, onFinished) {
304        var future = zone.handle.trySaveQueue(title);
305        future.onFinished.connect(onFinished);
306        return future.start();
307    }
308
309    function createSavedQueue(title, onFinished) {
310        var future = zone.handle.tryCreateSavedQueue(title);
311        future.onFinished.connect(onFinished);
312        return future.start();
313    }
314
315    function addItemToSavedQueue(playlistId, modelItem, containerUpdateID, onFinished) {
316        var future = zone.handle.tryAddItemToSavedQueue(playlistId, modelItem.payload, containerUpdateID);
317        future.onFinished.connect(onFinished);
318        return future.start();
319    }
320
321    function addMultipleItemsToSavedQueue(playlistId, payloads, containerUpdateID, onFinished) {
322        var future = zone.handle.tryAddMultipleItemsToSavedQueue(playlistId, payloads, containerUpdateID);
323        future.onFinished.connect(onFinished);
324        return future.start();
325    }
326
327    function removeTracksFromSavedQueue(playlistId, selectedIndices, containerUpdateID, onFinished) {
328        var future = zone.handle.tryRemoveTracksFromSavedQueue(playlistId, selectedIndices, containerUpdateID);
329        future.onFinished.connect(onFinished);
330        return future.start();
331    }
332
333    function reorderTrackInSavedQueue(playlistId, index, newIndex, containerUpdateID, onFinished) {
334        var future = zone.handle.tryReorderTrackInSavedQueue(playlistId, index, newIndex, containerUpdateID);
335        future.onFinished.connect(onFinished);
336        return future.start();
337    }
338
339    function configureSleepTimer(sec, onFinished) {
340        var future = zone.handle.tryConfigureSleepTimer(sec);
341        future.onFinished.connect(onFinished);
342        return future.start(false);
343    }
344
345    function remainingSleepTimerDuration(onFinished) {
346        var future = zone.handle.tryRemainingSleepTimerDuration();
347        future.onFinished.connect(onFinished);
348        return future.start(false);
349    }
350
351    function playStream(url, title, onFinished) {
352        var future = zone.handle.tryPlayStream(url, (title === "" ? qsTr("Untitled") : title));
353        // hack: run animation now without waiting event
354        future.finished.connect(function(result) {
355            if (!result)
356                handleZPPlaybackStateChanged();
357            onFinished(result);
358        });
359        if (future.start()) {
360            player.playbackState = "TRANSITIONING";
361            return true;
362        }
363        return false;
364    }
365
366    function playLineIN(onFinished) {
367        var future = zone.handle.tryPlayLineIN();
368        future.finished.connect(onFinished);
369        return future.start();
370    }
371
372    function playDigitalIN(onFinished) {
373        var future = zone.handle.tryPlayDigitalIN();
374        future.finished.connect(onFinished);
375        return future.start();
376    }
377
378    function playPulse(onFinished) {
379        var future = zone.handle.tryPlayPulse();
380        future.finished.connect(onFinished);
381        return future.start();
382    }
383
384    function isPulseStream() {
385        return zone.handle.isPulseStream(currentMetaSource);
386    }
387
388    function isMyStream(metaSource) {
389        return zone.handle.isMyStream(metaSource);
390    }
391
392    function playFavorite(modelItem, onFinished) {
393        var future = zone.handle.tryPlayFavorite(modelItem.payload);
394        // hack: run animation now without waiting event
395        future.finished.connect(function(result) {
396            if (!result)
397                handleZPPlaybackStateChanged();
398            onFinished(result);
399        });
400        if (future.start()) {
401            player.playbackState = "TRANSITIONING";
402            return true;
403        }
404        return false;
405    }
406
407    function syncTrackPosition() {
408        var future = zone.handle.tryCurrentTrackPosition();
409        future.finished.connect(function(result) {
410            var npos = (result > 0 ? 1000 * result : 0);
411            player.trackPosition = npos > player.trackDuration ? 0 : npos;
412            //customdebug("sync position to " + player.trackPosition);
413        });
414        future.start(false);
415    }
416
417    function isPlayingQueued() {
418        return player.trackDuration > 0;
419    }
420
421    function canSeekInStream() {
422        switch (currentProtocol) {
423        case 1:  // x-rincon-stream
424        case 2:  // x-rincon-mp3radio
425        case 5:  // x-sonos-htastream
426        case 14: // http-get
427            return false;
428        default:
429            // the noson streamer uses the protocol http(17)
430            return isPlayingQueued();
431        }
432    }
433
434    // reload the rendering model
435    // it should be triggered on signal renderingControlChanged
436    function refreshRendering() {
437        renderingModelLoader.item.load(zone.handle);
438    }
439
440    ////////////////////////////////////////////////////////////////////////////
441    // handlers connected to the ZP events
442    // they apply changes on the player properties and forward signals as needed
443    //
444    function handleZPConnectedChanged() {
445        player.connected = zone.handle.connected;
446        player.zoneId = zone.handle.zoneId;
447        player.zoneName = zone.handle.zoneName;
448        player.zoneShortName = zone.handle.zoneShortName;
449        player.controllerURI = zone.handle.controllerURI;
450        player.remainingSleepTimerDuration(function(result) {
451            player.sleepTimerEnabled = result > 0 ? true : false
452        });
453        trackQueue.initQueue(zone.handle);
454    }
455
456    function handleZPSourceChanged() {
457        // protect against undefined properties
458        player.currentMetaAlbum = zone.handle.currentMetaAlbum || "";
459        player.currentMetaArt = zone.handle.currentMetaArt || "";
460        player.currentMetaArtist = zone.handle.currentMetaArtist || "";
461        player.currentMetaSource = zone.handle.currentMetaSource || "";
462        player.currentMetaTitle = zone.handle.currentMetaTitle || "";
463        player.currentMetaURITitle = zone.handle.currentMetaURITitle || "";
464        player.currentIndex = zone.handle.currentIndex;
465        player.currentProtocol = zone.handle.currentProtocol;
466        player.trackDuration = 1000 * zone.handle.currentTrackDuration;
467        // reset position
468        if (zone.handle.currentTrackDuration > 0) {
469            player.syncTrackPosition();
470        } else {
471            player.trackPosition = 0;
472        }
473        // reset cover
474        if (player.currentProtocol == 1) {
475            player.covers = [{art: "qrc:/images/linein.png"}];
476        } else if (player.currentProtocol == 5) {
477            player.covers = [{art: "qrc:/images/tv.png"}];
478        } else {
479            player.covers = makeCoverSource(player.currentMetaArt, player.currentMetaArtist, player.currentMetaAlbum);
480        }
481        player.sourceChanged();
482        player.currentPositionChanged(player.trackPosition, player.trackDuration);
483    }
484
485    function handleZPPlayModeChanged() {
486        player.repeat = zone.handle.playMode === "REPEAT_ALL" || zone.handle.playMode === "REPEAT_ONE" || zone.handle.playMode === "SHUFFLE" ? true : false;
487        player.shuffle = zone.handle.playMode === "SHUFFLE" || zone.handle.playMode === "SHUFFLE_NOREPEAT" ? true : false;
488    }
489
490    function handleZPRenderingGroupChanged() {
491        player.volumeMaster = zone.handle.volumeMaster;
492        player.treble = zone.handle.treble;
493        player.bass = zone.handle.bass;
494        player.mute = zone.handle.muteMaster;
495        player.nightmodeEnabled = zone.handle.nightmode;
496        player.loudnessEnabled = zone.handle.loudness;
497        player.outputFixed = zone.handle.outputFixed;
498    }
499
500    function handleZPRenderingChanged() {
501        renderingControlChanged();
502    }
503
504    function handleZPRenderingCountChanged() {
505        player.renderingControlCount = zone.handle.renderingCount;
506    }
507
508    function handleZPPlaybackStateChanged() {
509        player.playbackState = zone.handle.playbackState;
510        if (zone.handle.playbackState === "PLAYING" && zone.handle.currentIndex >= 0 &&
511                zone.handle.currentTrackDuration > 0) {
512            // Starting playback of queued track the position can be resetted without any event
513            // from the SONOS device. This hack query to retrieve the real position after a short
514            // time (3sec).
515            delaySyncTrackPosition.start();
516        } else if (zone.handle.playbackState === "STOPPED") {
517            stopped();
518        }
519    }
520
521    Timer {
522        id: delaySyncTrackPosition
523        interval: 3000
524        onTriggered: {
525            if (isPlaying && trackDuration > 0) {
526                syncTrackPosition();
527            }
528        }
529    }
530
531    Connections {
532        target: AllZonesModel
533        function onZpConnectedChanged(pid) { if (pid === zone.pid) handleZPConnectedChanged(); }
534        function onZpSourceChanged(pid) { if (pid === zone.pid) handleZPSourceChanged(); }
535        function onZpRenderingCountChanged(pid) { if (pid === zone.pid) handleZPRenderingCountChanged(); }
536        function onZpRenderingGroupChanged(pid) { if (pid === zone.pid) handleZPRenderingGroupChanged(); }
537        function onZpRenderingChanged(pid) { if (pid === zone.pid) handleZPRenderingChanged(); }
538        function onZpPlayModeChanged(pid) { if (pid === zone.pid) handleZPPlayModeChanged(); }
539        function onZpPlaybackStateChanged(pid) { if (pid === zone.pid) handleZPPlaybackStateChanged(); }
540        function onZpSleepTimerChanged(pid) {
541            if (pid === zone.pid) {
542                player.remainingSleepTimerDuration(function(result) {
543                    player.sleepTimerEnabled = result > 0 ? true : false
544                });
545            }
546        }
547    }
548
549    QtObject {
550        id: zone
551        objectName: "playerZone"
552        property int pid: 0
553        property ZonePlayer handle: ZonePlayer { }
554    }
555
556    Loader {
557        id: renderingModelLoader
558        asynchronous: false
559        sourceComponent: Component {
560            RenderingModel {
561            }
562        }
563    }
564
565    // the track queue related to the player
566    // it must be plugged to the current instance of the zone player
567    // also it must be reloaded after any connection event
568    Loader {
569        id: trackQueueLoader
570        asynchronous: false
571        sourceComponent: Component {
572            TrackQueue {
573                // init is done by handleZPConnectedChanged
574                onTrackCountChanged: player.currentCount = trackCount
575            }
576        }
577    }
578
579    Timer {
580        id: playingTimer
581        interval: 1000;
582        running: (player.isPlaying && player.trackDuration > 1)
583        repeat: true;
584        onTriggered: {
585            var npos = player.trackPosition + interval;
586            player.trackPosition = npos > player.trackDuration ? 0 : npos;
587            player.currentPositionChanged(player.trackPosition, player.trackDuration);
588        }
589    }
590}
591