Merge lp:~submarine/unity-scope-clementine/clementine-previews into lp:unity-scope-clementine

Proposed by Mark Tully
Status: Merged
Approved by: David Callé
Approved revision: 32
Merged at revision: 23
Proposed branch: lp:~submarine/unity-scope-clementine/clementine-previews
Merge into: lp:unity-scope-clementine
Diff against target: 513 lines (+157/-233)
3 files modified
data/clementine.scope.in (+1/-1)
src/unity_clementine_daemon.py (+144/-220)
tests/test_clementine.py (+12/-12)
To merge this branch: bzr merge lp:~submarine/unity-scope-clementine/clementine-previews
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
David Callé Approve
Review via email: mp+159933@code.launchpad.net

Commit message

Add Previews and Activation.

Description of the change

Add Previews and Activation.

Now using SQL queries to do the searching rather than using SQL to grab everything and then searching within the scope

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
David Callé (davidc3) wrote :

Lookgs good. +1

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://jenkins.qa.ubuntu.com/job/unity-scope-clementine-autolanding/2/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-scope-clementine-raring-amd64-autolanding/2

review: Needs Fixing (continuous-integration)
Revision history for this message
David Callé (davidc3) wrote :

Jenkins hiccup. Approved again.

Revision history for this message
PS Jenkins bot (ps-jenkins) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/clementine.scope.in'
2--- data/clementine.scope.in 2013-03-20 16:43:38 +0000
3+++ data/clementine.scope.in 2013-04-22 13:02:11 +0000
4@@ -5,7 +5,7 @@
5 QueryBinary=clementine
6 _Keywords=clementine;
7 RequiredMetadata=
8-OptionalMetadata=
9+OptionalMetadata=album[s];artist[s];genre[s];year[i];track_length[i];track_number[i]
10 Loader=/usr/share/unity-scopes/clementine/unity_clementine_daemon.py
11 RemoteContent=false
12 Type=music
13
14=== modified file 'src/unity_clementine_daemon.py'
15--- src/unity_clementine_daemon.py 2013-03-19 15:40:44 +0000
16+++ src/unity_clementine_daemon.py 2013-04-22 13:02:11 +0000
17@@ -18,14 +18,11 @@
18 from gi.repository import Unity
19 import gettext
20 import urllib.parse
21-import dbus
22 import hashlib
23 import unicodedata
24 import os
25-import sys
26 import shutil
27 import sqlite3
28-import dbus
29
30 APP_NAME = 'unity-scope-clementine'
31 LOCAL_PATH = '/usr/share/locale/'
32@@ -45,7 +42,7 @@
33 DEFAULT_RESULT_MIMETYPE = 'taglib/mp3'
34 DEFAULT_RESULT_TYPE = Unity.ResultType.PERSONAL
35 CLEMENTINE_DBFILE = os.getenv("HOME") + "/.config/Clementine/clementine.db"
36-CLEMENTIME_BACKUP_FILE = os.getenv("HOME") + "/.config/Clementine/clementine-scope-backup.db"
37+CLEMENTINE_BACKUP_FILE = os.getenv("HOME") + "/.config/Clementine/clementine-scope-backup.db"
38
39 c1 = {'id': 'songs',
40 'name': _('Songs'),
41@@ -59,66 +56,64 @@
42
43 FILTERS = []
44
45-m1 = {'id' :'album',
46- 'type' :'s',
47- 'field':Unity.SchemaFieldType.OPTIONAL}
48-m2 = {'id' :'artist',
49- 'type' :'s',
50- 'field':Unity.SchemaFieldType.OPTIONAL}
51-m3 = {'id' :'genre',
52- 'type' :'s',
53- 'field':Unity.SchemaFieldType.OPTIONAL}
54-m4 = {'id' :'year',
55- 'type' :'i',
56- 'field':Unity.SchemaFieldType.OPTIONAL}
57-EXTRA_METADATA = [m1, m2, m3, m4]
58-
59-REFRESH_TIMEOUT = 300
60-PREVIEW_PLAYER_DBUS_NAME = "com.canonical.Unity.Lens.Music.PreviewPlayer"
61-PREVIEW_PLAYER_DBUS_PATH = "/com/canonical/Unity/Lens/Music/PreviewPlayer"
62-PREVIEW_PLAYER_DBUS_IFACE = PREVIEW_PLAYER_DBUS_NAME
63-
64-tracks = []
65-
66-
67-def get_music_from_clementine():
68- '''
69- Parses Clementine's database into a form we can use
70- '''
71- # Copy clementine's database to a backup so we can run searches on that rather than the main database
72+m1 = {'id': 'album',
73+ 'type': 's',
74+ 'field': Unity.SchemaFieldType.OPTIONAL}
75+m2 = {'id': 'artist',
76+ 'type': 's',
77+ 'field': Unity.SchemaFieldType.OPTIONAL}
78+m3 = {'id': 'genre',
79+ 'type': 's',
80+ 'field': Unity.SchemaFieldType.OPTIONAL}
81+m4 = {'id': 'year',
82+ 'type': 'i',
83+ 'field': Unity.SchemaFieldType.OPTIONAL}
84+m5 = {'id': 'track_length',
85+ 'type': 'i',
86+ 'field': Unity.SchemaFieldType.OPTIONAL}
87+m6 = {'id': 'track_number',
88+ 'type': 'i',
89+ 'field': Unity.SchemaFieldType.OPTIONAL}
90+EXTRA_METADATA = [m1, m2, m3, m4, m5, m6]
91+
92+SEARCH_SQL = '''SELECT title, filename, artist, album, albumartist, art_automatic, year, genre, art_manual, track, length
93+ FROM songs
94+ WHERE album LIKE '%%%s%%' OR artist LIKE '%%%s%%' OR title LIKE '%%%s%%' ORDER BY track'''
95+
96+ALBUM_SQL = '''SELECT title, filename, artist, album, albumartist, art_automatic, year, genre, art_manual, track, length
97+ FROM songs
98+ WHERE album LIKE '%%%s%%' AND artist LIKE '%%%s%%' ORDER BY track'''
99+
100+
101+def get_music_from_clementine(query):
102+ '''
103+ Parses Clementine's database into a form we can use using the supplied SQL query
104+ '''
105 tracks = []
106- if not os.path.exists(CLEMENTINE_DBFILE):
107+ if not os.path.exists(CLEMENTINE_BACKUP_FILE):
108 return tracks
109
110- shutil.copy2(CLEMENTINE_DBFILE, CLEMENTIME_BACKUP_FILE)
111-
112- # Grab all the data we need from the backup
113- conn = sqlite3.connect(CLEMENTIME_BACKUP_FILE)
114+ conn = sqlite3.connect(CLEMENTINE_BACKUP_FILE)
115 cursor = conn.cursor()
116- # Go through the safe and grab track names, their uris, the artist name, the album title and the track's mimetypes
117- # We'll have to call them all mp3s as the mimetype isn't explicitly in the database, but as long as it's an audio mimetype, it shouldn't matter
118- cursor.execute('''SELECT title, filename, artist, album, "taglib/mp3", albumartist, art_automatic, year, genre, art_manual, track, length
119- FROM songs
120- ORDER BY track''')
121+ cursor.execute(query)
122 tracks = cursor.fetchall()
123 cursor.close()
124- print("Updated tracks from Clementine database")
125 return tracks
126
127
128 def get_album_art(track):
129 # First try manually set album art
130- if not track[9] is None:
131- if not track[9] == "(embedded)":
132- return track[9]
133+ if not track[8] is None:
134+ if not track[8] == "(embedded)":
135+ return track[8]
136
137 # Next try automatically set album art
138- if not track[6] is None:
139- if not track[6] == "(embedded)":
140- return track[6]
141+ if not track[5] is None:
142+ if not track[5] == "(embedded)":
143+ return track[5]
144
145 # Try thumbnailing any embedded album art and use that
146- hashname = '%s\t%s' % (unicodedata.normalize("NFKD", track[5]), unicodedata.normalize("NFKD", track[3]))
147+ hashname = '%s\t%s' % (unicodedata.normalize("NFKD", track[4]), unicodedata.normalize("NFKD", track[3]))
148 file_hash = hashlib.md5(hashname.encode('utf-8')).hexdigest()
149 tb_filename = os.path.join(os.path.expanduser("~/.cache/media-art"), ("album-" + file_hash)) + ".jpg"
150 if os.path.exists(tb_filename):
151@@ -149,171 +144,81 @@
152 Search for help documents matching the search string
153 '''
154 results = []
155- tracks = get_music_from_clementine()
156- trackresults = []
157+ shutil.copy2(CLEMENTINE_DBFILE, CLEMENTINE_BACKUP_FILE)
158+ tracks = get_music_from_clementine(SEARCH_SQL % (search, search, search))
159 albumresults = []
160
161 for track in tracks:
162- title = u"" if track[0] is None else track[0]
163- uri = u"" if track[1] is None else track[1]
164- artist = u"" if track[2] is None else track[2]
165- album = u"" if track[3] is None else track[3]
166- mimetype = u"" if track[4] is None else track[4]
167- albumartist = u"" if track[5] is None else track[5]
168- year = 0 if track[7] is None else track[7]
169- genre = u"" if track[8] is None else track[8]
170- trackname = title + u" - " + album + u" - " + artist
171- if search.lower() in trackname.lower():
172- albumart = get_album_art(track)
173- albumuri = "album://" + albumartist + "/" + album
174- if track not in trackresults:
175- results.append({'uri': uri.decode('utf-8'),
176- 'icon': albumart,
177- 'category': 0,
178- 'mimetype': mimetype,
179- 'title': title,
180- 'comment': artist,
181- 'album':GLib.Variant('s', album),
182- 'artist':GLib.Variant('s', artist),
183- 'genre':GLib.Variant('s', genre),
184- 'year':GLib.Variant('i', year)})
185- trackresults.append(track)
186-
187- if album not in albumresults:
188- results.append({'uri': albumuri,
189- 'icon': albumart,
190- 'category': 1,
191- 'mimetype': mimetype,
192- 'title': album,
193- 'comment': artist,
194- 'album':GLib.Variant('s', album),
195- 'artist':GLib.Variant('s', artist),
196- 'genre':GLib.Variant('s', genre),
197- 'year':GLib.Variant('i', year)})
198- albumresults.append(album)
199+ title = "" if track[0] is None else track[0]
200+ uri = "" if track[1] is None else track[1].decode('utf-8')
201+ artist = "" if track[2] is None else track[2]
202+ album = "" if track[3] is None else track[3]
203+ albumartist = "" if track[4] is None else track[4]
204+ year = 0 if track[6] is None else track[6]
205+ genre = "" if track[7] is None else track[7]
206+ track_length = 0 if track[10] is None else track[10] / 1000000000
207+ track_number = 0 if track[9] is None else track[9]
208+
209+ albumart = get_album_art(track)
210+ albumuri = "album://" + albumartist + "/" + album
211+ results.append({'uri': urllib.parse.unquote(uri),
212+ 'icon': albumart,
213+ 'category': 0,
214+ 'title': title,
215+ 'album': GLib.Variant('s', album),
216+ 'artist': GLib.Variant('s', artist),
217+ 'genre': GLib.Variant('s', genre),
218+ 'year': GLib.Variant('i', year),
219+ 'track_length': GLib.Variant('i', track_length),
220+ 'track_number': GLib.Variant('i', track_number)})
221+
222+ if album not in albumresults:
223+ results.append({'uri': albumuri,
224+ 'icon': albumart,
225+ 'category': 1,
226+ 'title': album,
227+ 'album': GLib.Variant('s', album),
228+ 'artist': GLib.Variant('s', artist),
229+ 'genre': GLib.Variant('s', genre),
230+ 'year': GLib.Variant('i', year)})
231+ albumresults.append(album)
232 return results
233
234
235-def activate(scope, uri):
236- import subprocess
237- albumtracks = []
238- albumtracks.append("clementine")
239- albumtracks.append("-a")
240- # If uri starts with album:// then we need to play all the songs on it
241- if uri.startswith("album://"):
242- for track in tracks:
243- album = "album://" + track[5] + "/" + track[3]
244- if not album.find(uri) == -1:
245- albumtrack = urllib.parse.unquote(str(track[1]))
246- albumtracks.append(albumtrack)
247- subprocess.Popen(albumtracks)
248- else:
249- albumtracks.append(uri)
250- subprocess.Popen(albumtracks)
251- return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')
252-
253-
254-def show_in_folder(scope, uri):
255- """ Shows the folder containing the selected track as requested from the Preview
256- """
257- if uri.startswith("album://"):
258- for track in tracks:
259- album = "album://" + track[2] + "/" + track[3]
260- if not album.find(uri) == -1:
261- filename = track[1]
262- continue
263- else:
264- filename = uri
265- dirname = os.path.dirname(filename)
266- dirname = dirname.replace("%20", "\ ")
267- os.system("xdg-open '%s'" % str(dirname))
268- return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')
269-
270-
271-def preview_uri(scope, uri):
272- """Preview request handler"""
273- albumtracks = []
274- isalbum = False
275- if uri.startswith("album://"):
276- isalbum = True
277- for track in tracks:
278- album = "album://" + track[2] + "/" + track[3]
279- if not album.find(uri) == -1:
280- albumtracks.append(track)
281- albumtracks.sort(key=lambda track: int(track[7]))
282- else:
283- for track in tracks:
284- album = "file://" + track[1]
285- if not album.find(uri) == -1:
286- albumtracks.append(track)
287- iteration = model.get_first_iter()
288- end_iter = model.get_last_iter()
289- while iteration != end_iter:
290- if model.get_value(iteration, 0) == uri:
291- title = model.get_value(iteration, 5)
292- description = model.get_value(iteration, 6)
293- if model.get_value(iteration, 1) == "musique":
294- image = "file:///usr/share/icons/hicolor/scalable/apps/audacious.svg"
295- else:
296- image = "file://%s" % model.get_value(iteration, 1)
297-
298- preview = Unity.MusicPreview.new(title, description, None)
299- preview.props.image_source_uri = image
300- for albumtrack in albumtracks:
301- if isalbum:
302- track = Unity.TrackMetadata.full("file://" + urllib.parse.unquote(str(albumtrack[1])), # uri
303- int(albumtrack[7]), # track number
304- albumtrack[0], # track title
305- albumtrack[2], # artist
306- albumtrack[3], # album
307- int(albumtrack[8]) / 1000) # track length
308- else:
309- preview = Unity.MusicPreview.new(albumtrack[0], "", None)
310- preview.props.image_source_uri = image
311- track = Unity.TrackMetadata.full("file://" + urllib.parse.unquote(str(albumtrack[1])),
312- int(albumtrack[7]),
313- albumtrack[0],
314- albumtrack[2],
315- albumtrack[3],
316- int(albumtrack[8]) / 1000)
317+class Preview(Unity.ResultPreviewer):
318+
319+ def do_run(self):
320+ album = self.result.metadata['album'].get_string()
321+ artist = self.result.metadata['artist'].get_string()
322+ preview = Unity.MusicPreview.new(self.result.title, '', None)
323+ preview.props.image_source_uri = 'file://%s' % self.result.icon_hint
324+ preview.props.subtitle = self.result.metadata['artist'].get_string()
325+ if self.result.uri.startswith("album://"):
326+ tracks = get_music_from_clementine(ALBUM_SQL % (album, artist))
327+ for track in tracks:
328+ track = Unity.TrackMetadata.full(track[1].decode('utf-8'),
329+ track[9],
330+ track[0],
331+ track[2],
332+ track[3],
333+ track[10] / 1000000000)
334 preview.add_track(track)
335-
336- # Add the "Play" action
337- play_action = Unity.PreviewAction.new("activate_uri", "Play", None)
338- play_action.connect("activated", activate)
339- preview.add_action(play_action)
340-
341- # Add the "Show in folder" action
342- show_action = Unity.PreviewAction.new("show_in_folder", "Show In Folder", None)
343- show_action.connect("activated", show_in_folder)
344- preview.add_action(show_action)
345-
346- preview.connect("play", play)
347- preview.connect("pause", pause)
348- preview.connect("closed", closed)
349- break
350- iteration = model.next(iteration)
351- if preview is None:
352- print("Couldn't find model row for requested preview uri: '%s'", uri)
353- return preview
354-
355-
356-def play(preview, uri):
357- """Plays the selected track as selected in the Preview"""
358- player = self.bus.get_object(PREVIEW_PLAYER_DBUS_NAME, PREVIEW_PLAYER_DBUS_PATH)
359- dbus.Interface(player, PREVIEW_PLAYER_DBUS_IFACE).Play(uri)
360-
361-
362-def pause(preview, uri):
363- """Pauses the selected track as selected in the Preview"""
364- player = self.bus.get_object(PREVIEW_PLAYER_DBUS_NAME, PREVIEW_PLAYER_DBUS_PATH)
365- dbus.Interface(player, PREVIEW_PLAYER_DBUS_IFACE).Pause()
366-
367-
368-def closed(preview):
369- """Stops playing when the previre is closed"""
370- player = self.bus.get_object(PREVIEW_PLAYER_DBUS_NAME, PREVIEW_PLAYER_DBUS_PATH)
371- dbus.Interface(player, PREVIEW_PLAYER_DBUS_IFACE).Close()
372+ else:
373+ track = Unity.TrackMetadata.full(self.result.uri,
374+ self.result.metadata['track_number'].get_int32(),
375+ self.result.title,
376+ self.result.metadata['artist'].get_string(),
377+ self.result.metadata['album'].get_string(),
378+ self.result.metadata['track_length'].get_int32())
379+ preview.add_track(track)
380+
381+ icon = Gio.FileIcon.new(Gio.file_new_for_path(PROVIDER_ICON))
382+ view_action = Unity.PreviewAction.new("play", _("Play"), None)
383+ preview.add_action(view_action)
384+ show_action = Unity.PreviewAction.new("show", _("Show in Folder"), None)
385+ preview.add_action(show_action)
386+ return preview
387+
388
389 # Classes below this point establish communication
390 # with Unity, you probably shouldn't modify them.
391@@ -348,19 +253,8 @@
392 i['comment'] = ''
393 if not 'dnd_uri' in i or not i['dnd_uri'] or i['dnd_uri'] == '':
394 i['dnd_uri'] = i['uri']
395- i['metadata'] = {}
396- if EXTRA_METADATA:
397- for e in i:
398- for m in EXTRA_METADATA:
399- if m['id'] == e:
400- i['metadata'][e] = i[e]
401- i['metadata']['provider_credits'] = GLib.Variant('s', PROVIDER_CREDITS)
402- result = Unity.ScopeResult.create(str(i['uri']), str(i['icon']),
403- i['category'], i['result_type'],
404- str(i['mimetype']), str(i['title']),
405- str(i['comment']), str(i['dnd_uri']),
406- i['metadata'])
407- result_set.add_result(result)
408+ i['provider_credits'] = GLib.Variant('s', PROVIDER_CREDITS)
409+ result_set.add_result(**i)
410 except Exception as error:
411 print(error)
412
413@@ -416,6 +310,36 @@
414 se = MySearch(search_context)
415 return se
416
417+ def do_activate(self, result, metadata, id):
418+ album = result.metadata['album'].get_string()
419+ artist = result.metadata['artist'].get_string()
420+
421+ if id == 'show':
422+ if result.uri.startswith("album://"):
423+ tracks = get_music_from_clementine(ALBUM_SQL % (album, artist))
424+ filename = tracks[0][1].decode('utf-8')
425+ else:
426+ filename = result.uri
427+ dirname = os.path.dirname(filename)
428+ os.system("xdg-open '%s'" % str(dirname))
429+ else:
430+ albumtracks = ''
431+ if result.uri.startswith('album://'):
432+ tracks = get_music_from_clementine(ALBUM_SQL % (album, artist))
433+ for track in tracks:
434+ albumtracks = '%s \'%s\'' % (albumtracks, track[1].decode('utf-8'))
435+ else:
436+ albumtracks = result.uri
437+ os.system('clementine -a %s' % albumtracks)
438+
439+ return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri=None)
440+
441+ def do_create_previewer(self, result, metadata):
442+ rp = Preview()
443+ rp.set_scope_result(result)
444+ rp.set_search_metadata(metadata)
445+ return rp
446+
447
448 def load_scope():
449 return Scope()
450
451=== modified file 'tests/test_clementine.py'
452--- tests/test_clementine.py 2013-02-18 15:48:48 +0000
453+++ tests/test_clementine.py 2013-04-22 13:02:11 +0000
454@@ -11,17 +11,18 @@
455 self.results = []
456
457 def do_add_result(self, result):
458- self.results.append({'uri':result.uri,
459- 'title':result.title,
460- 'comment':result.comment,
461- 'icon':result.icon_hint})
462+ self.results.append({'uri': result.uri,
463+ 'title': result.title,
464+ 'comment': result.comment,
465+ 'icon': result.icon_hint})
466+
467
468 class ScopeTestCase(TestCase):
469 def init_scope(self, scope_path):
470 self.scope_module = imp.load_source('scope', scope_path)
471 self.scope = self.scope_module.load_scope()
472
473- def perform_query(self, query, filter_set = Unity.FilterSet.new()):
474+ def perform_query(self, query, filter_set=Unity.FilterSet.new()):
475 result_set = ResultSet()
476 ctx = Unity.SearchContext.create(query, 0, filter_set,
477 None, result_set, None)
478@@ -30,7 +31,7 @@
479 return result_set
480
481
482-class TestAskUbuntu(ScopeTestCase):
483+class TestClementine(ScopeTestCase):
484 def setUp(self):
485 self.init_scope('src/unity_clementine_daemon.py')
486
487@@ -38,10 +39,10 @@
488 self.scope = None
489 self.scope_module = None
490
491- def test_questions_search(self):
492+ def test_search(self):
493 self.scope_module.CLEMENTINE_DBFILE = 'tests/data/mock_clementine_pass.db'
494- self.scope_module.CLEMENTIME_BACKUP_FILE = 'tests/data/mock_clementine_backup.db'
495- expected_results = ["file:///home/mark/Music/Bell%20X1/Flock/01.%20Reacharound.mp3",
496+ self.scope_module.CLEMENTINE_BACKUP_FILE = 'tests/data/mock_clementine_backup.db'
497+ expected_results = ["file:///home/mark/Music/Bell X1/Flock/01. Reacharound.mp3",
498 "Reacharound",
499 "album://Bell X1/Flock",
500 "Flock"]
501@@ -54,10 +55,9 @@
502 results.append(result_set.results[1]['title'])
503 self.assertEqual(results, expected_results)
504
505-
506- def test_questions_failing_search(self):
507+ def test_failing_search(self):
508 self.scope_module.CLEMENTINE_DBFILE = 'tests/data/mock_clementine_fail'
509- self.scope_module.CLEMENTIME_BACKUP_FILE = 'tests/data/mock_clementine_backup.db'
510+ self.scope_module.CLEMENTINE_BACKUP_FILE = 'tests/data/mock_clementine_backup.db'
511 for s in ['upnriitnyt']:
512 result_set = self.perform_query(s)
513 self.assertEqual(len(result_set.results), 0)

Subscribers

People subscribed via source and target branches

to all changes: