diff --git a/etc/nginx/fastcgi_params b/etc/nginx/fastcgi_params
index 7e997c3fb..6f286814d 100755
--- a/etc/nginx/fastcgi_params
+++ b/etc/nginx/fastcgi_params
@@ -1,5 +1,7 @@
#
# 2017-MM-DD TC moOde 4.0
+# 2018-MM-DD TC moOde 4.3
+# - revised fastcgi_params
#
fastcgi_param QUERY_STRING $query_string;
@@ -28,9 +30,21 @@ fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
-# TC for performance bump
+# TC moOde 4.3 testing
+fastcgi_buffers 8 16k;
+fastcgi_buffer_size 32k;
fastcgi_read_timeout 600000;
-fastcgi_buffer_size 4k;
-fastcgi_buffers 4 32k;
fastcgi_busy_buffers_size 96k;
+# TC moOde 4.0 original
+#fastcgi_buffers 4 32k;
+#fastcgi_buffer_size 4k;
+#fastcgi_read_timeout 600000;
+#fastcgi_busy_buffers_size 96k;
+
+# TC article suggested
+#fastcgi_buffers 8 16k;
+#fastcgi_buffer_size 32k;
+#fastcgi_connect_timeout 300;
+#fastcgi_send_timeout 300;
+#fastcgi_read_timeout 300;
diff --git a/mpd/RADIO/Amys FM (320K).pls b/mpd/RADIO/Amys FM (320K).pls
new file mode 100755
index 000000000..e620dc6f1
--- /dev/null
+++ b/mpd/RADIO/Amys FM (320K).pls
@@ -0,0 +1,6 @@
+[playlist]
+numberofentries=1
+File1=http://94.23.222.12:8024/amysfm
+Title1=Amys FM (320K)
+Length1=-1
+Version=2
diff --git a/mpd/RADIO/Amys FM Spirit of Soul (320K).pls b/mpd/RADIO/Amys FM Spirit of Soul (320K).pls
new file mode 100755
index 000000000..b5e554ff5
--- /dev/null
+++ b/mpd/RADIO/Amys FM Spirit of Soul (320K).pls
@@ -0,0 +1,6 @@
+[playlist]
+numberofentries=1
+File1=http://91.121.59.45:10073/amysfmspiritofsoul
+Title1=Amys FM Spirit of Soul (320K)
+Length1=-1
+Version=2
diff --git a/mpd/mpd.conf.default b/mpd/mpd.conf.default
index e38116850..b3145f45d 100755
--- a/mpd/mpd.conf.default
+++ b/mpd/mpd.conf.default
@@ -29,7 +29,7 @@ buffer_before_play "10%"
max_output_buffer_size "81920"
id3v1_encoding "UTF-8"
filesystem_charset "UTF-8"
-max_connections "20"
+max_connections "128"
decoder {
plugin "ffmpeg"
diff --git a/mpd/playlists/Favorites.m3u b/mpd/playlists/Favorites.m3u
new file mode 100755
index 000000000..e69de29bb
diff --git a/other/build/build_recipe_v2.5.txt b/other/build/build_recipe_v2.6.txt
similarity index 94%
rename from other/build/build_recipe_v2.5.txt
rename to other/build/build_recipe_v2.6.txt
index a13740c29..0eced6382 100644
--- a/other/build/build_recipe_v2.5.txt
+++ b/other/build/build_recipe_v2.6.txt
@@ -1,8 +1,8 @@
################################################################
#
-# Build Recipe v2.5, 2018-07-11
+# Build Recipe v2.6, 2018-09-27
#
-# moOde 4.2
+# moOde 4.3
#
# These instructions are written for Linux Enthusiasts
# and System Integrators and provide a recipe for making
@@ -15,6 +15,15 @@
#
# Changes:
#
+# v2.6: Move timezone setting to after ssh login in STEP 2
+# Replace Browse tab w/Music tab in STEP 12 1b.
+# Correct STEP 1 Option 2 to use 2018-06-27 Stretch Lite
+# Update root symlink
+# Bump to kernel 4.14.72 in STEP 11
+# Add libaudiofile for wav to mpd compile in STEP 6
+# Add php7.0-gd in STEP 3
+# Add COMPONENT 4B - Librespot
+# Add 6. Patch for upmpdcli gmusic plugin to COMPONENT 6.
# v2.5: Add -DGPIO cflag to new Squeezelite compile in COMPONENT 5.
# Use BlueZ-master-4e926f8.zip in STEP 4
# Use bluez-alsa-master-88aefee.zip in STEP 4
@@ -151,8 +160,8 @@ sudo poweroff
// OPTION 2: Using Windows or Mac computer
////////////////////////////////////////////////////////////////
-1. Download Raspbian Stretch Lite 2017-11-29
-http://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2017-12-01/2017-11-29-raspbian-stretch-lite.zip
+1. Download Raspbian Stretch Lite 2018-06-27
+http://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2018-06-29/2018-06-27-raspbian-stretch-lite.zip
2. Unzip and install the .img file to an SD Card
https://www.raspberrypi.org/documentation/installation/installing-images/
@@ -194,9 +203,7 @@ net.ifnames=0
1. Insert the SD Card into a Raspberry Pi and POWER UP.
-2. sudo timedatectl set-timezone "America/Detroit"
-
-3. Change the current password (raspberry) to moodeaudio and the host name to moode.
+2. Change the current password (raspberry) to moodeaudio and the host name to moode.
ssh pi@raspberrypi (pwd=raspberry)
@@ -204,6 +211,10 @@ echo "pi:moodeaudio" | sudo chpasswd
sudo sed -i "s/raspberrypi/moode/" /etc/hostname
sudo sed -i "s/raspberrypi/moode/" /etc/hosts
+3. Change timezone to local time zone
+
+sudo timedatectl set-timezone "America/Detroit"
+
4. Download moOde application sources and configs.
//
@@ -213,8 +224,8 @@ sudo sed -i "s/raspberrypi/moode/" /etc/hosts
//
cd ~
-wget http://moodeaudio.org/downloads/prod/rel-stretch-r42.zip
-sudo unzip ./rel-stretch-r42.zip
+wget http://moodeaudio.org/downloads/prod/rel-stretch-r43.zip
+sudo unzip ./rel-stretch-r43.zip
5. Expand the root partition to 3GB.
@@ -257,7 +268,7 @@ sudo reboot
sudo apt-get update
-sudo apt-get -y install rpi-update php-fpm nginx sqlite3 php-sqlite3 memcached php-memcache mpc \
+sudo apt-get -y install rpi-update php-fpm nginx sqlite3 php-sqlite3 memcached php-memcache php7.0-gd mpc \
bs2b-ladspa libbs2b0 libasound2-plugin-equal telnet automake sysstat squashfs-tools tcpdump shellinabox \
samba smbclient udisks-glue ntfs-3g exfat-fuse git inotify-tools libav-tools avahi-utils
@@ -383,7 +394,7 @@ sudo chmod 0666 /etc/mpd.conf
2. Install MPD dev libs.
sudo apt-get -y install libmad0-dev libmpg123-dev libid3tag0-dev \
-libflac-dev libvorbis-dev libfaad-dev \
+libflac-dev libvorbis-dev libaudiofile-dev libfaad-dev \
libwavpack-dev \
libavcodec-dev libavformat-dev \
libmp3lame-dev \
@@ -423,7 +434,7 @@ sudo ./configure --enable-database --enable-libmpdclient --enable-alsa \
--disable-wildmidi --disable-sqlite --disable-jack --disable-ao --disable-oss \
--disable-ipv6 --disable-pulse --disable-nfs --disable-smbclient \
--disable-upnp --disable-expat --disable-lsr \
---disable-sndfile --disable-audiofile --disable-sidplay
+--disable-sndfile --disable-sidplay
5. Compile and install.
@@ -472,7 +483,7 @@ sudo mkdir /mnt/SDCARD
sudo ln -s /mnt/NAS /var/lib/mpd/music/NAS
sudo ln -s /mnt/SDCARD /var/lib/mpd/music/SDCARD
sudo ln -s /media /var/lib/mpd/music/USB
-sudo ln -s /var/lib/mpd/music /var/www/vlmm03846271
+sudo ln -s /var/lib/mpd/music /var/www/95187460
# Logs
sudo touch /var/log/moode.log
sudo chmod 0666 /var/log/moode.log
@@ -624,8 +635,8 @@ sudo reboot
// STEP 11 - Optionally install updated Linux Kernel
-# kernel ver 4.14.54
-echo "y" | sudo PRUNE_MODULES=1 rpi-update ec9d84e1d2ba701fd28897809269d8116b31dbf5
+# kernel ver 4.14.72
+echo "y" | sudo PRUNE_MODULES=1 rpi-update 0abe903f4a137e2738fe3be5f14a2a34afc9762b
sudo rm -rf /lib/modules.bak
sudo rm -rf /boot.bak
@@ -643,7 +654,7 @@ sudo reboot
1. Initial configuration
a. http://moode
-b. Browse Tab, Default Playlist, Add
+b. Music Tab, Browse button, Default Playlist, Add
c. Menu, Configure, Sources, UPDATE mpd database
d. Menu, Audio, Mpd options, EDIT SETTINGS, APPLY
e. Menu, System, Set timezone
@@ -746,14 +757,14 @@ sudo git clone https://github.com/hrkfdn/mpdas
cd mpdas
sudo make
sudo cp ./mpdas /usr/local/bin
-cd ~/
+cd ~
sudo rm -rf ./mpdas
sudo cp ./rel-stretch/usr/local/etc/mpdasrc.default /usr/local/etc/mpdasrc
sudo chmod 0755 /usr/local/etc/mpdasrc
////////////////////////////////////////////////////////////////
//
-// COMPONENT 4 - Shairport-sync
+// COMPONENT 4A - Shairport-sync
//
////////////////////////////////////////////////////////////////
@@ -772,6 +783,28 @@ cd ~
sudo rm -rf ./shairport-sync
sudo cp ./rel-stretch/usr/local/etc/shairport-sync.conf /usr/local/etc
+////////////////////////////////////////////////////////////////
+//
+// COMPONENT 4B - Librespot
+//
+////////////////////////////////////////////////////////////////
+
+sudo apt-get -y install portaudio19-dev
+
+cd ~
+git clone https://github.com/librespot-org/librespot
+curl https://sh.rustup.rs -sSf | sh
+# choose 1
+
+sudo reboot
+
+cd librespot/
+cargo build --release --features alsa-backend
+
+sudo cp target/release/librespot /usr/local/bin
+cd ~
+sudo rm -rf librespot
+
////////////////////////////////////////////////////////////////
//
// COMPONENT 5 - Squeezelite
@@ -860,6 +893,9 @@ sudo make install
cd ~
sudo rm -rf ./libupnppsamples-code
+6. Patch for upmpdcli gmusic plugin
+sudo cp ./rel-stretch/other/upmpdcli/session.py /usr/share/upmpdcli/cdplugins/gmusic
+
////////////////////////////////////////////////////////////////
//
// COMPONENT 7 - Optionally install gmusicapi
diff --git a/other/librespot/librespot-a4e0f58 b/other/librespot/librespot-a4e0f58
new file mode 100755
index 000000000..1e2e0eacb
Binary files /dev/null and b/other/librespot/librespot-a4e0f58 differ
diff --git a/other/librespot/librespot-master-a4e0f58.zip b/other/librespot/librespot-master-a4e0f58.zip
new file mode 100644
index 000000000..584563be5
Binary files /dev/null and b/other/librespot/librespot-master-a4e0f58.zip differ
diff --git a/other/mpd/mpd-0.20.20 b/other/mpd/mpd-0.20.20
index bf2c5bb56..9906ed96a 100755
Binary files a/other/mpd/mpd-0.20.20 and b/other/mpd/mpd-0.20.20 differ
diff --git a/other/shairport-sync/shairport-sync-3.1.7 b/other/shairport-sync/shairport-sync-3.1.7
deleted file mode 100755
index cc7eb93bf..000000000
Binary files a/other/shairport-sync/shairport-sync-3.1.7 and /dev/null differ
diff --git a/other/shairport-sync/shairport-sync-3.1.7.zip b/other/shairport-sync/shairport-sync-3.1.7.zip
deleted file mode 100644
index 2d3136f65..000000000
Binary files a/other/shairport-sync/shairport-sync-3.1.7.zip and /dev/null differ
diff --git a/other/shairport-sync/shairport-sync-3.2.1 b/other/shairport-sync/shairport-sync-3.2.1
new file mode 100755
index 000000000..ecbfc87cd
Binary files /dev/null and b/other/shairport-sync/shairport-sync-3.2.1 differ
diff --git a/other/shairport-sync/shairport-sync-3.2.1.zip b/other/shairport-sync/shairport-sync-3.2.1.zip
new file mode 100644
index 000000000..279a746b2
Binary files /dev/null and b/other/shairport-sync/shairport-sync-3.2.1.zip differ
diff --git a/other/upmpdcli/session.py b/other/upmpdcli/session.py
new file mode 100644
index 000000000..874c2036b
--- /dev/null
+++ b/other/upmpdcli/session.py
@@ -0,0 +1,396 @@
+# Copyright (C) 2016 J.F.Dockes
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+from __future__ import print_function
+
+import sys
+import json
+import datetime
+import time
+from upmplgmodels import Artist, Album, Track, Playlist, SearchResult, \
+ Category, Genre
+from gmusicapi import Mobileclient
+from upmplgutils import uplog
+
+class Session(object):
+ def __init__(self):
+ self.api = None
+ self.user = None
+ self.lib_albums = {}
+ self.lib_artists = {}
+ self.lib_tracks = {}
+ self.lib_updatetime = 0
+ self.sitdata = []
+ self.sitbyid = {}
+ self.sitdataupdtime = 0
+
+ def dmpdata(self, who, data):
+ uplog("%s: %s" % (who, json.dumps(data, indent=4)))
+
+ # Look for an Android device id in the registered devices.
+ def find_device_id(self, data):
+ for entry in data:
+ if "type" in entry and entry["type"] == u"ANDROID":
+ # Get rid of 0x
+ id = entry["id"][2:]
+ uplog("Using deviceid %s" % id)
+ return id
+ return None
+
+ def login(self, username, password, deviceid=None):
+ self.api = Mobileclient(debug_logging=False)
+
+ if deviceid is None:
+ logged_in = self.api.login(username, password,
+ Mobileclient.FROM_MAC_ADDRESS)
+ if logged_in:
+ # Try to re-login with a valid deviceid
+ data = self.api.get_registered_devices()
+ #self.dmpdata("registered devices", data)
+ deviceid = self.find_device_id(data)
+ if deviceid:
+ logged_in = self.login(username, password, deviceid)
+ else:
+ logged_in = self.api.login(username, password, deviceid)
+
+ isauth = self.api.is_authenticated()
+ #uplog("login: Logged in: %s. Auth ok: %s" % (logged_in, isauth))
+ return logged_in
+
+ def _get_user_library(self):
+ now = time.time()
+ if now - self.lib_updatetime < 300:
+ return
+ data = self.api.get_all_songs()
+ #self.dmpdata("all_songs", data)
+ self.lib_updatetime = now
+ tracks = [_parse_track(t) for t in data]
+ self.lib_tracks = dict([(t.id, t) for t in tracks])
+ for track in tracks:
+ # We would like to use the album id here, but gmusic
+ # associates the tracks with any compilations after
+ # uploading (does not use the metadata apparently), so
+ # that we can't (we would end up with multiple
+ # albums). OTOH, the album name is correct (so seems to
+ # come from the metadata). What we should do is test the
+ # album ids for one album with a matching title, but we're
+ # not sure to succeed. So at this point, the album id we
+ # end up storing could be for a different albums, and we
+ # should have a special library-local get_album_tracks
+ self.lib_albums[track.album.name] = track.album
+ self.lib_artists[track.artist.id] = track.artist
+
+ def get_user_albums(self):
+ self._get_user_library()
+ return self.lib_albums.values()
+
+ def get_user_artists(self):
+ self._get_user_library()
+ return self.lib_artists.values()
+
+ def get_user_playlists(self):
+ pldata = self.api.get_all_playlists()
+ #self.dmpdata("playlists", pldata)
+ return [_parse_playlist(pl) for pl in pldata]
+
+ def get_user_playlist_tracks(self, playlist_id):
+ self._get_user_library()
+ data = self.api.get_all_user_playlist_contents()
+ entries = []
+ for item in data:
+ if item['id'] == playlist_id:
+ entries = item['tracks']
+ break
+ if not entries:
+ return []
+ #self.dmpdata("user_playlist_content", entries)
+ tracks = []
+ for entry in entries:
+ if entry['deleted']:
+ continue
+ if entry['source'] == u'1':
+ tracks.append(self.lib_tracks[entry['trackId']])
+ elif 'track' in entry:
+ tracks.append(_parse_track(entry['track']) )
+ return tracks
+
+ def create_station_for_genre(self, genre_id):
+ id = self.api.create_station("station"+genre_id, genre_id=genre_id)
+ return id
+
+ def get_user_stations(self):
+ data = self.api.get_all_stations()
+ # parse_playlist works fine for stations
+ stations = [_parse_playlist(d) for d in data]
+ return stations
+
+ def delete_user_station(self, id):
+ self.api.delete_stations(id)
+
+ # not working right now
+ def listen_now(self):
+ print("api.get_listen_now_items()", file=sys.stderr)
+ ret = {'albums' : [], 'stations' : []}
+ try:
+ data = self.api.get_listen_now_items()
+ except Exception as err:
+ print("api.get_listen_now_items failed: %s" % err, file=sys.stderr)
+ data = None
+
+ # listen_now entries are not like normal albums or stations,
+ # and need special parsing. I could not make obvious sense of
+ # the station-like listen_now entries, so left them aside for
+ # now. Maybe should use create_station on the artist id?
+ if data:
+ ret['albums'] = [_parse_ln_album(a['album']) \
+ for a in data if 'album' in a]
+ #ret['stations'] = [_parse_ln_station(d['radio_station']) \
+ # for d in data if 'radio_station' in d]
+ else:
+ print("listen_now: no items returned !", file=sys.stderr)
+ print("get_listen_now_items: returning %d albums and %d stations" %\
+ (len(ret['albums']), len(ret['stations'])), file=sys.stderr)
+ return ret
+
+ def get_situation_content(self, id = None):
+ ret = {'situations' : [], 'stations' : []}
+ now = time.time()
+ if id is None and now - self.sitdataupdtime > 300:
+ self.sitbyid = {}
+ self.sitdata = self.api.get_listen_now_situations()
+ self.sitdataupdtime = now
+
+ # Root is special, it's a list of situations
+ if id is None:
+ ret['situations'] = [self._parse_situation(s) \
+ for s in self.sitdata]
+ return ret
+
+ # not root
+ if id not in self.sitbyid:
+ print("get_situation_content: %s unknown" % id, file=sys.stderr)
+ return ret
+
+ situation = self.sitbyid[id]
+ #self.dmpdata("situation", situation)
+ if 'situations' in situation:
+ ret['situations'] = [self._parse_situation(s) \
+ for s in situation['situations']]
+ if 'stations' in situation:
+ ret['stations'] = [_parse_situation_station(s) \
+ for s in situation['stations']]
+
+ return ret
+
+ def _parse_situation(self, data):
+ self.sitbyid[data['id']] = data
+ return Playlist(id=data['id'], name=data['title'])
+
+ def create_curated_and_get_tracks(self, id):
+ sid = self.api.create_station("station"+id, curated_station_id=id)
+ print("create_curated: sid %s"%sid, file=sys.stderr)
+ tracks = [_parse_track(t) for t in self.api.get_station_tracks(sid)]
+ #print("curated tracks: %s"%tracks, file=sys.stderr)
+ self.api.delete_stations(sid)
+ return tracks
+
+ def get_station_tracks(self, id):
+ return [_parse_track(t) for t in self.api.get_station_tracks(id)]
+
+ def get_media_url(self, song_id, quality=u'med'):
+ url = self.api.get_stream_url(song_id, quality=quality)
+ print("get_media_url got: %s" % url, file=sys.stderr)
+ return url
+
+ def get_album_tracks(self, album_id):
+ data = self.api.get_album_info(album_id, include_tracks=True)
+ album = _parse_album(data)
+ return [_parse_track(t, album) for t in data['tracks']]
+
+ def get_promoted_tracks(self):
+ data = self.api.get_promoted_songs()
+ #self.dmpdata("promoted_tracks", data)
+ return [_parse_track(t) for t in data]
+
+ def get_genres(self, parent=None):
+ data = self.api.get_genres(parent_genre_id=parent)
+ return [_parse_genre(g) for g in data]
+
+ def get_artist_info(self, artist_id, doRelated=False):
+ ret = {"albums" : [], "toptracks" : [], "related" : []}
+ # Happens,some library tracks have no artistId entry
+ if artist_id is None or artist_id == 'None':
+ uplog("get_artist_albums: artist_id is None")
+ return ret
+ else:
+ uplog("get_artist_albums: artist_id %s" % artist_id)
+
+ maxrel = 20 if doRelated else 0
+ maxtop = 0 if doRelated else 10
+ incalbs = False if doRelated else True
+ data = self.api.get_artist_info(artist_id, include_albums=incalbs,
+ max_top_tracks=maxtop,
+ max_rel_artist=maxrel)
+ #self.dmpdata("artist_info", data)
+ if 'albums' in data:
+ ret["albums"] = [_parse_album(alb) for alb in data['albums']]
+ if 'topTracks' in data:
+ ret["toptracks"] = [_parse_track(t) for t in data['topTracks']]
+ if 'related_artists' in data:
+ ret["related"] = [_parse_artist(a) for a in data['related_artists']]
+ return ret
+
+ def get_artist_related(self, artist_id):
+ data = self.get_artist_info(artist_id, doRelated=True)
+ return data["related"]
+
+ def search(self, query):
+ data = self.api.search(query, max_results=50)
+ #self.dmpdata("Search", data)
+
+ tr = [_parse_track(i['track']) for i in data['song_hits']]
+ ar = [_parse_artist(i['artist']) for i in data['artist_hits']]
+ al = [_parse_album(i['album']) for i in data['album_hits']]
+ #self.dmpdata("Search playlists", data['playlist_hits'])
+ try:
+ pl = [_parse_splaylist(i) for i in data['playlist_hits']]
+ except:
+ pl = []
+ return SearchResult(artists=ar, albums=al, playlists=pl, tracks=tr)
+
+
+
+def entryOrUnknown(data, name, default="Unknown"):
+ return data[name] if name in data else default
+
+
+def _parse_artist(data):
+ return Artist(id=data['artistId'], name=data['name'])
+
+def _parse_genre(data):
+ return Genre(id=data['id'], name=data['name'])
+
+def _parse_playlist(data):
+ return Playlist(id=data['id'], name=data['name'])
+
+def _parse_splaylist(data):
+ return Playlist(id=data['playlist']['shareToken'],
+ name=data['playlist']['name'])
+
+def _parse_situation_station(data):
+ return Playlist(id=data['seed']['curatedStationId'], name=data['name'])
+
+
+# 'id' source when initiated from playlist data:
+#
+# The previous version used the 'id' entry from the track data if set,
+# else 'nid'. This only worked for source=='1' entries, pointing to
+# user library data.
+#
+# The initial version for parsing non-user-lib playlist entries (which
+# have an embedded track record) used 'trackId' from the playlist
+# wrapper if set, else 'storeId' from the embedded track entry:
+#
+# {
+# 'trackid' : 'somevalue',
+# 'track': {
+# 'storeId': 'usuallysamevalue',
+# ...
+# },
+#
+# The merged version, for non-user entries, discards the data from the
+# playlist wrapper, and uses trackId or storeId from the embedded
+# track object. (trackId is usually not set inside the track record
+# and track['storeId'] appears to be the same as the wrapper trackId)
+#
+# Note that there is also an 'id' entry in the playlist wrapper, which
+# was never tried.
+def _parse_track(data, album=None):
+ artist_name = entryOrUnknown(data, 'artist')
+ albartist_name = entryOrUnknown(data, 'albumArtist', None)
+ #uplog("_parse_track: artist %s albartist %s"%(artist_name,albartist_name))
+ artistid = data["artistId"][0] if "artistId" in data else None
+ artist = Artist(id=artistid, name = artist_name)
+ albartist = Artist(id=artistid, name=albartist_name) if \
+ albartist_name is not None else artist
+ albid = entryOrUnknown(data, 'albumId', None)
+
+ if album is None:
+ #alb_artist = data['albumArtist'] if 'albumArtist' in data else ""
+ alb_art= data['albumArtRef'][0]["url"] if 'albumArtRef' in data else ""
+ alb_tt = entryOrUnknown(data, 'album')
+ album = Album(id=albid, name=alb_tt, image=alb_art, artist=artist)
+
+ if 'id' in data:
+ trackid = data['id']
+ elif 'trackId' in data:
+ trackid = data['trackId']
+ elif 'storeId' in data:
+ trackid = data['storeId']
+ elif 'nid' in data:
+ trackid = data['nid']
+ else:
+ trackid = ''
+
+ kwargs = {
+ 'id': trackid,
+ 'name': data['title'],
+ 'duration': int(data['durationMillis'])/1000,
+ 'track_num': data['trackNumber'],
+ 'disc_num': data['discNumber'],
+ 'artist': artist,
+ 'album': album,
+ #'artists': artists,
+ }
+ if 'genre' in data:
+ kwargs['genre'] = data['genre']
+ return Track(**kwargs)
+
+
+def _parse_ln_album(data):
+ artist = Artist(id=data['artist_metajam_id'], name=data['artist_name'])
+ kwargs = {
+ 'id': data['id']['metajamCompactKey'],
+ 'name' : data['id']['title'],
+ 'artist' : artist,
+ }
+ if 'images' in data:
+ kwargs['image'] = data['images'][0]['url']
+
+ return Album(**kwargs)
+
+
+def _parse_album(data, artist=None):
+ if artist is None:
+ artist_name = "Unknown"
+ if 'artist' in data:
+ artist_name = data['artist']
+ elif 'albumArtist' in data:
+ artist_name = data['albumArtist']
+ artist = Artist(name=artist_name)
+
+ kwargs = {
+ 'id': data['albumId'],
+ 'name': data['name'],
+ 'artist': artist,
+ }
+ if 'albumArtRef' in data:
+ kwargs['image'] = data['albumArtRef']
+
+ if 'year' in data:
+ kwargs['release_date'] = data['year']
+
+ return Album(**kwargs)
diff --git a/var/local/www/commandw/spotevent.sh b/var/local/www/commandw/spotevent.sh
new file mode 100755
index 000000000..158a10651
--- /dev/null
+++ b/var/local/www/commandw/spotevent.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+#
+# moOde audio player (C) 2014 Tim Curtis
+# http://moodeaudio.org
+#
+# This Program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# This Program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see