~ricmm/+junk/unity-lens_music-sc

« back to all changes in this revision

Viewing changes to src/rhythmbox-collection.vala

  • Committer: Ricardo Mendoza
  • Date: 2012-09-05 14:20:15 UTC
  • Revision ID: ricardo.mendoza@canonical.com-20120905142015-prem6hiyfshwgm8q
Initial commit

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
        TYPE,
 
30
        URI,
 
31
        TITLE,
 
32
        ARTIST,
 
33
        ALBUM,
 
34
        ARTWORK,
 
35
        MIMETYPE,
 
36
        GENRE,
 
37
        ALBUM_ARTIST,
 
38
        TRACK_NUMBER,
 
39
        YEAR,
 
40
        PLAY_COUNT,
 
41
        DURATION,
 
42
 
 
43
        N_COLUMNS
 
44
    }
 
45
 
 
46
    class RhythmboxCollection : Object
 
47
    {
 
48
        const string UNKNOWN_ALBUM = _("Unknown");
 
49
 
 
50
        SequenceModel all_tracks;
 
51
        ModelTag<int> album_art_tag;
 
52
        FilterModel tracks_by_play_count;
 
53
        HashTable<string, GenericArray<ModelIter>> album_to_tracks_map;
 
54
 
 
55
        TDB.Database album_art_tdb;
 
56
        FileMonitor tdb_monitor;
 
57
        int current_album_art_tag;
 
58
 
 
59
        HashTable<unowned string, Variant> variant_store;
 
60
        HashTable<int, Variant> int_variant_store;
 
61
        Variant row_buffer[13];
 
62
 
 
63
        Analyzer analyzer;
 
64
        Index? index;
 
65
        ICUTermFilter ascii_filter;
 
66
 
 
67
        string media_art_dir;
 
68
 
 
69
        public class XmlParser: Object
 
70
        {
 
71
          const MarkupParser parser =
 
72
          {
 
73
            start_tag,
 
74
            end_tag,
 
75
            process_text,
 
76
            null,
 
77
            null
 
78
          };
 
79
 
 
80
          // contains genre maps
 
81
          Genre genre = new Genre ();
 
82
 
 
83
          MarkupParseContext context;
 
84
          bool is_rhythmdb_xml = false;
 
85
 
 
86
          construct
 
87
          {
 
88
            context = new MarkupParseContext (parser, 0, this, null);
 
89
          }
 
90
 
 
91
          public bool parse (string content, size_t len) throws MarkupError
 
92
          {
 
93
            return context.parse (content, (ssize_t) len);
 
94
          }
 
95
 
 
96
          bool processing_track;
 
97
          Track current_track;
 
98
          int current_data = -1;
 
99
 
 
100
          private void start_tag (MarkupParseContext context, string name,
 
101
            [CCode (array_length = false, array_null_terminated = true)] string[] attr_names, [CCode (array_length = false, array_null_terminated = true)] string[] attr_values)
 
102
            throws MarkupError
 
103
          {
 
104
            if (!processing_track)
 
105
            {
 
106
              switch (name)
 
107
              {
 
108
                case "rhythmdb": is_rhythmdb_xml = true; break;
 
109
                case "entry":
 
110
                  string accepted_element_name = null;
 
111
                  for (int i = 0; attr_names[i] != null; i++)
 
112
                  {
 
113
                    if (attr_names[i] == "type" && (attr_values[i] == "song"
 
114
                        || attr_values[i] == "iradio"))
 
115
                      accepted_element_name = attr_values[i];
 
116
                  }
 
117
                  if (accepted_element_name == null) return;
 
118
                  processing_track = true;
 
119
                  current_track = new Track ();
 
120
                  current_track.type_track = accepted_element_name == "song" ?
 
121
                                             TrackType.SONG : TrackType.RADIO;
 
122
                  break;
 
123
              }
 
124
            }
 
125
            else
 
126
            {
 
127
              switch (name)
 
128
              {
 
129
                case "location": current_data = Columns.URI; break;
 
130
                case "title": current_data = Columns.TITLE; break;
 
131
                case "duration": current_data = Columns.DURATION; break;
 
132
                case "artist": current_data = Columns.ARTIST; break;
 
133
                case "album": current_data = Columns.ALBUM; break;
 
134
                case "genre": current_data = Columns.GENRE; break;
 
135
                case "track-number": current_data = Columns.TRACK_NUMBER; break;
 
136
                case "play-count": current_data = Columns.PLAY_COUNT; break;
 
137
                case "date": current_data = Columns.YEAR; break;
 
138
                case "media-type": current_data = Columns.MIMETYPE; break;
 
139
                case "album-artist": current_data = Columns.ALBUM_ARTIST; break;
 
140
                default: current_data = -1; break;
 
141
              }
 
142
            }
 
143
          }
 
144
 
 
145
          public signal void track_info_ready (Track track);
 
146
 
 
147
          private void end_tag (MarkupParseContext content, string name)
 
148
            throws MarkupError
 
149
          {
 
150
            switch (name)
 
151
            {
 
152
              case "location":
 
153
              case "title":
 
154
              case "duration":
 
155
              case "artist":
 
156
              case "album":
 
157
              case "genre":
 
158
              case "track-number":
 
159
              case "play-count":
 
160
              case "date":
 
161
              case "media-type":
 
162
              case "album-artist":
 
163
                if (current_data >= 0) current_data = -1;
 
164
                break;
 
165
              case "hidden":
 
166
                if (processing_track) processing_track = false;
 
167
                break;
 
168
              case "entry":
 
169
                if (processing_track && current_track != null)
 
170
                {
 
171
                  track_info_ready (current_track);
 
172
                }
 
173
                processing_track = false;
 
174
                break;
 
175
            }
 
176
          }
 
177
 
 
178
          private void process_text (MarkupParseContext context,
 
179
                                     string text, size_t text_len)
 
180
            throws MarkupError
 
181
          {
 
182
            if (!processing_track || current_data < 0) return;
 
183
            switch (current_data)
 
184
            {
 
185
              case Columns.URI: current_track.uri = text; break;
 
186
              case Columns.TITLE: current_track.title = text; break;
 
187
              case Columns.ARTIST: current_track.artist = text; break;
 
188
              case Columns.ALBUM: current_track.album = text; break;
 
189
              case Columns.ALBUM_ARTIST: 
 
190
                current_track.album_artist = text;
 
191
                break;
 
192
              case Columns.GENRE:
 
193
                current_track.genre = genre.get_id_for_genre (text.down ());
 
194
                break;
 
195
              case Columns.MIMETYPE:
 
196
                current_track.mime_type = text;
 
197
                break;
 
198
              case Columns.YEAR:
 
199
                current_track.year = int.parse (text) / 365;
 
200
                break;
 
201
              case Columns.PLAY_COUNT:
 
202
                current_track.play_count = int.parse (text);
 
203
                break;
 
204
              case Columns.TRACK_NUMBER:
 
205
                current_track.track_number = int.parse (text);
 
206
                break;
 
207
              case Columns.DURATION:
 
208
                current_track.duration = int.parse (text);
 
209
                break;
 
210
            }
 
211
          }
 
212
        }
 
213
 
 
214
        construct
 
215
        {
 
216
          static_assert (13 == Columns.N_COLUMNS); // sync with row_buffer size
 
217
          media_art_dir = Path.build_filename (
 
218
              Environment.get_user_cache_dir (), "media-art");
 
219
 
 
220
          variant_store = new HashTable<unowned string, Variant> (str_hash,
 
221
                                                                  str_equal);
 
222
          int_variant_store = new HashTable<int, Variant> (direct_hash,
 
223
                                                           direct_equal);
 
224
          all_tracks = new SequenceModel ();
 
225
          // the columns correspond to the Columns enum
 
226
          all_tracks.set_schema ("i", "s", "s", "s", "s", "s", "s",
 
227
                                 "s", "s", "i", "i", "i", "i");
 
228
          assert (all_tracks.get_schema ().length == Columns.N_COLUMNS);
 
229
          album_art_tag = new ModelTag<int> (all_tracks);
 
230
          album_to_tracks_map =
 
231
            new HashTable<string, GenericArray<ModelIter>> (str_hash,
 
232
                                                            str_equal);
 
233
 
 
234
          var filter = Dee.Filter.new_sort ((row1, row2) =>
 
235
          {
 
236
            int a = row1[Columns.PLAY_COUNT].get_int32 ();
 
237
            int b = row2[Columns.PLAY_COUNT].get_int32 ();
 
238
 
 
239
            return b - a; // higher play count first
 
240
          });
 
241
          tracks_by_play_count = new FilterModel (all_tracks, filter);
 
242
 
 
243
          ascii_filter = new ICUTermFilter.ascii_folder ();
 
244
          analyzer = new TextAnalyzer ();
 
245
          analyzer.add_term_filter ((terms_in, terms_out) =>
 
246
          {
 
247
            foreach (unowned string term in terms_in)
 
248
            {
 
249
              var folded = ascii_filter.apply (term);
 
250
              terms_out.add_term (term);
 
251
              if (folded != term) terms_out.add_term (folded);
 
252
            }
 
253
          });
 
254
          initialize_index ();
 
255
        }
 
256
 
 
257
        private void initialize_index ()
 
258
        {
 
259
          var reader = ModelReader.new ((model, iter) =>
 
260
          {
 
261
            var s ="%s\n%s\n%s".printf (model.get_string (iter, Columns.TITLE),
 
262
                                        model.get_string (iter, Columns.ARTIST),
 
263
                                        model.get_string (iter, Columns.ALBUM));
 
264
            return s;
 
265
          });
 
266
 
 
267
          index = new TreeIndex (all_tracks, analyzer, reader);
 
268
        }
 
269
 
 
270
        private string? check_album_art_tdb (string artist, string album)
 
271
        {
 
272
          if (album_art_tdb == null) return null;
 
273
 
 
274
          uint8 null_helper[1] = { 0 };
 
275
          ByteArray byte_arr = new ByteArray ();
 
276
          byte_arr.append ("album".data);
 
277
          byte_arr.append (null_helper);
 
278
          byte_arr.append (album.data);
 
279
          byte_arr.append (null_helper);
 
280
          byte_arr.append ("artist".data);
 
281
          byte_arr.append (null_helper);
 
282
          byte_arr.append (artist.data);
 
283
          byte_arr.append (null_helper);
 
284
 
 
285
          TDB.Data key = TDB.NULL_DATA;
 
286
          key.data = byte_arr.data;
 
287
          var val = album_art_tdb.fetch (key);
 
288
 
 
289
          if (val.data != null)
 
290
          {
 
291
            Variant v = Variant.new_from_data<int> (new VariantType ("a{sv}"), val.data, false);
 
292
            var file_variant = v.lookup_value ("file", VariantType.STRING);
 
293
            if (file_variant != null)
 
294
            {
 
295
              return file_variant.get_string ();
 
296
            }
 
297
          }
 
298
 
 
299
          return null;
 
300
        }
 
301
        
 
302
        private string? get_albumart (Track track)
 
303
        {
 
304
            string filename;
 
305
            var artist = track.album_artist ?? track.artist;
 
306
            var album = track.album;
 
307
 
 
308
            var artist_norm = artist.normalize (-1, NormalizeMode.NFKD);
 
309
            var album_norm = album.normalize (-1, NormalizeMode.NFKD);
 
310
 
 
311
            filename = check_album_art_tdb (artist, album);
 
312
            if (filename != null)
 
313
            {
 
314
              filename = Path.build_filename (Environment.get_user_cache_dir (),
 
315
                                              "rhythmbox", "album-art",
 
316
                                              filename);
 
317
 
 
318
              if (FileUtils.test (filename, FileTest.EXISTS)) return filename;
 
319
            }
 
320
 
 
321
            var artist_md5 = Checksum.compute_for_string (ChecksumType.MD5,
 
322
                                                          artist_norm);
 
323
            var album_md5 = Checksum.compute_for_string (ChecksumType.MD5,
 
324
                                                         album_norm);
 
325
 
 
326
            filename = Path.build_filename (media_art_dir,
 
327
                "album-%s-%s".printf (artist_md5, album_md5));
 
328
            if (FileUtils.test (filename, FileTest.EXISTS)) return filename;
 
329
 
 
330
            var combined = "%s\t%s".printf (artist, album).normalize (-1, NormalizeMode.NFKD);
 
331
            filename = Path.build_filename (media_art_dir,
 
332
                "album-%s.jpg".printf (Checksum.compute_for_string (
 
333
                    ChecksumType.MD5, combined)));
 
334
            if (FileUtils.test (filename, FileTest.EXISTS)) return filename;
 
335
 
 
336
            // Try Nautilus thumbnails
 
337
            try
 
338
            {
 
339
                File artwork_file = File.new_for_uri (track.uri);
 
340
                var info = artwork_file.query_info (FILE_ATTRIBUTE_THUMBNAIL_PATH, 0, null);
 
341
                var thumbnail_path = info.get_attribute_string (FILE_ATTRIBUTE_THUMBNAIL_PATH);
 
342
                if (thumbnail_path != null) return thumbnail_path;
 
343
            } catch {}
 
344
 
 
345
            // Try covers folder
 
346
            string artwork = Path.build_filename (
 
347
                Environment.get_user_cache_dir (), "rhythmbox", "covers",
 
348
                "%s - %s.jpg".printf (track.artist, track.album));
 
349
            if (FileUtils.test (artwork, FileTest.EXISTS)) return artwork;
 
350
 
 
351
            return null;
 
352
        }
 
353
 
 
354
        public SList<unowned string> get_album_tracks (string album_key)
 
355
        {
 
356
          SList<unowned string> results = new SList<unowned string> ();
 
357
 
 
358
          var iter_arr = album_to_tracks_map[album_key];
 
359
          if (iter_arr != null)
 
360
          {
 
361
            for (int i = iter_arr.length - 1; i >= 0; i--)
 
362
            {
 
363
              results.prepend (all_tracks.get_string (iter_arr[i], Columns.URI));
 
364
            }
 
365
          }
 
366
 
 
367
          return results;
 
368
        }
 
369
 
 
370
        private Track get_track (ModelIter iter)
 
371
        {
 
372
            Track track = new Track();
 
373
                
 
374
            track.uri = all_tracks.get_string (iter, Columns.URI);
 
375
            track.title = all_tracks.get_string (iter, Columns.TITLE);
 
376
            track.artist = all_tracks.get_string (iter, Columns.ARTIST);
 
377
            track.album = all_tracks.get_string (iter, Columns.ALBUM);
 
378
            track.artwork_path = all_tracks.get_string (iter, Columns.ARTWORK);
 
379
            track.mime_type = all_tracks.get_string (iter, Columns.MIMETYPE);
 
380
            track.genre = all_tracks.get_string (iter, Columns.GENRE);
 
381
            track.album_artist = all_tracks.get_string (iter, Columns.ALBUM_ARTIST);
 
382
            track.track_number = all_tracks.get_int32 (iter, Columns.TRACK_NUMBER);
 
383
            track.year = all_tracks.get_int32 (iter, Columns.YEAR);
 
384
            track.play_count = all_tracks.get_int32 (iter, Columns.PLAY_COUNT);
 
385
            track.duration = all_tracks.get_int32 (iter, Columns.DURATION);
 
386
            
 
387
            return track;
 
388
        }
 
389
 
 
390
        public Track? get_album_track (string uri)
 
391
        {
 
392
            var iter = all_tracks.get_first_iter ();
 
393
            var end_iter = all_tracks.get_last_iter ();
 
394
 
 
395
            // FIXME: linear search, change to insert_sorted / find_sorted
 
396
            while (iter != end_iter)
 
397
            {
 
398
                if (all_tracks.get_string (iter, Columns.URI) == uri) {
 
399
                    return get_track (iter);
 
400
                }
 
401
                iter = all_tracks.next (iter);
 
402
            }
 
403
            return null;
 
404
        }
 
405
 
 
406
        public SList<Track> get_album_tracks_detailed (string album_key)
 
407
        {
 
408
          var results = new SList<Track> ();
 
409
 
 
410
          var iter_arr = album_to_tracks_map[album_key];
 
411
          if (iter_arr != null)
 
412
          {
 
413
            for (int i = iter_arr.length - 1; i >= 0; i--)
 
414
            {
 
415
                results.prepend (get_track (iter_arr[i]));
 
416
            }
 
417
          }
 
418
 
 
419
          return results;
 
420
        }
 
421
 
 
422
        private Variant cached_variant_for_string (string? input)
 
423
        {
 
424
          unowned string text = input != null ? input : "";
 
425
          Variant? v = variant_store[text];
 
426
          if (v != null) return v;
 
427
 
 
428
          v = new Variant.string (text);
 
429
          // key is owned by value... awesome right?
 
430
          variant_store[v.get_string ()] = v;
 
431
          return v;
 
432
        }
 
433
 
 
434
        private Variant cached_variant_for_int (int input)
 
435
        {
 
436
          Variant? v = int_variant_store[input];
 
437
          if (v != null) return v;
 
438
 
 
439
          v = new Variant.int32 (input);
 
440
          // let's not cache every random integer
 
441
          if (input < 128)
 
442
            int_variant_store[input] = v;
 
443
          return v;
 
444
        }
 
445
 
 
446
        private void prepare_row_buffer (Track track)
 
447
        {
 
448
          Variant type = cached_variant_for_int (track.type_track);
 
449
          Variant uri = new Variant.string (track.uri);
 
450
          Variant title = new Variant.string (track.title);
 
451
          Variant artist = cached_variant_for_string (track.artist);
 
452
          Variant album_artist = cached_variant_for_string (track.album_artist);
 
453
          Variant album = cached_variant_for_string (track.album);
 
454
          Variant mime_type = cached_variant_for_string (track.mime_type);
 
455
          Variant artwork = cached_variant_for_string (track.artwork_path);
 
456
          Variant genre = cached_variant_for_string (track.genre);
 
457
          Variant track_number = cached_variant_for_int (track.track_number);
 
458
          Variant year = cached_variant_for_int (track.year);
 
459
          Variant play_count = cached_variant_for_int (track.play_count);
 
460
          Variant duration = cached_variant_for_int (track.duration);
 
461
 
 
462
          row_buffer[0] = type;
 
463
          row_buffer[1] = uri;
 
464
          row_buffer[2] = title;
 
465
          row_buffer[3] = artist;
 
466
          row_buffer[4] = album;
 
467
          row_buffer[5] = artwork;
 
468
          row_buffer[6] = mime_type;
 
469
          row_buffer[7] = genre;
 
470
          row_buffer[8] = album_artist;
 
471
          row_buffer[9] = track_number;
 
472
          row_buffer[10] = year;
 
473
          row_buffer[11] = play_count;
 
474
          row_buffer[12] = duration;
 
475
        }
 
476
 
 
477
        public void parse_metadata_file (string path)
 
478
        {
 
479
          if (album_art_tdb != null) return;
 
480
 
 
481
          if (tdb_monitor == null)
 
482
          {
 
483
            var tdb_file = File.new_for_path (path);
 
484
            try
 
485
            {
 
486
              tdb_monitor = tdb_file.monitor (FileMonitorFlags.NONE);
 
487
              tdb_monitor.changed.connect (() =>
 
488
              {
 
489
                if (album_art_tdb == null) parse_metadata_file (path);
 
490
                else current_album_art_tag++;
 
491
              });
 
492
            }
 
493
            catch (Error err)
 
494
            {
 
495
              warning ("%s", err.message);
 
496
            }
 
497
          }
 
498
 
 
499
          var flags = TDB.OpenFlags.INCOMPATIBLE_HASH | TDB.OpenFlags.SEQNUM | TDB.OpenFlags.NOLOCK;
 
500
          album_art_tdb = new TDB.Database (path, 999, flags,
 
501
                                            Posix.O_RDONLY, 0600);
 
502
          if (album_art_tdb == null)
 
503
          {
 
504
            warning ("Unable to open album-art DB!");
 
505
            return;
 
506
          }
 
507
 
 
508
          /*
 
509
          album_art_tdb.traverse ((db, key, val) =>
 
510
          {
 
511
            var byte_arr = new ByteArray.sized ((uint) val.data_size);
 
512
            byte_arr.append (val.data);
 
513
            Variant v = Variant.new_from_data<ByteArray> (new VariantType ("a{sv}"), byte_arr.data, false, byte_arr);
 
514
            message ("value: %s", v.print (true));
 
515
 
 
516
            return 0;
 
517
          });
 
518
          */
 
519
        }
 
520
 
 
521
        public void parse_file (string path)
 
522
        {
 
523
          // this could be really expensive if the index was already built, so
 
524
          // we'll destroy it first
 
525
          index = null;
 
526
          all_tracks.clear ();
 
527
          initialize_index ();
 
528
          current_album_art_tag = 0;
 
529
          album_to_tracks_map.remove_all ();
 
530
 
 
531
          var parser = new XmlParser ();
 
532
          parser.track_info_ready.connect ((track) =>
 
533
          {
 
534
            // Get cover art
 
535
            string albumart = get_albumart (track);
 
536
            if (albumart != null)
 
537
              track.artwork_path = albumart;
 
538
            else
 
539
              track.artwork_path = "audio-x-generic";
 
540
 
 
541
            prepare_row_buffer (track);
 
542
            var iter = all_tracks.append_row (row_buffer);
 
543
 
 
544
            if (track.album == "" || track.album == UNKNOWN_ALBUM) return;
 
545
            var album_key = "%s - %s".printf (track.album, track.album_artist != null ? track.album_artist : track.artist);
 
546
            var arr = album_to_tracks_map[album_key];
 
547
            if (arr == null)
 
548
            {
 
549
              arr = new GenericArray<ModelIter> ();
 
550
              album_to_tracks_map[album_key] = arr;
 
551
            }
 
552
            arr.add (iter);
 
553
          });
 
554
 
 
555
          var file = File.new_for_path (path);
 
556
 
 
557
          try
 
558
          {
 
559
            var stream = file.read (null);
 
560
            uint8 buffer[65536];
 
561
 
 
562
            size_t bytes_read;
 
563
            while ((bytes_read = stream.read (buffer, null)) > 0)
 
564
            {
 
565
              parser.parse ((string) buffer, bytes_read);
 
566
            }
 
567
          }
 
568
          catch (Error err)
 
569
          {
 
570
            warning ("Error while parsing rhythmbox DB: %s", err.message);
 
571
          }
 
572
 
 
573
          GLib.List<unowned string> all_albums = album_to_tracks_map.get_keys ();
 
574
          foreach (unowned string s in all_albums)
 
575
          {
 
576
            album_to_tracks_map[s].sort_with_data ((a, b) =>
 
577
            {
 
578
              var trackno1 = all_tracks.get_int32 (a, Columns.TRACK_NUMBER);
 
579
              var trackno2 = all_tracks.get_int32 (b, Columns.TRACK_NUMBER);
 
580
 
 
581
              return trackno1 - trackno2;
 
582
            });
 
583
          }
 
584
        }
 
585
 
 
586
        private enum ResultType
 
587
        {
 
588
          ALBUM,
 
589
          SONG,
 
590
          RADIO
 
591
        }
 
592
 
 
593
        private void add_result (Model results_model, Model model,
 
594
                                 ModelIter iter, ResultType result_type,
 
595
                                 uint category_id)
 
596
        {
 
597
          // check for updated album art
 
598
          var tag = album_art_tag[model, iter];
 
599
          if (tag < current_album_art_tag)
 
600
          {
 
601
            unowned string album = model.get_string (iter, Columns.ALBUM);
 
602
            unowned string artist = model.get_string (iter,
 
603
                                                      Columns.ALBUM_ARTIST);
 
604
            if (artist == "")
 
605
              artist = model.get_string (iter, Columns.ARTIST);
 
606
 
 
607
            var album_art_string = check_album_art_tdb (artist, album);
 
608
            if (album_art_string != null)
 
609
            {
 
610
              string filename;
 
611
              filename = Path.build_filename (Environment.get_user_cache_dir (),
 
612
                                              "rhythmbox", "album-art",
 
613
                                              album_art_string);
 
614
              album_art_string = FileUtils.test (filename, FileTest.EXISTS) ?
 
615
                filename : "audio-x-generic";
 
616
 
 
617
              if (album_art_string != model.get_string (iter, Columns.ARTWORK))
 
618
              {
 
619
                model.set_value (iter, Columns.ARTWORK,
 
620
                                 cached_variant_for_string (album_art_string));
 
621
              }
 
622
            }
 
623
            album_art_tag[model, iter] = current_album_art_tag;
 
624
          }
 
625
 
 
626
          var title_col = (result_type == ResultType.SONG
 
627
                           || result_type == ResultType.RADIO) ?
 
628
            Columns.TITLE : Columns.ALBUM;
 
629
          unowned string title = model.get_string (iter, title_col);
 
630
          var uri = model.get_string (iter, Columns.URI);
 
631
          var dnd_uri = model.get_string (iter, Columns.URI);
 
632
          if (result_type == ResultType.ALBUM)
 
633
          {
 
634
            if (title == "" || title == UNKNOWN_ALBUM) return;
 
635
            unowned string artist = model.get_string (iter,
 
636
                                                      Columns.ALBUM_ARTIST);
 
637
            if (artist == "")
 
638
              artist = model.get_string (iter, Columns.ARTIST);
 
639
            var album_key = "%s - %s".printf (title, artist);
 
640
            StringBuilder sb = new StringBuilder ();
 
641
            foreach (unowned string track_uri in get_album_tracks (album_key))
 
642
            {
 
643
              sb.append_printf ("%s\r\n", track_uri);
 
644
            }
 
645
            dnd_uri = (owned) sb.str;
 
646
            uri = "album://%s".printf (album_key);
 
647
          }
 
648
 
 
649
          results_model.append (uri,
 
650
                                model.get_string (iter, Columns.ARTWORK),
 
651
                                category_id,
 
652
                                model.get_string (iter, Columns.MIMETYPE),
 
653
                                title,
 
654
                                model.get_string (iter, Columns.ARTIST),
 
655
                                dnd_uri);
 
656
        }
 
657
 
 
658
        public void search (LensSearch search, 
 
659
                            SearchType search_type, 
 
660
                            GLib.List<FilterParser>? filters = null,
 
661
                            int max_results = -1,
 
662
                            int category_override = -1)
 
663
        {
 
664
            int num_results = 0;
 
665
            var empty_search = search.search_string.strip () == "";
 
666
            int min_year;
 
667
            int max_year;
 
668
            int category_id;
 
669
            ResultType result_type;
 
670
 
 
671
            Model model = all_tracks;
 
672
            get_decade_filter (filters, out min_year, out max_year);
 
673
            var active_genres = get_genre_filter (filters);
 
674
 
 
675
            // we need this to be able to sort the albums properly
 
676
            var helper_model = search.results_model;
 
677
            if (category_override >= 0)
 
678
            {
 
679
              helper_model = new Dee.SequenceModel ();
 
680
              helper_model.set_schema_full (search.results_model.get_schema ());
 
681
            }
 
682
 
 
683
            if (empty_search)
 
684
            {
 
685
                // display a couple of most played songs
 
686
                model = tracks_by_play_count;
 
687
                var iter = model.get_first_iter ();
 
688
                var end_iter = model.get_last_iter ();
 
689
                var albums_list_nosearch = new HashSet<string> ();
 
690
 
 
691
                while (iter != end_iter)
 
692
                {
 
693
                    int year = model.get_int32 (iter, Columns.YEAR);
 
694
                    unowned string genre = model.get_string (iter, Columns.GENRE);
 
695
 
 
696
                    // check filters
 
697
                    if (year < min_year || year > max_year)
 
698
                    {
 
699
                        iter = model.next (iter);
 
700
                        continue;
 
701
                    }
 
702
                    
 
703
                    // check filters
 
704
                    if (active_genres != null) {
 
705
                        if (!(genre in active_genres)) {
 
706
                            iter = model.next (iter);
 
707
                            continue;
 
708
                        }
 
709
                    }
 
710
 
 
711
                    if (model.get_int32 (iter, Columns.TYPE) == TrackType.SONG)
 
712
                    {
 
713
                      unowned string album = model.get_string (iter,
 
714
                                                               Columns.ALBUM);
 
715
                      // it's not first as in track #1, but first found from album
 
716
                      bool first_track_from_album = !(album in albums_list_nosearch);
 
717
                      albums_list_nosearch.add (album);
 
718
                    
 
719
                      if (first_track_from_album)
 
720
                      {
 
721
                          category_id = category_override >= 0 ?
 
722
                              category_override : Category.ALBUMS;
 
723
                          
 
724
                          add_result (search.results_model, model, iter,
 
725
                                      ResultType.ALBUM, category_id);
 
726
                      }
 
727
 
 
728
                      category_id = Category.SONGS;
 
729
                      result_type = ResultType.SONG;
 
730
                    }
 
731
                    else
 
732
                    {
 
733
                      category_id = Category.RADIOS;
 
734
                      result_type = ResultType.RADIO;
 
735
                    }
 
736
                    if (category_override >= 0)
 
737
                      category_id = category_override;
 
738
 
 
739
                    add_result (helper_model, model, iter,
 
740
                                result_type, category_id);
 
741
 
 
742
                    num_results++;
 
743
                    if (max_results >= 0 && num_results >= max_results) break;
 
744
 
 
745
                    iter = model.next (iter);
 
746
                }
 
747
                return;
 
748
            }
 
749
 
 
750
 
 
751
            var term_list = Object.new (typeof (Dee.TermList)) as Dee.TermList;
 
752
            // search only the folded terms, FIXME: is that a good idea?
 
753
            analyzer.tokenize (ascii_filter.apply (search.search_string),
 
754
                               term_list);
 
755
 
 
756
            var matches = new Sequence<Dee.ModelIter> ();
 
757
            bool first_pass = true;
 
758
            foreach (unowned string term in term_list)
 
759
            {
 
760
                // FIXME: use PREFIX search only for the last term?
 
761
                var result_set = index.lookup (term, TermMatchFlag.PREFIX);
 
762
 
 
763
                CompareDataFunc<Dee.ModelIter> cmp_func = (a, b) =>
 
764
                {
 
765
                    return a == b ? 0 : ((void*) a > (void*) b ? 1 : -1);
 
766
                };
 
767
 
 
768
                // intersect the results (cause we want to AND the terms)
 
769
                var remaining = new Sequence<Dee.ModelIter> ();
 
770
                foreach (var item in result_set)
 
771
                {
 
772
                    if (first_pass)
 
773
                        matches.insert_sorted (item, cmp_func);
 
774
                    else if (matches.lookup (item, cmp_func) != null)
 
775
                        remaining.insert_sorted (item, cmp_func);
 
776
                }
 
777
                if (!first_pass) matches = (owned) remaining;
 
778
                // final result set empty already?
 
779
                if (matches.get_begin_iter () == matches.get_end_iter ()) break;
 
780
 
 
781
                first_pass = false;
 
782
            }
 
783
 
 
784
            // matches now contain iterators into the all_tracks model which
 
785
            // match the search string
 
786
            var seq_iter = matches.get_begin_iter ();
 
787
            var seq_end_iter = matches.get_end_iter ();
 
788
            
 
789
            var albums_list = new HashSet<string> ();
 
790
            while (seq_iter != seq_end_iter)
 
791
            {
 
792
                var model_iter = seq_iter.get ();
 
793
                int year = model.get_int32 (model_iter, Columns.YEAR);
 
794
                string genre = model.get_string (model_iter, Columns.GENRE);
 
795
 
 
796
                // check filters
 
797
                if (year < min_year || year > max_year)
 
798
                {
 
799
                    seq_iter = seq_iter.next ();
 
800
                    continue;
 
801
                }
 
802
                
 
803
                // check filters
 
804
                if (active_genres != null) {
 
805
                    bool genre_match = (genre in active_genres);
 
806
                    if (!genre_match) {
 
807
                        seq_iter = seq_iter.next ();
 
808
                        continue;
 
809
                    }
 
810
                }
 
811
 
 
812
 
 
813
                if (model.get_int32 (model_iter, Columns.TYPE) == TrackType.SONG)
 
814
                {
 
815
                  unowned string album = model.get_string (model_iter,
 
816
                                                           Columns.ALBUM);
 
817
                  // it's not first as in track #1, but first found from album
 
818
                  bool first_track_from_album = !(album in albums_list);
 
819
                  albums_list.add (album);
 
820
  
 
821
                  if (first_track_from_album)
 
822
                  {
 
823
                      category_id = category_override >= 0 ?
 
824
                          category_override : Category.ALBUMS;
 
825
  
 
826
                      add_result (search.results_model, model, model_iter,
 
827
                                  ResultType.ALBUM, category_id);
 
828
                  }
 
829
 
 
830
                  category_id = Category.SONGS;
 
831
                  result_type = ResultType.SONG;
 
832
                }
 
833
                else
 
834
                {
 
835
                  category_id = Category.RADIOS;
 
836
                  result_type = ResultType.RADIO;
 
837
                }
 
838
                if (category_override >= 0)
 
839
                  category_id = category_override;
 
840
 
 
841
                add_result (helper_model, model, model_iter,
 
842
                            result_type, category_id);
 
843
 
 
844
                num_results++;
 
845
                if (max_results >= 0 && num_results >= max_results) break;
 
846
 
 
847
                seq_iter = seq_iter.next ();
 
848
            }
 
849
 
 
850
            if (helper_model == search.results_model) return;
 
851
 
 
852
            // we need to do this because the dash doesn't care about position
 
853
            // of a newly added rows in the model - it just appends it
 
854
            var iter = helper_model.get_first_iter ();
 
855
            var last = helper_model.get_last_iter ();
 
856
            while (iter != last)
 
857
            {
 
858
              var row = helper_model.get_row (iter);
 
859
              search.results_model.append_row (row);
 
860
              iter = helper_model.next (iter);
 
861
            }
 
862
        }
 
863
 
 
864
        private void get_decade_filter (GLib.List<FilterParser> filters,
 
865
                                        out int min_year, out int max_year)
 
866
        {
 
867
            Filter? filter = null;
 
868
            foreach (var parser in filters)
 
869
            {
 
870
                if (parser is DecadeFilterParser) filter = parser.filter;
 
871
            }
 
872
 
 
873
            if (filter == null || !filter.filtering)
 
874
            {
 
875
                min_year = 0;
 
876
                max_year = int.MAX;
 
877
                return;
 
878
            }
 
879
 
 
880
            var mrf = filter as MultiRangeFilter;
 
881
            min_year = int.parse (mrf.get_first_active ().id);
 
882
            // it's supposed to be a decade, so 2000-2009
 
883
            max_year = int.parse (mrf.get_last_active ().id) + 9;
 
884
        }
 
885
        
 
886
        private Set<string>? get_genre_filter (GLib.List<FilterParser> filters)
 
887
        {
 
888
            Filter? filter = null;
 
889
            foreach (var parser in filters)
 
890
            {
 
891
                if (parser is GenreFilterParser) filter = parser.filter;
 
892
            }
 
893
            if (filter == null || !filter.filtering)
 
894
            {
 
895
                return null;
 
896
            }
 
897
 
 
898
            var active_genres = new HashSet<string> ();
 
899
            var all_genres = filter as CheckOptionFilterCompact;
 
900
            foreach (FilterOption option in all_genres.options)
 
901
            {
 
902
                if (option.id == null || !option.active) continue;
 
903
                active_genres.add (option.id);
 
904
            }
 
905
 
 
906
            return active_genres;
 
907
        }
 
908
    }
 
909
}