From fa19bba102553df8378921ce6c7bb79fa2dedc8a Mon Sep 17 00:00:00 2001 From: John Ellis Date: Tue, 31 Jan 2017 23:03:11 -0500 Subject: [PATCH 1/6] Initial version of Google Music radio playlists. This could have some really, really bad dependency nukage --- README.md | 2 +- debian/control | 2 +- etc/hack-clock.conf | 6 +- lib/hackclock/runapp/Libs/GStreamer.py | 49 ++++++++---- lib/hackclock/runapp/Libs/GoogleMusic.py | 33 ++++++++ requirements.txt | 6 ++ srv/hackclock/views/blocks/editor.tpl | 3 +- srv/hackclock/views/blocks/js/blocks/audio.js | 77 +++++++++++++++++++ .../views/blocks/js/blocks/speaker.js | 61 --------------- .../views/blocks/js/generators/audio.js | 21 +++++ .../views/blocks/js/generators/speaker.js | 15 ---- tests/localsettings.conf | 2 + 12 files changed, 179 insertions(+), 98 deletions(-) create mode 100644 lib/hackclock/runapp/Libs/GoogleMusic.py delete mode 100644 srv/hackclock/views/blocks/js/blocks/speaker.js delete mode 100644 srv/hackclock/views/blocks/js/generators/speaker.js diff --git a/README.md b/README.md index 2091164..255cfe5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ I'm assuming that you are starting with the Raspian Minimal Linux distribution. 1. Make sure your Raspberry Pi is up to date with the latest packages & firmware with `sudo apt-get update; sudo apt-get dist-upgrade` 2. Enable I2C by executing `sudo raspi-config` as described in Adafruit's tutorial: https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c -3. Add the necessary Python and GStreamer dependencies using `sudo apt-get install wiringpi python-bottle python-setuptools python-pip python-dev python-dateutil python-smbus gstreamer0.10-x gstreamer-tools gstreamer0.10-plugins-base gstreamer0.10-plugins-good gstreamer0.10-plugins-bad python-gst0.10` +3. Add the necessary Python and GStreamer dependencies using the command: `sudo apt-get install wiringpi python-bottle python-protobuf python-requests python-oauth2client python-httplib2 python-setuptools python-pip python-dev python-dateutil python-smbus gstreamer0.10-x gstreamer-tools gstreamer0.10-plugins-base gstreamer0.10-plugins-good gstreamer0.10-plugins-bad python-gst0.10` 4. Install hack-clock via `wget https://github.com/deckerego/hack-clock/releases/download/2.0.0/python-hackclock_2.0.1-1_all.deb; sudo dpkg -i python-hackclock_2.0.1-1_all.deb` 5. Tweak `/etc/hack-clock.conf` and `/etc/default/hack-clock` to fit your needs (GPIO pins, correct weather station, etc.). A list of observed weather stations is available at http://forecast.weather.gov/stations.php 6. Reboot your Pi to re-load modules and start the IDE web server diff --git a/debian/control b/debian/control index 81a9def..3b5eee0 100644 --- a/debian/control +++ b/debian/control @@ -10,5 +10,5 @@ X-Python-Version: >= 2.7 Package: python-hackclock Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, ntp, python-bottle, wiringpi, python-setuptools, python-pip, python-dev, python-dateutil, python-smbus, gstreamer0.10-x, gstreamer-tools, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-bad, python-gst0.10 +Depends: ${misc:Depends}, ${python:Depends}, ntp, python-bottle, python-requests, python-protobuf, python-httplib2, python-oauth2client, wiringpi, python-setuptools, python-pip, python-dev, python-dateutil, python-smbus, gstreamer0.10-x, gstreamer-tools, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-bad, python-gst0.10 Description: A hackable alarm clock for the Raspberry Pi diff --git a/etc/hack-clock.conf b/etc/hack-clock.conf index 36eb8ea..94492aa 100644 --- a/etc/hack-clock.conf +++ b/etc/hack-clock.conf @@ -1,8 +1,12 @@ { "default_editor": "/blocks/edit", "disable_editor_button": false, + "buttons_gpio": [23, 24], + "switches_gpio": [25] "weather_station": "KGPI", "ifttt_maker_key": "", + "google_username": "", + "google_password": "", "python_file": "/home/pi/hack-clock/run_clock.py", "blocks_file": "/home/pi/hack-clock/blocks_clock.xml", "audio_files": "/home/pi/hack-clock/audio", @@ -10,6 +14,4 @@ "lesson_files": "/home/pi/hack-clock/lessons", "webapp_files": "/srv/hackclock", "file_filter": ["README.md", ".DS_Store"], - "buttons_gpio": [23, 24], - "switches_gpio": [25] } diff --git a/lib/hackclock/runapp/Libs/GStreamer.py b/lib/hackclock/runapp/Libs/GStreamer.py index 5457f1c..e970543 100644 --- a/lib/hackclock/runapp/Libs/GStreamer.py +++ b/lib/hackclock/runapp/Libs/GStreamer.py @@ -5,9 +5,18 @@ import gst import gobject import os +import re +import logging from hackclock.config import configuration +logger = logging.getLogger('speaker') + +console = logging.StreamHandler() +console.setLevel(logging.INFO) +logger.addHandler(console) + class Speaker: + __HTTP_PATTERN = re.compile("http[s]*://.*") def __init__(self): self.pl = None @@ -17,26 +26,26 @@ def __init__(self): def play(self, fileName): self.playList([fileName]) - def playList(self, fileNames): + def playList(self, trackNames): if not self.isPlaying(): - # Create a queue of URIs - audio_dir = configuration.get('audio_files') - self.playlist = [ "file://"+os.path.abspath("%s/%s" % (audio_dir, fileName)) for fileName in fileNames ] + self.playlist = trackNames self.playlist.reverse() + self.play() - # Create the pipeline - self.pl = gst.element_factory_make("playbin2", "player") - self.pl.set_state(gst.STATE_READY) + def play(self): + # Create the pipeline + self.pl = gst.element_factory_make("playbin2", "player") + self.pl.set_state(gst.STATE_READY) - # Create the event bus - self.bus = self.pl.get_bus() - self.bus.add_signal_watch() - self.bus.connect("message", self.onMessage) - self.eventLoop = GtkEventLoop() - self.eventLoop.start() + # Create the event bus + self.bus = self.pl.get_bus() + self.bus.add_signal_watch() + self.bus.connect("message", self.onMessage) + self.eventLoop = GtkEventLoop() + self.eventLoop.start() - # Play next track on playlist - self.next() + # Play next track on playlist + self.next() def stop(self): self.pl.set_state(gst.STATE_NULL) @@ -47,8 +56,16 @@ def isPlaying(self): def next(self): if self.playlist: + track = self.playlist.pop() + + if not self.__HTTP_PATTERN.match(track): + # This is a filename - convert to URI + audio_dir = configuration.get('audio_files') + track = "file://"+os.path.abspath("%s/%s" % (audio_dir, track)) + + logger.info("Playing: %s" % track) self.pl.set_state(gst.STATE_READY) - self.pl.set_property('uri', self.playlist.pop()) + self.pl.set_property('uri', track) self.pl.set_state(gst.STATE_PLAYING) else: self.stop() diff --git a/lib/hackclock/runapp/Libs/GoogleMusic.py b/lib/hackclock/runapp/Libs/GoogleMusic.py new file mode 100644 index 0000000..5dc0951 --- /dev/null +++ b/lib/hackclock/runapp/Libs/GoogleMusic.py @@ -0,0 +1,33 @@ +from gmusicapi import Mobileclient +from GStreamer import Speaker +import logging +from hackclock.config import configuration + +logger = logging.getLogger('google_music') + +console = logging.StreamHandler() +console.setLevel(logging.INFO) +logger.addHandler(console) + +class GoogleMusic: + __username = configuration.get('google_username') + __password = configuration.get('google_password') + __track_prefetch = 10 + __client = None + + def __init__(self): + self.__client = Mobileclient() + self.__client.login(username, password, Mobileclient.FROM_MAC_ADDRESS) + + def __del__(self): + if self.__client: + self.__client.logout() + + def radioPlaylist(self, station_id = 'IFL'): + if not self.__client or not self.__client.is_authenticated(): + logger.error("Client is not authenticated!") + return [] + + tracklist = client.get_station_tracks(station_id, num_tracks=track_prefetch) + trackidlist = [track['id'] for track in tracklist if 'id' in track] + return [client.get_stream_url(trackid, quality='low') for trackid in trackidlist] diff --git a/requirements.txt b/requirements.txt index 12fc36d..cfa5289 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,8 @@ +# Base level requirements bitstring>=3.1.3 wiringpi2>=2.23.1 +# Google Music API requirements +gmusicapi>=10.1.0 +future>=0.16.0 +gpsoauth>=0.4.0 +MechanicalSoup>=0.6.0 diff --git a/srv/hackclock/views/blocks/editor.tpl b/srv/hackclock/views/blocks/editor.tpl index 94942bc..038f236 100644 --- a/srv/hackclock/views/blocks/editor.tpl +++ b/srv/hackclock/views/blocks/editor.tpl @@ -120,6 +120,7 @@ + @@ -182,7 +183,6 @@ - @@ -190,7 +190,6 @@ - diff --git a/srv/hackclock/views/blocks/js/blocks/audio.js b/srv/hackclock/views/blocks/js/blocks/audio.js index db91007..638949e 100644 --- a/srv/hackclock/views/blocks/js/blocks/audio.js +++ b/srv/hackclock/views/blocks/js/blocks/audio.js @@ -53,6 +53,83 @@ function loadAudioTools(workspace, toolbox, callback) { request.send(); } +Blockly.Blocks['speaker'] = { + init: function() { + this.jsonInit({ + "type": "block_type", + "message0": "Audio Speaker", + "output": 'Speaker', + "colour": 60, + "tooltip": "A speaker that lets you listen to music", + "helpUrl": "http://hackclock.deckerego.net/" + }); + + var thisBlock = this; + + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + 'A speaker that lets you listen to music'; + }); + } +}; + +Blockly.Blocks['play_list'] = { + init: function() { + this.jsonInit({ + "type": "play_list", + "message0": "On %1 Play %2", + "args0": [ + { + "type": "input_value", + "name": "speaker", + "check": "Speaker" + }, + { + "type": "input_value", + "name": "songs", + "check": "Array" + } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": 255, + "tooltip": "Play this list of songs", + "helpUrl": "http://hackclock.deckerego.net/" + }); + + var thisBlock = this; + + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + 'Play this list of songs'; + }); + } +}; + +Blockly.Blocks['google_music_radio'] = { + init: function() { + this.jsonInit({ + "type": "block_type", + "message0": "Google Music Radio", + "output": "Array", + "colour": 60, + "tooltip": "Play the \"I'm Feeling Lucky\" radio station on Google Music", + "helpUrl": "https://play.google.com/music/listen" + }); + + var thisBlock = this; + + this.setTooltip(function() { + var parent = thisBlock.getParent(); + return (parent && parent.getInputsInline() && parent.tooltip) || + 'Play the "I\'m Feeling Lucky" radio station on Google Music'; + }); + } +}; + Blockly.Blocks['is_playing'] = { init: function() { this.jsonInit({ diff --git a/srv/hackclock/views/blocks/js/blocks/speaker.js b/srv/hackclock/views/blocks/js/blocks/speaker.js deleted file mode 100644 index ef3538b..0000000 --- a/srv/hackclock/views/blocks/js/blocks/speaker.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -goog.provide('Blockly.Blocks.speaker'); - -goog.require('Blockly.Blocks'); - -Blockly.Blocks['speaker'] = { - init: function() { - this.jsonInit({ - "type": "block_type", - "message0": "Audio Speaker", - "output": 'Speaker', - "colour": 60, - "tooltip": "A speaker that lets you listen to music", - "helpUrl": "http://hackclock.deckerego.net/" - }); - - var thisBlock = this; - - this.setTooltip(function() { - var parent = thisBlock.getParent(); - return (parent && parent.getInputsInline() && parent.tooltip) || - 'A speaker that lets you listen to music'; - }); - } -}; - -Blockly.Blocks['play_list'] = { - init: function() { - this.jsonInit({ - "type": "play_list", - "message0": "On %1 Play %2", - "args0": [ - { - "type": "input_value", - "name": "speaker", - "check": "Speaker" - }, - { - "type": "input_value", - "name": "songs", - "check": "Array" - } - ], - "inputsInline": true, - "previousStatement": null, - "nextStatement": null, - "colour": 255, - "tooltip": "Play this list of songs", - "helpUrl": "http://hackclock.deckerego.net/" - }); - - var thisBlock = this; - - this.setTooltip(function() { - var parent = thisBlock.getParent(); - return (parent && parent.getInputsInline() && parent.tooltip) || - 'Play this list of songs'; - }); - } -}; diff --git a/srv/hackclock/views/blocks/js/generators/audio.js b/srv/hackclock/views/blocks/js/generators/audio.js index b5d9e0e..695882c 100644 --- a/srv/hackclock/views/blocks/js/generators/audio.js +++ b/srv/hackclock/views/blocks/js/generators/audio.js @@ -23,6 +23,27 @@ function loadAudioParser() { request.send(); } +Blockly.Python['speaker'] = function(block) { + Blockly.Python.definitions_['import_speaker'] = 'from hackclock.runapp.Libs.GStreamer import Speaker'; + Blockly.Python.definitions_['init_speaker'] = 'speaker = Speaker()'; + return ['speaker', Blockly.Python.ORDER_ATOMIC]; +}; + +Blockly.Python['google_music_radio'] = function(block) { + Blockly.Python.definitions_['import_speaker'] = 'from hackclock.runapp.Libs.GStreamer import Speaker'; + Blockly.Python.definitions_['import_googlemusic'] = 'from hackclock.runapp.Libs.GoogleMusic import GoogleMusic'; + Blockly.Python.definitions_['init_speaker'] = 'speaker = Speaker()'; + Blockly.Python.definitions_['init_googlemusic'] = 'google_music = GoogleMusic(speaker)'; + + return ['google_music.radioPlaylist()', Blockly.Python.ORDER_NONE]; +}; + +Blockly.Python['play_list'] = function(block) { + var value_speaker = Blockly.Python.valueToCode(block, 'speaker', Blockly.Python.ORDER_ATOMIC); + var value_songs = Blockly.Python.valueToCode(block, 'songs', Blockly.Python.ORDER_ATOMIC); + return ''+value_speaker+'.playList('+value_songs+')\n'; +}; + Blockly.Python['is_playing'] = function(block) { var value_speaker = Blockly.Python.valueToCode(block, 'speaker', Blockly.Python.ORDER_ATOMIC); return [value_speaker + '.isPlaying()', Blockly.Python.ORDER_NONE]; diff --git a/srv/hackclock/views/blocks/js/generators/speaker.js b/srv/hackclock/views/blocks/js/generators/speaker.js deleted file mode 100644 index 6b1f78a..0000000 --- a/srv/hackclock/views/blocks/js/generators/speaker.js +++ /dev/null @@ -1,15 +0,0 @@ -goog.provide('Blockly.Python.speaker'); - -goog.require('Blockly.Python'); - -Blockly.Python['speaker'] = function(block) { - Blockly.Python.definitions_['import_speaker'] = 'from hackclock.runapp.Libs.GStreamer import Speaker'; - Blockly.Python.definitions_['init_speaker'] = 'speaker = Speaker()'; - return ['speaker', Blockly.Python.ORDER_ATOMIC]; -}; - -Blockly.Python['play_list'] = function(block) { - var value_speaker = Blockly.Python.valueToCode(block, 'speaker', Blockly.Python.ORDER_ATOMIC); - var value_songs = Blockly.Python.valueToCode(block, 'songs', Blockly.Python.ORDER_ATOMIC); - return ''+value_speaker+'.playList('+value_songs+')\n'; -}; diff --git a/tests/localsettings.conf b/tests/localsettings.conf index f05a944..e3df3c3 100644 --- a/tests/localsettings.conf +++ b/tests/localsettings.conf @@ -1,6 +1,8 @@ { "weather_station": "KIND", "ifttt_maker_key": "", + "google_username": "", + "google_password": "", "default_editor": "/blocks/edit", "disable_editor_button": false, "python_file": "../../home/pi/hack-clock/run_clock.py", From 2198f4d9e6a38b0c887d8ca0e2f3442ddf0efe5a Mon Sep 17 00:00:00 2001 From: John Ellis Date: Wed, 1 Feb 2017 06:20:35 -0500 Subject: [PATCH 2/6] Trying to prune down requirements, but dependency resolution causes all of setuptools to be corrupted. --- debian/control | 2 +- requirements.txt | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/debian/control b/debian/control index 3b5eee0..38d02c9 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: hackclock Maintainer: DeckerEgo Section: python Priority: optional -Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 7.4.3) +Build-Depends: python-setuptools (>= 2.6.6-3), debhelper (>= 7.4.3) Standards-Version: 3.9.1 X-Python-Version: >= 2.7 diff --git a/requirements.txt b/requirements.txt index cfa5289..c597605 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,3 @@ -# Base level requirements bitstring>=3.1.3 wiringpi2>=2.23.1 -# Google Music API requirements gmusicapi>=10.1.0 -future>=0.16.0 -gpsoauth>=0.4.0 -MechanicalSoup>=0.6.0 From acb7e1659e95cd8096714f45c67bd2a3e1f6eabe Mon Sep 17 00:00:00 2001 From: John Ellis Date: Wed, 1 Feb 2017 22:29:35 -0500 Subject: [PATCH 3/6] Trying to avoid catastrophe with pip installations --- debian/python-hackclock.postinst | 2 +- tests/max_volume.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/python-hackclock.postinst b/debian/python-hackclock.postinst index a807291..1df1076 100644 --- a/debian/python-hackclock.postinst +++ b/debian/python-hackclock.postinst @@ -8,7 +8,7 @@ chown -R pi:pi /home/pi/hack-clock # Install those Python modules that only exist via pip echo "Installing missing Python modules..." -pip install --upgrade -r /usr/share/doc/hack-clock/requirements.txt +pip install -r /usr/share/doc/hack-clock/requirements.txt # Set the hifiberry-dac i2s module echo "Installing I2S audio..." diff --git a/tests/max_volume.sh b/tests/max_volume.sh index b3c405d..cbf49ab 100644 --- a/tests/max_volume.sh +++ b/tests/max_volume.sh @@ -1,3 +1,4 @@ #!/bin/sh amixer set PCM -- 100% +speaker-test -c 2 From 83c98562a09d4aa9843d96342bbaebd5f38d5de7 Mon Sep 17 00:00:00 2001 From: John Ellis Date: Wed, 1 Feb 2017 23:14:08 -0500 Subject: [PATCH 4/6] Fixed config file typo --- etc/hack-clock.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/hack-clock.conf b/etc/hack-clock.conf index 94492aa..e043165 100644 --- a/etc/hack-clock.conf +++ b/etc/hack-clock.conf @@ -2,7 +2,7 @@ "default_editor": "/blocks/edit", "disable_editor_button": false, "buttons_gpio": [23, 24], - "switches_gpio": [25] + "switches_gpio": [25], "weather_station": "KGPI", "ifttt_maker_key": "", "google_username": "", @@ -13,5 +13,5 @@ "backup_files": "/home/pi/hack-clock/backups", "lesson_files": "/home/pi/hack-clock/lessons", "webapp_files": "/srv/hackclock", - "file_filter": ["README.md", ".DS_Store"], + "file_filter": ["README.md", ".DS_Store"] } From c251786fd381fca8ffb9d2e11f77ad714969b800 Mon Sep 17 00:00:00 2001 From: John Ellis Date: Thu, 2 Feb 2017 23:02:40 -0500 Subject: [PATCH 5/6] Working version (kinda) of Google Music integration --- README.md | 2 +- debian/control | 2 +- lib/hackclock/runapp/Libs/GoogleMusic.py | 10 +++--- .../views/blocks/js/generators/audio.js | 2 +- tests/googlemusic.py | 34 +++++++++++++++++++ 5 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 tests/googlemusic.py diff --git a/README.md b/README.md index 255cfe5..1cd0d2a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ I'm assuming that you are starting with the Raspian Minimal Linux distribution. 1. Make sure your Raspberry Pi is up to date with the latest packages & firmware with `sudo apt-get update; sudo apt-get dist-upgrade` 2. Enable I2C by executing `sudo raspi-config` as described in Adafruit's tutorial: https://learn.adafruit.com/adafruits-raspberry-pi-lesson-4-gpio-setup/configuring-i2c -3. Add the necessary Python and GStreamer dependencies using the command: `sudo apt-get install wiringpi python-bottle python-protobuf python-requests python-oauth2client python-httplib2 python-setuptools python-pip python-dev python-dateutil python-smbus gstreamer0.10-x gstreamer-tools gstreamer0.10-plugins-base gstreamer0.10-plugins-good gstreamer0.10-plugins-bad python-gst0.10` +3. Add the necessary Python and GStreamer dependencies using the command: `sudo apt-get install wiringpi python-bottle python-requests python-oauth2client python-httplib2 python-setuptools python-pip python-dev python-dateutil python-smbus gstreamer0.10-x gstreamer-tools gstreamer0.10-plugins-base gstreamer0.10-plugins-good gstreamer0.10-plugins-bad gstreamer0.10-plugins-ugly python-gst0.10` 4. Install hack-clock via `wget https://github.com/deckerego/hack-clock/releases/download/2.0.0/python-hackclock_2.0.1-1_all.deb; sudo dpkg -i python-hackclock_2.0.1-1_all.deb` 5. Tweak `/etc/hack-clock.conf` and `/etc/default/hack-clock` to fit your needs (GPIO pins, correct weather station, etc.). A list of observed weather stations is available at http://forecast.weather.gov/stations.php 6. Reboot your Pi to re-load modules and start the IDE web server diff --git a/debian/control b/debian/control index 38d02c9..00e0764 100644 --- a/debian/control +++ b/debian/control @@ -10,5 +10,5 @@ X-Python-Version: >= 2.7 Package: python-hackclock Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, ntp, python-bottle, python-requests, python-protobuf, python-httplib2, python-oauth2client, wiringpi, python-setuptools, python-pip, python-dev, python-dateutil, python-smbus, gstreamer0.10-x, gstreamer-tools, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-bad, python-gst0.10 +Depends: ${misc:Depends}, ${python:Depends}, ntp, python-bottle, python-requests, python-httplib2, python-oauth2client, wiringpi, python-setuptools, python-pip, python-dev, python-dateutil, python-smbus, gstreamer0.10-x, gstreamer-tools, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-bad, python-gst0.10 Description: A hackable alarm clock for the Raspberry Pi diff --git a/lib/hackclock/runapp/Libs/GoogleMusic.py b/lib/hackclock/runapp/Libs/GoogleMusic.py index 5dc0951..edeac8e 100644 --- a/lib/hackclock/runapp/Libs/GoogleMusic.py +++ b/lib/hackclock/runapp/Libs/GoogleMusic.py @@ -12,12 +12,12 @@ class GoogleMusic: __username = configuration.get('google_username') __password = configuration.get('google_password') - __track_prefetch = 10 + __track_prefetch = 25 __client = None def __init__(self): self.__client = Mobileclient() - self.__client.login(username, password, Mobileclient.FROM_MAC_ADDRESS) + self.__client.login(self.__username, self.__password, Mobileclient.FROM_MAC_ADDRESS) def __del__(self): if self.__client: @@ -28,6 +28,8 @@ def radioPlaylist(self, station_id = 'IFL'): logger.error("Client is not authenticated!") return [] - tracklist = client.get_station_tracks(station_id, num_tracks=track_prefetch) + tracklist = self.__client.get_station_tracks(station_id, num_tracks=self.__track_prefetch) + logger.info("Received tracks: %r" % str(tracklist)) + trackidlist = [track['id'] for track in tracklist if 'id' in track] - return [client.get_stream_url(trackid, quality='low') for trackid in trackidlist] + return [self.__client.get_stream_url(trackid, quality='low') for trackid in trackidlist] diff --git a/srv/hackclock/views/blocks/js/generators/audio.js b/srv/hackclock/views/blocks/js/generators/audio.js index 695882c..3b46044 100644 --- a/srv/hackclock/views/blocks/js/generators/audio.js +++ b/srv/hackclock/views/blocks/js/generators/audio.js @@ -33,7 +33,7 @@ Blockly.Python['google_music_radio'] = function(block) { Blockly.Python.definitions_['import_speaker'] = 'from hackclock.runapp.Libs.GStreamer import Speaker'; Blockly.Python.definitions_['import_googlemusic'] = 'from hackclock.runapp.Libs.GoogleMusic import GoogleMusic'; Blockly.Python.definitions_['init_speaker'] = 'speaker = Speaker()'; - Blockly.Python.definitions_['init_googlemusic'] = 'google_music = GoogleMusic(speaker)'; + Blockly.Python.definitions_['init_googlemusic'] = 'google_music = GoogleMusic()'; return ['google_music.radioPlaylist()', Blockly.Python.ORDER_NONE]; }; diff --git a/tests/googlemusic.py b/tests/googlemusic.py new file mode 100644 index 0000000..8bae409 --- /dev/null +++ b/tests/googlemusic.py @@ -0,0 +1,34 @@ +#/bin/python + +print "Loading Libraries" +from GoogleMusic import GoogleMusic +import pygst +pygst.require('0.10') +import gst + +def on_tag(bus, msg): + taglist = msg.parse_tag() + print 'on_tag:' + for key in taglist.keys(): + print '\t%s = %s' % (key, taglist[key]) + +print "Initializing Google Music" +music = GoogleMusic() + +print "Fetching Playlist" +playlist = music.radioPlaylist() +print playlist + +print "Initializing Player" +player = gst.element_factory_make("playbin", "player") +player.set_state(gst.STATE_READY) +player.set_property('uri', playlist[0]) +player.set_state(gst.STATE_PLAYING) + +print "Monitoring Bus" +bus = player.get_bus() +bus.enable_sync_message_emission() +bus.add_signal_watch() +bus.connect('message::tag', on_tag) + +raw_input('Press enter to stop playing...') From f99ae132c06c8fd698c396d7e31d9738d03c9fc8 Mon Sep 17 00:00:00 2001 From: John Ellis Date: Fri, 3 Feb 2017 20:46:30 -0500 Subject: [PATCH 6/6] Changed Google Music to have its stream act more like a list, since the URLs expire in 60s --- .gitignore | 1 + etc/default/hack-clock | 2 +- lib/hackclock/runapp/Libs/GoogleMusic.py | 41 +++++++++++++++---- lib/hackclock/webapp/routes.py | 23 +++++++---- srv/hackclock/views/blocks/editor.tpl | 4 ++ .../views/blocks/js/generators/audio.js | 6 +-- tests/googlemusic.py | 10 ++--- tests/localsettings.conf | 16 -------- tests/max_volume.sh | 5 ++- 9 files changed, 63 insertions(+), 45 deletions(-) delete mode 100644 tests/localsettings.conf diff --git a/.gitignore b/.gitignore index 5fcefaa..820b4c5 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ home/pi/hack-clock/backups/run_clock.* home/pi/hack-clock/backups/blocks_clock.* home/pi/hack-clock/blocks_clock.xml home/pi/hack-clock/run_clock.py +tests/localsettings.conf diff --git a/etc/default/hack-clock b/etc/default/hack-clock index cdb4846..1dfff4d 100644 --- a/etc/default/hack-clock +++ b/etc/default/hack-clock @@ -5,4 +5,4 @@ BUTTON_BCM_PIN=(23 24) SWITCH_BCM_PIN=(25) # Volume level to feed to ALSA SoftVol -MASTER_VOLUME='75%' +MASTER_VOLUME='65%' diff --git a/lib/hackclock/runapp/Libs/GoogleMusic.py b/lib/hackclock/runapp/Libs/GoogleMusic.py index edeac8e..9313eb1 100644 --- a/lib/hackclock/runapp/Libs/GoogleMusic.py +++ b/lib/hackclock/runapp/Libs/GoogleMusic.py @@ -1,35 +1,58 @@ from gmusicapi import Mobileclient -from GStreamer import Speaker +from gmusicapi import exceptions import logging from hackclock.config import configuration +import json logger = logging.getLogger('google_music') console = logging.StreamHandler() -console.setLevel(logging.INFO) +console.setLevel(logging.WARNING) logger.addHandler(console) -class GoogleMusic: +class AudioStream: __username = configuration.get('google_username') __password = configuration.get('google_password') - __track_prefetch = 25 + __track_prefetch = 15 __client = None + __playlist = [] - def __init__(self): + def __init__(self, station_id = 'IFL'): self.__client = Mobileclient() self.__client.login(self.__username, self.__password, Mobileclient.FROM_MAC_ADDRESS) + self.__playlist = self.__fetchTrackIDs(station_id) def __del__(self): if self.__client: self.__client.logout() - def radioPlaylist(self, station_id = 'IFL'): + def __fetchTrackIDs(self, station_id): if not self.__client or not self.__client.is_authenticated(): logger.error("Client is not authenticated!") return [] tracklist = self.__client.get_station_tracks(station_id, num_tracks=self.__track_prefetch) - logger.info("Received tracks: %r" % str(tracklist)) + logger.info("Received tracks: %r" % json.dumps(tracklist)) - trackidlist = [track['id'] for track in tracklist if 'id' in track] - return [self.__client.get_stream_url(trackid, quality='low') for trackid in trackidlist] + songids = [track['id'] for track in tracklist if 'id' in track] + nautids = [track['nid'] for track in tracklist if 'nid' in track] + + return songids + nautids + + def pop(self): + while self.__playlist: + try: + track_id = self.__playlist.pop() + stream_url = self.__client.get_stream_url(track_id, quality='low') + return stream_url + except(exceptions.CallFailure): + logger.warning("Failed to fetch Stream URL for ID %s" % trackid) + + raise IndexError("pop from empty list") + + def reverse(self): + # Reverse just returns itself, since the playlist is already chaos + return self + + def __len__(self): + return len(self.__playlist) diff --git a/lib/hackclock/webapp/routes.py b/lib/hackclock/webapp/routes.py index 973d65d..8ceb295 100644 --- a/lib/hackclock/webapp/routes.py +++ b/lib/hackclock/webapp/routes.py @@ -146,7 +146,7 @@ def python_lesson_event_loop(clock, file_id): try: code_file = open(lesson_file, 'r') python_save_event_loop(clock, code_file) - return template('python/editor', switch_visible=switch_visible, status="Saved") + return template('python/editor', switch_visible=switch_visible, status="Opened") except: return template('python/editor', switch_visible=switch_visible, status="Failed") @@ -161,7 +161,7 @@ def python_restore_event_loop(clock, file_id): try: code_file = open(restored_file, 'r') python_save_event_loop(clock, code_file) - return template('python/editor', switch_visible=switch_visible, status="Saved") + return template('python/editor', switch_visible=switch_visible, status="Opened") except: return template('python/editor', switch_visible=switch_visible, status="Failed") @@ -181,7 +181,10 @@ def send_blocks_js(filename): @application.get('/blocks/edit') def blocks_edit_event_loop(clock): switch_visible = not configuration.get('disable_editor_button') - return template('blocks/editor', switch_visible=switch_visible, status="Opened") + google_music = configuration.get('google_username') and configuration.get('google_password') + ifttt_maker = configuration.get('ifttt_maker_key') + + return template('blocks/editor', switch_visible=switch_visible, google_music=google_music, ifttt_maker=ifttt_maker, status="Opened") @application.get('/blocks/read') def blocks_read_event_loop(clock): @@ -258,14 +261,17 @@ def blocks_backup_list(clock): def blocks_lesson_event_loop(clock, file_id): lesson_dir = configuration.get('lesson_files') lesson_file = "%s/%s/blocks_clock.xml" % (lesson_dir, file_id) + switch_visible = not configuration.get('disable_editor_button') + google_music = configuration.get('google_username') and configuration.get('google_password') + ifttt_maker = configuration.get('ifttt_maker_key') try: code_file = open(lesson_file, 'r') blocks_save_event_loop(clock, code_file) - return template('blocks/editor', switch_visible=switch_visible, status="Saved") + return template('blocks/editor', switch_visible=switch_visible, google_music=google_music, ifttt_maker=ifttt_maker, status="Opened") except: - return template('blocks/editor', switch_visible=switch_visible, status="Failed") + return template('blocks/editor', switch_visible=switch_visible, google_music=google_music, ifttt_maker=ifttt_maker, status="Failed") @application.get('/blocks/restore/') def blocks_restore_event_loop(clock, file_id): @@ -273,14 +279,17 @@ def blocks_restore_event_loop(clock, file_id): files = listdir(version_dir) restored_files = filter(lambda f: f.startswith('blocks_clock.') and int(parser.parse(f.lstrip("blocks_clock.")).strftime("%s")) == file_id, files) restored_file = "%s/%s" % (version_dir, restored_files[0]) + switch_visible = not configuration.get('disable_editor_button') + google_music = configuration.get('google_username') and configuration.get('google_password') + ifttt_maker = configuration.get('ifttt_maker_key') try: code_file = open(restored_file, 'r') blocks_save_event_loop(clock, code_file) - return template('blocks/editor', switch_visible=switch_visible, status="Saved") + return template('blocks/editor', switch_visible=switch_visible, google_music=google_music, ifttt_maker=ifttt_maker, status="Opened") except: - return template('blocks/editor', switch_visible=switch_visible, status="Failed") + return template('blocks/editor', switch_visible=switch_visible, google_music=google_music, ifttt_maker=ifttt_maker, status="Failed") # Clock REST API @application.post('/clock/restart') diff --git a/srv/hackclock/views/blocks/editor.tpl b/srv/hackclock/views/blocks/editor.tpl index 038f236..5af763e 100644 --- a/srv/hackclock/views/blocks/editor.tpl +++ b/srv/hackclock/views/blocks/editor.tpl @@ -120,7 +120,9 @@ + % if google_music: + % end @@ -132,9 +134,11 @@ + % if ifttt_maker: + % end diff --git a/srv/hackclock/views/blocks/js/generators/audio.js b/srv/hackclock/views/blocks/js/generators/audio.js index 3b46044..02bc26a 100644 --- a/srv/hackclock/views/blocks/js/generators/audio.js +++ b/srv/hackclock/views/blocks/js/generators/audio.js @@ -31,11 +31,11 @@ Blockly.Python['speaker'] = function(block) { Blockly.Python['google_music_radio'] = function(block) { Blockly.Python.definitions_['import_speaker'] = 'from hackclock.runapp.Libs.GStreamer import Speaker'; - Blockly.Python.definitions_['import_googlemusic'] = 'from hackclock.runapp.Libs.GoogleMusic import GoogleMusic'; + Blockly.Python.definitions_['import_googlemusic'] = 'from hackclock.runapp.Libs.GoogleMusic import AudioStream'; Blockly.Python.definitions_['init_speaker'] = 'speaker = Speaker()'; - Blockly.Python.definitions_['init_googlemusic'] = 'google_music = GoogleMusic()'; + Blockly.Python.definitions_['init_googlemusic'] = 'audio_stream = AudioStream()'; - return ['google_music.radioPlaylist()', Blockly.Python.ORDER_NONE]; + return ['audio_stream', Blockly.Python.ORDER_NONE]; }; Blockly.Python['play_list'] = function(block) { diff --git a/tests/googlemusic.py b/tests/googlemusic.py index 8bae409..dc9a244 100644 --- a/tests/googlemusic.py +++ b/tests/googlemusic.py @@ -1,7 +1,7 @@ #/bin/python print "Loading Libraries" -from GoogleMusic import GoogleMusic +from GoogleMusic import AudioStream import pygst pygst.require('0.10') import gst @@ -13,16 +13,12 @@ def on_tag(bus, msg): print '\t%s = %s' % (key, taglist[key]) print "Initializing Google Music" -music = GoogleMusic() - -print "Fetching Playlist" -playlist = music.radioPlaylist() -print playlist +audio_stream = AudioStream() print "Initializing Player" player = gst.element_factory_make("playbin", "player") player.set_state(gst.STATE_READY) -player.set_property('uri', playlist[0]) +player.set_property('uri', audio_stream.pop()) player.set_state(gst.STATE_PLAYING) print "Monitoring Bus" diff --git a/tests/localsettings.conf b/tests/localsettings.conf deleted file mode 100644 index e3df3c3..0000000 --- a/tests/localsettings.conf +++ /dev/null @@ -1,16 +0,0 @@ -{ - "weather_station": "KIND", - "ifttt_maker_key": "", - "google_username": "", - "google_password": "", - "default_editor": "/blocks/edit", - "disable_editor_button": false, - "python_file": "../../home/pi/hack-clock/run_clock.py", - "blocks_file": "../../home/pi/hack-clock/blocks_clock.xml", - "audio_files": "../../home/pi/hack-clock/audio", - "backup_files": "../../home/pi/hack-clock/backups", - "lesson_files": "../../home/pi/hack-clock/lessons", - "file_filter": ["README.md", ".DS_Store"], - "buttons_gpio": [23, 24], - "switches_gpio": [25] -} diff --git a/tests/max_volume.sh b/tests/max_volume.sh index cbf49ab..287f5cd 100644 --- a/tests/max_volume.sh +++ b/tests/max_volume.sh @@ -1,4 +1,5 @@ #!/bin/sh -amixer set PCM -- 100% -speaker-test -c 2 +rm ~/.asoundrc +amixer set Master -- 100% +speaker-test -c2 -twav