@@ -10,12 +10,14 @@ class DownloadPool final : public DeletedAtShutdown {
1010 virtual ~DownloadListener () = default ;
1111 virtual void downloadProgressed (hash32 hash, float progress) { }
1212 virtual void databaseDownloadCompleted (HeapArray<std::pair<PatchInfo, int >> const & patches) { }
13+ virtual void databaseDownloadFailed () { }
1314 virtual void patchDownloadCompleted (hash32 hash, bool success) { }
1415 virtual void imageDownloadCompleted (hash32 hash, MemoryBlock const & imageData) { } // calls back on worker thread, so we can still process the image without consuming message thread time
1516 };
1617
1718 ~DownloadPool () override
1819 {
20+ cancelledImageDownload = true ;
1921 imagePool.removeAllJobs (true , -1 );
2022 patchPool.removeAllJobs (true , -1 );
2123 clearSingletonInstance ();
@@ -35,27 +37,42 @@ class DownloadPool final : public DeletedAtShutdown {
3537
3638 void cancelImageDownloads ()
3739 {
38- imagePool.removeAllJobs (true , 500 );
40+ cancelledImageDownload = true ;
41+ imagePool.removeAllJobs (true , -1 );
42+ cancelledImageDownload = false ;
3943 }
4044
4145 void downloadDatabase ()
4246 {
47+ cancelImageDownloads ();
4348 imagePool.addJob ([this ] {
4449 SmallArray<PatchInfo> patches;
50+ int statusCode = 0 ;
51+ auto const webstream = URL (" https://plugdata.org/store.json" ).createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress).withConnectionTimeoutMs (10000 ).withStatusCode (&statusCode));
52+
53+ if (!webstream || statusCode >= 400 ) {
54+ MessageManager::callAsync ([this ] {
55+ for (auto & listener : listeners) {
56+ listener->databaseDownloadFailed ();
57+ }
58+ });
59+ return ;
60+ }
4561
46- auto const webstream = std::make_unique<WebInputStream>( URL ( " https://plugdata.org/store.json " ), false ) ;
47- webstream-> connect ( nullptr );
62+ MemoryBlock jsonData ;
63+ MemoryOutputStream mo (jsonData, false );
4864
49- if (webstream->isError () || webstream->getStatusCode () == 400 ) {
50- // TODO: show error
51- return ;
65+ mo.preallocate (32000 ); // fit store.json file with some extra space
66+ while (true )
67+ {
68+ auto const written = mo.writeFromInputStream (*webstream, 1 <<14 );
69+ if (written == 0 )
70+ break ;
5271 }
5372
54- MemoryBlock block;
55- webstream->readIntoMemoryBlock (block);
56- MemoryInputStream memstream (block, false );
73+ MemoryInputStream jsonStream (jsonData, false );
5774
58- auto const parsedData = JSON::parse (memstream );
75+ auto const parsedData = JSON::parse (jsonStream );
5976 auto patchData = parsedData[" Patches" ];
6077 if (patchData.isArray ()) {
6178 for (int i = 0 ; i < patchData.size (); ++i) {
@@ -98,35 +115,52 @@ class DownloadPool final : public DeletedAtShutdown {
98115
99116 void downloadImage (hash32 hash, URL location)
100117 {
101- imagePool.addJob ([this , hash, location] {
118+ imagePool.addJob ([this , hash, location] () {
102119 static UnorderedMap<hash32, MemoryBlock> downloadImageCache;
103- static CriticalSection cacheMutex;
120+ static CriticalSection cacheMutex; // Prevent threadpool jobs from touching cache at the same time
104121
105122 // Try loading from cache
106- MemoryBlock block ;
123+ MemoryBlock imageData ;
107124 {
108125 ScopedLock lock (cacheMutex);
109126
110127 if (downloadImageCache.contains (hash)) {
111128 if (auto blockIter = downloadImageCache.find (hash); blockIter != downloadImageCache.end ()) {
112- block = blockIter->second ;
129+ imageData = blockIter->second ;
113130 }
114131 }
115132 }
116133
117134 // Load the image data from the URL
118- if (block.getSize () == 0 ) {
119- WebInputStream memstream (location, false );
120- memstream.readIntoMemoryBlock (block);
135+ if (imageData.getSize () == 0 ) {
136+ int statusCode = 0 ;
137+ auto const webstream = location.createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress).withConnectionTimeoutMs (10000 ).withStatusCode (&statusCode));
138+ if (!webstream || statusCode >= 400 ) {
139+ return ; // For images, we just quietly fail
140+ }
141+ MemoryOutputStream mo (imageData, false );
142+
143+ mo.preallocate (300000 ); // fits most store thumbnails
144+ while (true )
145+ {
146+ if (cancelledImageDownload) return ;
147+
148+ auto const written = mo.writeFromInputStream (*webstream, 1 <<16 );
149+ if (written == 0 )
150+ break ;
151+ }
152+
121153 {
122154 ScopedLock lock (cacheMutex);
123- downloadImageCache[hash] = block ;
155+ downloadImageCache[hash] = imageData ;
124156 }
125157 }
158+
159+ if (cancelledImageDownload) return ;
126160
127161 ScopedLock const lock (listenersLock);
128162 for (auto & listener : listeners) {
129- listener->imageDownloadCompleted (hash, block );
163+ listener->imageDownloadCompleted (hash, imageData );
130164 }
131165 });
132166 }
@@ -154,14 +188,17 @@ class DownloadPool final : public DeletedAtShutdown {
154188
155189 float progress = static_cast <long double >(bytesDownloaded) / static_cast <long double >(totalBytes);
156190
157- if (cancelledDownloads.contains (downloadHash)) {
158- cancelledDownloads.erase (downloadHash);
159- MessageManager::callAsync ([this , downloadHash]() mutable {
160- for (auto & listener : listeners) {
161- listener->patchDownloadCompleted (downloadHash, false );
162- }
163- });
164- return ;
191+ {
192+ ScopedLock const cancelLock (cancelledDownloadsLock);
193+ if (cancelledDownloads.contains (downloadHash)) {
194+ cancelledDownloads.erase (downloadHash);
195+ MessageManager::callAsync ([this , downloadHash]() mutable {
196+ for (auto & listener : listeners) {
197+ listener->patchDownloadCompleted (downloadHash, false );
198+ }
199+ });
200+ return ;
201+ }
165202 }
166203
167204 MessageManager::callAsync ([this , downloadHash, progress]() mutable {
@@ -210,6 +247,7 @@ class DownloadPool final : public DeletedAtShutdown {
210247
211248 void cancelDownload (hash32 hash)
212249 {
250+ ScopedLock const cancelLock (cancelledDownloadsLock);
213251 cancelledDownloads.insert (hash);
214252 }
215253
@@ -222,6 +260,8 @@ class DownloadPool final : public DeletedAtShutdown {
222260
223261 ThreadPool imagePool = ThreadPool(3 );
224262 ThreadPool patchPool = ThreadPool(2 );
263+
264+ std::atomic<bool > cancelledImageDownload = false ;
225265
226266public:
227267 JUCE_DECLARE_SINGLETON (DownloadPool, false );
@@ -316,14 +356,12 @@ class OnlineImage final : public Component
316356 }
317357 }
318358
319- assert (webpImage.isValid ());
320359 MessageManager::callAsync ([_this = SafePointer (this ), image = webpImage.createCopy ()] {
321360 if (_this)
322361 {
323362 _this->image = image;
324363 _this->repaint ();
325364 _this->spinner .stopSpinning ();
326- assert (image.isValid ());
327365 }
328366 });
329367 }
@@ -973,6 +1011,8 @@ struct PatchStore final : public Component
9731011
9741012 SearchEditor input;
9751013 Spinner spinner;
1014+
1015+ bool connectionError = false ;
9761016
9771017 PatchStore ()
9781018 {
@@ -1082,6 +1122,11 @@ struct PatchStore final : public Component
10821122
10831123 g.setColour (findColour (PlugDataColour::toolbarOutlineColourId));
10841124 g.drawLine (0 , 40 , getWidth (), 40 );
1125+
1126+ if (connectionError)
1127+ {
1128+ Fonts::drawStyledText (g, " Error: could not connect to server" , Rectangle<float >(0 .0f , 54 .0f , getWidth (), 32 .0f ), Colours::orange, Semibold, 15 , Justification::centred);
1129+ }
10851130 }
10861131
10871132 void resized () override
@@ -1107,8 +1152,19 @@ struct PatchStore final : public Component
11071152
11081153 void databaseDownloadCompleted (HeapArray<std::pair<PatchInfo, int >> const & patches) override
11091154 {
1155+ connectionError = false ;
11101156 patchContainer.showPatches (patches);
11111157 refreshButton.setEnabled (true );
11121158 spinner.stopSpinning ();
1159+ repaint ();
1160+ }
1161+
1162+ void databaseDownloadFailed () override
1163+ {
1164+ connectionError = true ;
1165+ patchContainer.showPatches ({});
1166+ refreshButton.setEnabled (true );
1167+ spinner.stopSpinning ();
1168+ repaint ();
11131169 }
11141170};
0 commit comments