Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,16 @@ by passing -r glib2 to twistd.
- py.test -f
- grunt watch-test

runs on port 8080

To use GoogleAPI you must add your email and password to the config.py
The user name and password must be valid google music accounts

Example
------
storage = GoogleMusicScanner('username', 'password')
If you have two factor auth this password needs to be added as an application password
The password needs to be added to your google account

Both py.test and grunt will stay running and retun the tests when the files
change. Grunt will also redeploy the JS and CSS.
7 changes: 6 additions & 1 deletion jukebox.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ storage: &storage
!!python/object/apply:jukebox.storage.MemoryStorage
args:

scanner:
scanners:
!!python/object/apply:jukebox.scanner.DirScanner
args:
- *storage
- '/home/armooo/Music/'
!!python/object/apply:jukebox.scanner.GoogleMusicScanner
args:
- *storage
- 'username/email'
- 'password'

encoder: !!python/name:jukebox.encoders.GSTEncoder

Expand Down
2 changes: 1 addition & 1 deletion jukebox/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def get_playlist(self, request):
@app.route('/playlist/add', methods=['POST'])
@defer.inlineCallbacks
def add_to_playlist(self, request):
pk = int(json.loads(request.content.getvalue())['pk'])
pk = json.loads(request.content.getvalue())['pk']
song = yield self.storage.get_song(pk)
self.playlist.add_song(song)
defer.returnValue('')
Expand Down
6 changes: 3 additions & 3 deletions jukebox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ def make_root_resource():


storage = config['storage']
scanner = config['scanner']
scanners = config['scanners']
playlist = config['playlist']
encoder = config['encoder']

reactor.callInThread(scanner.scan)
for scanner in scanners:
reactor.callInThread(scanner.scan)
api_server = API(storage, playlist)
source = Source(playlist, encoder)
httpd = HTTPd(api_server.app.resource(), Stream(source))
Expand Down
13 changes: 8 additions & 5 deletions jukebox/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,21 @@ class GSTEncoder(object):
zope.interface.classProvides(IEncoder)

def __init__(self, song, data_callback, done_callback):
import pygst
pygst.require('0.10')
import gst

self.song = song
self.data_callback = data_callback
self.done_callback = done_callback
self.url_callback = self.song.get_song_uri()
self.url_callback.addCallback(self.encode_callback)

def encode_callback(self, url):
import pygst
pygst.require('0.10')
import gst
self.encoder = gst.Pipeline('encoder')

decodebin = gst.element_factory_make('uridecodebin', 'uridecodebin')
decodebin.set_property('uri', self.song.uri)

decodebin.set_property('uri', url)
decodebin.connect('pad-added', self.on_new_pad)

audioconvert = gst.element_factory_make('audioconvert', 'audioconvert')
Expand Down
19 changes: 19 additions & 0 deletions jukebox/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import zope.interface
import mutagen

from gmusicapi import Mobileclient, Webclient

import jukebox.song


Expand All @@ -23,6 +25,23 @@ def stop():
"""


class GoogleMusicScanner(object):
zope.interface.implements(IScanner)

def __init__(self, storage, email, password):
self.storage = storage
self.email = email
self.password = password

def scan(self):
wclient = Webclient()
wclient.login(self.email, self.password)
mobile_client = Mobileclient()
mobile_client.login(self.email, self.password)
for song in mobile_client.get_all_songs():
self.storage.add_song(jukebox.song.GoogleSong(song, wclient))


class DirScanner(object):
zope.interface.implements(IScanner)

Expand Down
33 changes: 33 additions & 0 deletions jukebox/song.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from twisted.internet import threads
from twisted.internet.defer import Deferred, inlineCallbacks, returnValue


class Song(object):
def __init__(self, title, album, artist, uri):
self.pk = None
Expand All @@ -9,6 +13,35 @@ def __init__(self, title, album, artist, uri):
def __repr__(self):
return '<Song(pk={}, title={}>'.format(self.pk, self.title)

@inlineCallbacks
def get_song_uri(self):
d = Deferred()
d.callback(self.uri)
uriYield = yield d
returnValue(uriYield)

class GoogleSong(Song):
def __init__(self, google_song, web_api):
self.instance = google_song
self.web_api = web_api
self.title = google_song['title']
self.album = google_song['album']
self.artist = google_song['artist']
self.pk = self.id = google_song['id']

def uri(self):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the bad.

urls = self.web_api.get_stream_urls(self.id)
# Normal tracks return a single url, All Access tracks return multiple urls,
# which must be combined and are not supported
if len(urls) == 1:
return urls[0]
raise Exception(u'Bad Url')

@inlineCallbacks
def get_song_uri(self):
url = yield threads.deferToThread(self.uri)
returnValue(url)


class Playlist(object):
def __init__(self):
Expand Down
14 changes: 13 additions & 1 deletion jukebox/storage.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import itertools
from gmusicapi import Mobileclient, Webclient

import zope.interface
import twisted.internet.defer as defer
from twisted.internet.defer import (
Deferred,
DeferredList,
inlineCallbacks,
returnValue
)
from jukebox.song import Song, GoogleSong


class GoogleAuthenticationError(Exception):
pass


class IStorage(zope.interface.Interface):
Expand Down Expand Up @@ -53,7 +65,7 @@ def get_song(self, pk):

def add_song(self, song):
d = defer.Deferred()
song.pk = next(self._seq)
song.pk = str(next(self._seq))
self._store[song.pk] = song
d.callback(song.pk)
return d
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
author='Jason Michalski',
author_email='armooo@armooo.net',
packages=find_packages(exclude=['tests']),
install_requires=['klein', 'mutagen', 'pyyaml',],
install_requires=['klein', 'mutagen', 'pyyaml', 'gmusicapi'],
)
28 changes: 17 additions & 11 deletions test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_few_songs(self):
request = mock.Mock(name='request')
storage = jukebox.storage.MemoryStorage()
pks = []
for i in range(3):
for i in range(1, 4):
song = jukebox.song.Song(
title='song %s' % i,
album='album %s' % i,
Expand All @@ -63,34 +63,40 @@ def test_few_songs(self):
)
pk = yield storage.add_song(song)
pks.append(pk)

api = jukebox.api.API(storage=storage)

result = yield api.all_songs(request)
feed_result = yield api.all_songs(request)

request.setHeader.assert_called_with(
'Content-Type', 'application/json'
)

assert result == json.dumps({'songs': [
result = json.loads(feed_result)

self.assertTrue('songs' in result)
sorted_result = sorted(result['songs'], key=lambda song: song['pk'])

assert sorted_result == [
{
'pk': pks[0],
'title': 'song 0',
'album': 'album 0',
'artist': 'artist 0',
},
{
'pk': pks[1],
'title': 'song 1',
'album': 'album 1',
'artist': 'artist 1',
},
{
'pk': pks[2],
'pk': pks[1],
'title': 'song 2',
'album': 'album 2',
'artist': 'artist 2',
},
]})
{
'pk': pks[2],
'title': 'song 3',
'album': 'album 3',
'artist': 'artist 3',
},
]


def test_get_playlist_queue():
Expand Down
2 changes: 2 additions & 0 deletions test/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import twisted.internet.defer as defer

import jukebox.storage
from jukebox.storage import GoogleAuthenticationError
import jukebox.song

import util
from mock import patch, Mock


class IStorage(object):
Expand Down