Skip to content
This repository was archived by the owner on Apr 8, 2025. It is now read-only.

Commit 4e4bc07

Browse files
committed
Merge pull request #123 from sindrig/v1.5.0
V1.5.0
2 parents 1dc6c5e + b4e996b commit 4e4bc07

File tree

7 files changed

+108
-38
lines changed

7 files changed

+108
-38
lines changed

README.rst

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ Requirements
77

88
Python 2.7 or Python 3.3+
99

10-
See requirements.txt for required python packages.
10+
See requirements.txt for required python packages. They will be automatically installed when you install via pip.
1111

12-
You will need a Spotify Premium account.
12+
You will need a Spotify Premium account and a Spotify username (if you signed up via Facebook you can `follow the instructions here <https://community.spotify.com/t5/Help-Accounts-and-Subscriptions/How-do-i-find-my-username-when-using-Facebook-login/td-p/859795>`_ to get your username).
1313

1414
You will need libspotify, libffi-dev and libasound2-dev installed.
1515

16-
Use your distribution's package manager for libffi-dev and libasound2-dev.
16+
Use your distribution's package manager for libffi-dev and libasound2-dev (f.x. :code:`apt-get install libffi-dev libasound2-dev`).
1717

1818
To install libspotify, see `Pyspotify installation <https://pyspotify.mopidy.com/en/latest/installation/#install-from-source>`_. (It's also available in the `AUR <https://aur.archlinux.org/packages/libspotify/>`_).
1919

@@ -34,10 +34,7 @@ Development
3434
1. Create a virtualenv (python 2.7 or python 3.3+)
3535
2. Clone this project
3636
3. Activate your virtualenv
37-
4. Install requirements
38-
39-
* pip install -r requirements.txt
40-
37+
4. Install requirements (:code:`pip install -r requirements.txt`)
4138
5. Run :code:`python scripts/spoppy` (you will be asked for username/password)
4239

4340
DBus integration
@@ -46,13 +43,9 @@ DBus integration
4643
1. Run `make install_dbus`
4744
2. Make sure you have python-gobject2 installed
4845
3. Symlink gi (and possibly glib) to your virtualenv (that is, if you're not installing globally!)
49-
50-
* ln -s /usr/lib/python3.5/site-packages/gi/ $VIRTUAL_ENV/lib/python3.5/site-packages/gi
51-
* ln -s /usr/lib/python3.5/site-packages/glib/ $VIRTUAL_ENV/lib/python3.5/site-packages/glib
52-
5346
4. The service will be available at "/com/spoppy" (f.x. :code:`qdbus com.spoppy /com/spoppy com.spoppy.PlayPause`)
5447

5548
Testing
5649
=========
5750

58-
:code:`python setup.py test`
51+
:code:`make test`

spoppy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
def get_version():
13-
return '1.4.6'
13+
return '1.5.0'
1414

1515
if click:
1616
@click.command()

spoppy/menus.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class Menu(object):
101101
BACKSPACE = b'\x7f'
102102
UP_ARROW = b'\x1b[A'
103103
DOWN_ARROW = b'\x1b[B'
104+
PAGE_UP = b'\x1b[5~'
105+
PAGE_DOWN = b'\x1b[6~'
104106
PAGE = 0
105107

106108
def __init__(self, navigator):
@@ -126,14 +128,17 @@ def get_response(self):
126128
if response == Menu.BACKSPACE:
127129
self.filter = self.filter[:-1]
128130
return responses.NOOP
129-
elif response == Menu.UP_ARROW:
130-
logger.debug('Got UP_ARROW')
131+
elif response in (Menu.UP_ARROW, Menu.PAGE_UP):
132+
logger.debug('Got UP_ARROW/PAGE_UP')
131133
self.PAGE = max([self.PAGE - 1, 0])
132134
return responses.NOOP
133-
elif response == Menu.DOWN_ARROW:
134-
logger.debug('Got DOWN_ARROW')
135+
elif response in (Menu.DOWN_ARROW, Menu.PAGE_DOWN):
136+
logger.debug('Got DOWN_ARROW/PAGE_DOWN')
135137
self.PAGE += 1
136138
return responses.NOOP
139+
elif response.startswith(b'\x1b'):
140+
logger.debug('Got unknown character %s' % repr(response))
141+
return responses.NOOP
137142

138143
self.filter += response.decode('utf-8')
139144
if self.filter.endswith('\n'):
@@ -211,6 +216,10 @@ def get_options(self):
211216
),
212217
'st': MenuValue('Search for tracks', TrackSearch(self.navigator)),
213218
'sa': MenuValue('Search for albums', AlbumSearch(self.navigator)),
219+
'ss': MenuValue(
220+
'Search for artists',
221+
ArtistSearch(self.navigator)
222+
),
214223
}
215224
if not self.navigator.spotipy_client:
216225
res['li'] = MenuValue(
@@ -386,7 +395,6 @@ def album_selected():
386395
def get_options_from_search(self):
387396
results = {}
388397
for i, album in enumerate(
389-
album for album in
390398
self.search.results.results
391399
):
392400
results[str(self.get_res_idx(i)).rjust(4)] = MenuValue(
@@ -403,6 +411,36 @@ def get_mock_playlist(self):
403411
)
404412

405413

414+
class ArtistSearchResults(TrackSearchResults):
415+
search = None
416+
417+
def select_artist(self, artist_idx):
418+
def artist_selected():
419+
res = ArtistSelected(self.navigator)
420+
res.artist = self.search.results.results[artist_idx]
421+
return res
422+
return artist_selected
423+
424+
def get_options_from_search(self):
425+
results = {}
426+
for i, artist_browser in enumerate(
427+
self.search.results.results
428+
):
429+
results[str(self.get_res_idx(i)).rjust(4)] = MenuValue(
430+
artist_browser.artist.name, self.select_artist(i)
431+
)
432+
return results
433+
434+
def get_mock_playlist(self):
435+
track_results = list(chain(*[
436+
artist.tracks for
437+
artist in self.search.results.results
438+
]))
439+
return MockPlaylist(
440+
self.get_mock_playlist_name(), track_results
441+
)
442+
443+
406444
class TrackSearch(Menu):
407445
is_searching = False
408446
search_pattern = ''
@@ -466,6 +504,11 @@ class AlbumSearch(TrackSearch):
466504
result_cls = AlbumSearchResults
467505

468506

507+
class ArtistSearch(TrackSearch):
508+
search_type = 'artists'
509+
result_cls = ArtistSearchResults
510+
511+
469512
class PlayListSelected(Menu):
470513
playlist = None
471514
deleting = False
@@ -506,7 +549,11 @@ def cancel_delete_playlist(self):
506549
return self
507550

508551
def get_tracks(self):
509-
return self.playlist.tracks
552+
return [
553+
track for track in
554+
self.playlist.tracks
555+
if track.availability != TrackAvailability.UNAVAILABLE
556+
]
510557

511558
def get_name(self):
512559
return self.playlist.name
@@ -517,11 +564,7 @@ def get_options(self):
517564
results['y'] = MenuValue('Yes', self.do_delete_playlist)
518565
results['n'] = MenuValue('No', self.cancel_delete_playlist)
519566
else:
520-
for i, track in enumerate(
521-
track for track in
522-
self.get_tracks()
523-
if track.availability != TrackAvailability.UNAVAILABLE
524-
):
567+
for i, track in enumerate(self.get_tracks()):
525568
results[str(i + 1).rjust(4)] = MenuValue(
526569
format_track(track), self.select_song(i)
527570
)
@@ -553,6 +596,12 @@ def get_header(self):
553596
return 'Are you sure you want to delete playlist [%s]' % (
554597
self.get_name()
555598
)
599+
return '%s (total %d tracks)' % (
600+
self.get_header_text(),
601+
len(self.get_tracks())
602+
)
603+
604+
def get_header_text(self):
556605
return 'Playlist [%s] selected' % self.get_name()
557606

558607

@@ -572,10 +621,27 @@ def get_tracks(self):
572621
def get_name(self):
573622
return format_album(self.album)
574623

575-
def get_header(self):
624+
def get_header_text(self):
576625
return 'Album [%s] selected' % self.get_name()
577626

578627

628+
class ArtistSelected(AlbumSelected):
629+
artist = None
630+
_tracks = None
631+
632+
def get_tracks(self):
633+
if not self._tracks:
634+
self._tracks = self.artist.tracks
635+
logger.debug('Artist has %d tracks' % len(self._tracks))
636+
return self._tracks
637+
638+
def get_name(self):
639+
return self.artist.artist.name
640+
641+
def get_header_text(self):
642+
return 'Artist [%s] selected' % self.get_name()
643+
644+
579645
class SongSelectedWhilePlaying(Menu):
580646
playlist = None
581647
track = None

spoppy/navigation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def update_progress(self, status, start, perc, end):
127127
end or ''
128128
)
129129
progress_width = self.get_ui_width() - len(s) + 2
130-
s = s % ('#'*int(perc*progress_width)).ljust(progress_width)
130+
if perc > 1:
131+
perc = 1
132+
s = s % ('#' * int(perc * progress_width)).ljust(progress_width)
131133

