Merge lp:~jamesh/mediascanner2/model-auto-update into lp:mediascanner2

Proposed by James Henstridge on 2014-08-07
Status: Merged
Approved by: Jussi Pakkanen on 2014-08-14
Approved revision: 270
Merged at revision: 261
Proposed branch: lp:~jamesh/mediascanner2/model-auto-update
Merge into: lp:mediascanner2
Diff against target: 1081 lines (+287/-172)
21 files modified
debian/control (+0/-1)
src/daemon/InvalidationSender.cc (+28/-15)
src/daemon/InvalidationSender.hh (+7/-2)
src/daemon/scannerdaemon.cc (+44/-5)
src/qml/Ubuntu/MediaScanner/AlbumsModel.cc (+1/-8)
src/qml/Ubuntu/MediaScanner/ArtistsModel.cc (+4/-9)
src/qml/Ubuntu/MediaScanner/CMakeLists.txt (+1/-1)
src/qml/Ubuntu/MediaScanner/GenresModel.cc (+1/-8)
src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.cc (+12/-0)
src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.hh (+6/-0)
src/qml/Ubuntu/MediaScanner/SongsModel.cc (+1/-8)
src/qml/Ubuntu/MediaScanner/SongsSearchModel.cc (+1/-6)
src/qml/Ubuntu/MediaScanner/StreamingModel.cc (+49/-10)
src/qml/Ubuntu/MediaScanner/StreamingModel.hh (+17/-4)
src/qml/Ubuntu/MediaScanner/plugin.qmltypes (+11/-1)
test/basic.cc (+0/-2)
test/qml/tst_albumsmodel.qml (+26/-27)
test/qml/tst_artistsmodel.qml (+22/-20)
test/qml/tst_genresmodel.qml (+11/-4)
test/qml/tst_songsearchmodel.qml (+14/-6)
test/qml/tst_songsmodel.qml (+31/-35)
To merge this branch: bzr merge lp:~jamesh/mediascanner2/model-auto-update
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve on 2014-08-12
Jussi Pakkanen (community) 2014-08-07 Approve on 2014-08-11
Review via email: mp+229903@code.launchpad.net

Commit message

Automatically update models when the daemon sends the InvalidateResults D-Bus signal. Add a status property to models to let loading progress be tracked. Rename rowCount property to count, keeping the old name around for compatibility.

Description of the change

Automatically update the QML models when the scanner daemon sends out an invalidation signal.

I am reusing the signal sent for the benefit of the dash, and as part of the changes brought the broadcast code in-process so that it could be matched with a well known bus name.

I also added a StreamingModel::status property to replace the "filled" signal (which was only being used in tests), following the model of the QtQuick Image's property of the same name. This should make it easier to provide progress feedback in the UI.

Lastly, I've renamed the "rowCount" property to "count" to match e.g. SortFilterModel from the Ubuntu UI toolkit. The old name has been retained for backward compatibility though.

To post a comment you must log in.
Jussi Pakkanen (jpakkane) wrote :

Looking good. Only nitpick:

ModelStatus status_;

Why the trailing underscore? No other variable in the class has decoration. If it's this:

void StreamingModel::setStatus(StreamingModel::ModelStatus status)

then change the argument to new_status or something similar.

Regardless of that, this looks fine.

review: Approve
James Henstridge (jamesh) wrote :

It was the StreamingModel::status() method, actually. I guess that could be renamed to getStatus() instead.

270. By James Henstridge on 2014-08-12

Rename method and member based on Jussi's review.

