2
* Copyright (C) 2012 Canonical Ltd
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.
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.
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/>.
16
* Authored by David Calle <davidc@framli.eu>
17
* Michal Hruby <michal.hruby@canonical.com>
24
namespace Unity.MusicLens
42
class RhythmboxCollection : Object
45
GenericArray<Track> tracks = new GenericArray<Track> ();
46
SequenceModel all_tracks;
47
FilterModel tracks_by_play_count;
51
ICUTermFilter ascii_filter;
55
// contains genre maps
56
Genre genre = new Genre ();
60
media_art_dir = Path.build_filename (
61
Environment.get_user_cache_dir (), "media-art");
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");
67
var filter = Dee.Filter.new_sort ((row1_in, row2_in) =>
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);
75
int a = row1[Columns.PLAY_COUNT].get_int32 ();
76
int b = row2[Columns.PLAY_COUNT].get_int32 ();
78
return b - a; // higher play count first
80
tracks_by_play_count = new FilterModel (all_tracks, filter);
82
ascii_filter = new ICUTermFilter.ascii_folder ();
83
analyzer = new TextAnalyzer ();
84
analyzer.add_term_filter ((terms_in, terms_out) =>
86
foreach (unowned string term in terms_in)
88
var folded = ascii_filter.apply (term);
89
terms_out.add_term (term);
90
if (folded != term) terms_out.add_term (folded);
93
var reader = ModelReader.new ((model, iter) =>
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));
101
index = new TreeIndex (all_tracks, analyzer, reader);
104
private string? get_albumart (Track track)
106
var artist = track.album_artist ?? track.artist;
107
var album = track.album;
109
var artist_norm = artist.normalize (-1, NormalizeMode.NFKD);
110
var album_norm = album.normalize (-1, NormalizeMode.NFKD);
112
var artist_md5 = Checksum.compute_for_string (ChecksumType.MD5,
114
var album_md5 = Checksum.compute_for_string (ChecksumType.MD5,
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;
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;
128
// Try Nautilus thumbnails
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;
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;
146
public void parse_file (string path)
150
Xml.Doc* doc = Xml.Parser.parse_file (path);
155
Xml.Node* root = doc->get_root_element ();
163
Xml.Parser.cleanup ();
166
private void parse_node (Xml.Node* node)
168
for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {
169
if (iter->type != Xml.ElementType.ELEMENT_NODE) {
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 ();
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) {
182
switch (iter_track->name) {
184
string node_content = iter_track->get_content ();
185
track.title = node_content;
188
string node_content = iter_track->get_content ();
189
track.uri = node_content;
192
string node_content = iter_track->get_content ();
193
track.artist = node_content;
196
string node_content = iter_track->get_content ();
197
track.mime_type = node_content;
200
string node_content = iter_track->get_content ();
201
track.album = node_content;
204
string node_content = iter_track->get_content ();
205
track.genre = node_content;
208
string node_content = iter_track->get_content ();
209
track.track_number = node_content;
212
string node_content = iter_track->get_content ();
213
track.album_artist = node_content;
216
string node_content = iter_track->get_content ();
217
track.year = int.parse (node_content);
220
string node_content = iter_track->get_content ();
221
track.play_count = int.parse (node_content);
225
// append to tracks array
229
string albumart = get_albumart (track);
230
if (albumart != null)
231
track.artwork_path = albumart;
233
track.artwork_path = "audio-x-generic";
235
// Get genre filter id
236
track.genre = genre.get_id_for_genre(track.genre.down ());
238
if (track.year > 0) {
242
all_tracks.append (track.uri, track.title,
243
track.artist, track.album,
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)
265
var empty_search = search.search_string.strip () == "";
270
Model model = all_tracks;
271
get_decade_filter (filters, out min_year, out max_year);
272
var active_genres = get_genre_filter (filters);
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> ();
282
while (iter != end_iter)
284
int year = model.get_int32 (iter, Columns.YEAR);
285
unowned string genre = model.get_string (iter, Columns.GENRE);
288
if (year < min_year || year > max_year)
290
iter = model.next (iter);
295
if (active_genres != null) {
296
if (!(genre in active_genres)) {
297
iter = model.next (iter);
302
unowned string album = model.get_string (iter,
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);
308
if (first_track_from_album)
310
category_id = category_override >= 0 ?
311
category_override : Category.ALBUMS;
313
search.results_model.append (
314
model.get_string (iter, Columns.URI),
315
model.get_string (iter, Columns.ARTWORK),
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));
323
category_id = category_override >= 0 ?
324
category_override : Category.SONGS;
326
search.results_model.append (
327
model.get_string (iter, Columns.URI),
328
model.get_string (iter, Columns.ARTWORK),
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));
336
if (max_results >= 0 && num_results >= max_results) break;
338
iter = model.next (iter);
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),
349
var matches = new Sequence<Dee.ModelIter> ();
350
bool first_pass = true;
351
foreach (unowned string term in term_list)
353
// FIXME: use PREFIX search only for the last term?
354
var result_set = index.lookup (term, TermMatchFlag.PREFIX);
356
CompareDataFunc<Dee.ModelIter> cmp_func = (a, b) =>
358
return a == b ? 0 : ((void*) a > (void*) b ? 1 : -1);
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)
366
matches.insert_sorted (item, cmp_func);
367
else if (matches.lookup (item, cmp_func) != null)
368
remaining.insert_sorted (item, cmp_func);
370
if (!first_pass) matches = (owned) remaining;
371
// final result set empty already?
372
if (matches.get_begin_iter () == matches.get_end_iter ()) break;
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 ();
382
var albums_list = new HashSet<string> ();
383
while (seq_iter != seq_end_iter)
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);
390
if (year < min_year || year > max_year)
392
seq_iter = seq_iter.next ();
397
if (active_genres != null) {
398
bool genre_match = (genre in active_genres);
400
seq_iter = seq_iter.next ();
405
unowned string album = model.get_string (model_iter,
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);
411
if (first_track_from_album)
413
category_id = category_override >= 0 ?
414
category_override : Category.ALBUMS;
416
search.results_model.append (
417
model.get_string (model_iter, Columns.URI),
418
model.get_string (model_iter, Columns.ARTWORK),
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));
426
category_id = category_override >= 0 ?
427
category_override : Category.SONGS;
429
search.results_model.append (
430
model.get_string (model_iter, Columns.URI),
431
model.get_string (model_iter, Columns.ARTWORK),
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));
439
if (max_results >= 0 && num_results >= max_results) break;
441
seq_iter = seq_iter.next ();
445
private void get_decade_filter (GLib.List<FilterParser> filters,
446
out int min_year, out int max_year)
448
Filter? filter = null;
449
foreach (var parser in filters)
451
if (parser is DecadeFilterParser) filter = parser.filter;
454
if (filter == null || !filter.filtering)
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);
466
private Set<string>? get_genre_filter (GLib.List<FilterParser> filters)
468
Filter? filter = null;
469
foreach (var parser in filters)
471
if (parser is GenreFilterParser) filter = parser.filter;
473
if (filter == null || !filter.filtering)
478
var active_genres = new HashSet<string> ();
479
var all_genres = filter as CheckOptionFilterCompact;
480
foreach (FilterOption option in all_genres.options)
482
if (option.id == null || !option.active) continue;
483
active_genres.add (option.id);
486
return active_genres;