~kroq-gar78/unity-lens-music/fix-972987

« back to all changes in this revision

Viewing changes to src/rhythmbox-collection.vala

  • Committer: Tarmac
  • Author(s): David Callé, Michal Hruby
  • Date: 2012-03-21 12:40:37 UTC
  • mfrom: (69.1.33 rb-scope)
  • Revision ID: tarmac-20120321124037-p1lsomndx12g9qt7
Implement a Rhythmbox scope.. Fixes: https://bugs.launchpad.net/bugs/948860. Approved by Michal Hruby, Mikkel Kamstrup Erlandsen, Neil J. Patel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (C) 2012 Canonical Ltd
 
3
 *
 
4
 * This program is free software: you can redistribute it and/or modify
 
5
 * it under the terms of the GNU General Public License version 3 as
 
6
 * published by the Free Software Foundation.
 
7
 *
 
8
 * This program is distributed in the hope that it will be useful,
 
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
 * GNU General Public License for more details.
 
12
 *
 
13
 * You should have received a copy of the GNU General Public License
 
14
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
15
 *
 
16
 * Authored by David Calle <davidc@framli.eu>
 
17
 *             Michal Hruby <michal.hruby@canonical.com>
 
18
 *
 
19
 */
 
20
 
 
21
using Dee;
 
22
using Gee;
 
23
 
 
24
namespace Unity.MusicLens
 
