diff --git a/app/src/About.purs b/app/src/About.purs
index 5eb021d..6df1a71 100644
--- a/app/src/About.purs
+++ b/app/src/About.purs
@@ -101,6 +101,7 @@ updateScanStatus elems (ScanStatus status) =
ScanPreProcessingThumbnails -> "Discovering existing thumbnails …"
ScanGeneratingThumbnails -> "Generating new thumbnails …"
ScanLoadingThumbnails -> "Loading thumbnails …"
+ ScanReloading -> "Reloading data …"
ScanDone -> "Scan complete"
valuePair (show status.filesDiscovered) "files discovered"
diff --git a/app/src/AlbumListView.purs b/app/src/AlbumListView.purs
index 3870deb..60b36f0 100644
--- a/app/src/AlbumListView.purs
+++ b/app/src/AlbumListView.purs
@@ -141,6 +141,7 @@ renderAlbum :: (Event -> Aff Unit) -> Album -> Html Unit
renderAlbum postEvent (Album album) = Html.div $ do
Html.addClass "album"
Html.img (Model.thumbUrl album.id) (album.title <> " by " <> album.artist) $ do
+ Html.setBackgroundColor album.color
Html.addClass "thumb"
Html.span $ do
Html.addClass "title"
diff --git a/app/src/Dom.js b/app/src/Dom.js
index 6deaeae..c5b63be 100644
--- a/app/src/Dom.js
+++ b/app/src/Dom.js
@@ -207,6 +207,12 @@ export const setMaskImageImpl = function(mask, element) {
}
}
+export const setBackgroundColorImpl = function(hexColor, element) {
+ return function() {
+ element.style.backgroundColor = hexColor;
+ }
+}
+
export const setWidthImpl = function(width, element) {
return function() {
element.style.width = width;
diff --git a/app/src/Dom.purs b/app/src/Dom.purs
index 84361bf..eccc57a 100644
--- a/app/src/Dom.purs
+++ b/app/src/Dom.purs
@@ -33,6 +33,7 @@ module Dom
, renderHtml
, scrollIntoView
, setAttribute
+ , setBackgroundColor
, setHeight
, setId
, setMaskImage
@@ -90,6 +91,7 @@ foreign import onScrollImpl :: Fn2 (Effect Unit) Element (Effect Unit)
foreign import removeChildImpl :: Fn2 Element Element (Effect Unit)
foreign import removeClassImpl :: Fn2 String Element (Effect Unit)
foreign import setAttributeImpl :: Fn3 String String Element (Effect Unit)
+foreign import setBackgroundColorImpl :: Fn2 String Element (Effect Unit)
foreign import setHeightImpl :: Fn2 String Element (Effect Unit)
foreign import setIdImpl :: Fn2 String Element (Effect Unit)
foreign import setMaskImageImpl :: Fn2 String Element (Effect Unit)
@@ -143,6 +145,9 @@ setMaskImage mask element = runFn2 setMaskImageImpl mask element
setScrollTop :: Number -> Element -> Effect Unit
setScrollTop off element = runFn2 setScrollTopImpl off element
+setBackgroundColor :: String -> Element -> Effect Unit
+setBackgroundColor hexColor element = runFn2 setBackgroundColorImpl hexColor element
+
setAttribute :: String -> String -> Element -> Effect Unit
setAttribute attribute value element = runFn3 setAttributeImpl attribute value element
diff --git a/app/src/Event.purs b/app/src/Event.purs
index b7217cc..d62b527 100644
--- a/app/src/Event.purs
+++ b/app/src/Event.purs
@@ -8,13 +8,14 @@
module Event
( Event (..)
, HistoryMode (..)
+ , SearchSeq (..)
, SortField (..)
, SortDirection (..)
, SortMode
) where
import Prelude
-import Model (Album, QueuedTrack, ScanStatus)
+import Model (Album, QueuedTrack, ScanStatus, SearchResults)
import Navigation (Location)
data HistoryMode
@@ -36,6 +37,13 @@ data SortDirection
type SortMode = { field :: SortField, direction :: SortDirection }
+-- Search queries contain a sequence number, so we can correlate results with
+-- searches and discard results for outdated queries.
+data SearchSeq = SearchSeq Int
+
+derive instance searchSeqEq :: Eq SearchSeq
+derive instance searchSeqOrd :: Ord SearchSeq
+
data Event
= Initialize (Array Album) (Array QueuedTrack)
| UpdateQueue (Array QueuedTrack)
@@ -57,5 +65,11 @@ data Event
| UpdateProgress
-- The user typed the keyboard shortcut for 'search'.
| SearchKeyPressed
+ -- The user searched for this query. Queries contain a sequence number
+ -- generated by the search box itself, so that we process results in the
+ -- right order, even when results or even query events arrive in a different
+ -- order.
+ | Search SearchSeq String
+ | SearchResult SearchSeq SearchResults
-- A new scan status was received.
| UpdateScanStatus ScanStatus
diff --git a/app/src/Html.purs b/app/src/Html.purs
index 59904f2..7f8f34f 100644
--- a/app/src/Html.purs
+++ b/app/src/Html.purs
@@ -28,6 +28,7 @@ module Html
, p
, removeClass
, scrollIntoView
+ , setBackgroundColor
, setHeight
, setId
, setMaskImage
@@ -135,6 +136,13 @@ onInput callback = ReaderT $ \container ->
scrollIntoView :: Html Unit
scrollIntoView = ReaderT Dom.scrollIntoView
+-- Set the background color to the given hex color. Should not include the
+-- leading `#`, we prepend that here, to avoid having to store and transfer
+-- the additional byte.
+setBackgroundColor :: String -> Html Unit
+setBackgroundColor hexColor = ReaderT $
+ \container -> Dom.setBackgroundColor ("#" <> hexColor) container
+
setScrollTop :: Number -> Html Unit
setScrollTop off = ReaderT $ \container -> Dom.setScrollTop off container
diff --git a/app/src/Model.purs b/app/src/Model.purs
index 3446ae7..012acb3 100644
--- a/app/src/Model.purs
+++ b/app/src/Model.purs
@@ -138,6 +138,7 @@ newtype Album = Album
, artistIds :: NonEmptyArray ArtistId
, releaseDate :: String
, firstSeen :: String
+ , color :: String
, discoverScore :: Number
, trendingScore :: Number
, forNowScore :: Number
@@ -155,6 +156,7 @@ instance decodeJsonAlbum :: DecodeJson Album where
artist <- Json.getField obj "artist"
releaseDate <- Json.getField obj "release_date"
firstSeen <- Json.getField obj "first_seen"
+ color <- Json.getField obj "color"
discoverScore <- Json.getField obj "discover_score"
trendingScore <- Json.getField obj "trending_score"
forNowScore <- Json.getField obj "for_now_score"
@@ -165,6 +167,7 @@ instance decodeJsonAlbum :: DecodeJson Album where
, artistIds
, releaseDate
, firstSeen
+ , color
, discoverScore
, trendingScore
, forNowScore
@@ -294,6 +297,7 @@ data ScanStage
| ScanPreProcessingThumbnails
| ScanGeneratingThumbnails
| ScanLoadingThumbnails
+ | ScanReloading
| ScanDone
derive instance eqScanStage :: Eq ScanStage
@@ -312,6 +316,7 @@ instance decodeJsonScanStage :: DecodeJson ScanStage where
"preprocessing_thumbnails" -> pure ScanPreProcessingThumbnails
"generating_thumbnails" -> pure ScanGeneratingThumbnails
"loading_thumbnails" -> pure ScanLoadingThumbnails
+ "reloading" -> pure ScanReloading
"done" -> pure ScanDone
_ -> Left $ UnexpectedValue json
diff --git a/app/src/Search.purs b/app/src/Search.purs
index aa803dd..f6a526a 100644
--- a/app/src/Search.purs
+++ b/app/src/Search.purs
@@ -10,35 +10,50 @@ module Search
, new
, focus
, clear
+ , renderSearchResults
) where
import Control.Monad.Reader.Class (ask, local)
import Data.Array as Array
import Data.Foldable (for_)
+import Data.Maybe (Maybe (..))
import Data.String.CodeUnits as CodeUnits
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class (liftEffect)
-import Effect.Class.Console as Console
+import Foreign.Object (Object)
+import Foreign.Object as Object
import Prelude
import Dom (Element)
import Dom as Dom
-import Event (Event, HistoryMode (RecordHistory))
+import Event (Event, HistoryMode (RecordHistory), SearchSeq (SearchSeq))
import Event as Event
import Html (Html)
import Html as Html
-import Model (SearchArtist (..), SearchAlbum (..), SearchTrack (..))
+import Model (Album (..), AlbumId (..), SearchArtist (..), SearchAlbum (..), SearchResults (..), SearchTrack (..))
import Model as Model
import Navigation as Navigation
+import Var as Var
type SearchElements =
{ searchBox :: Element
, resultBox :: Element
}
-renderSearchArtist :: (Event -> Aff Unit) -> SearchArtist -> Html Unit
-renderSearchArtist postEvent (SearchArtist artist) = do
+-- Look up the album by id in the album collection. If we found it, set the
+-- background color to that album's color. Intended to be curried.
+setAlbumColor :: Object Album -> AlbumId -> Html Unit
+setAlbumColor albumsById (AlbumId id) = case Object.lookup id albumsById of
+ Nothing -> pure unit
+ Just (Album album) -> Html.setBackgroundColor album.color
+
+renderSearchArtist
+ :: (Event -> Aff Unit)
+ -> (AlbumId -> Html Unit)
+ -> SearchArtist
+ -> Html Unit
+renderSearchArtist postEvent setColor (SearchArtist artist) = do
Html.li $ do
Html.addClass "artist"
Html.div $ do
@@ -46,19 +61,26 @@ renderSearchArtist postEvent (SearchArtist artist) = do
Html.text artist.name
Html.div $ do
Html.addClass "discography"
- for_ artist.albums $ \albumId -> do
- Html.img (Model.thumbUrl albumId) ("An album by " <> artist.name) $ pure unit
+ for_ artist.albums $ \albumId -> Html.img
+ (Model.thumbUrl albumId)
+ ("An album by " <> artist.name)
+ (setColor albumId)
Html.onClick $ launchAff_ $ postEvent $ Event.NavigateTo
(Navigation.Artist $ artist.id)
RecordHistory
-- TODO: Deduplicate between here and album component.
-renderSearchAlbum :: (Event -> Aff Unit) -> SearchAlbum -> Html Unit
-renderSearchAlbum postEvent (SearchAlbum album) = do
+renderSearchAlbum
+ :: (Event -> Aff Unit)
+ -> (AlbumId -> Html Unit)
+ -> SearchAlbum
+ -> Html Unit
+renderSearchAlbum postEvent setColor (SearchAlbum album) = do
Html.li $ do
Html.addClass "album"
Html.img (Model.thumbUrl album.id) (album.title <> " by " <> album.artist) $ do
+ setColor album.id
Html.addClass "thumb"
Html.span $ do
Html.addClass "title"
@@ -77,11 +99,16 @@ renderSearchAlbum postEvent (SearchAlbum album) = do
(Navigation.Album $ album.id)
RecordHistory
-renderSearchTrack :: (Event -> Aff Unit) -> SearchTrack -> Html Unit
-renderSearchTrack postEvent (SearchTrack track) = do
+renderSearchTrack
+ :: (Event -> Aff Unit)
+ -> (AlbumId -> Html Unit)
+ -> SearchTrack
+ -> Html Unit
+renderSearchTrack postEvent setColor (SearchTrack track) = do
Html.li $ do
Html.addClass "track"
Html.img (Model.thumbUrl track.albumId) track.album $ do
+ setColor track.albumId
Html.addClass "thumb"
Html.span $ do
Html.addClass "title"
@@ -107,47 +134,71 @@ new postEvent = do
ask
local (const searchBox) $ do
+ -- We maintain the search sequence number here, because the input handler
+ -- runs as an Effect rather than Aff, so we can be sure that the sequence
+ -- numbers match the order of the input events. In the main loop, we only
+ -- process search results if they are for a newer search than the last one
+ -- we processed, to ensure that a slow search query that arrives later does
+ -- not overwrite current search results. (That can happen especially at the
+ -- beginning, as a short query string matches more, so the response is
+ -- larger and takes longer to serialize/transfer/deserialize.)
+ searchSeq <- liftEffect $ Var.create 0
Html.onInput $ \query -> do
- -- Fire off the search query and render it when it comes in.
- -- TODO: Pass these through the event loop, to ensure that the result
- -- matches the query, and perhaps for caching as well.
- launchAff_ $ do
- Model.SearchResults result <- Model.search query
- Console.log $ "Received artists: " <> (show $ Array.length $ result.artists)
- Console.log $ "Received albums: " <> (show $ Array.length $ result.albums)
- Console.log $ "Received tracks: " <> (show $ Array.length $ result.tracks)
- liftEffect $ do
- Html.withElement resultBox $ do
- Html.clear
- Html.div $ do
- Html.addClass "search-results-list"
-
- when (not $ Array.null result.artists) $ do
- Html.div $ do
- Html.setId "search-artists"
- Html.h2 $ Html.text "Artists"
- -- Limit the number of results rendered at once to keep search
- -- responsive. TODO: Render overflow button.
- Html.ul $ for_ (Array.take 10 result.artists) $ renderSearchArtist postEvent
-
- when (not $ Array.null result.albums) $ do
- Html.div $ do
- Html.setId "search-albums"
- Html.h2 $ Html.text "Albums"
- -- Limit the number of results rendered at once to keep search
- -- responsive. TODO: Render overflow button.
- Html.ul $ for_ (Array.take 25 result.albums) $ renderSearchAlbum postEvent
-
- when (not $ Array.null result.tracks) $ do
- Html.div $ do
- Html.setId "search-tracks"
- Html.h2 $ Html.text "Tracks"
- -- Limit the number of results rendered at once to keep search
- -- responsive. TODO: Render overflow button.
- Html.ul $ for_ (Array.take 25 result.tracks) $ renderSearchTrack postEvent
+ currentSeq <- Var.get searchSeq
+ let nextSeq = currentSeq + 1
+ Var.set searchSeq nextSeq
+ launchAff_ $ postEvent $ Event.Search (SearchSeq nextSeq) query
pure $ { searchBox, resultBox }
+renderSearchResults
+ :: (Event -> Aff Unit)
+ -> SearchElements
+ -> Object Album
+ -> SearchResults
+ -> Effect Unit
+renderSearchResults postEvent elements albumsById (SearchResults result) =
+ let
+ setColor = setAlbumColor albumsById
+ in
+ Html.withElement elements.resultBox $ do
+ -- TODO: On low-power devices, there can be a brief 1-frame flicker for
+ -- images to load after extending the query, even if the results were
+ -- visible previously -- presumably because we delete the nodes, so
+ -- Chromium deallocates the images, and has to decode them again when we
+ -- promptly add the nodes again. We could do better by reycling
+ -- those image nodes.
+ Html.clear
+ Html.div $ do
+ Html.addClass "search-results-list"
+
+ when (not $ Array.null result.artists) $ do
+ Html.div $ do
+ Html.setId "search-artists"
+ Html.h2 $ Html.text "Artists"
+ -- Limit the number of results rendered at once to keep search
+ -- responsive. TODO: Render overflow button.
+ Html.ul $ for_ (Array.take 10 result.artists) $
+ renderSearchArtist postEvent setColor
+
+ when (not $ Array.null result.albums) $ do
+ Html.div $ do
+ Html.setId "search-albums"
+ Html.h2 $ Html.text "Albums"
+ -- Limit the number of results rendered at once to keep search
+ -- responsive. TODO: Render overflow button.
+ Html.ul $ for_ (Array.take 25 result.albums) $
+ renderSearchAlbum postEvent setColor
+
+ when (not $ Array.null result.tracks) $ do
+ Html.div $ do
+ Html.setId "search-tracks"
+ Html.h2 $ Html.text "Tracks"
+ -- Limit the number of results rendered at once to keep search
+ -- responsive. TODO: Render overflow button.
+ Html.ul $ for_ (Array.take 25 result.tracks) $
+ renderSearchTrack postEvent setColor
+
clear :: SearchElements -> Effect Unit
clear elements = do
Dom.setValue "" elements.searchBox
diff --git a/app/src/State.purs b/app/src/State.purs
index f45a684..5d214d3 100644
--- a/app/src/State.purs
+++ b/app/src/State.purs
@@ -39,7 +39,7 @@ import AlbumView (AlbumViewState)
import AlbumView as AlbumView
import Dom (Element)
import Dom as Dom
-import Event (Event, HistoryMode, SortMode, SortField (..), SortDirection (..))
+import Event (Event, HistoryMode, SearchSeq (..), SortMode, SortField (..), SortDirection (..))
import Event as Event
import History as History
import Html as Html
@@ -93,6 +93,7 @@ type AppState =
, navBar :: NavBarState
, statusBar :: StatusBarState
, location :: Location
+ , lastSearchRendered :: SearchSeq
, lastArtists :: Array ArtistId
, lastAlbum :: Maybe AlbumId
, currentTrack :: Maybe QueueId
@@ -213,6 +214,7 @@ new bus = do
, statusBar: statusBar
, location: Navigation.Library
, lastArtists: []
+ , lastSearchRendered: SearchSeq 0
, lastAlbum: Nothing
, currentTrack: Nothing
, prefetchedThumb: Nothing
@@ -584,6 +586,29 @@ handleEvent event state = case event of
_notSearch ->
handleEvent (Event.NavigateTo Navigation.Search Event.RecordHistory) state
+ Event.Search seq query -> do
+ -- We fork a fiber to run the search query and make it post an event when
+ -- it's done, we don't block the main event loop on it, so that other logic
+ -- can continue even when a search is in progress, and also so that multiple
+ -- search queries can run in parallel when the user is typing fast.
+ _fiber <- Aff.forkAff $ do
+ result <- Model.search query
+ state.postEvent $ Event.SearchResult seq result
+ pure state
+
+ Event.SearchResult seq results ->
+ -- We only render the search result if it is for a newer query than what
+ -- we already rendered. (But it still may not be for the latest query we
+ -- sent!) This ensures that slow responses get dropped, rather than
+ -- overwriting later results that we already displayed.
+ if seq <= state.lastSearchRendered then pure state else do
+ liftEffect $ Search.renderSearchResults
+ state.postEvent
+ state.elements.search
+ state.albumsById
+ results
+ pure $ state { lastSearchRendered = seq }
+
beforeSwitchPane :: AppState -> Aff AppState
beforeSwitchPane state =
case state.location of
diff --git a/docs/changelog.md b/docs/changelog.md
index 7697157..a9252f7 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -22,6 +22,18 @@ Musium versions are named `MAJOR.MINOR.PATCH`.
## Next
+**Compatibility:**
+
+ * The schema of the thumbnail table changed. The easiest way to deal with this
+ is to drop the table with `sqlite3 db.sqlite3 'drop table thumbnails;'` and
+ then re-index with `musium scan`, where `db.sqlite3` is your database [as
+ configured](configuration.md#db_path). This will re-compute all thumbnails
+ and analyze their colors, which may take a while.
+
+Features:
+
+ * The queue tab in the webinterface is now implemented, including buttons to
+ shuffle and clear the queue.
* A new sort option is available in the album list: _For Now_. This ranking
shows you albums that you played at similar times of the day, week, and year
in the past. For example, if you tend to listen to more quiet music in the
@@ -31,10 +43,21 @@ Musium versions are named `MAJOR.MINOR.PATCH`.
recent popularity to show you albums worth listening to again. Like the
_For Now_ ranking, it takes into account the time of the day, week, and year,
to show the most relevant suggestions.
- * The queue tab in the webinterface is now implemented, including buttons to
- shuffle and clear the queue.
+ * Musium now computes and saves the dominant color of album cover art. This
+ color is used as a placeholder in the webinterface, to reduce visual flicker
+ when thumbnails are loading.
* Add support for Czech diacritics in text normalization.
+Bugfixes:
+
+ * When a scan discovered new files and computed loudness, that loudness data
+ would only be used after a subsequent scan or restart. Now we properly apply
+ the loudness measurement directly after the scan.
+ * When a search query in the webinterface was slow to load (which tends to be
+ the case initially during search, because short queries have many matches),
+ the results for an earlier query could in rare cases replace the search
+ results for a later query. Now such late results are properly discarded.
+
## 0.16.0
Released 2025-02-02.
diff --git a/src/build.rs b/src/build.rs
index 6db9a10..99d23ba 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -9,9 +9,12 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
use std::str::FromStr;
-use crate::database::{FileMetadata, Transaction, self as db};
-use crate::prim::{AlbumId, Album, AlbumArtistsRef, ArtistId, Artist, FileId, Instant, TrackId, Track, Date, Lufs, FilenameRef, StringRef};
-use crate::string_utils::{StringDeduper, normalize_words};
+use crate::database::{self as db, FileMetadata, Transaction};
+use crate::prim::{
+ Album, AlbumArtistsRef, AlbumId, Artist, ArtistId, Color, Date, FileId, FilenameRef, Instant,
+ Lufs, StringRef, Track, TrackId,
+};
+use crate::string_utils::{normalize_words, StringDeduper};
use crate::word_index::WordMeta;
pub enum BuildError {
@@ -801,13 +804,19 @@ impl BuildMetaIndex {
// TODO: It's inefficient to query the database once per track for the
// album loudness.
- let track_loudness = db::select_track_loudness_lufs(tx, track_id.0 as i64)?.map(Lufs::from_f64);
- let album_loudness = db::select_album_loudness_lufs(tx, album_id.0 as i64)?.map(Lufs::from_f64);
+ let track_loudness =
+ db::select_track_loudness_lufs(tx, track_id.0 as i64)?.map(Lufs::from_f64);
+ let album_loudness =
+ db::select_album_loudness_lufs(tx, album_id.0 as i64)?.map(Lufs::from_f64);
+ let album_color = db::select_album_color(tx, album_id.0 as i64)?
+ .map(|c| Color::parse(&c).expect("Database should store only valid colors"));
// Insert all the album artists if no artist with the given id existed
// yet. If one did exist, verify consistency. Also fill the vector of
// album artists so the album can refer to this.
- let album_artists_ref = self.album_artists.insert(album_artists.iter().map(|tuple| tuple.0));
+ let album_artists_ref = self
+ .album_artists
+ .insert(album_artists.iter().map(|tuple| tuple.0));
for (artist_id, aa_name, aa_name_sort) in album_artists {
let artist = Artist {
@@ -844,6 +853,7 @@ impl BuildMetaIndex {
original_release_date: release_date,
first_seen: file.mtime,
loudness: album_loudness,
+ color: album_color.unwrap_or_default(),
};
let mut add_album = true;
diff --git a/src/database.rs b/src/database.rs
index bde0656..8e6cbb4 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -333,8 +333,13 @@ pub fn ensure_schema_exists(tx: &mut Transaction) -> Result<()> {
create table if not exists thumbnails
( album_id integer primary key
, file_id integer not null references files (id) on delete cascade
+ -- We store the color as hex string, even though that's larger than just
+ -- storing the 24-bit integer. The data blob is kilobytes anyway, a few bytes
+ -- here makes little difference, and it makes the database more readable for
+ -- humans.
+ , color text not null
, data blob not null
- );
+ ) strict;
"#;
let statement = match tx.statements.entry(sql.as_ptr()) {
Occupied(entry) => entry.into_mut(),
@@ -550,11 +555,11 @@ pub fn iter_file_tags<'i, 't, 'a>(tx: &'i mut Transaction<'t, 'a>, file_id: i64)
Ok(result)
}
-pub fn insert_album_thumbnail(tx: &mut Transaction, album_id: i64, file_id: i64, data: &[u8]) -> Result<()> {
+pub fn insert_album_thumbnail(tx: &mut Transaction, album_id: i64, file_id: i64, color: &str, data: &[u8]) -> Result<()> {
let sql = r#"
- insert into thumbnails (album_id, file_id, data)
- values (:album_id, :file_id, :data)
- on conflict (album_id) do update set data = :data;
+ insert into thumbnails (album_id, file_id, color, data)
+ values (:album_id, :file_id, :color, :data)
+ on conflict (album_id) do update set color = :color, data = :data;
"#;
let statement = match tx.statements.entry(sql.as_ptr()) {
Occupied(entry) => entry.into_mut(),
@@ -563,7 +568,8 @@ pub fn insert_album_thumbnail(tx: &mut Transaction, album_id: i64, file_id: i64,
statement.reset()?;
statement.bind(1, album_id)?;
statement.bind(2, file_id)?;
- statement.bind(3, data)?;
+ statement.bind(3, color)?;
+ statement.bind(4, data)?;
let result = match statement.next()? {
Row => panic!("Query 'insert_album_thumbnail' unexpectedly returned a row."),
Done => (),
@@ -743,6 +749,29 @@ pub fn update_listen_completed(tx: &mut Transaction, listen_id: i64, queue_id: i
Ok(result)
}
+pub fn select_album_color(tx: &mut Transaction, album_id: i64) -> Result