1 /*
2  * search-model.cc
3  * Copyright 2011-2019 John Lindgren and René J.V. Bertin
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice,
9  *    this list of conditions, and the following disclaimer.
10  *
11  * 2. Redistributions in binary form must reproduce the above copyright notice,
12  *    this list of conditions, and the following disclaimer in the documentation
13  *    provided with the distribution.
14  *
15  * This software is provided "as is" and without any warranty, express or
16  * implied. In no event shall the authors be liable for any damages arising from
17  * the use of this software.
18  */
19 
20 #include "search-model.h"
21 
22 #include <QMimeData>
23 #include <QUrl>
24 
25 #include <libaudcore/i18n.h>
26 
create_item_label(const Item & item)27 static QString create_item_label (const Item & item)
28 {
29     static constexpr aud::array<SearchField, const char *> start_tags =
30         {"", "<b>", "<i>", ""};
31     static constexpr aud::array<SearchField, const char *> end_tags =
32         {"", "</b>", "</i>", ""};
33 
34     QString string = start_tags[item.field];
35 
36     string += QString ((item.field == SearchField::Genre) ?
37                        str_toupper_utf8 (item.name) : item.name).toHtmlEscaped ();
38 
39     string += end_tags[item.field];
40 
41 #ifdef Q_OS_MAC  // Mac-specific font tweaks
42     string += "<br>&nbsp;";
43 #else
44     string += "<br><small>&nbsp;";
45 #endif
46 
47     if (item.field != SearchField::Title)
48     {
49         string += str_printf (dngettext (PACKAGE, "%d song", "%d songs",
50          item.matches.len ()), item.matches.len ());
51 
52         if (item.field == SearchField::Genre || item.parent)
53             string += ' ';
54     }
55 
56     if (item.field == SearchField::Genre)
57     {
58         string += _("of this genre");
59     }
60     else if (item.parent)
61     {
62         auto parent = (item.parent->parent ? item.parent->parent : item.parent);
63 
64         string += (parent->field == SearchField::Album) ? _("on") : _("by");
65         string += ' ';
66         string += start_tags[parent->field];
67         string += QString (parent->name).toHtmlEscaped ();
68         string += end_tags[parent->field];
69     }
70 
71 #ifndef Q_OS_MAC  // Mac-specific font tweaks
72     string += "</small>";
73 #endif
74 
75     return string;
76 }
77 
data(const QModelIndex & index,int role) const78 QVariant SearchModel::data (const QModelIndex & index, int role) const
79 {
80     if (role == Qt::DisplayRole)
81     {
82         int row = index.row ();
83         if (row < 0 || row >= m_items.len ())
84             return QVariant ();
85 
86         return create_item_label (* m_items[row]);
87     }
88 
89     return QVariant ();
90 }
91 
mimeData(const QModelIndexList & indexes) const92 QMimeData * SearchModel::mimeData (const QModelIndexList & indexes) const
93 {
94     m_playlist.select_all (false);
95 
96     QList<QUrl> urls;
97     for (auto & index : indexes)
98     {
99         int row = index.row ();
100         if (row < 0 || row >= m_items.len ())
101             continue;
102 
103         for (int entry : m_items[row]->matches)
104         {
105             urls.append (QString (m_playlist.entry_filename (entry)));
106             m_playlist.select_entry (entry, true);
107         }
108     }
109 
110     m_playlist.cache_selected ();
111 
112     auto data = new QMimeData;
113     data->setUrls (urls);
114     return data;
115 }
116 
update()117 void SearchModel::update ()
118 {
119     int rows = m_items.len ();
120     int keep = aud::min (rows, m_rows);
121 
122     if (rows < m_rows)
123     {
124         beginRemoveRows (QModelIndex (), rows, m_rows - 1);
125         m_rows = rows;
126         endRemoveRows ();
127     }
128     else if (rows > m_rows)
129     {
130         beginInsertRows (QModelIndex (), m_rows, rows - 1);
131         m_rows = rows;
132         endInsertRows ();
133     }
134 
135     if (keep > 0)
136     {
137         auto topLeft = createIndex (0, 0);
138         auto bottomRight = createIndex (keep - 1, 0);
139         emit dataChanged (topLeft, bottomRight);
140     }
141 }
142 
destroy_database()143 void SearchModel::destroy_database ()
144 {
145     m_playlist = Playlist ();
146     m_items.clear ();
147     m_hidden_items = 0;
148     m_database.clear ();
149 }
150 
create_database(Playlist playlist)151 void SearchModel::create_database (Playlist playlist)
152 {
153     destroy_database ();
154 
155     int entries = playlist.n_entries ();
156 
157     for (int e = 0; e < entries; e ++)
158     {
159         Tuple tuple = playlist.entry_tuple (e, Playlist::NoWait);
160 
161         aud::array<SearchField, String> fields;
162         fields[SearchField::Genre] = tuple.get_str (Tuple::Genre);
163         fields[SearchField::Artist] = tuple.get_str (Tuple::Artist);
164         fields[SearchField::Album] = tuple.get_str (Tuple::Album);
165         fields[SearchField::Title] = tuple.get_str (Tuple::Title);
166 
167         Item * parent = nullptr;
168         SimpleHash<Key, Item> * hash = & m_database;
169 
170         for (auto f : aud::range<SearchField> ())
171         {
172             if (fields[f])
173             {
174                 Key key = {f, fields[f]};
175                 Item * item = hash->lookup (key);
176 
177                 if (! item)
178                     item = hash->add (key, Item (f, fields[f], parent));
179 
180                 item->matches.append (e);
181 
182                 /* genre is outside the normal hierarchy */
183                 if (f != SearchField::Genre)
184                 {
185                     parent = item;
186                     hash = & item->children;
187                 }
188             }
189         }
190     }
191 
192     m_playlist = playlist;
193 }
194 
search_recurse(SimpleHash<Key,Item> & domain,const Index<String> & terms,int mask,Index<const Item * > & results)195 static void search_recurse (SimpleHash<Key, Item> & domain,
196  const Index<String> & terms, int mask, Index<const Item *> & results)
197 {
198     domain.iterate ([&] (const Key & key, Item & item)
199     {
200         int count = terms.len ();
201         int new_mask = mask;
202 
203         for (int t = 0, bit = 1; t < count; t ++, bit <<= 1)
204         {
205             if (! (new_mask & bit))
206                 continue; /* skip term if it is already found */
207 
208             if (strstr (item.folded, terms[t]))
209                 new_mask &= ~bit; /* we found it */
210             else if (! item.children.n_items ())
211                 break; /* quit early if there are no children to search */
212         }
213 
214         /* adding an item with exactly one child is redundant, so avoid it */
215         if (! new_mask && item.children.n_items () != 1)
216             results.append (& item);
217 
218         search_recurse (item.children, terms, new_mask, results);
219     });
220 }
221 
item_compare(const Item * const & a,const Item * const & b)222 static int item_compare (const Item * const & a, const Item * const & b)
223 {
224     if (a->field < b->field)
225         return -1;
226     if (a->field > b->field)
227         return 1;
228 
229     int val = str_compare (a->name, b->name);
230     if (val)
231         return val;
232 
233     if (a->parent)
234         return b->parent ? item_compare (a->parent, b->parent) : 1;
235     else
236         return b->parent ? -1 : 0;
237 }
238 
item_compare_pass1(const Item * const & a,const Item * const & b)239 static int item_compare_pass1 (const Item * const & a, const Item * const & b)
240 {
241     if (a->matches.len () > b->matches.len ())
242         return -1;
243     if (a->matches.len () < b->matches.len ())
244         return 1;
245 
246     return item_compare (a, b);
247 }
248 
do_search(const Index<String> & terms,int max_results)249 void SearchModel::do_search (const Index<String> & terms, int max_results)
250 {
251     m_items.clear ();
252     m_hidden_items = 0;
253 
254     /* effectively limits number of search terms to 32 */
255     search_recurse (m_database, terms, (1 << terms.len ()) - 1, m_items);
256 
257     /* first sort by number of songs per item */
258     m_items.sort (item_compare_pass1);
259 
260     /* limit to items with most songs */
261     if (m_items.len () > max_results)
262     {
263         m_hidden_items = m_items.len () - max_results;
264         m_items.remove (max_results, -1);
265     }
266 
267     /* sort by item type, then item name */
268     m_items.sort (item_compare);
269 }
270