25
{
 
26
 
 
27
    private enum Columns
 
28
    {
 
29
        URI,
 
30
        TITLE,
 
31
        ARTIST,
 
32
        ALBUM,
 
33
        ARTWORK,
 
34
        MIMETYPE,
 
35
        GENRE,
 
36
        TRACK_NUMBER,
 
37
        ALBUM_ARTIST,
 
38
        YEAR,
 
39
        PLAY_COUNT
 
40
    }
 
41
 
 
42
    class RhythmboxCollection : Object
 
43
    {
 
44
 
 
45
        GenericArray<Track> tracks = new GenericArray<Track> ();
 
46
        SequenceModel all_tracks;
 
47
        FilterModel tracks_by_play_count;
 
48
 
 
49
        Analyzer analyzer;
 
50
        Index index;
 
51
        ICUTermFilter ascii_filter;
 
52
 
 
53
        string media_art_dir;
 
54
 
 
55
        // contains genre maps
 
56
        Genre genre = new Genre ();
 
57
 
 
58
        construct
 
59
        {
 
60
          media_art_dir = Path.build_filename (
 
61
              Environment.get_user_cache_dir (), "media-art");
 
62
 
 
63
          all_tracks = new SequenceModel ();
 
64
          // the columns correspond to the Columns enum
 
65
          all_tracks.set_schema ("s", "s", "s", "s", "s", "s", "s", "s", "s", "i", "i");
 
66
 
 
67
          var filter = Dee.Filter.new_sort ((row1_in, row2_in) =>
 
68
          {
 
69
            // magic typecasting because dee has wrong vapi (1.0.6)
 
70
            void* row1_ptr = row1_in;
 
71
            void* row2_ptr = row2_in;
 
72
            unowned Variant[] row1 = (Variant[]) (row1_ptr);
 
73
            unowned Variant[] row2 = (Variant[]) (row2_ptr);
 
74
 
 
75
            int a = row1[Columns.PLAY_COUNT].get_int32 ();
 
76
            int b = row2[Columns.PLAY_COUNT].get_int32 ();
 
77
 
 
78
            return b - a; // higher play count first
 
79
          });
 
80
          tracks_by_play_count = new FilterModel (all_tracks, filter);
 
81
 
 
82
          ascii_filter = new ICUTermFilter.ascii_folder ();
 
83
          analyzer = new TextAnalyzer ();
 
84
          analyzer.add_term_filter ((terms_in, terms_out) =>
 
85
          {
 
86
            foreach (unowned string term in terms_in)
 
87
            {
 
88
              var folded = ascii_filter.apply (term);
 
89
              terms_out.add_term (term);
 
90
              if (folded != term) terms_out.add_term (folded);
 
91
            }
 
92
          });
 
93
          var reader = ModelReader.new ((model, iter) =>
 
94
          {
 
95
            var s ="%s\n%s\n%s".printf (model.get_string (iter, Columns.TITLE),
 
96
                                        model.get_string (iter, Columns.ARTIST),
 
97
                                        model.get_string (iter, Columns.ALBUM));
 
98
            return s;
 
99
          });
 
100
 
 
101
          index = new TreeIndex (all_tracks, analyzer, reader);
 
102
        }
 
103
        
 
104
        private string? get_albumart (Track track)
 
105
        {
 
106
            var artist = track.album_artist ?? track.artist;
 
107
            var album = track.album;
 
108
 
 
109
            var artist_norm = artist.normalize (-1, NormalizeMode.NFKD);
 
110
            var album_norm = album.normalize (-1, NormalizeMode.NFKD);
 
111
 
 
112
            var artist_md5 = Checksum.compute_for_string (ChecksumType.MD5,
 
113
                                                          artist_norm);
 
114
            var album_md5 = Checksum.compute_for_string (ChecksumType.MD5,
 
115
                                                         album_norm);
 
116
 
 
117
            string filename;
 
118
            filename = Path.build_filename (media_art_dir,
 
119
                "album-%s-%s".printf (artist_md5, album_md5));
 
120
            if (FileUtils.test (filename, FileTest.EXISTS)) return filename;
 
121
 
 
122
            var combined = "%s\t%s".printf (artist, album).normalize (-1, NormalizeMode.NFKD);
 
123
            filename = Path.build_filename (media_art_dir,
 
124
                "album-%s.jpg".printf (Checksum.compute_for_string (
 
125
                    ChecksumType.MD5, combined)));
 
126
            if (FileUtils.test (filename, FileTest.EXISTS)) return filename;
 
127
 
 
128
            // Try Nautilus thumbnails
 
129
            try
 
130
            {
 
131
                File artwork_file = File.new_for_uri (track.uri);
 
132
                var info = artwork_file.query_info (FILE_ATTRIBUTE_THUMBNAIL_PATH, 0, null);
 
133
                var thumbnail_path = info.get_attribute_string (FILE_ATTRIBUTE_THUMBNAIL_PATH);
 
134
                if (thumbnail_path != null) return thumbnail_path;
 
135
            } catch {}
 
136
 
 
137
            // Try covers folder
 
138
            string artwork = Path.build_filename (
 
139
                Environment.get_user_cache_dir (), "rhythmbox", "covers",
 
140
                "%s - %s.jpg".printf (track.artist, track.album));
 
141
            if (FileUtils.test (artwork, FileTest.EXISTS)) return artwork;
 
142
 
 
143
            return null;
 
144
        }
 
145
 
 
146
        public void parse_file (string path)
 
147
        {
 
148
            Xml.Parser.init ();
 
149
 
 
150
            Xml.Doc* doc = Xml.Parser.parse_file (path);
 
151
            if (doc == null) {
 
152
                return;
 
153
            }
 
154
 
 
155
            Xml.Node* root = doc->get_root_element ();
 
156
            if (root == null) {
 
157
                delete doc;
 
158
                return;
 
159
            }
 
160
            parse_node (root);
 
161
            delete doc;
 
162
 
 
163
            Xml.Parser.cleanup ();
 
164
        }
 
165
 
 
166
        private void parse_node (Xml.Node* node)
 
167
        {
 
168
            for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {
 
169
                if (iter->type != Xml.ElementType.ELEMENT_NODE) {
 
170
                    continue;
 
171
                }
 
172
                for (Xml.Attr* prop = iter->properties; prop != null; prop = prop->next) {
 
173
                    string attr_content = prop->children->content;
 
174
                    if (attr_content == "song") {
 
175
                        Track track = new Track ();
 
176
              
 
177
                        for (Xml.Node* iter_track = iter->children; iter_track != null; 
 
178
                            iter_track = iter_track->next) {
 
179
                            if (iter_track->type != Xml.ElementType.ELEMENT_NODE) {
 
180
                                continue;
 
181
                            }
 
182
                            switch (iter_track->name) {
 
183
                            case "title":
 
184
                                string node_content = iter_track->get_content ();
 
185
                                track.title = node_content;
 
186
                                break;
 
187
                            case "location":
 
188
                                string node_content = iter_track->get_content ();
 
189
                                track.uri = node_content;
 
190
                                break;
 
191
                            case "artist":
 
192
                                string node_content = iter_track->get_content ();
 
193
                                track.artist = node_content;
 
194
                                break;
 
195
                            case "media-type":
 
196
                                string node_content = iter_track->get_content ();
 
197
                                track.mime_type = node_content;
 
198
                                break;
 
199
                            case "album":
 
200
                                string node_content = iter_track->get_content ();
 
201
                                track.album = node_content;
 
202
                                break;
 
203
                            case "genre":
 
204
                                string node_content = iter_track->get_content ();
 
205
                                track.genre = node_content;
 
206
                                break;
 
207
                            case "track-number":
 
208
                                string node_content = iter_track->get_content ();
 
209
                                track.track_number = node_content;
 
210
                                break;
 
211
                            case "album-artist":
 
212
                                string node_content = iter_track->get_content ();
 
213
                                track.album_artist = node_content;
 
214
                                break;
 
215
                            case "date":
 
216
                                string node_content = iter_track->get_content ();
 
217
                                track.year = int.parse (node_content);
 
218
                                break;
 
219
                            case "play-count":
 
220
                                string node_content = iter_track->get_content ();
 
221
                                track.play_count = int.parse (node_content);
 
222
                                break;
 
223
                            }
 
224
                        }
 
225
                        // append to tracks array
 
226
                        tracks.add (track);
 
227
 
 
228
                        // Get cover art
 
229
                        string albumart = get_albumart (track);
 
230
                        if (albumart != null)
 
231
                            track.artwork_path = albumart;
 
232
                        else
 
233
                            track.artwork_path = "audio-x-generic";
 
234
                        
 
235
                        // Get genre filter id
 
236
                        track.genre = genre.get_id_for_genre(track.genre.down ()); 
 
237
                        
 
238
                        if (track.year > 0) {
 
239
                            track.year /= 365;
 
240
                        }
 
241
 
 
242
                        all_tracks.append (track.uri, track.title,
 
243
                                           track.artist, track.album,
 
244
                                           track.artwork_path,
 
245
                                           track.mime_type,
 
246
                                           track.genre,
 
247
                                           track.track_number,
 
248
                                           track.album_artist,
 
249
                                           track.year,
 
250
                                           track.play_count);
 
251
                    }
 
252
                    // Next track
 
253
                    parse_node (iter);
 
254
                }
 
255
            }
 
256
        }
 
257
 
 
258
        public void search (LensSearch search, 
 
259
                            SearchType search_type, 
 
260
                            GLib.List<FilterParser>? filters = null,
 
261
                            int max_results = -1,
 
262
                            int category_override = -1)
 
263
        {
 
264
            int num_results = 0;
 
265
            var empty_search = search.search_string.strip () == "";
 
266
            int min_year;
 
267
            int max_year;
 
268
            int category_id;
 
269
 
 
270
            Model model = all_tracks;
 
271
            get_decade_filter (filters, out min_year, out max_year);
 
272
            var active_genres = get_genre_filter (filters);
 
273
 
 
274
            if (empty_search)
 
275
            {
 
276
                // display a couple of most played songs
 
277
                model = tracks_by_play_count;
 
278
                var iter = model.get_first_iter ();
 
279
                var end_iter = model.get_last_iter ();
 
280
                var albums_list_nosearch = new HashSet<string> ();
 
281
 
 
282
                while (iter != end_iter)
 
283
                {
 
284
                    int year = model.get_int32 (iter, Columns.YEAR);
 
285
                    unowned string genre = model.get_string (iter, Columns.GENRE);
 
286
 
 
287
                    // check filters
 
288
                    if (year < min_year || year > max_year)
 
289
                    {
 
290
                        iter = model.next (iter);
 
291
                        continue;
 
292
                    }
 
293
                    
 
294
                    // check filters
 
295
                    if (active_genres != null) {
 
296
                        if (!(genre in active_genres)) {
 
297
                            iter = model.next (iter);
 
298
                            continue;
 
299
                        }
 
300
                    }
 
301
 
 
302
                    unowned string album = model.get_string (iter,
 
303
                                                             Columns.ALBUM);
 
304
                    // it's not first as in track #1, but first found from album
 
305
                    bool first_track_from_album = !(album in albums_list_nosearch);
 
306
                    albums_list_nosearch.add (album);
 
307
                    
 
308
                    if (first_track_from_album)
 
309
                    {
 
310
                        category_id = category_override >= 0 ?
 
311
                            category_override : Category.ALBUMS;
 
312
 
 
313
                        search.results_model.append (
 
314
                            model.get_string (iter, Columns.URI),
 
315
                            model.get_string (iter, Columns.ARTWORK),
 
316
                            category_id,
 
317
                            model.get_string (iter, Columns.MIMETYPE),
 
318
                            model.get_string (iter, Columns.ALBUM),
 
319
                            model.get_string (iter, Columns.ARTIST),
 
320
                            model.get_string (iter, Columns.URI));
 
321
                    }
 
322
 
 
323
                    category_id = category_override >= 0 ?
 
324
                        category_override : Category.SONGS;
 
325
 
 
326
                    search.results_model.append (
 
327
                        model.get_string (iter, Columns.URI),
 
328
                        model.get_string (iter, Columns.ARTWORK),
 
329
                        category_id,
 
330
                        model.get_string (iter, Columns.MIMETYPE),
 
331
                        model.get_string (iter, Columns.TITLE),
 
332
                        model.get_string (iter, Columns.ARTIST),
 
333
                        model.get_string (iter, Columns.URI));
 
334
 
 
335
                    num_results++;
 
336
                    if (max_results >= 0 && num_results >= max_results) break;
 
337
 
 
338
                    iter = model.next (iter);
 
339
                }
 
340
                return;
 
341
            }
 
342
 
 
343
 
 
344
            var term_list = Object.new (typeof (Dee.TermList)) as Dee.TermList;
 
345
            // search only the folded terms, FIXME: is that a good idea?
 
346
            analyzer.tokenize (ascii_filter.apply (search.search_string),
 
347
                               term_list);
 
348
 
 
349
            var matches = new Sequence<Dee.ModelIter> ();
 
350
            bool first_pass = true;
 
351
            foreach (unowned string term in term_list)
 
352
            {
 
353
                // FIXME: use PREFIX search only for the last term?
 
354
                var result_set = index.lookup (term, TermMatchFlag.PREFIX);
 
355
 
 
356
                CompareDataFunc<Dee.ModelIter> cmp_func = (a, b) =>
 
357
                {
 
358
                    return a == b ? 0 : ((void*) a > (void*) b ? 1 : -1);
 
359
                };
 
360
 
 
361
                // intersect the results (cause we want to AND the terms)
 
362
                var remaining = new Sequence<Dee.ModelIter> ();
 
363
                foreach (var item in result_set)
 
364
                {
 
365
                    if (first_pass)
 
366
                        matches.insert_sorted (item, cmp_func);
 
367
                    else if (matches.lookup (item, cmp_func) != null)
 
368
                        remaining.insert_sorted (item, cmp_func);
 
369
                }
 
370
                if (!first_pass) matches = (owned) remaining;
 
371
                // final result set empty already?
 
372
                if (matches.get_begin_iter () == matches.get_end_iter ()) break;
 
373
 
 
374
                first_pass = false;
 
375
            }
 
376
 
 
377
            // matches now contain iterators into the all_tracks model which
 
378
            // match the search string
 
379
            var seq_iter = matches.get_begin_iter ();
 
380
            var seq_end_iter = matches.get_end_iter ();
 
381
            
 
382
            var albums_list = new HashSet<string> ();
 
383
            while (seq_iter != seq_end_iter)
 
384
            {
 
385
                var model_iter = seq_iter.get ();
 
386
                int year = model.get_int32 (model_iter, Columns.YEAR);
 
387
                string genre = model.get_string (model_iter, Columns.GENRE);
 
388
 
 
389
                // check filters
 
390
                if (year < min_year || year > max_year)
 
391
                {
 
392
                    seq_iter = seq_iter.next ();
 
393
                    continue;
 
394
                }
 
395
                
 
396
                // check filters
 
397
                if (active_genres != null) {
 
398
                    bool genre_match = (genre in active_genres);
 
399
                    if (!genre_match) {
 
400
                        seq_iter = seq_iter.next ();
 
401
                        continue;
 
402
                    }
 
403
                }
 
404
 
 
405
                unowned string album = model.get_string (model_iter,
 
406
                                                         Columns.ALBUM);
 
407
                // it's not first as in track #1, but first found from album
 
408
                bool first_track_from_album = !(album in albums_list);
 
409
                albums_list.add (album);
 
410
 
 
411
                if (first_track_from_album)
 
412
                {
 
413
                    category_id = category_override >= 0 ?
 
414
                        category_override : Category.ALBUMS;
 
415
 
 
416
                    search.results_model.append (
 
417
                        model.get_string (model_iter, Columns.URI),
 
418
                        model.get_string (model_iter, Columns.ARTWORK),
 
419
                        category_id,
 
420
                        model.get_string (model_iter, Columns.MIMETYPE),
 
421
                        model.get_string (model_iter, Columns.ALBUM),
 
422
                        model.get_string (model_iter, Columns.ARTIST),
 
423
                        model.get_string (model_iter, Columns.URI));
 
424
                }
 
425
 
 
426
                category_id = category_override >= 0 ?
 
427
                    category_override : Category.SONGS;
 
428
 
 
429
                search.results_model.append (
 
430
                    model.get_string (model_iter, Columns.URI),
 
431
                    model.get_string (model_iter, Columns.ARTWORK),
 
432
                    category_id,
 
433
                    model.get_string (model_iter, Columns.MIMETYPE),
 
434
                    model.get_string (model_iter, Columns.TITLE),
 
435
                    model.get_string (model_iter, Columns.ARTIST),
 
436
                    model.get_string (model_iter, Columns.URI));
 
437
 
 
438
                num_results++;
 
439
                if (max_results >= 0 && num_results >= max_results) break;
 
440
 
 
441
                seq_iter = seq_iter.next ();
 
442
            }
 
443
        }
 
444
 
 
445
        private void get_decade_filter (GLib.List<FilterParser> filters,
 
446
                                        out int min_year, out int max_year)
 
447
        {
 
448
            Filter? filter = null;
 
449
            foreach (var parser in filters)
 
450
            {
 
451
                if (parser is DecadeFilterParser) filter = parser.filter;
 
452
            }
 
453
 
 
454
            if (filter == null || !filter.filtering)
 
455
            {
 
456
                min_year = 0;
 
457
                max_year = int.MAX;
 
458
                return;
 
459
            }
 
460
 
 
461
            var mrf = filter as MultiRangeFilter;
 
462
            min_year = int.parse (mrf.get_first_active ().id);
 
463
            max_year = int.parse (mrf.get_last_active ().id);
 
464
        }
 
465
        
 
466
        private Set<string>? get_genre_filter (GLib.List<FilterParser> filters)
 
467
        {
 
468
            Filter? filter = null;
 
469
            foreach (var parser in filters)
 
470
            {
 
471
                if (parser is GenreFilterParser) filter = parser.filter;
 
472
            }
 
473
            if (filter == null || !filter.filtering)
 
474
            {
 
475
                return null;
 
476
            }
 
477
 
 
478
            var active_genres = new HashSet<string> ();
 
479
            var all_genres = filter as CheckOptionFilterCompact;
 
480
            foreach (FilterOption option in all_genres.options)
 
481
            {
 
482
                if (option.id == null || !option.active) continue;
 
483
                active_genres.add (option.id);
 
484
            }
 
485
 
 
486
            return active_genres;
 
487
        }
 
488
    }
 
489
}