Merge lp:~scopes-hackers/unity-lens-music/rb-scope into lp:unity-lens-music
- rb-scope
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michal Hruby | ||||
Approved revision: | 102 | ||||
Merged at revision: | 73 | ||||
Proposed branch: | lp:~scopes-hackers/unity-lens-music/rb-scope | ||||
Merge into: | lp:unity-lens-music | ||||
Diff against target: |
813 lines (+663/-3) 11 files modified
configure.ac (+2/-1) po/POTFILES.in (+2/-0) po/POTFILES.skip (+2/-0) src/Makefile.am (+3/-0) src/banshee-scope.vala (+1/-0) src/daemon.vala (+4/-0) src/genre.vala (+18/-1) src/rhythmbox-collection.vala (+489/-0) src/rhythmbox-scope.vala (+116/-0) src/simple-scope.vala (+19/-0) src/track.vala (+7/-1) |
||||
To merge this branch: | bzr merge lp:~scopes-hackers/unity-lens-music/rb-scope | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michal Hruby (community) | Approve | ||
Mikkel Kamstrup Erlandsen (community) | Approve | ||
Neil J. Patel (community) | Approve | ||
Review via email:
|
Commit message
Implement a Rhythmbox scope.
Description of the change
Implement a Rhythmbox scope.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alex Launi (alexlauni) wrote : | # |
This looks pretty ok, but there's a commented out FIXME where one would launch an album, so it appears that playing an album hasn't been implemented. Did I miss something?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michal Hruby (mhr3) wrote : | # |
> This looks pretty ok, but there's a commented out FIXME where one would launch
> an album, so it appears that playing an album hasn't been implemented. Did I
> miss something?
Currently albums aren't really handled - they behave same way like the song they are "extracted" from.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
David Callé (davidc3) wrote : | # |
For the record, Rhythmbox doesn't provide any way to launch albums.
One solution would be, on activation of an album result, to :
rhythmbox-client --play <track1 of album>
rhythmbox-client --enqueue <track2> <track3> <track4> ...
Or, to do what the python rbox scope does :
rhythmbox-client --play <track1 of album>
and "# pray for rhythmbox to be ordered by tracks"
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alex Launi (alexlauni) wrote : | # |
the first solution is what's done in banshee, and what i would suggest doing.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alex Launi (alexlauni) wrote : | # |
well, not exactly what's done in banshee but that's what i would do
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
I took the lens for a test spin but I am not getting any results from it. Looking at the code I think everything looks alright. I am not sure what gives (and I don't get any errors on stdout).
Nice to see that all the bells and whistles of Dee is being put to use! :-D
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michal Hruby (mhr3) wrote : | # |
Mikkel, are you running fixed Dee (the non-C-locale issue)? If not run the lens with LC_ALL=C ./...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Thanks Michal, that makes it work. I think that we can bump the build dep, now that Dee has been post-release-
Search works nicely, but it seems that the Genre filter doesn't work?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Neil J. Patel (njpatel) wrote : | # |
Works well for me, genres included.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Mikkel Kamstrup Erlandsen (kamstrup) wrote : | # |
Ok, on a clean jhbuild wiht everything up-to-date this is working fine. Running well here.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Unity Merger (unity-merger) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Unity Merger (unity-merger) wrote : | # |
The Jenkins job https:/
Not merging it.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Didier Roche-Tolomelli (didrocks) wrote : | # |
New build-dep added.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Unity Merger (unity-merger) wrote : | # |
The Jenkins job https:/
Not merging it.
- 102. By Michal Hruby
-
Update POTFILES
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michal Hruby (mhr3) : | # |
Preview Diff
1 | === modified file 'configure.ac' |
2 | --- configure.ac 2012-02-17 10:45:31 +0000 |
3 | +++ configure.ac 2012-03-21 12:24:18 +0000 |
4 | @@ -60,9 +60,10 @@ |
5 | gobject-2.0 >= $GLIB_REQUIRED |
6 | gio-2.0 >= $GLIB_REQUIRED |
7 | gio-unix-2.0 >= $GLIB_REQUIRED |
8 | - dee-1.0 >= 0.5.16 |
9 | + dee-1.0 >= 1.0.7 |
10 | sqlite3 >= 3.7.7 |
11 | gee-1.0 |
12 | + libxml-2.0 |
13 | json-glib-1.0 |
14 | unity >= 4.99.0) |
15 | |
16 | |
17 | === modified file 'po/POTFILES.in' |
18 | --- po/POTFILES.in 2012-01-23 13:57:10 +0000 |
19 | +++ po/POTFILES.in 2012-03-21 12:24:18 +0000 |
20 | @@ -1,6 +1,8 @@ |
21 | [encoding: UTF-8] |
22 | src/daemon.vala |
23 | src/main.vala |
24 | +src/banshee-scope.vala |
25 | +src/rhythmbox-scope.vala |
26 | src/simple-scope.vala |
27 | [type: gettext/ini]music.lens.in.in |
28 | |
29 | |
30 | === modified file 'po/POTFILES.skip' |
31 | --- po/POTFILES.skip 2012-01-23 13:57:10 +0000 |
32 | +++ po/POTFILES.skip 2012-03-21 12:24:18 +0000 |
33 | @@ -2,3 +2,5 @@ |
34 | src/daemon.c |
35 | src/utils.c |
36 | src/simple-scope.c |
37 | +src/banshee-scope.c |
38 | +src/rhythmbox-scope.c |
39 | |
40 | === modified file 'src/Makefile.am' |
41 | --- src/Makefile.am 2012-01-20 11:48:10 +0000 |
42 | +++ src/Makefile.am 2012-03-21 12:24:18 +0000 |
43 | @@ -28,6 +28,7 @@ |
44 | --pkg gio-2.0 \ |
45 | --pkg gio-unix-2.0 \ |
46 | --pkg glib-2.0 \ |
47 | + --pkg libxml-2.0 \ |
48 | $(MAINTAINER_VALAFLAGS) |
49 | |
50 | unity_music_daemon_LDADD = \ |
51 | @@ -49,6 +50,8 @@ |
52 | filter-parser-genre.vala \ |
53 | genre.vala \ |
54 | main.vala \ |
55 | + rhythmbox-scope.vala \ |
56 | + rhythmbox-collection.vala \ |
57 | simple-scope.vala \ |
58 | track.vala \ |
59 | $(NULL) |
60 | |
61 | === modified file 'src/banshee-scope.vala' |
62 | --- src/banshee-scope.vala 2012-02-07 08:59:17 +0000 |
63 | +++ src/banshee-scope.vala 2012-03-21 12:24:18 +0000 |
64 | @@ -32,6 +32,7 @@ |
65 | scope = new Unity.Scope ("/com/canonical/unity/scope/banshee"); |
66 | scope.search_in_global = true; |
67 | scope.activate_uri.connect (activate); |
68 | + scope.sources.add_option ("banshee", _("Banshee"), null); |
69 | |
70 | base.initialize (); |
71 | |
72 | |
73 | === modified file 'src/daemon.vala' |
74 | --- src/daemon.vala 2012-01-20 11:48:10 +0000 |
75 | +++ src/daemon.vala 2012-03-21 12:24:18 +0000 |
76 | @@ -28,20 +28,24 @@ |
77 | { |
78 | private Unity.Lens lens; |
79 | private BansheeScopeProxy banshee; |
80 | + private RhythmboxScope rb; |
81 | |
82 | construct |
83 | { |
84 | banshee = new BansheeScopeProxy (); |
85 | + rb = new RhythmboxScope (); |
86 | |
87 | lens = new Unity.Lens("/com/canonical/unity/lens/music", "music"); |
88 | lens.search_in_global = true; |
89 | lens.search_hint = _("Search Music Collection"); |
90 | + lens.sources_display_name = _("Sources"); |
91 | lens.visible = true; |
92 | |
93 | populate_categories (); |
94 | populate_filters(); |
95 | |
96 | lens.add_local_scope (banshee.scope); |
97 | + lens.add_local_scope (rb.scope); |
98 | |
99 | try { |
100 | lens.export (); |
101 | |
102 | === modified file 'src/genre.vala' |
103 | --- src/genre.vala 2011-09-28 18:44:11 +0000 |
104 | +++ src/genre.vala 2012-03-21 12:24:18 +0000 |
105 | @@ -43,6 +43,7 @@ |
106 | public static const string OTHER_ID = "other"; |
107 | |
108 | private static TreeMultiMap<string, string> map; |
109 | + private static Map<string, string> inverted_map; |
110 | |
111 | static construct |
112 | { |
113 | @@ -123,6 +124,16 @@ |
114 | map.set (OTHER_ID, "ambient"); |
115 | map.set (OTHER_ID, "asian"); |
116 | map.set (OTHER_ID, "brazilian"); |
117 | + |
118 | + inverted_map = new HashMap<string, string> (); |
119 | + foreach (var key in map.get_keys ()) |
120 | + { |
121 | + var values_collection = map[key]; |
122 | + foreach (var val in values_collection) |
123 | + { |
124 | + inverted_map[val] = key; |
125 | + } |
126 | + } |
127 | } |
128 | |
129 | public Collection<string> get_genre_synonyms (string genre_id) |
130 | @@ -132,5 +143,11 @@ |
131 | |
132 | return new LinkedList<string> (); |
133 | } |
134 | + |
135 | + public string get_id_for_genre (string genre) |
136 | + { |
137 | + return inverted_map[genre] ?? OTHER_ID; |
138 | + } |
139 | } |
140 | -} |
141 | \ No newline at end of file |
142 | +} |
143 | + |
144 | |
145 | === added file 'src/rhythmbox-collection.vala' |
146 | --- src/rhythmbox-collection.vala 1970-01-01 00:00:00 +0000 |
147 | +++ src/rhythmbox-collection.vala 2012-03-21 12:24:18 +0000 |
148 | @@ -0,0 +1,489 @@ |
149 | +/* |
150 | + * Copyright (C) 2012 Canonical Ltd |
151 | + * |
152 | + * This program is free software: you can redistribute it and/or modify |
153 | + * it under the terms of the GNU General Public License version 3 as |
154 | + * published by the Free Software Foundation. |
155 | + * |
156 | + * This program is distributed in the hope that it will be useful, |
157 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
158 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
159 | + * GNU General Public License for more details. |
160 | + * |
161 | + * You should have received a copy of the GNU General Public License |
162 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
163 | + * |
164 | + * Authored by David Calle <davidc@framli.eu> |
165 | + * Michal Hruby <michal.hruby@canonical.com> |
166 | + * |
167 | + */ |
168 | + |
169 | +using Dee; |
170 | +using Gee; |
171 | + |
172 | +namespace Unity.MusicLens |
173 | +{ |
174 | + |
175 | + private enum Columns |
176 | + { |
177 | + URI, |
178 | + TITLE, |
179 | + ARTIST, |
180 | + ALBUM, |
181 | + ARTWORK, |
182 | + MIMETYPE, |
183 | + GENRE, |
184 | + TRACK_NUMBER, |
185 | + ALBUM_ARTIST, |
186 | + YEAR, |
187 | + PLAY_COUNT |
188 | + } |
189 | + |
190 | + class RhythmboxCollection : Object |
191 | + { |
192 | + |
193 | + GenericArray<Track> tracks = new GenericArray<Track> (); |
194 | + SequenceModel all_tracks; |
195 | + FilterModel tracks_by_play_count; |
196 | + |
197 | + Analyzer analyzer; |
198 | + Index index; |
199 | + ICUTermFilter ascii_filter; |
200 | + |
201 | + string media_art_dir; |
202 | + |
203 | + // contains genre maps |
204 | + Genre genre = new Genre (); |
205 | + |
206 | + construct |
207 | + { |
208 | + media_art_dir = Path.build_filename ( |
209 | + Environment.get_user_cache_dir (), "media-art"); |
210 | + |
211 | + all_tracks = new SequenceModel (); |
212 | + // the columns correspond to the Columns enum |
213 | + all_tracks.set_schema ("s", "s", "s", "s", "s", "s", "s", "s", "s", "i", "i"); |
214 | + |
215 | + var filter = Dee.Filter.new_sort ((row1_in, row2_in) => |
216 | + { |
217 | + // magic typecasting because dee has wrong vapi (1.0.6) |
218 | + void* row1_ptr = row1_in; |
219 | + void* row2_ptr = row2_in; |
220 | + unowned Variant[] row1 = (Variant[]) (row1_ptr); |
221 | + unowned Variant[] row2 = (Variant[]) (row2_ptr); |
222 | + |
223 | + int a = row1[Columns.PLAY_COUNT].get_int32 (); |
224 | + int b = row2[Columns.PLAY_COUNT].get_int32 (); |
225 | + |
226 | + return b - a; // higher play count first |
227 | + }); |
228 | + tracks_by_play_count = new FilterModel (all_tracks, filter); |
229 | + |
230 | + ascii_filter = new ICUTermFilter.ascii_folder (); |
231 | + analyzer = new TextAnalyzer (); |
232 | + analyzer.add_term_filter ((terms_in, terms_out) => |
233 | + { |
234 | + foreach (unowned string term in terms_in) |
235 | + { |
236 | + var folded = ascii_filter.apply (term); |
237 | + terms_out.add_term (term); |
238 | + if (folded != term) terms_out.add_term (folded); |
239 | + } |
240 | + }); |
241 | + var reader = ModelReader.new ((model, iter) => |
242 | + { |
243 | + var s ="%s\n%s\n%s".printf (model.get_string (iter, Columns.TITLE), |
244 | + model.get_string (iter, Columns.ARTIST), |
245 | + model.get_string (iter, Columns.ALBUM)); |
246 | + return s; |
247 | + }); |
248 | + |
249 | + index = new TreeIndex (all_tracks, analyzer, reader); |
250 | + } |
251 | + |
252 | + private string? get_albumart (Track track) |
253 | + { |
254 | + var artist = track.album_artist ?? track.artist; |
255 | + var album = track.album; |
256 | + |
257 | + var artist_norm = artist.normalize (-1, NormalizeMode.NFKD); |
258 | + var album_norm = album.normalize (-1, NormalizeMode.NFKD); |
259 | + |
260 | + var artist_md5 = Checksum.compute_for_string (ChecksumType.MD5, |
261 | + artist_norm); |
262 | + var album_md5 = Checksum.compute_for_string (ChecksumType.MD5, |
263 | + album_norm); |
264 | + |
265 | + string filename; |
266 | + filename = Path.build_filename (media_art_dir, |
267 | + "album-%s-%s".printf (artist_md5, album_md5)); |
268 | + if (FileUtils.test (filename, FileTest.EXISTS)) return filename; |
269 | + |
270 | + var combined = "%s\t%s".printf (artist, album).normalize (-1, NormalizeMode.NFKD); |
271 | + filename = Path.build_filename (media_art_dir, |
272 | + "album-%s.jpg".printf (Checksum.compute_for_string ( |
273 | + ChecksumType.MD5, combined))); |
274 | + if (FileUtils.test (filename, FileTest.EXISTS)) return filename; |
275 | + |
276 | + // Try Nautilus thumbnails |
277 | + try |
278 | + { |
279 | + File artwork_file = File.new_for_uri (track.uri); |
280 | + var info = artwork_file.query_info (FILE_ATTRIBUTE_THUMBNAIL_PATH, 0, null); |
281 | + var thumbnail_path = info.get_attribute_string (FILE_ATTRIBUTE_THUMBNAIL_PATH); |
282 | + if (thumbnail_path != null) return thumbnail_path; |
283 | + } catch {} |
284 | + |
285 | + // Try covers folder |
286 | + string artwork = Path.build_filename ( |
287 | + Environment.get_user_cache_dir (), "rhythmbox", "covers", |
288 | + "%s - %s.jpg".printf (track.artist, track.album)); |
289 | + if (FileUtils.test (artwork, FileTest.EXISTS)) return artwork; |
290 | + |
291 | + return null; |
292 | + } |
293 | + |
294 | + public void parse_file (string path) |
295 | + { |
296 | + Xml.Parser.init (); |
297 | + |
298 | + Xml.Doc* doc = Xml.Parser.parse_file (path); |
299 | + if (doc == null) { |
300 | + return; |
301 | + } |
302 | + |
303 | + Xml.Node* root = doc->get_root_element (); |
304 | + if (root == null) { |
305 | + delete doc; |
306 | + return; |
307 | + } |
308 | + parse_node (root); |
309 | + delete doc; |
310 | + |
311 | + Xml.Parser.cleanup (); |
312 | + } |
313 | + |
314 | + private void parse_node (Xml.Node* node) |
315 | + { |
316 | + for (Xml.Node* iter = node->children; iter != null; iter = iter->next) { |
317 | + if (iter->type != Xml.ElementType.ELEMENT_NODE) { |
318 | + continue; |
319 | + } |
320 | + for (Xml.Attr* prop = iter->properties; prop != null; prop = prop->next) { |
321 | + string attr_content = prop->children->content; |
322 | + if (attr_content == "song") { |
323 | + Track track = new Track (); |
324 | + |
325 | + for (Xml.Node* iter_track = iter->children; iter_track != null; |
326 | + iter_track = iter_track->next) { |
327 | + if (iter_track->type != Xml.ElementType.ELEMENT_NODE) { |
328 | + continue; |
329 | + } |
330 | + switch (iter_track->name) { |
331 | + case "title": |
332 | + string node_content = iter_track->get_content (); |
333 | + track.title = node_content; |
334 | + break; |
335 | + case "location": |
336 | + string node_content = iter_track->get_content (); |
337 | + track.uri = node_content; |
338 | + break; |
339 | + case "artist": |
340 | + string node_content = iter_track->get_content (); |
341 | + track.artist = node_content; |
342 | + break; |
343 | + case "media-type": |
344 | + string node_content = iter_track->get_content (); |
345 | + track.mime_type = node_content; |
346 | + break; |
347 | + case "album": |
348 | + string node_content = iter_track->get_content (); |
349 | + track.album = node_content; |
350 | + break; |
351 | + case "genre": |
352 | + string node_content = iter_track->get_content (); |
353 | + track.genre = node_content; |
354 | + break; |
355 | + case "track-number": |
356 | + string node_content = iter_track->get_content (); |
357 | + track.track_number = node_content; |
358 | + break; |
359 | + case "album-artist": |
360 | + string node_content = iter_track->get_content (); |
361 | + track.album_artist = node_content; |
362 | + break; |
363 | + case "date": |
364 | + string node_content = iter_track->get_content (); |
365 | + track.year = int.parse (node_content); |
366 | + break; |
367 | + case "play-count": |
368 | + string node_content = iter_track->get_content (); |
369 | + track.play_count = int.parse (node_content); |
370 | + break; |
371 | + } |
372 | + } |
373 | + // append to tracks array |
374 | + tracks.add (track); |
375 | + |
376 | + // Get cover art |
377 | + string albumart = get_albumart (track); |
378 | + if (albumart != null) |
379 | + track.artwork_path = albumart; |
380 | + else |
381 | + track.artwork_path = "audio-x-generic"; |
382 | + |
383 | + // Get genre filter id |
384 | + track.genre = genre.get_id_for_genre(track.genre.down ()); |
385 | + |
386 | + if (track.year > 0) { |
387 | + track.year /= 365; |
388 | + } |
389 | + |
390 | + all_tracks.append (track.uri, track.title, |
391 | + track.artist, track.album, |
392 | + track.artwork_path, |
393 | + track.mime_type, |
394 | + track.genre, |
395 | + track.track_number, |
396 | + track.album_artist, |
397 | + track.year, |
398 | + track.play_count); |
399 | + } |
400 | + // Next track |
401 | + parse_node (iter); |
402 | + } |
403 | + } |
404 | + } |
405 | + |
406 | + public void search (LensSearch search, |
407 | + SearchType search_type, |
408 | + GLib.List<FilterParser>? filters = null, |
409 | + int max_results = -1, |
410 | + int category_override = -1) |
411 | + { |
412 | + int num_results = 0; |
413 | + var empty_search = search.search_string.strip () == ""; |
414 | + int min_year; |
415 | + int max_year; |
416 | + int category_id; |
417 | + |
418 | + Model model = all_tracks; |
419 | + get_decade_filter (filters, out min_year, out max_year); |
420 | + var active_genres = get_genre_filter (filters); |
421 | + |
422 | + if (empty_search) |
423 | + { |
424 | + // display a couple of most played songs |
425 | + model = tracks_by_play_count; |
426 | + var iter = model.get_first_iter (); |
427 | + var end_iter = model.get_last_iter (); |
428 | + var albums_list_nosearch = new HashSet<string> (); |
429 | + |
430 | + while (iter != end_iter) |
431 | + { |
432 | + int year = model.get_int32 (iter, Columns.YEAR); |
433 | + unowned string genre = model.get_string (iter, Columns.GENRE); |
434 | + |
435 | + // check filters |
436 | + if (year < min_year || year > max_year) |
437 | + { |
438 | + iter = model.next (iter); |
439 | + continue; |
440 | + } |
441 | + |
442 | + // check filters |
443 | + if (active_genres != null) { |
444 | + if (!(genre in active_genres)) { |
445 | + iter = model.next (iter); |
446 | + continue; |
447 | + } |
448 | + } |
449 | + |
450 | + unowned string album = model.get_string (iter, |
451 | + Columns.ALBUM); |
452 | + // it's not first as in track #1, but first found from album |
453 | + bool first_track_from_album = !(album in albums_list_nosearch); |
454 | + albums_list_nosearch.add (album); |
455 | + |
456 | + if (first_track_from_album) |
457 | + { |
458 | + category_id = category_override >= 0 ? |
459 | + category_override : Category.ALBUMS; |
460 | + |
461 | + search.results_model.append ( |
462 | + model.get_string (iter, Columns.URI), |
463 | + model.get_string (iter, Columns.ARTWORK), |
464 | + category_id, |
465 | + model.get_string (iter, Columns.MIMETYPE), |
466 | + model.get_string (iter, Columns.ALBUM), |
467 | + model.get_string (iter, Columns.ARTIST), |
468 | + model.get_string (iter, Columns.URI)); |
469 | + } |
470 | + |
471 | + category_id = category_override >= 0 ? |
472 | + category_override : Category.SONGS; |
473 | + |
474 | + search.results_model.append ( |
475 | + model.get_string (iter, Columns.URI), |
476 | + model.get_string (iter, Columns.ARTWORK), |
477 | + category_id, |
478 | + model.get_string (iter, Columns.MIMETYPE), |
479 | + model.get_string (iter, Columns.TITLE), |
480 | + model.get_string (iter, Columns.ARTIST), |
481 | + model.get_string (iter, Columns.URI)); |
482 | + |
483 | + num_results++; |
484 | + if (max_results >= 0 && num_results >= max_results) break; |
485 | + |
486 | + iter = model.next (iter); |
487 | + } |
488 | + return; |
489 | + } |
490 | + |
491 | + |
492 | + var term_list = Object.new (typeof (Dee.TermList)) as Dee.TermList; |
493 | + // search only the folded terms, FIXME: is that a good idea? |
494 | + analyzer.tokenize (ascii_filter.apply (search.search_string), |
495 | + term_list); |
496 | + |
497 | + var matches = new Sequence<Dee.ModelIter> (); |
498 | + bool first_pass = true; |
499 | + foreach (unowned string term in term_list) |
500 | + { |
501 | + // FIXME: use PREFIX search only for the last term? |
502 | + var result_set = index.lookup (term, TermMatchFlag.PREFIX); |
503 | + |
504 | + CompareDataFunc<Dee.ModelIter> cmp_func = (a, b) => |
505 | + { |
506 | + return a == b ? 0 : ((void*) a > (void*) b ? 1 : -1); |
507 | + }; |
508 | + |
509 | + // intersect the results (cause we want to AND the terms) |
510 | + var remaining = new Sequence<Dee.ModelIter> (); |
511 | + foreach (var item in result_set) |
512 | + { |
513 | + if (first_pass) |
514 | + matches.insert_sorted (item, cmp_func); |
515 | + else if (matches.lookup (item, cmp_func) != null) |
516 | + remaining.insert_sorted (item, cmp_func); |
517 | + } |
518 | + if (!first_pass) matches = (owned) remaining; |
519 | + // final result set empty already? |
520 | + if (matches.get_begin_iter () == matches.get_end_iter ()) break; |
521 | + |
522 | + first_pass = false; |
523 | + } |
524 | + |
525 | + // matches now contain iterators into the all_tracks model which |
526 | + // match the search string |
527 | + var seq_iter = matches.get_begin_iter (); |
528 | + var seq_end_iter = matches.get_end_iter (); |
529 | + |
530 | + var albums_list = new HashSet<string> (); |
531 | + while (seq_iter != seq_end_iter) |
532 | + { |
533 | + var model_iter = seq_iter.get (); |
534 | + int year = model.get_int32 (model_iter, Columns.YEAR); |
535 | + string genre = model.get_string (model_iter, Columns.GENRE); |
536 | + |
537 | + // check filters |
538 | + if (year < min_year || year > max_year) |
539 | + { |
540 | + seq_iter = seq_iter.next (); |
541 | + continue; |
542 | + } |
543 | + |
544 | + // check filters |
545 | + if (active_genres != null) { |
546 | + bool genre_match = (genre in active_genres); |
547 | + if (!genre_match) { |
548 | + seq_iter = seq_iter.next (); |
549 | + continue; |
550 | + } |
551 | + } |
552 | + |
553 | + unowned string album = model.get_string (model_iter, |
554 | + Columns.ALBUM); |
555 | + // it's not first as in track #1, but first found from album |
556 | + bool first_track_from_album = !(album in albums_list); |
557 | + albums_list.add (album); |
558 | + |
559 | + if (first_track_from_album) |
560 | + { |
561 | + category_id = category_override >= 0 ? |
562 | + category_override : Category.ALBUMS; |
563 | + |
564 | + search.results_model.append ( |
565 | + model.get_string (model_iter, Columns.URI), |
566 | + model.get_string (model_iter, Columns.ARTWORK), |
567 | + category_id, |
568 | + model.get_string (model_iter, Columns.MIMETYPE), |
569 | + model.get_string (model_iter, Columns.ALBUM), |
570 | + model.get_string (model_iter, Columns.ARTIST), |
571 | + model.get_string (model_iter, Columns.URI)); |
572 | + } |
573 | + |
574 | + category_id = category_override >= 0 ? |
575 | + category_override : Category.SONGS; |
576 | + |
577 | + search.results_model.append ( |
578 | + model.get_string (model_iter, Columns.URI), |
579 | + model.get_string (model_iter, Columns.ARTWORK), |
580 | + category_id, |
581 | + model.get_string (model_iter, Columns.MIMETYPE), |
582 | + model.get_string (model_iter, Columns.TITLE), |
583 | + model.get_string (model_iter, Columns.ARTIST), |
584 | + model.get_string (model_iter, Columns.URI)); |
585 | + |
586 | + num_results++; |
587 | + if (max_results >= 0 && num_results >= max_results) break; |
588 | + |
589 | + seq_iter = seq_iter.next (); |
590 | + } |
591 | + } |
592 | + |
593 | + private void get_decade_filter (GLib.List<FilterParser> filters, |
594 | + out int min_year, out int max_year) |
595 | + { |
596 | + Filter? filter = null; |
597 | + foreach (var parser in filters) |
598 | + { |
599 | + if (parser is DecadeFilterParser) filter = parser.filter; |
600 | + } |
601 | + |
602 | + if (filter == null || !filter.filtering) |
603 | + { |
604 | + min_year = 0; |
605 | + max_year = int.MAX; |
606 | + return; |
607 | + } |
608 | + |
609 | + var mrf = filter as MultiRangeFilter; |
610 | + min_year = int.parse (mrf.get_first_active ().id); |
611 | + max_year = int.parse (mrf.get_last_active ().id); |
612 | + } |
613 | + |
614 | + private Set<string>? get_genre_filter (GLib.List<FilterParser> filters) |
615 | + { |
616 | + Filter? filter = null; |
617 | + foreach (var parser in filters) |
618 | + { |
619 | + if (parser is GenreFilterParser) filter = parser.filter; |
620 | + } |
621 | + if (filter == null || !filter.filtering) |
622 | + { |
623 | + return null; |
624 | + } |
625 | + |
626 | + var active_genres = new HashSet<string> (); |
627 | + var all_genres = filter as CheckOptionFilterCompact; |
628 | + foreach (FilterOption option in all_genres.options) |
629 | + { |
630 | + if (option.id == null || !option.active) continue; |
631 | + active_genres.add (option.id); |
632 | + } |
633 | + |
634 | + return active_genres; |
635 | + } |
636 | + } |
637 | +} |
638 | |
639 | === added file 'src/rhythmbox-scope.vala' |
640 | --- src/rhythmbox-scope.vala 1970-01-01 00:00:00 +0000 |
641 | +++ src/rhythmbox-scope.vala 2012-03-21 12:24:18 +0000 |
642 | @@ -0,0 +1,116 @@ |
643 | +/* |
644 | + * Copyright (C) 2011 Canonical Ltd |
645 | + * |
646 | + * This program is free software: you can redistribute it and/or modify |
647 | + * it under the terms of the GNU General Public License version 3 as |
648 | + * published by the Free Software Foundation. |
649 | + * |
650 | + * This program is distributed in the hope that it will be useful, |
651 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
652 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
653 | + * GNU General Public License for more details. |
654 | + * |
655 | + * You should have received a copy of the GNU General Public License |
656 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
657 | + * |
658 | + * Authored by Alex Launi <alex.launi@canonical.com> |
659 | + * |
660 | + */ |
661 | + |
662 | +using GLib; |
663 | + |
664 | +namespace Unity.MusicLens { |
665 | + |
666 | + public class RhythmboxScope : SimpleScope |
667 | + { |
668 | + private RhythmboxCollection collection; |
669 | + private bool db_ready; |
670 | + |
671 | + public RhythmboxScope () |
672 | + { |
673 | + base (); |
674 | + |
675 | + scope = new Unity.Scope ("/com/canonical/unity/scope/rhythmbox"); |
676 | + scope.search_in_global = true; |
677 | + scope.activate_uri.connect (activate); |
678 | + scope.sources.add_option ("rhythmbox", _("Rhythmbox"), null); |
679 | + |
680 | + base.initialize (); |
681 | + collection = new RhythmboxCollection (); |
682 | + db_ready = false; |
683 | + } |
684 | + |
685 | + protected override int num_results_without_search { get {return 100; } } |
686 | + protected override int num_results_global_search { get { return 20; } } |
687 | + protected override int num_results_lens_search { get { return 50; } } |
688 | + |
689 | + /** |
690 | + * Tells banshee to play the selected uri(s) |
691 | + */ |
692 | + public Unity.ActivationResponse activate (string uri) |
693 | + { |
694 | + string[] exec = {"rhythmbox-client", "--play-uri"}; |
695 | + |
696 | + try { |
697 | + if (Uri.parse_scheme (uri) == "album") |
698 | + { |
699 | + debug (@"searching for tracks for $uri"); |
700 | + string[] split = uri.split ("/"); |
701 | + string artist = split[2]; |
702 | + string title = split[3]; |
703 | + |
704 | + Album album = new Album (); |
705 | + album.artist = artist; |
706 | + album.title = title; |
707 | + // FIXME there must be a better way.. |
708 | +// foreach (string track in collection.get_track_uris (album)) |
709 | +// exec += track; |
710 | + } |
711 | + else |
712 | + { |
713 | + exec += uri; |
714 | + } |
715 | + |
716 | + exec += null; |
717 | + |
718 | + debug ("Spawning rb '%s'", string.joinv (" ", exec)); |
719 | + Process.spawn_async (null, |
720 | + exec, |
721 | + null, |
722 | + SpawnFlags.SEARCH_PATH, |
723 | + null, |
724 | + null); |
725 | + |
726 | + return new Unity.ActivationResponse (Unity.HandledType.HIDE_DASH); |
727 | + } catch (SpawnError error) { |
728 | + warning ("Failed to launch URI %s", uri); |
729 | + return new Unity.ActivationResponse (Unity.HandledType.NOT_HANDLED); |
730 | + } |
731 | + } |
732 | + |
733 | + public override async void perform_search (LensSearch search, |
734 | + SearchType search_type, |
735 | + owned List<FilterParser> filters, |
736 | + int max_results = -1, |
737 | + Cancellable? cancellable = null) |
738 | + { |
739 | + int category_override = -1; |
740 | + if (search_type == SearchType.GLOBAL) |
741 | + { |
742 | + category_override = Category.MUSIC; |
743 | + // the lens shouldn't display anything for empty search |
744 | + if (is_search_empty (search)) return; |
745 | + } |
746 | + |
747 | + if (!db_ready) |
748 | + { |
749 | + // parse the DB lazily |
750 | + collection.parse_file ("%s/.local/share/rhythmbox/rhythmdb.xml".printf (Environment.get_home_dir ())); |
751 | + db_ready = true; |
752 | + } |
753 | + |
754 | + collection.search (search, search_type, filters, |
755 | + max_results, category_override); |
756 | + } |
757 | + } |
758 | +} |
759 | |
760 | === modified file 'src/simple-scope.vala' |
761 | --- src/simple-scope.vala 2012-02-07 08:59:17 +0000 |
762 | +++ src/simple-scope.vala 2012-03-21 12:24:18 +0000 |
763 | @@ -46,6 +46,10 @@ |
764 | scope.queue_search_changed (SearchType.DEFAULT); |
765 | }); |
766 | |
767 | + scope.active_sources_changed.connect (() => { |
768 | + scope.queue_search_changed (SearchType.DEFAULT); |
769 | + }); |
770 | + |
771 | /* No need to search if only the whitespace changes */ |
772 | scope.generate_search_key.connect ((lens_search) => { |
773 | return lens_search.search_string.strip (); |
774 | @@ -98,6 +102,21 @@ |
775 | |
776 | results_model.clear (); |
777 | |
778 | + // don't perform search is all sources are inactive |
779 | + if (scope.sources.options.length () > 0 && scope.sources.filtering) |
780 | + { |
781 | + bool any_active = false; |
782 | + foreach (var source in scope.sources.options) |
783 | + { |
784 | + if (source.active) any_active = true; |
785 | + } |
786 | + if (!any_active) |
787 | + { |
788 | + search.finished (); |
789 | + return; |
790 | + } |
791 | + } |
792 | + |
793 | yield perform_search (search, search_type, (owned) filters, max_results, cancellable); |
794 | |
795 | if (results_model.get_n_rows () == 0) |
796 | |
797 | === modified file 'src/track.vala' |
798 | --- src/track.vala 2011-09-14 18:33:52 +0000 |
799 | +++ src/track.vala 2012-03-21 12:24:18 +0000 |
800 | @@ -26,5 +26,11 @@ |
801 | public string artist { get; set; } |
802 | public string mime_type { get; set; } |
803 | public string artwork_path { get; set; } |
804 | + public string album { get; set; } |
805 | + public string album_artist { get; set; } |
806 | + public string genre { get; set; } |
807 | + public string track_number { get; set; } |
808 | + public int year { get; set; } |
809 | + public int play_count { get; set; } |
810 | } |
811 | -} |
812 | \ No newline at end of file |
813 | +} |
There are still a couple of rough edges (better album handling, monitoring changes to the xml file), but I think it's good to go in for beta2.