132134
click.echo(s, nl=False)
133135

spoppy/players.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def get_ui(self):
240240
right_side_items = [
241241
'Shuffle on' if self.shuffle else '',
242242
'Repeat: %s' % self.repeat,
243+
'%d of %d' % (self.current_track_idx + 1, len(self.song_order))
243244
]
244245
songs_to_show = (
245246
list(range(

spoppy/search.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import requests
1010
from spotify.track import Track, TrackAvailability
1111
from spotify.album import Album
12+
from spotify.artist import Artist
1213

1314
logger = logging.getLogger(__name__)
1415

@@ -41,7 +42,7 @@ class Search(threading.Thread):
4142
),
4243
'artists': (
4344
u'/v1/search?query={query}&offset=0&limit=20&type=artist',
44-
None
45+
Artist
4546
),
4647
}
4748
BASE_URL = 'https://api.spotify.com'
@@ -64,6 +65,8 @@ def __init__(self, session, query='', callback=None,
6465
self.BASE_URL + endpoint.format(query=self.query)
6566
)
6667

68+
self.results = self.get_empty_results()
69+
6770
super(Search, self).__init__()
6871

6972
self.start()
@@ -73,13 +76,14 @@ def run(self):
7376
logger.debug('Getting %s' % self.endpoint)
7477
r = requests.get(self.endpoint)
7578
r.raise_for_status()
76-
except requests.exceptions.RequestException:
77-
self.results = self.get_empty_results()
78-
else:
7979
response_data = r.json()[self.search_type]
80-
self.handle_results(response_data)
81-
82-
self.loaded_event.set()
80+
self.results = self.handle_results(response_data)
81+
except requests.exceptions.RequestException:
82+
logger.exception('RequestException')
83+
except Exception:
84+
logger.exception('Something went wrong while handling results')
85+
finally:
86+
self.loaded_event.set()
8387

8488
def get_empty_results(self):
8589
return SearchResults(self.query, [], 0, 0)
@@ -90,7 +94,7 @@ def handle_results(self, response_data):
9094
for item in response_data['items']
9195
])
9296

93-
self.results = SearchResults(
97+
return SearchResults(
9498
self.query,
9599
item_results,
96100
response_data['offset'],
@@ -111,4 +115,8 @@ def manipulate_items(self, items):
111115
item.load() for item in items
112116
if item.availability != TrackAvailability.UNAVAILABLE
113117
]
118+
elif self.search_type == 'artists':
119+
return [
120+
item.browse().load() for item in items
121+
]
114122
raise TypeError('Unknown search type %s' % self.search_type)

spoppy/util.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ def format_track(track):
5656
)
5757

5858

59-
def format_album(album):
59+
def format_album(album_browser):
6060
return '%s by %s [%s]' % (
61-
album.album.name,
62-
album.artist.name,
63-
album.album.year
61+
album_browser.album.name,
62+
album_browser.artist.name,
63+
album_browser.album.year
6464
)
6565

6666

0 commit comments

Comments
 (0)