Can the music app devs be notified when this hits a silo so we can do some testing?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2014-07-30 08:38:15 +0000
3+++ debian/control 2014-08-12 05:42:14 +0000
4@@ -52,7 +52,6 @@
5 Pre-Depends: ${misc:Pre-Depends},
6 Depends: gstreamer1.0-plugins-base,
7 gstreamer1.0-plugins-good,
8-# For dbus-send.
9 dbus,
10 ${misc:Depends},
11 ${shlibs:Depends},
12
13=== modified file 'src/daemon/InvalidationSender.cc'
14--- src/daemon/InvalidationSender.cc 2014-03-06 02:49:18 +0000
15+++ src/daemon/InvalidationSender.cc 2014-08-12 05:42:14 +0000
16@@ -22,13 +22,19 @@
17 #include<cstdlib>
18 #include<cstdio>
19 #include <glib.h>
20+#include <gio/gio.h>
21
22 using namespace std;
23
24 // timer delay in seconds
25 const unsigned int DELAY = 1;
26
27-InvalidationSender::InvalidationSender() : enabled(true), timeout_id(0) {
28+static const char SCOPES_DBUS_IFACE[] = "com.canonical.unity.scopes";
29+static const char SCOPES_DBUS_PATH[] = "/com/canonical/unity/scopes";
30+static const char SCOPES_INVALIDATE_RESULTS[] = "InvalidateResults";
31+
32+InvalidationSender::InvalidationSender() :
33+ bus(nullptr, g_object_unref), timeout_id(0) {
34 }
35
36 InvalidationSender::~InvalidationSender() {
37@@ -37,8 +43,12 @@
38 }
39 }
40
41+void InvalidationSender::setBus(GDBusConnection *bus) {
42+ this->bus.reset(static_cast<GDBusConnection*>(g_object_ref(bus)));
43+}
44+
45 void InvalidationSender::invalidate() {
46- if (!enabled) {
47+ if (!bus) {
48 return;
49 }
50 if (timeout_id != 0) {
51@@ -49,22 +59,25 @@
52
53 int InvalidationSender::callback(void *data) {
54 auto invalidator = static_cast<InvalidationSender*>(data);
55+ GError *error = nullptr;
56
57- string invocation("dbus-send /com/canonical/unity/scopes ");
58- invocation += "com.canonical.unity.scopes.InvalidateResults string:";
59- const string m_invoc = invocation + "mediascanner-music";
60- const string v_invoc = invocation + "mediascanner-video";
61- if(system(m_invoc.c_str()) != 0) {
62- fprintf(stderr, "Could not invalidate music scope results.\n");
63+ if (!g_dbus_connection_emit_signal(
64+ invalidator->bus.get(), nullptr,
65+ SCOPES_DBUS_PATH, SCOPES_DBUS_IFACE, SCOPES_INVALIDATE_RESULTS,
66+ g_variant_new("(s)", "mediascanner-music"), &error)) {
67+ fprintf(stderr, "Could not invalidate music scope results: %s\n", error->message);
68+ g_error_free(error);
69+ error = nullptr;
70 }
71- if(system(v_invoc.c_str()) != 0) {
72- fprintf(stderr, "Could not invalidate video scope results.\n");
73+ if (!g_dbus_connection_emit_signal(
74+ invalidator->bus.get(), nullptr,
75+ SCOPES_DBUS_PATH, SCOPES_DBUS_IFACE, SCOPES_INVALIDATE_RESULTS,
76+ g_variant_new("(s)", "mediascanner-video"), &error)) {
77+ fprintf(stderr, "Could not invalidate video scope results: %s\n", error->message);
78+ g_error_free(error);
79+ error = nullptr;
80 }
81
82 invalidator->timeout_id = 0;
83- return FALSE;
84-}
85-
86-void InvalidationSender::disable() {
87- enabled = false;
88+ return G_SOURCE_REMOVE;
89 }
90
91=== modified file 'src/daemon/InvalidationSender.hh'
92--- src/daemon/InvalidationSender.hh 2014-03-06 02:49:18 +0000
93+++ src/daemon/InvalidationSender.hh 2014-08-12 05:42:14 +0000
94@@ -20,6 +20,10 @@
95 #ifndef INVALIDATIONSENDER_HH
96 #define INVALIDATIONSENDER_HH
97
98+#include <memory>
99+
100+typedef struct _GDBusConnection GDBusConnection;
101+
102 /**
103 * A class that sends a broadcast signal that the state of media
104 * files has changed.
105@@ -33,11 +37,12 @@
106 InvalidationSender& operator=(const InvalidationSender &o) = delete;
107
108 void invalidate();
109- void disable();
110+ void setBus(GDBusConnection *bus);
111+
112 private:
113 static int callback(void *data);
114
115- bool enabled;
116+ std::unique_ptr<GDBusConnection, void(*)(void*)> bus;
117 unsigned int timeout_id;
118 };
119
120
121=== modified file 'src/daemon/scannerdaemon.cc'
122--- src/daemon/scannerdaemon.cc 2014-07-25 09:31:02 +0000
123+++ src/daemon/scannerdaemon.cc 2014-08-12 05:42:14 +0000
124@@ -32,6 +32,7 @@
125
126 #include<glib.h>
127 #include<glib-unix.h>
128+#include<gio/gio.h>
129 #include<gst/gst.h>
130
131 #include "../mediascanner/MediaFile.hh"
132@@ -46,6 +47,8 @@
133
134 using namespace mediascanner;
135
136+static const char BUS_NAME[] = "com.canonical.MediaScanner2.Daemon";
137+
138 class ScannerDaemon final {
139 public:
140 ScannerDaemon();
141@@ -54,6 +57,7 @@
142
143 private:
144
145+ void setupBus();
146 void setupSignals();
147 void setupMountWatcher();
148 void readFiles(MediaStore &store, const string &subdir, const MediaType type);
149@@ -61,12 +65,13 @@
150 void removeDir(const string &dir);
151 static gboolean sourceCallback(int, GIOCondition, gpointer data);
152 static gboolean signalCallback(gpointer data);
153+ static void busNameLostCallback(GDBusConnection *connection, const char *name, gpointer data);
154 void processEvents();
155 void addMountedVolumes();
156
157 int mountfd;
158 unique_ptr<GSource,void(*)(GSource*)> mount_source;
159- int sigint_id, sigterm_id;
160+ unsigned int sigint_id = 0, sigterm_id = 0;
161 string mountDir;
162 string cachedir;
163 unique_ptr<MediaStore> store;
164@@ -74,6 +79,8 @@
165 map<string, unique_ptr<SubtreeWatcher>> subtrees;
166 InvalidationSender invalidator;
167 unique_ptr<GMainLoop,void(*)(GMainLoop*)> main_loop;
168+ unique_ptr<GDBusConnection,void(*)(void*)> session_bus;
169+ unsigned int bus_name_id = 0;
170 };
171
172 static std::string getCurrentUser() {
173@@ -88,8 +95,10 @@
174 }
175
176 ScannerDaemon::ScannerDaemon() :
177- mount_source(nullptr, g_source_unref), sigint_id(0), sigterm_id(0),
178- main_loop(g_main_loop_new(nullptr, FALSE), g_main_loop_unref) {
179+ mount_source(nullptr, g_source_unref),
180+ main_loop(g_main_loop_new(nullptr, FALSE), g_main_loop_unref),
181+ session_bus(nullptr, g_object_unref) {
182+ setupBus();
183 mountDir = string("/media/") + getCurrentUser();
184 unique_ptr<MediaStore> tmp(new MediaStore(MS_READ_WRITE, "/media/"));
185 store = move(tmp);
186@@ -127,13 +136,43 @@
187 if (mount_source) {
188 g_source_destroy(mount_source.get());
189 }
190+ if (bus_name_id != 0) {
191+ g_bus_unown_name(bus_name_id);
192+ }
193 close(mountfd);
194 }
195
196+void ScannerDaemon::busNameLostCallback(GDBusConnection *, const char *name,
197+ gpointer data) {
198+ ScannerDaemon *daemon = static_cast<ScannerDaemon*>(data);
199+ fprintf(stderr, "Exiting due to loss of control of bus name %s\n", name);
200+ daemon->bus_name_id = 0;
201+ g_main_loop_quit(daemon->main_loop.get());
202+}
203+
204+void ScannerDaemon::setupBus() {
205+ GError *error = nullptr;
206+ session_bus.reset(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
207+ if (!session_bus) {
208+ string errortxt(error->message);
209+ g_error_free(error);
210+ string msg = "Failed to connect to session bus: ";
211+ msg += errortxt;
212+ throw runtime_error(msg);
213+ }
214+ invalidator.setBus(session_bus.get());
215+
216+ bus_name_id = g_bus_own_name_on_connection(
217+ session_bus.get(), BUS_NAME, static_cast<GBusNameOwnerFlags>(
218+ G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
219+ G_BUS_NAME_OWNER_FLAGS_REPLACE),
220+ nullptr, &ScannerDaemon::busNameLostCallback, this, nullptr);
221+}
222+
223 gboolean ScannerDaemon::signalCallback(gpointer data) {
224 ScannerDaemon *daemon = static_cast<ScannerDaemon*>(data);
225 g_main_loop_quit(daemon->main_loop.get());
226- return TRUE;
227+ return G_SOURCE_CONTINUE;
228 }
229
230 void ScannerDaemon::setupSignals() {
231@@ -201,7 +240,7 @@
232 gboolean ScannerDaemon::sourceCallback(int, GIOCondition, gpointer data) {
233 ScannerDaemon *daemon = static_cast<ScannerDaemon*>(data);
234 daemon->processEvents();
235- return TRUE;
236+ return G_SOURCE_CONTINUE;
237 }
238
239 void ScannerDaemon::setupMountWatcher() {
240
241=== modified file 'src/qml/Ubuntu/MediaScanner/AlbumsModel.cc'
242--- src/qml/Ubuntu/MediaScanner/AlbumsModel.cc 2014-07-10 06:44:15 +0000
243+++ src/qml/Ubuntu/MediaScanner/AlbumsModel.cc 2014-08-12 05:42:14 +0000
244@@ -18,7 +18,6 @@
245 */
246
247 #include "AlbumsModel.hh"
248-#include <exception>
249 #include <QDebug>
250
251 using namespace mediascanner::qml;
252@@ -102,12 +101,6 @@
253 auto limit_filter = filter;
254 limit_filter.setLimit(limit);
255 limit_filter.setOffset(offset);
256- std::vector<mediascanner::Album> albums;
257- try {
258- albums = store->listAlbums(limit_filter);
259- } catch (const std::exception &e) {
260- qWarning() << "Failed to retrieve album list:" << e.what();
261- }
262 return std::unique_ptr<StreamingModel::RowData>(
263- new AlbumRowData(std::move(albums)));
264+ new AlbumRowData(store->listAlbums(limit_filter)));
265 }
266
267=== modified file 'src/qml/Ubuntu/MediaScanner/ArtistsModel.cc'
268--- src/qml/Ubuntu/MediaScanner/ArtistsModel.cc 2014-07-10 06:01:58 +0000
269+++ src/qml/Ubuntu/MediaScanner/ArtistsModel.cc 2014-08-12 05:42:14 +0000
270@@ -18,7 +18,6 @@
271 */
272
273 #include "ArtistsModel.hh"
274-#include <exception>
275 #include <QDebug>
276
277 using namespace mediascanner::qml;
278@@ -104,14 +103,10 @@
279 limit_filter.setLimit(limit);
280 limit_filter.setOffset(offset);
281 std::vector<std::string> artists;
282- try {
283- if (album_artists) {
284- artists = store->listAlbumArtists(limit_filter);
285- } else {
286- artists = store->listArtists(limit_filter);
287- }
288- } catch (const std::exception &e) {
289- qWarning() << "Failed to retrieve artist list:" << e.what();
290+ if (album_artists) {
291+ artists = store->listAlbumArtists(limit_filter);
292+ } else {
293+ artists = store->listArtists(limit_filter);
294 }
295 return std::unique_ptr<StreamingModel::RowData>(
296 new ArtistRowData(std::move(artists)));
297
298=== modified file 'src/qml/Ubuntu/MediaScanner/CMakeLists.txt'
299--- src/qml/Ubuntu/MediaScanner/CMakeLists.txt 2014-07-10 03:02:27 +0000
300+++ src/qml/Ubuntu/MediaScanner/CMakeLists.txt 2014-08-12 05:42:14 +0000
301@@ -19,7 +19,7 @@
302 )
303
304 set_target_properties(mediascanner-qml PROPERTIES AUTOMOC TRUE)
305-qt5_use_modules(mediascanner-qml Qml Concurrent)
306+qt5_use_modules(mediascanner-qml Qml Concurrent DBus)
307 target_link_libraries(mediascanner-qml mediascanner ms-dbus)
308
309 install(
310
311=== modified file 'src/qml/Ubuntu/MediaScanner/GenresModel.cc'
312--- src/qml/Ubuntu/MediaScanner/GenresModel.cc 2014-07-10 06:01:58 +0000
313+++ src/qml/Ubuntu/MediaScanner/GenresModel.cc 2014-08-12 05:42:14 +0000
314@@ -18,7 +18,6 @@
315 */
316
317 #include "GenresModel.hh"
318-#include <exception>
319 #include <QDebug>
320
321 using namespace mediascanner::qml;
322@@ -70,14 +69,8 @@
323 auto limit_filter = filter;
324 limit_filter.setLimit(limit);
325 limit_filter.setOffset(offset);
326- std::vector<std::string> genres;
327- try {
328- genres = store->listGenres(limit_filter);
329- } catch (const std::exception &e) {
330- qWarning() << "Failed to retrieve genre list:" << e.what();
331- }
332 return std::unique_ptr<StreamingModel::RowData>(
333- new GenreRowData(std::move(genres)));
334+ new GenreRowData(store->listGenres(limit_filter)));
335 }
336
337 void GenresModel::appendRows(std::unique_ptr<StreamingModel::RowData> &&row_data) {
338
339=== modified file 'src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.cc'
340--- src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.cc 2014-06-24 13:43:25 +0000
341+++ src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.cc 2014-08-12 05:42:14 +0000
342@@ -21,6 +21,7 @@
343 #include <cstdlib>
344 #include <cstring>
345 #include <exception>
346+#include <QDBusConnection>
347 #include <QDebug>
348 #include <QQmlEngine>
349
350@@ -49,6 +50,13 @@
351 } else {
352 store.reset(new mediascanner::MediaStore(MS_READ_ONLY));
353 }
354+
355+ QDBusConnection::sessionBus().connect(
356+ "com.canonical.MediaScanner2.Daemon",
357+ "/com/canonical/unity/scopes",
358+ "com.canonical.unity.scopes", "InvalidateResults",
359+ QStringList{"mediascanner-music"}, "s",
360+ this, SLOT(resultsInvalidated()));
361 }
362
363 QList<QObject*> MediaStoreWrapper::query(const QString &q, MediaType type) {
364@@ -75,3 +83,7 @@
365 QQmlEngine::setObjectOwnership(wrapper, QQmlEngine::JavaScriptOwnership);
366 return wrapper;
367 }
368+
369+void MediaStoreWrapper::resultsInvalidated() {
370+ Q_EMIT updated();
371+}
372
373=== modified file 'src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.hh'
374--- src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.hh 2014-07-10 03:02:27 +0000
375+++ src/qml/Ubuntu/MediaScanner/MediaStoreWrapper.hh 2014-08-12 05:42:14 +0000
376@@ -49,6 +49,12 @@
377 Q_INVOKABLE mediascanner::qml::MediaFileWrapper *lookup(const QString &filename);
378
379 std::shared_ptr<mediascanner::MediaStoreBase> store;
380+
381+Q_SIGNALS:
382+ void updated();
383+
384+private Q_SLOTS:
385+ void resultsInvalidated();
386 };
387
388 }
389
390=== modified file 'src/qml/Ubuntu/MediaScanner/SongsModel.cc'
391--- src/qml/Ubuntu/MediaScanner/SongsModel.cc 2014-07-10 06:44:15 +0000
392+++ src/qml/Ubuntu/MediaScanner/SongsModel.cc 2014-08-12 05:42:14 +0000
393@@ -18,7 +18,6 @@
394 */
395
396 #include "SongsModel.hh"
397-#include <exception>
398 #include <QDebug>
399
400 using namespace mediascanner::qml;
401@@ -123,12 +122,6 @@
402 auto limit_filter = filter;
403 limit_filter.setLimit(limit);
404 limit_filter.setOffset(offset);
405- std::vector<mediascanner::MediaFile> songs;
406- try {
407- songs = store->listSongs(limit_filter);
408- } catch (const std::exception &e) {
409- qWarning() << "Failed to retrieve song list:" << e.what();
410- }
411 return std::unique_ptr<StreamingModel::RowData>(
412- new MediaFileRowData(std::move(songs)));
413+ new MediaFileRowData(store->listSongs(limit_filter)));
414 }
415
416=== modified file 'src/qml/Ubuntu/MediaScanner/SongsSearchModel.cc'
417--- src/qml/Ubuntu/MediaScanner/SongsSearchModel.cc 2014-07-10 06:44:15 +0000
418+++ src/qml/Ubuntu/MediaScanner/SongsSearchModel.cc 2014-08-12 05:42:14 +0000
419@@ -18,7 +18,6 @@
420 */
421
422 #include "SongsSearchModel.hh"
423-#include <exception>
424 #include <QDebug>
425
426 using namespace mediascanner::qml;
427@@ -42,11 +41,7 @@
428 std::vector<mediascanner::MediaFile> songs;
429 // No batching support, so only send results for the zero offset.
430 if (offset == 0) {
431- try {
432- songs = store->query(query.toStdString(), mediascanner::AudioMedia);
433- } catch (const std::exception &e) {
434- qWarning() << "Failed to retrieve song search results:" << e.what();
435- }
436+ songs = store->query(query.toStdString(), mediascanner::AudioMedia);
437 }
438 return std::unique_ptr<StreamingModel::RowData>(
439 new MediaFileRowData(std::move(songs)));
440
441=== modified file 'src/qml/Ubuntu/MediaScanner/StreamingModel.cc'
442--- src/qml/Ubuntu/MediaScanner/StreamingModel.cc 2014-08-05 10:47:07 +0000
443+++ src/qml/Ubuntu/MediaScanner/StreamingModel.cc 2014-08-12 05:42:14 +0000
444@@ -20,6 +20,7 @@
445 #include "StreamingModel.hh"
446
447 #include <cassert>
448+#include <exception>
449
450 #include <QEvent>
451 #include <QCoreApplication>
452@@ -35,14 +36,22 @@
453 class AdditionEvent : public QEvent {
454 private:
455 std::unique_ptr<StreamingModel::RowData> rows;
456+ bool error = false;
457 int generation;
458
459 public:
460- AdditionEvent(std::unique_ptr<StreamingModel::RowData> &&rows, int generation) :
461- QEvent(AdditionEvent::additionEventType()), rows(std::move(rows)), generation(generation) {
462+ AdditionEvent(int generation) :
463+ QEvent(AdditionEvent::additionEventType()), generation(generation) {
464 }
465
466+ void setRows(std::unique_ptr<StreamingModel::RowData> &&r) {
467+ rows = std::move(r);
468+ }
469+ void setError(bool e) {
470+ error = e;
471+ }
472 std::unique_ptr<StreamingModel::RowData>& getRows() { return rows; }
473+ bool getError() const { return error; }
474 int getGeneration() const { return generation; }
475
476 static QEvent::Type additionEventType()
477@@ -62,8 +71,14 @@
478 if(model->shouldWorkerStop()) {
479 return;
480 }
481- QScopedPointer<AdditionEvent> e(
482- new AdditionEvent(model->retrieveRows(store, BATCH_SIZE, offset), generation));
483+ QScopedPointer<AdditionEvent> e(new AdditionEvent(generation));
484+ try {
485+ e->setRows(model->retrieveRows(store, BATCH_SIZE, offset));
486+ } catch (const std::exception &exc) {
487+ qWarning() << "Failed to retrieve rows:" << exc.what();
488+ e->setError(true);
489+ return;
490+ }
491 cursize = e->getRows()->size();
492 if (model->shouldWorkerStop()) {
493 return;
494@@ -75,7 +90,8 @@
495
496 }
497
498-StreamingModel::StreamingModel(QObject *parent) : QAbstractListModel(parent), generation(0) {
499+StreamingModel::StreamingModel(QObject *parent) :
500+ QAbstractListModel(parent), generation(0), status(Ready) {
501 }
502
503 StreamingModel::~StreamingModel() {
504@@ -90,9 +106,10 @@
505 void StreamingModel::updateModel() {
506 if (store.isNull()) {
507 query_future = QFuture<void>();
508- Q_EMIT filled();
509+ setStatus(Ready);
510 return;
511 }
512+ setStatus(Loading);
513 setWorkerStop(false);
514 query_future = QtConcurrent::run(runQuery, ++generation, this, store->store);
515 }
516@@ -114,35 +131,57 @@
517 return true;
518 }
519
520+ if (ae->getError()) {
521+ setStatus(Error);
522+ return true;
523+ }
524+
525 auto &newrows = ae->getRows();
526 bool lastBatch = newrows->size() < BATCH_SIZE;
527 beginInsertRows(QModelIndex(), rowCount(), rowCount()+newrows->size()-1);
528 appendRows(std::move(newrows));
529 endInsertRows();
530- Q_EMIT rowCountChanged();
531+ Q_EMIT countChanged();
532 if (lastBatch) {
533- Q_EMIT filled();
534+ setStatus(Ready);
535 }
536 return true;
537 }
538
539-MediaStoreWrapper *StreamingModel::getStore() {
540+MediaStoreWrapper *StreamingModel::getStore() const {
541 return store.data();
542 }
543
544 void StreamingModel::setStore(MediaStoreWrapper *store) {
545 if (this->store != store) {
546+ if (this->store) {
547+ disconnect(this->store, &MediaStoreWrapper::updated,
548+ this, &StreamingModel::invalidate);
549+ }
550 this->store = store;
551+ if (store) {
552+ connect(this->store, &MediaStoreWrapper::updated,
553+ this, &StreamingModel::invalidate);
554+ }
555 invalidate();
556 }
557 }
558
559+StreamingModel::ModelStatus StreamingModel::getStatus() const {
560+ return status;
561+}
562+
563+void StreamingModel::setStatus(StreamingModel::ModelStatus status) {
564+ this->status = status;
565+ Q_EMIT statusChanged();
566+}
567+
568 void StreamingModel::invalidate() {
569 setWorkerStop(true);
570 query_future.waitForFinished();
571 beginResetModel();
572 clearBacking();
573 endResetModel();
574- Q_EMIT rowCountChanged();
575+ Q_EMIT countChanged();
576 updateModel();
577 }
578
579=== modified file 'src/qml/Ubuntu/MediaScanner/StreamingModel.hh'
580--- src/qml/Ubuntu/MediaScanner/StreamingModel.hh 2014-07-17 10:17:23 +0000
581+++ src/qml/Ubuntu/MediaScanner/StreamingModel.hh 2014-08-12 05:42:14 +0000
582@@ -34,9 +34,18 @@
583
584 class StreamingModel : public QAbstractListModel {
585 Q_OBJECT
586+ Q_ENUMS(ModelStatus)
587 Q_PROPERTY(mediascanner::qml::MediaStoreWrapper* store READ getStore WRITE setStore)
588- Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged)
589+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
590+ Q_PROPERTY(int rowCount READ rowCount NOTIFY countChanged)
591+ Q_PROPERTY(ModelStatus status READ getStatus NOTIFY statusChanged)
592 public:
593+ enum ModelStatus {
594+ Ready,
595+ Loading,
596+ Error
597+ };
598+
599 explicit StreamingModel(QObject *parent=nullptr);
600 virtual ~StreamingModel();
601
602@@ -56,9 +65,12 @@
603 virtual void clearBacking() = 0;
604
605 protected:
606- MediaStoreWrapper *getStore();
607+ MediaStoreWrapper *getStore() const;
608 void setStore(MediaStoreWrapper *store);
609
610+ ModelStatus getStatus() const;
611+ void setStatus(ModelStatus status);
612+
613 private:
614 void updateModel();
615 void setWorkerStop(bool new_stop_status) noexcept { stopflag.store(new_stop_status, std::memory_order_release); }
616@@ -67,10 +79,11 @@
617 QFuture<void> query_future;
618 int generation;
619 std::atomic<bool> stopflag;
620+ ModelStatus status;
621
622 Q_SIGNALS:
623- void rowCountChanged();
624- void filled();
625+ void countChanged();
626+ void statusChanged();
627
628 public Q_SLOTS:
629
630
631=== modified file 'src/qml/Ubuntu/MediaScanner/plugin.qmltypes'
632--- src/qml/Ubuntu/MediaScanner/plugin.qmltypes 2014-07-10 06:46:09 +0000
633+++ src/qml/Ubuntu/MediaScanner/plugin.qmltypes 2014-08-12 05:42:14 +0000
634@@ -124,6 +124,7 @@
635 "AllMedia": 255
636 }
637 }
638+ Signal { name: "updated" }
639 Method {
640 name: "query"
641 type: "QList<QObject*>"
642@@ -157,9 +158,18 @@
643 Component {
644 name: "mediascanner::qml::StreamingModel"
645 prototype: "QAbstractListModel"
646+ Enum {
647+ name: "ModelStatus"
648+ values: {
649+ "Ready": 0,
650+ "Loading": 1,
651+ "Error": 2
652+ }
653+ }
654 Property { name: "store"; type: "mediascanner::qml::MediaStoreWrapper"; isPointer: true }
655+ Property { name: "count"; type: "int"; isReadonly: true }
656 Property { name: "rowCount"; type: "int"; isReadonly: true }
657- Signal { name: "filled" }
658+ Property { name: "status"; type: "ModelStatus"; isReadonly: true }
659 Method { name: "invalidate" }
660 Method {
661 name: "get"
662
663=== modified file 'test/basic.cc'
664--- test/basic.cc 2014-05-05 09:19:17 +0000
665+++ test/basic.cc 2014-08-12 05:42:14 +0000
666@@ -99,7 +99,6 @@
667 MediaStore store(":memory:", MS_READ_WRITE);
668 MetadataExtractor extractor;
669 InvalidationSender invalidator;
670- invalidator.disable();
671 SubtreeWatcher watcher(store, extractor, invalidator);
672 watcher.addDir(subdir);
673 ASSERT_EQ(store.size(), 0);
674@@ -122,7 +121,6 @@
675 MediaStore store(":memory:", MS_READ_WRITE);
676 MetadataExtractor extractor;
677 InvalidationSender invalidator;
678- invalidator.disable();
679 SubtreeWatcher watcher(store, extractor, invalidator);
680 ASSERT_EQ(watcher.directoryCount(), 0);
681 watcher.addDir(testdir);
682
683=== modified file 'test/qml/tst_albumsmodel.qml'
684--- test/qml/tst_albumsmodel.qml 2014-07-10 06:44:15 +0000
685+++ test/qml/tst_albumsmodel.qml 2014-08-12 05:42:14 +0000
686@@ -15,27 +15,26 @@
687 }
688
689 SignalSpy {
690- id: modelFilled
691+ id: modelStatus
692 target: model
693- signalName: "filled"
694+ signalName: "statusChanged"
695 }
696
697 TestCase {
698 name: "AlbumsModelTests"
699
700+ function waitForReady() {
701+ while (model.status == AlbumsModel.Loading) {
702+ modelStatus.wait();
703+ }
704+ compare(model.status, AlbumsModel.Ready);
705+ }
706+
707 function cleanup() {
708- if (model.artist != undefined) {
709- model.artist = undefined;
710- modelFilled.wait();
711- }
712- if (model.albumArtist != undefined) {
713- model.albumArtist = undefined;
714- modelFilled.wait();
715- }
716- if (model.genre != undefined) {
717- model.genre = undefined;
718- modelFilled.wait();
719- }
720+ model.artist = undefined;
721+ model.albumArtist = undefined;
722+ model.genre = undefined;
723+ waitForReady();
724 }
725
726 function test_initial_state() {
727@@ -43,7 +42,7 @@
728 compare(model.albumArtist, undefined);
729 compare(model.genre, undefined);
730
731- compare(model.rowCount, 4);
732+ compare(model.count, 4);
733 compare(model.get(0, AlbumsModel.RoleTitle), "Ivy and the Big Apples");
734 compare(model.get(0, AlbumsModel.RoleArtist), "Spiderbait");
735
736@@ -67,40 +66,40 @@
737
738 function test_artist() {
739 model.artist = "The John Butler Trio";
740- modelFilled.wait();
741- compare(model.rowCount, 2);
742+ waitForReady();
743+ compare(model.count, 2);
744
745 compare(model.get(0, AlbumsModel.RoleTitle), "April Uprising");
746 compare(model.get(0, AlbumsModel.RoleArtist), "The John Butler Trio");
747
748 model.artist = "unknown";
749- modelFilled.wait();
750- compare(model.rowCount, 0);
751+ waitForReady();
752+ compare(model.count, 0);
753 }
754
755 function test_album_artist() {
756 model.albumArtist = "The John Butler Trio";
757- modelFilled.wait();
758- compare(model.rowCount, 2);
759+ waitForReady();
760+ compare(model.count, 2);
761
762 compare(model.get(0, AlbumsModel.RoleTitle), "April Uprising");
763 compare(model.get(0, AlbumsModel.RoleArtist), "The John Butler Trio");
764
765 model.albumArtist = "unknown";
766- modelFilled.wait();
767- compare(model.rowCount, 0);
768+ waitForReady();
769+ compare(model.count, 0);
770 }
771
772 function test_genre() {
773 model.genre = "rock";
774- modelFilled.wait();
775- compare(model.rowCount, 2);
776+ waitForReady();
777+ compare(model.count, 2);
778 compare(model.get(0, AlbumsModel.RoleTitle), "Ivy and the Big Apples");
779 compare(model.get(1, AlbumsModel.RoleTitle), "Spiderbait");
780
781 model.genre = "unknown";
782- modelFilled.wait();
783- compare(model.rowCount, 0);
784+ waitForReady();
785+ compare(model.count, 0);
786 }
787
788 }
789
790=== modified file 'test/qml/tst_artistsmodel.qml'
791--- test/qml/tst_artistsmodel.qml 2014-07-10 06:01:58 +0000
792+++ test/qml/tst_artistsmodel.qml 2014-08-12 05:42:14 +0000
793@@ -15,31 +15,33 @@
794 }
795
796 SignalSpy {
797- id: modelFilled
798+ id: modelStatus
799 target: model
800- signalName: "filled"
801+ signalName: "statusChanged"
802 }
803
804 TestCase {
805 name: "ArtistsModelTests"
806
807+ function waitForReady() {
808+ while (model.status == ArtistsModel.Loading) {
809+ modelStatus.wait();
810+ }
811+ compare(model.status, ArtistsModel.Ready);
812+ }
813+
814 function cleanup() {
815- if (model.albumArtists != false) {
816- model.albumArtists = false;
817- modelFilled.wait();
818- }
819- if (model.genre != undefined) {
820- model.genre = undefined;
821- modelFilled.wait();
822- }
823+ model.albumArtists = false;
824+ model.genre = undefined;
825+ waitForReady();
826 }
827
828 function test_initial_state() {
829 compare(model.albumArtists, false);
830 compare(model.genre, undefined);
831
832- //modelFilled.wait();
833- compare(model.rowCount, 2);
834+ waitForReady();
835+ compare(model.count, 2);
836 compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait");
837 compare(model.get(1, ArtistsModel.RoleArtist), "The John Butler Trio");
838 }
839@@ -54,8 +56,8 @@
840
841 function test_album_artists() {
842 model.albumArtists = true;
843- modelFilled.wait();
844- compare(model.rowCount, 2);
845+ waitForReady();
846+ compare(model.count, 2);
847
848 compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait");
849 compare(model.get(1, ArtistsModel.RoleArtist), "The John Butler Trio");
850@@ -63,18 +65,18 @@
851
852 function test_genre() {
853 model.genre = "rock";
854- modelFilled.wait();
855- compare(model.rowCount, 1);
856+ waitForReady();
857+ compare(model.count, 1);
858 compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait");
859
860 model.genre = "roots";
861- modelFilled.wait();
862- compare(model.rowCount, 1);
863+ waitForReady();
864+ compare(model.count, 1);
865 compare(model.get(0, ArtistsModel.RoleArtist), "The John Butler Trio");
866
867 model.genre = "unknown";
868- modelFilled.wait();
869- compare(model.rowCount, 0);
870+ waitForReady();
871+ compare(model.count, 0);
872 }
873
874 }
875
876=== modified file 'test/qml/tst_genresmodel.qml'
877--- test/qml/tst_genresmodel.qml 2014-07-10 06:01:58 +0000
878+++ test/qml/tst_genresmodel.qml 2014-08-12 05:42:14 +0000
879@@ -15,20 +15,27 @@
880 }
881
882 SignalSpy {
883- id: modelFilled
884+ id: modelStatus
885 target: model
886- signalName: "filled"
887+ signalName: "statusChanged"
888 }
889
890 TestCase {
891 name: "GenresModelTests"
892
893+ function waitForReady() {
894+ while (model.status == GenresModel.Loading) {
895+ modelStatus.wait();
896+ }
897+ compare(model.status, GenresModel.Ready);
898+ }
899+
900 function cleanup() {
901 }
902
903 function test_initial_state() {
904- modelFilled.wait();
905- compare(model.rowCount, 2);
906+ waitForReady();
907+ compare(model.count, 2);
908 compare(model.get(0, ArtistsModel.RoleGenre), "rock");
909 compare(model.get(1, ArtistsModel.RoleGenre), "roots");
910 }
911
912=== modified file 'test/qml/tst_songsearchmodel.qml'
913--- test/qml/tst_songsearchmodel.qml 2014-07-10 07:19:57 +0000
914+++ test/qml/tst_songsearchmodel.qml 2014-08-12 05:42:14 +0000
915@@ -15,21 +15,29 @@
916 }
917
918 SignalSpy {
919- id: modelFilled
920+ id: modelStatus
921 target: model
922- signalName: "filled"
923+ signalName: "statusChanged"
924 }
925
926 TestCase {
927 name: "SongsSearchModelTests"
928+
929+ function waitForReady() {
930+ while (model.status == SongsSearchModel.Loading) {
931+ modelStatus.wait();
932+ }
933+ compare(model.status, SongsSearchModel.Ready);
934+ }
935+
936 function test_search() {
937 // By default, the model lists all songs.
938- modelFilled.wait();
939- compare(model.rowCount, 7, "songs_model.rowCount == 7");
940+ waitForReady();
941+ compare(model.count, 7, "songs_model.count == 7");
942
943 model.query = "revolution";
944- modelFilled.wait();
945- compare(model.rowCount, 1, "songs_model.rowCount == 1");
946+ waitForReady();
947+ compare(model.count, 1, "songs_model.count == 1");
948 compare(model.get(0, SongsSearchModel.RoleTitle), "Revolution");
949 }
950 }
951
952=== modified file 'test/qml/tst_songsmodel.qml'
953--- test/qml/tst_songsmodel.qml 2014-07-10 06:44:15 +0000
954+++ test/qml/tst_songsmodel.qml 2014-08-12 05:42:14 +0000
955@@ -15,31 +15,27 @@
956 }
957
958 SignalSpy {
959- id: modelFilled
960+ id: modelStatus
961 target: model
962- signalName: "filled"
963+ signalName: "statusChanged"
964 }
965
966 TestCase {
967 name: "SongsModelTests"
968
969+ function waitForReady() {
970+ while (model.status == SongsModel.Loading) {
971+ modelStatus.wait();
972+ }
973+ compare(model.status, SongsModel.Ready);
974+ }
975+
976 function cleanup() {
977- if (model.artist != undefined) {
978- model.artist = undefined;
979- modelFilled.wait();
980- }
981- if (model.albumArtist != undefined) {
982- model.albumArtist = undefined;
983- modelFilled.wait();
984- }
985- if (model.album != undefined) {
986- model.album = undefined;
987- modelFilled.wait();
988- }
989- if (model.genre != undefined) {
990- model.genre = undefined;
991- modelFilled.wait();
992- }
993+ model.artist = undefined;
994+ model.albumArtist = undefined;
995+ model.album = undefined;
996+ model.genre = undefined;
997+ waitForReady();
998 }
999
1000 function test_initial_state() {
1001@@ -47,7 +43,7 @@
1002 compare(model.albumArtist, undefined);
1003 compare(model.album, undefined);
1004
1005- compare(model.rowCount, 7);
1006+ compare(model.count, 7);
1007 compare(model.get(0, SongsModel.RoleTitle), "Buy Me a Pony")
1008 compare(model.get(0, SongsModel.RoleAlbum), "Ivy and the Big Apples");
1009 compare(model.get(0, SongsModel.RoleAuthor), "Spiderbait");
1010@@ -70,55 +66,55 @@
1011
1012 function test_artist() {
1013 model.artist = "The John Butler Trio";
1014- modelFilled.wait();
1015- compare(model.rowCount, 4);
1016+ waitForReady();
1017+ compare(model.count, 4);
1018
1019 compare(model.get(0, SongsModel.RoleTitle), "Revolution");
1020 compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio");
1021
1022 model.artist = "unknown";
1023- modelFilled.wait();
1024- compare(model.rowCount, 0);
1025+ waitForReady();
1026+ compare(model.count, 0);
1027 }
1028
1029 function test_album_artist() {
1030 model.albumArtist = "The John Butler Trio";
1031- modelFilled.wait();
1032- compare(model.rowCount, 4);
1033+ waitForReady();
1034+ compare(model.count, 4);
1035
1036 compare(model.get(0, SongsModel.RoleTitle), "Revolution");
1037 compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio");
1038
1039 model.albumArtist = "unknown";
1040- modelFilled.wait();
1041- compare(model.rowCount, 0);
1042+ waitForReady();
1043+ compare(model.count, 0);
1044 }
1045
1046 function test_album() {
1047 model.album = "Sunrise Over Sea";
1048- modelFilled.wait();
1049- compare(model.rowCount, 2);
1050+ waitForReady();
1051+ compare(model.count, 2);
1052
1053 compare(model.get(0, SongsModel.RoleTitle), "Peaches & Cream");
1054 compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio");
1055
1056 model.albumArtist = "unknown";
1057- modelFilled.wait();
1058- compare(model.rowCount, 0);
1059+ waitForReady();
1060+ compare(model.count, 0);
1061 }
1062
1063 function test_genre() {
1064 model.genre = "rock";
1065- modelFilled.wait();
1066- compare(model.rowCount, 3);
1067+ waitForReady();
1068+ compare(model.count, 3);
1069
1070 compare(model.get(0, SongsModel.RoleTitle), "Buy Me a Pony");
1071 compare(model.get(1, SongsModel.RoleTitle), "Straight Through The Sun");
1072 compare(model.get(2, SongsModel.RoleTitle), "It's Beautiful");
1073
1074 model.albumArtist = "unknown";
1075- modelFilled.wait();
1076- compare(model.rowCount, 0);
1077+ waitForReady();
1078+ compare(model.count, 0);
1079 }
1080
1081 }

Subscribers

People subscribed via source and target branches