From 50c85874bc5fb1b2e446189e59f786c2caa08ba4 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Sat, 15 Aug 2026 20:59:27 +0100 Subject: [PATCH 1/7] redesign of updateStandaloneArtwork() Signed-off-by: darrell-k --- SQL/SQLite/schema_optimize.sql | 2 +- SQL/SQLite/schema_scanner.sql | 13 +- SQL/mysql/schema_optimize.sql | 2 +- SQL/mysql/schema_scanner.sql | 13 +- Slim/Music/Artwork.pm | 232 ++++++++++++++---------------- Slim/Schema.pm | 13 +- Slim/Utils/Scanner/Local.pm | 15 ++ Slim/Utils/Scanner/Local/Async.pm | 32 +++-- 8 files changed, 175 insertions(+), 147 deletions(-) diff --git a/SQL/SQLite/schema_optimize.sql b/SQL/SQLite/schema_optimize.sql index 7882bc7bf9..11f6b4e2de 100644 --- a/SQL/SQLite/schema_optimize.sql +++ b/SQL/SQLite/schema_optimize.sql @@ -3,7 +3,7 @@ -- This is done here as it is faster to do in sql than in the server. -- -DELETE FROM scanned_pics; +--DELETE FROM scanned_pics; ### Removed temporarily for debugging -- XXX This appears to not be needed anymore as contributors are properly -- removed by the new scanner diff --git a/SQL/SQLite/schema_scanner.sql b/SQL/SQLite/schema_scanner.sql index b25cf70341..d7ade89cd5 100644 --- a/SQL/SQLite/schema_scanner.sql +++ b/SQL/SQLite/schema_scanner.sql @@ -8,8 +8,15 @@ CREATE INDEX scannedUrlIndex ON scanned_files (url); DROP TABLE IF EXISTS scanned_pics; CREATE TABLE scanned_pics ( - url text NOT NULL, + dir text, + path text NOT NULL, timestamp int(10), - filesize int(10) + filesize int(10), + coverid char(8), + status char(1) ); -CREATE INDEX scannedPicUrlIndex ON scanned_pics (url); +CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); +CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); +create index scannedPicStatusidx on scanned_pics(status); + +CREATE INDEX IF NOT EXISTS trackscoveridx ON tracks(cover); diff --git a/SQL/mysql/schema_optimize.sql b/SQL/mysql/schema_optimize.sql index 4727c6b32d..35b2c9f72b 100644 --- a/SQL/mysql/schema_optimize.sql +++ b/SQL/mysql/schema_optimize.sql @@ -3,7 +3,7 @@ -- This is done here as it is faster to do in sql than in the server. -- -DELETE FROM scanned_pics; +--DELETE FROM scanned_pics; ### Removed temporarily for debugging -- XXX This appears to not be needed anymore as contributors are properly -- removed by the new scanner diff --git a/SQL/mysql/schema_scanner.sql b/SQL/mysql/schema_scanner.sql index 87502086c8..d7ade89cd5 100644 --- a/SQL/mysql/schema_scanner.sql +++ b/SQL/mysql/schema_scanner.sql @@ -8,8 +8,15 @@ CREATE INDEX scannedUrlIndex ON scanned_files (url); DROP TABLE IF EXISTS scanned_pics; CREATE TABLE scanned_pics ( - url text NOT NULL COLLATE NOCASE, -- URL must be case insensitive, or we might duplicate tracks if the filename changes case only (https://github.com/LMS-Community/slimserver/issues/705#issuecomment-1026229542) + dir text, + path text NOT NULL, timestamp int(10), - filesize int(10) + filesize int(10), + coverid char(8), + status char(1) ); -CREATE INDEX scannedPicUrlIndex ON scanned_pics (url); +CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); +CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); +create index scannedPicStatusidx on scanned_pics(status); + +CREATE INDEX IF NOT EXISTS trackscoveridx ON tracks(cover); diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 727fbe7601..d3b9536425 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -87,7 +87,10 @@ sub findStandaloneArtwork { my $formatStr = $1; my $suffix = $2; + # Maybe a track instance was passed in, but no longer from updateStandaloneArtwork() which gives us + # the trackid instead, as we only need to instantiate a track if 'titleformatter' artwork naming is in use. my $track = $trackAttributes && delete $trackAttributes->{_track}; + $track ||= Slim::Schema->find('Track', $trackAttributes->{_trackid}) if $trackAttributes->{_trackid}; # Merge attributes to use with TitleFormatter # XXX This may break for some people as it's not using a Track object anymore @@ -200,23 +203,21 @@ sub _findStandaloneArtwork { my @images; if (main::SCANNER) { - my $sql = 'SELECT url FROM scanned_pics WHERE url '; + my $sql = "SELECT path FROM scanned_pics WHERE (status IS NULL OR status <> 'D') AND "; if (scalar @candidates) { - $sql .= sprintf('IN (%s)', join(',', map { '?' } @candidates)); + $sql .= sprintf('path IN (%s)', join(',', map { '?' } @candidates)); } else { - # doing a range search helps us avoid a LIKE query, which would result in a scan - $sql .= '>= ? AND url < ?'; - my $pathUrl = Slim::Utils::Misc::fileURLFromPath($parentDir); - push @candidates, $pathUrl, $pathUrl . chr(0xff); + $sql .= 'dir = ?'; + push @candidates, $parentDir; } my $sth = Slim::Schema->dbh->prepare_cached($sql); @images = Slim::Utils::Misc::uniq(map { - Slim::Utils::Misc::pathFromFileURL($_->[0]); + $_->[0] } @{ - $dbh->selectall_arrayref($sth, undef, map { Slim::Utils::Misc::fileURLFromPath($_) } @candidates) + $dbh->selectall_arrayref($sth, undef, @candidates) }); } else { @@ -256,71 +257,48 @@ sub updateStandaloneArtwork { my $class = shift; my $cb = shift; # optional callback when done (main process async mode) - my $dbh = Slim::Schema->dbh; - - my $where = qq{ - tracks.cover LIKE '%jpg' - OR tracks.cover LIKE '%jpeg' - OR tracks.cover LIKE '%png' - OR tracks.cover LIKE '%gif' - OR tracks.cover LIKE 'http%' - OR tracks.coverid IS NULL - }; - - # get singledir parameter from the scanner if available - my $singledir = main::SCANNER ? $ARGV[-1] : undef; - if ($singledir && $singledir eq 'onlinelibrary') { - # shortcut for online library scan only - ignore local files - $where = qq{ - tracks.url NOT LIKE 'file://%' - AND tracks.cover LIKE 'http%' - AND tracks.coverid IS NULL - }; - } - elsif ($singledir) { - $singledir = Slim::Utils::Misc::fileURLFromPath(Slim::Utils::Unicode::encode_locale($singledir)); - $where = qq{ - tracks.url LIKE '$singledir%' - AND ($where) - }; - } +### I might have missed it, but I can't see where this might be called in main process async mode. +### If it is, we'll need more work to populate scanned_pics in the main process or just keep a version of the old subroutine for that use. - # Find all tracks with un-cached artwork: - # * All distinct cover values where cover isn't 0 and cover_cached is null - # * Tracks share the same cover art when the cover field is the same - # (same path or same embedded art length). - my $sql = qq{ - SELECT - tracks.id, - tracks.url, - tracks.cover, - tracks.coverid, - albums.id AS albumid, - albums.title AS album_title, - albums.artwork AS album_artwork - FROM tracks - JOIN albums ON (tracks.album = albums.id) - WHERE $where - GROUP BY tracks.cover, tracks.album - }; + my $dbh = Slim::Schema->dbh; - my $sth_update_tracks = $dbh->prepare( qq{ - UPDATE tracks - SET cover = ?, coverid = ?, cover_cached = NULL - WHERE album = ? + # unflag existing unchanged artwork + $dbh->do( qq{ + UPDATE scanned_pics SET status = NULL + WHERE status = 'E' + AND EXISTS (SELECT * FROM tracks WHERE tracks.coverid = scanned_pics.coverid) } ); - my $sth_update_albums = $dbh->prepare( qq{ + # for online artwork, update album artwork to first track coverid + ### SQLITE ONLY, there's a different syntax for MySql. + ### I considered adding rows to scanned_pics for remote images so that they'd be processed in the loop below, but I think this is more efficient. + $dbh->do( qq{ UPDATE albums - SET artwork = ? - WHERE id = ? + SET artwork = tracks.coverid + FROM tracks, ( + SELECT coverid + FROM tracks + LIMIT 1 + ) + WHERE tracks.album = albums.id + AND tracks.cover LIKE 'https%' + AND tracks.coverid <> albums.artwork } ); + Slim::Schema->forceCommit; + + my $sql_scanned_pics = qq{ + SELECT path, coverid, GROUP_CONCAT(status) + FROM scanned_pics + WHERE status IS NOT NULL + GROUP BY path, coverid + }; + my ($count) = $dbh->selectrow_array( qq{ - SELECT COUNT(*) FROM ( $sql ) AS t1 + SELECT COUNT(*) FROM ( $sql_scanned_pics ) AS t1 } ); - $log->error("Starting updateStandaloneArtwork for $count albums"); + $log->error("Starting updateStandaloneArtwork for $count images"); if ( !$count ) { $cb && $cb->(); @@ -335,82 +313,79 @@ sub updateStandaloneArtwork { bar => 1, } ); - my $sth = $dbh->prepare($sql); - $sth->execute; + my $pic_sth = $dbh->prepare($sql_scanned_pics); + my ($picPath, $picCoverid, $status); + $pic_sth->bind_columns(\$picPath, \$picCoverid, \$status); - my ($trackid, $url, $cover, $coverid, $albumid, $album_title, $album_artwork); - $sth->bind_columns(\$trackid, \$url, \$cover, \$coverid, \$albumid, \$album_title, \$album_artwork); + my $sql_tracks = qq{ + SELECT tracks.id, tracks.url, + tracks.cover, + albums.id AS albumid, + albums.title AS album_title, + albums.artwork AS album_artwork + FROM tracks JOIN albums ON albums.id = tracks.album + WHERE url BETWEEN ? AND ? + AND instr(substr(url, length(?)+2), "/") < 1 + ORDER BY albums.id + }; + my $tracks_sth = $dbh->prepare($sql_tracks); + + my $sth_scanned_pics = $dbh->prepare( qq{ + SELECT coverid FROM scanned_pics WHERE path = ? + } ); + + my $sth_update_tracks = $dbh->prepare( qq{ + UPDATE tracks + SET cover = ?, coverid = ?, cover_cached = NULL + WHERE id = ? + } ); + + my $sth_update_albums = $dbh->prepare( qq{ + UPDATE albums + SET artwork = ? + WHERE id = ? + } ); + + my $previousAlbum = undef; my $i = 0; my $t = 0; - my $work = sub { - if ( $sth->fetch ) { - my $newCoverId; + $pic_sth->execute; - $progress->update( $album_title ); + my $work = sub { + if ( $pic_sth->fetch ) { if ( $t < time ) { Slim::Schema->forceCommit; $t = time + 5; } - # check for updated artwork - if ( $cover ) { - $newCoverId = Slim::Schema::Track->generateCoverId({ - cover => $cover, - url => $url, - }); - } + my $imageDirUrl = Slim::Utils::Misc::fileURLFromPath(dirname($picPath)); + my @params = ($imageDirUrl, $imageDirUrl . chr(0xff), $imageDirUrl); - # check for new artwork to unchanged file - # - !$cover: there wasn't any previously - # - !$newCoverId: existing file has disappeared - if ( (!$cover || !$newCoverId) && Slim::Music::Info::isFileURL($url) ) { - # store properties in a hash - my $track = Slim::Schema->find('Track', $trackid); - - if ($track) { - my $newCover = Slim::Music::Artwork->findStandaloneArtwork( - { _track => $track }, # pass track object to avoid deflation unless necessary - {}, - Slim::Utils::Misc::fileURLFromPath( - dirname(Slim::Utils::Misc::pathFromFileURL($url)) - ), - ); - - if ($newCover) { - $cover = $newCover; - - $newCoverId = Slim::Schema::Track->generateCoverId({ - cover => $newCover, - url => $url, - }); - } - } - } + my @tracks = @{ + $dbh->selectall_arrayref($tracks_sth, { Slice => {} }, @params) + }; - if ( $newCoverId && ($coverid || '') ne $newCoverId ) { - # Make sure album.artwork points to this track, as it may not - # be pointing there now because we did not join tracks via the - # artwork column. - if ( ($album_artwork || '') ne $newCoverId ) { - $sth_update_albums->execute( $newCoverId, $albumid ); - } + foreach my $track (@tracks) { - # Update the rest of the tracks on this album - # to use the same coverid and cover_cached status - $sth_update_tracks->execute( $cover, $newCoverId, $albumid ); + my $newCover = Slim::Music::Artwork->findStandaloneArtwork( + { _trackid => $track->{id} }, + {}, + $imageDirUrl + ); - if ( ++$i % 50 == 0 ) { - Slim::Schema->forceCommit; - $t = time + 5; - } + if ( $track->{cover} ne $newCover ) { + my ($newCoverid) = $dbh->selectrow_array($sth_scanned_pics, undef, $newCover); + $sth_update_tracks->execute( $newCover, $newCoverid, $track->{id} ); - Slim::Utils::Scheduler::unpause() if !main::SCANNER; - } - elsif ( $cover =~ /^https?:/ && (!$album_artwork || $album_artwork ne $cover) ) { - $sth_update_albums->execute( $newCoverId, $albumid ); + if ( $previousAlbum ne $track->{albumid} && $newCoverid ne $track->{album_artwork} ) { + $progress->update( $track->{album_title} ); + $sth_update_albums->execute( $newCoverid, $track->{albumid} ); + $log->warn('Artwork has been removed for ' . $track->{album_title}) if !$newCoverid; + } + } if ( ++$i % 50 == 0 ) { Slim::Schema->forceCommit; @@ -418,16 +393,12 @@ sub updateStandaloneArtwork { } Slim::Utils::Scheduler::unpause() if !main::SCANNER; - } - # cover art has disappeared - elsif ( !$newCoverId ) { - $sth_update_albums->execute( undef, $albumid ); - $sth_update_tracks->execute( 0, undef, $albumid ); - $log->warn('Artwork has been removed for ' . $album_title); + $previousAlbum = $track->{albumid}; } return 1; + } $progress->final; @@ -435,6 +406,7 @@ sub updateStandaloneArtwork { $cb && $cb->(); return 0; + }; if ( main::SCANNER ) { @@ -447,6 +419,7 @@ sub updateStandaloneArtwork { # Run async in main process Slim::Utils::Scheduler::add_ordered_task($work); } + } sub getImageContentAndType { @@ -499,6 +472,7 @@ sub generateImageId { if ( $image =~ /^https?/ ) { $mtime = $size = 1; + $args->{url} = $image; # use the image url, not the music file url } elsif ( $image =~ /^\d+$/ ) { # Cache is based on mtime/size of the file containing embedded art @@ -506,8 +480,10 @@ sub generateImageId { $size = $args->{size}; } elsif ( -e $image ) { + # We will no longer get here from the scanner process, as we already got the coverid from the scanned_pics table. # Cache is based on mtime/size of artwork file ($size, $mtime) = (stat _)[7, 9]; + $args->{url} = $image; # use the image path, not the music file url } if ( $mtime && $size ) { diff --git a/Slim/Schema.pm b/Slim/Schema.pm index 0d3ee64cff..63f1e010cd 100644 --- a/Slim/Schema.pm +++ b/Slim/Schema.pm @@ -442,7 +442,7 @@ sub migrateDB { # initialize scanner helper tables Slim::Utils::SQLHelper->executeSQLFile( $driver, $class->storage->dbh, "schema_scanner.sql" - ); + ) if main::SCANNER; ### temporary, so we keep the contents from the last scan for debugging # Migrate to the latest schema version - see SQL/$driver/schema_\d+_up.sql my $dbix = DBIx::Migration->new({ @@ -1682,6 +1682,12 @@ sub _newTrack { return undef; } + my $dbh = $self->dbh; + my $sql_scanned_pics = qq{ + SELECT coverid FROM scanned_pics WHERE path = ? + }; + my $sth_scanned_pics = $dbh->prepare($sql_scanned_pics); + my $dirname = dirname($url); my $deferredAttributes = {}; @@ -1820,9 +1826,12 @@ sub _newTrack { $columnValueHash{cover} = $cover; } +# if ( $columnValueHash{cover} =~ /^https?/ || $columnValueHash{cover} =~ /^\d+$/ ) { ###combine the regex if this works!!! if ( $columnValueHash{cover} ) { # Generate coverid value based on artwork, mtime, filesize - $columnValueHash{coverid} = Slim::Schema::Track->generateCoverId( { + # Get coverid from scanned_pics if possible (ie when running in the scanner) + ($columnValueHash{coverid}) = $dbh->selectrow_array($sth_scanned_pics, undef, $columnValueHash{cover}); + $columnValueHash{coverid} ||= Slim::Schema::Track->generateCoverId( { cover => $columnValueHash{cover}, url => $url, mtime => $columnValueHash{timestamp}, diff --git a/Slim/Utils/Scanner/Local.pm b/Slim/Utils/Scanner/Local.pm index 30ff4d7f64..87a2c8e69f 100644 --- a/Slim/Utils/Scanner/Local.pm +++ b/Slim/Utils/Scanner/Local.pm @@ -64,6 +64,7 @@ sub find { # XXX how best to delete files in non-recursive mode? # Delete the directory itself and all children $dbh->do("DELETE FROM scanned_files WHERE url = '${file}' OR url LIKE '${file}/%'"); + $dbh->do("DELETE FROM scanned_pics WHERE dir LIKE '${path}/%'"); } stat $path; @@ -240,6 +241,20 @@ sub rescan { AND content_type $ctFilter } ); + # add removed artwork to scanned_pics with a status of Deleted + $dbh->do( qq{ + INSERT INTO scanned_pics (path, status) + SELECT DISTINCT(cover), 'D' + FROM tracks + WHERE NOT EXISTS ( + SELECT path FROM scanned_pics + WHERE scanned_pics.path = tracks.cover + ) + AND cover NOT LIKE 'https%' + AND CAST(CAST(cover AS INTEGER) AS TEXT) <> cover + AND url LIKE '$basedir%' + } ); + my $inDBOnlySQL = qq{ SELECT url FROM dbonly diff --git a/Slim/Utils/Scanner/Local/Async.pm b/Slim/Utils/Scanner/Local/Async.pm index 98f18afa6c..8b2d9baada 100644 --- a/Slim/Utils/Scanner/Local/Async.pm +++ b/Slim/Utils/Scanner/Local/Async.pm @@ -13,7 +13,7 @@ package Slim::Utils::Scanner::Local::Async; use strict; -use File::Basename qw(fileparse); +use File::Basename qw(basename dirname); use File::Next; use File::Spec (); use Path::Class (); @@ -46,11 +46,14 @@ sub find { (?, ?, ?) } ); + # Populate enhanced scanned_pics table my $imageSth = $dbh->prepare_cached( qq{ INSERT INTO scanned_pics - (url, timestamp, filesize) + (dir, path, timestamp, filesize, coverid, status) VALUES - (?, ?, ?) + (?, ?, ?, ?, ?, + CASE WHEN (SELECT COUNT(*) FROM tracks WHERE tracks.cover = ?) = 0 THEN 'N' ELSE 'E' END + ) } ); my $types = Slim::Music::Info::validTypeExtensions( ($args->{types} || 'audio') . '|image' ); @@ -169,13 +172,24 @@ sub find { my $size = -d $file ? 0 : ($stat[7] || 0); my ($ext) = $file =~ /\.([^.]+)$/; - my $sth = ($ext && Slim::Music::Info::isImage($file, lc($ext))) ? $imageSth : $audioSth; - $sth->execute( - Slim::Utils::Misc::fileURLFromPath($file), - $mtime, - $size, - ); + if ( $ext && Slim::Music::Info::isImage($file, lc($ext)) ) { + $imageSth->execute( + dirname($file), + $file, + $mtime, + $size, + substr( safe_md5_hex( $file . $mtime . $size ), 0, 8 ), + $file + ); + } + else { + $audioSth->execute( + Slim::Utils::Misc::fileURLFromPath($file), + $mtime, + $size + ); + } return 1; }; From af8169b62d13df909f91a4c30fcc71558ddc26d3 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Sun, 16 Aug 2026 15:09:45 +0100 Subject: [PATCH 2/7] some fixes Signed-off-by: darrell-k --- SQL/SQLite/schema_scanner.sql | 7 ++++++- SQL/mysql/schema_scanner.sql | 7 ++++++- Slim/Music/Artwork.pm | 7 ++++++- Slim/Utils/Scanner/Local.pm | 2 +- Slim/Utils/Scanner/Local/Async.pm | 9 +++++---- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/SQL/SQLite/schema_scanner.sql b/SQL/SQLite/schema_scanner.sql index d7ade89cd5..cfa7f649ba 100644 --- a/SQL/SQLite/schema_scanner.sql +++ b/SQL/SQLite/schema_scanner.sql @@ -13,7 +13,12 @@ CREATE TABLE scanned_pics ( timestamp int(10), filesize int(10), coverid char(8), - status char(1) + status char(1) CHECK (status IN ('D', 'E', 'N')) + -- D = image deleted (ie used in the database (tracks.cover) but no longer existing on disk) + -- E = an existing image present in tracks.cover + -- N = a new image (eg on disk but not used in tracks.cover - might actually have been on disk before, but passed over in a previous scan) + -- NULL = we'll set status 'E' to NULL if after n&c music files have been processed the image is still being used. + -- This improves performance as we'll only process the tracks if there's also a N(ew) image ); CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); diff --git a/SQL/mysql/schema_scanner.sql b/SQL/mysql/schema_scanner.sql index d7ade89cd5..cfa7f649ba 100644 --- a/SQL/mysql/schema_scanner.sql +++ b/SQL/mysql/schema_scanner.sql @@ -13,7 +13,12 @@ CREATE TABLE scanned_pics ( timestamp int(10), filesize int(10), coverid char(8), - status char(1) + status char(1) CHECK (status IN ('D', 'E', 'N')) + -- D = image deleted (ie used in the database (tracks.cover) but no longer existing on disk) + -- E = an existing image present in tracks.cover + -- N = a new image (eg on disk but not used in tracks.cover - might actually have been on disk before, but passed over in a previous scan) + -- NULL = we'll set status 'E' to NULL if after n&c music files have been processed the image is still being used. + -- This improves performance as we'll only process the tracks if there's also a N(ew) image ); CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index d3b9536425..7a9e033e4d 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -487,12 +487,17 @@ sub generateImageId { } if ( $mtime && $size ) { - $imageId = substr( safe_md5_hex( $args->{url} . $mtime . $size ), 0, 8 ); + $imageId = $class->calculateCoverId($args->{url}, $mtime, $size); } return $imageId; } +sub calculateCoverId { + my ( $class, $file, $mtime, $size ) = @_; + + return substr( safe_md5_hex( $file . $mtime . $size ), 0, 8 ); +} # Private class methods sub _imageContentType { diff --git a/Slim/Utils/Scanner/Local.pm b/Slim/Utils/Scanner/Local.pm index 87a2c8e69f..c6e349491e 100644 --- a/Slim/Utils/Scanner/Local.pm +++ b/Slim/Utils/Scanner/Local.pm @@ -252,7 +252,7 @@ sub rescan { ) AND cover NOT LIKE 'https%' AND CAST(CAST(cover AS INTEGER) AS TEXT) <> cover - AND url LIKE '$basedir%' + AND url LIKE '$basedir%' } ); my $inDBOnlySQL = qq{ diff --git a/Slim/Utils/Scanner/Local/Async.pm b/Slim/Utils/Scanner/Local/Async.pm index 8b2d9baada..a10d2d4733 100644 --- a/Slim/Utils/Scanner/Local/Async.pm +++ b/Slim/Utils/Scanner/Local/Async.pm @@ -46,13 +46,13 @@ sub find { (?, ?, ?) } ); - # Populate enhanced scanned_pics table + # Populate enhanced scanned_pics table (status E = already exists in the tracks table, status N = new) my $imageSth = $dbh->prepare_cached( qq{ INSERT INTO scanned_pics (dir, path, timestamp, filesize, coverid, status) VALUES (?, ?, ?, ?, ?, - CASE WHEN (SELECT COUNT(*) FROM tracks WHERE tracks.cover = ?) = 0 THEN 'N' ELSE 'E' END + CASE WHEN EXISTS (SELECT 1 FROM tracks WHERE tracks.coverid = ?) THEN 'E' ELSE 'N' END ) } ); @@ -174,13 +174,14 @@ sub find { my ($ext) = $file =~ /\.([^.]+)$/; if ( $ext && Slim::Music::Info::isImage($file, lc($ext)) ) { + my $coverid = Slim::Music::Artwork->calculateCoverId($file, $mtime, $size); $imageSth->execute( dirname($file), $file, $mtime, $size, - substr( safe_md5_hex( $file . $mtime . $size ), 0, 8 ), - $file + $coverid, + $coverid ); } else { From 4da574ef3d256ac970f2baeb026895dd28f535b8 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Mon, 17 Aug 2026 15:44:56 +0100 Subject: [PATCH 3/7] rename scanned_pics columns Signed-off-by: darrell-k --- SQL/SQLite/schema_scanner.sql | 8 ++++---- SQL/mysql/schema_scanner.sql | 8 ++++---- Slim/Music/Artwork.pm | 12 ++++++------ Slim/Schema.pm | 2 +- Slim/Utils/Scanner/Local.pm | 8 ++++---- Slim/Utils/Scanner/Local/Async.pm | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/SQL/SQLite/schema_scanner.sql b/SQL/SQLite/schema_scanner.sql index cfa7f649ba..affdc71915 100644 --- a/SQL/SQLite/schema_scanner.sql +++ b/SQL/SQLite/schema_scanner.sql @@ -8,8 +8,8 @@ CREATE INDEX scannedUrlIndex ON scanned_files (url); DROP TABLE IF EXISTS scanned_pics; CREATE TABLE scanned_pics ( - dir text, - path text NOT NULL, + folder text, + full_path text NOT NULL, timestamp int(10), filesize int(10), coverid char(8), @@ -20,8 +20,8 @@ CREATE TABLE scanned_pics ( -- NULL = we'll set status 'E' to NULL if after n&c music files have been processed the image is still being used. -- This improves performance as we'll only process the tracks if there's also a N(ew) image ); -CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); -CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); +CREATE INDEX scannedPicUrlIndex ON scanned_pics (full_path); +CREATE INDEX scannedPicDirIndex ON scanned_pics (folder); create index scannedPicStatusidx on scanned_pics(status); CREATE INDEX IF NOT EXISTS trackscoveridx ON tracks(cover); diff --git a/SQL/mysql/schema_scanner.sql b/SQL/mysql/schema_scanner.sql index cfa7f649ba..affdc71915 100644 --- a/SQL/mysql/schema_scanner.sql +++ b/SQL/mysql/schema_scanner.sql @@ -8,8 +8,8 @@ CREATE INDEX scannedUrlIndex ON scanned_files (url); DROP TABLE IF EXISTS scanned_pics; CREATE TABLE scanned_pics ( - dir text, - path text NOT NULL, + folder text, + full_path text NOT NULL, timestamp int(10), filesize int(10), coverid char(8), @@ -20,8 +20,8 @@ CREATE TABLE scanned_pics ( -- NULL = we'll set status 'E' to NULL if after n&c music files have been processed the image is still being used. -- This improves performance as we'll only process the tracks if there's also a N(ew) image ); -CREATE INDEX scannedPicUrlIndex ON scanned_pics (path); -CREATE INDEX scannedPicDirIndex ON scanned_pics (dir); +CREATE INDEX scannedPicUrlIndex ON scanned_pics (full_path); +CREATE INDEX scannedPicDirIndex ON scanned_pics (folder); create index scannedPicStatusidx on scanned_pics(status); CREATE INDEX IF NOT EXISTS trackscoveridx ON tracks(cover); diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 7a9e033e4d..f4b7e73827 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -203,12 +203,12 @@ sub _findStandaloneArtwork { my @images; if (main::SCANNER) { - my $sql = "SELECT path FROM scanned_pics WHERE (status IS NULL OR status <> 'D') AND "; + my $sql = "SELECT full_path FROM scanned_pics WHERE (status IS NULL OR status <> 'D') AND "; if (scalar @candidates) { - $sql .= sprintf('path IN (%s)', join(',', map { '?' } @candidates)); + $sql .= sprintf('full_path IN (%s)', join(',', map { '?' } @candidates)); } else { - $sql .= 'dir = ?'; + $sql .= 'folder = ?'; push @candidates, $parentDir; } @@ -288,10 +288,10 @@ sub updateStandaloneArtwork { Slim::Schema->forceCommit; my $sql_scanned_pics = qq{ - SELECT path, coverid, GROUP_CONCAT(status) + SELECT full_path, coverid, GROUP_CONCAT(status) FROM scanned_pics WHERE status IS NOT NULL - GROUP BY path, coverid + GROUP BY full_path, coverid }; my ($count) = $dbh->selectrow_array( qq{ @@ -331,7 +331,7 @@ sub updateStandaloneArtwork { my $tracks_sth = $dbh->prepare($sql_tracks); my $sth_scanned_pics = $dbh->prepare( qq{ - SELECT coverid FROM scanned_pics WHERE path = ? + SELECT coverid FROM scanned_pics WHERE full_path = ? } ); my $sth_update_tracks = $dbh->prepare( qq{ diff --git a/Slim/Schema.pm b/Slim/Schema.pm index 63f1e010cd..d838eef82b 100644 --- a/Slim/Schema.pm +++ b/Slim/Schema.pm @@ -1684,7 +1684,7 @@ sub _newTrack { my $dbh = $self->dbh; my $sql_scanned_pics = qq{ - SELECT coverid FROM scanned_pics WHERE path = ? + SELECT coverid FROM scanned_pics WHERE full_path = ? }; my $sth_scanned_pics = $dbh->prepare($sql_scanned_pics); diff --git a/Slim/Utils/Scanner/Local.pm b/Slim/Utils/Scanner/Local.pm index c6e349491e..6871bbc271 100644 --- a/Slim/Utils/Scanner/Local.pm +++ b/Slim/Utils/Scanner/Local.pm @@ -64,7 +64,7 @@ sub find { # XXX how best to delete files in non-recursive mode? # Delete the directory itself and all children $dbh->do("DELETE FROM scanned_files WHERE url = '${file}' OR url LIKE '${file}/%'"); - $dbh->do("DELETE FROM scanned_pics WHERE dir LIKE '${path}/%'"); + $dbh->do("DELETE FROM scanned_pics WHERE folder LIKE '${path}%'"); } stat $path; @@ -243,12 +243,12 @@ sub rescan { # add removed artwork to scanned_pics with a status of Deleted $dbh->do( qq{ - INSERT INTO scanned_pics (path, status) + INSERT INTO scanned_pics (full_path, status) SELECT DISTINCT(cover), 'D' FROM tracks WHERE NOT EXISTS ( - SELECT path FROM scanned_pics - WHERE scanned_pics.path = tracks.cover + SELECT 1 FROM scanned_pics + WHERE scanned_pics.full_path = tracks.cover ) AND cover NOT LIKE 'https%' AND CAST(CAST(cover AS INTEGER) AS TEXT) <> cover diff --git a/Slim/Utils/Scanner/Local/Async.pm b/Slim/Utils/Scanner/Local/Async.pm index a10d2d4733..fe83d4823d 100644 --- a/Slim/Utils/Scanner/Local/Async.pm +++ b/Slim/Utils/Scanner/Local/Async.pm @@ -49,7 +49,7 @@ sub find { # Populate enhanced scanned_pics table (status E = already exists in the tracks table, status N = new) my $imageSth = $dbh->prepare_cached( qq{ INSERT INTO scanned_pics - (dir, path, timestamp, filesize, coverid, status) + (folder, full_path, timestamp, filesize, coverid, status) VALUES (?, ?, ?, ?, ?, CASE WHEN EXISTS (SELECT 1 FROM tracks WHERE tracks.coverid = ?) THEN 'E' ELSE 'N' END From 6a5a6bd78c6c69da9fb93162c697e413a1ee19d1 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Tue, 18 Aug 2026 17:31:12 +0100 Subject: [PATCH 4/7] add MySql support Signed-off-by: darrell-k --- Slim/Music/Artwork.pm | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index f4b7e73827..45272d804c 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -38,6 +38,7 @@ use Slim::Utils::Unicode; use Slim::Utils::OSDetect; use constant MAX_RETRIES => 5; +use constant IS_SQLITE => (Slim::Utils::OSDetect->getOS()->sqlHelperClass() =~ /SQLite/ ? 1 : 0); # Global caches: my $artworkDir = ''; @@ -270,20 +271,28 @@ sub updateStandaloneArtwork { } ); # for online artwork, update album artwork to first track coverid - ### SQLITE ONLY, there's a different syntax for MySql. ### I considered adding rows to scanned_pics for remote images so that they'd be processed in the loop below, but I think this is more efficient. - $dbh->do( qq{ - UPDATE albums - SET artwork = tracks.coverid - FROM tracks, ( - SELECT coverid - FROM tracks - LIMIT 1 - ) - WHERE tracks.album = albums.id - AND tracks.cover LIKE 'https%' - AND tracks.coverid <> albums.artwork - } ); + #there's a different syntax for MySql. + my $sql = IS_SQLITE + ? qq{ + UPDATE albums + SET artwork = tracks.coverid + FROM tracks, ( + SELECT coverid + FROM tracks + LIMIT 1 + ) + WHERE tracks.album = albums.id + AND tracks.cover LIKE 'https%' + AND (tracks.coverid <> albums.artwork OR albums.artwork IS NULL) + } + : qq{ + UPDATE albums JOIN tracks ON albums.id = tracks.album + SET albums.artwork = tracks.coverid + WHERE tracks.cover LIKE 'https%' + AND (albums.artwork IS NULL OR tracks.coverid <> albums.artwork); + }; + $dbh->do( $sql ); Slim::Schema->forceCommit; From 7c62c4db72d2e19abd80c61a2061fecc285533f3 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Tue, 18 Aug 2026 19:45:40 +0100 Subject: [PATCH 5/7] simplify SQLITE online artwork update, I misunderstood the example code I used Signed-off-by: darrell-k --- Slim/Music/Artwork.pm | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 45272d804c..77ef01e4fc 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -277,11 +277,7 @@ sub updateStandaloneArtwork { ? qq{ UPDATE albums SET artwork = tracks.coverid - FROM tracks, ( - SELECT coverid - FROM tracks - LIMIT 1 - ) + FROM tracks WHERE tracks.album = albums.id AND tracks.cover LIKE 'https%' AND (tracks.coverid <> albums.artwork OR albums.artwork IS NULL) From faafb9f9d3b1a5d10ee1f4ff652340f411f297f1 Mon Sep 17 00:00:00 2001 From: darrell-k Date: Wed, 19 Aug 2026 18:59:44 +0100 Subject: [PATCH 6/7] fix addition & removal of embedded artwork Signed-off-by: darrell-k --- Slim/Music/Artwork.pm | 67 +++++++++++++++++++------------------------ Slim/Schema.pm | 23 ++++++++------- 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 77ef01e4fc..9b5d5f42d5 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -270,28 +270,6 @@ sub updateStandaloneArtwork { AND EXISTS (SELECT * FROM tracks WHERE tracks.coverid = scanned_pics.coverid) } ); - # for online artwork, update album artwork to first track coverid - ### I considered adding rows to scanned_pics for remote images so that they'd be processed in the loop below, but I think this is more efficient. - #there's a different syntax for MySql. - my $sql = IS_SQLITE - ? qq{ - UPDATE albums - SET artwork = tracks.coverid - FROM tracks - WHERE tracks.album = albums.id - AND tracks.cover LIKE 'https%' - AND (tracks.coverid <> albums.artwork OR albums.artwork IS NULL) - } - : qq{ - UPDATE albums JOIN tracks ON albums.id = tracks.album - SET albums.artwork = tracks.coverid - WHERE tracks.cover LIKE 'https%' - AND (albums.artwork IS NULL OR tracks.coverid <> albums.artwork); - }; - $dbh->do( $sql ); - - Slim::Schema->forceCommit; - my $sql_scanned_pics = qq{ SELECT full_path, coverid, GROUP_CONCAT(status) FROM scanned_pics @@ -331,6 +309,7 @@ sub updateStandaloneArtwork { FROM tracks JOIN albums ON albums.id = tracks.album WHERE url BETWEEN ? AND ? AND instr(substr(url, length(?)+2), "/") < 1 + AND (cover IS NULL OR cover = '0' OR CAST(CAST(cover AS INTEGER) AS TEXT) <> cover) ORDER BY albums.id }; my $tracks_sth = $dbh->prepare($sql_tracks); @@ -345,14 +324,6 @@ sub updateStandaloneArtwork { WHERE id = ? } ); - my $sth_update_albums = $dbh->prepare( qq{ - UPDATE albums - SET artwork = ? - WHERE id = ? - } ); - - my $previousAlbum = undef; - my $i = 0; my $t = 0; @@ -384,12 +355,6 @@ sub updateStandaloneArtwork { if ( $track->{cover} ne $newCover ) { my ($newCoverid) = $dbh->selectrow_array($sth_scanned_pics, undef, $newCover); $sth_update_tracks->execute( $newCover, $newCoverid, $track->{id} ); - - if ( $previousAlbum ne $track->{albumid} && $newCoverid ne $track->{album_artwork} ) { - $progress->update( $track->{album_title} ); - $sth_update_albums->execute( $newCoverid, $track->{albumid} ); - $log->warn('Artwork has been removed for ' . $track->{album_title}) if !$newCoverid; - } } if ( ++$i % 50 == 0 ) { @@ -399,13 +364,41 @@ sub updateStandaloneArtwork { Slim::Utils::Scheduler::unpause() if !main::SCANNER; - $previousAlbum = $track->{albumid}; } return 1; } + # update album artwork to first track coverid + ### I considered adding rows to scanned_pics for remote images so that they'd be processed in the loop above, but I think this is more efficient. + #there's a different syntax for MySql. + my $sql = IS_SQLITE + ? qq{ + UPDATE albums + SET artwork = tracks.coverid + FROM tracks + WHERE tracks.album = albums.id + AND ( + tracks.coverid IS NULL AND albums.artwork IS NOT NULL + OR tracks.coverid IS NOT NULL AND albums.artwork IS NULL + OR tracks.coverid <> albums.artwork + ) + } + : qq{ + UPDATE albums JOIN tracks ON albums.id = tracks.album + SET albums.artwork = tracks.coverid + WHERE ( + tracks.coverid IS NULL AND albums.artwork IS NOT NULL + OR tracks.coverid IS NOT NULL AND albums.artwork IS NULL + OR tracks.coverid <> albums.artwork + ) + }; + + $dbh->do( $sql ); + + Slim::Schema->forceCommit; + $progress->final; $cb && $cb->(); diff --git a/Slim/Schema.pm b/Slim/Schema.pm index d838eef82b..cf684fcc67 100644 --- a/Slim/Schema.pm +++ b/Slim/Schema.pm @@ -2021,23 +2021,24 @@ sub updateOrCreateBase { # Update timestamp $attributeHash->{updated_time} = time(); - my $nullableColumns = { - performance => 1, - grouping => 1, - discsubtitle => 1, - musicbrainz_id => 1, + my $defaultCols = { + performance => '', + grouping => '', + discsubtitle => '', + musicbrainz_id => '', + cover => 0, }; - # Some taggers will not supply blank tags so create the attributes for columns which need to be nulled. - foreach my $col (keys %$nullableColumns) { - $attributeHash->{uc($col)} = undef if !exists $attributeHash->{uc($col)}; + # Some taggers will not supply blank tags so create the attributes for columns which need to be defaulted. + foreach my $col (keys %$defaultCols) { + $attributeHash->{uc($col)} = $defaultCols->{$col} if !exists $attributeHash->{uc($col)}; } while (my ($key, $val) = each %$attributeHash) { $key = lc($key); - # Some columns should be set to null if no value passed in (may have had a value before this scan) - if ( (defined $val && $val ne '' || $nullableColumns->{$key}) && exists $trackAttrs->{$key} ) { + # Some columns should be set to a default, defined in $defaultCols above, if no value passed in (may have had a value before this scan) + if ( (defined $val && $val ne '' || exists $defaultCols->{$key}) && exists $trackAttrs->{$key} ) { # Bug 7731, filter out duplicate keys that end up as array refs # https://github.com/LMS-Community/slimserver/issues/1378 @@ -2049,7 +2050,7 @@ sub updateOrCreateBase { } # Metadata is only included if it contains a non zero value - if ( main::STATISTICS && ($val || $nullableColumns->{$key}) && blessed($trackPersistent) && exists $trackPersistentAttrs->{$key} ) { + if ( main::STATISTICS && ($val || exists $defaultCols->{$key}) && blessed($trackPersistent) && exists $trackPersistentAttrs->{$key} ) { main::INFOLOG && $log->is_info && $log->info("Updating persistent $url : $key to $val"); From d6932bd53ae05b83909bfd87ac259e389ec7005a Mon Sep 17 00:00:00 2001 From: darrell-k Date: Sun, 23 Aug 2026 00:12:45 +0100 Subject: [PATCH 7/7] Some rework Signed-off-by: darrell-k --- Slim/Music/Artwork.pm | 85 +++++++++++++++++++++++-------------------- Slim/Schema.pm | 8 ++-- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/Slim/Music/Artwork.pm b/Slim/Music/Artwork.pm index 9b5d5f42d5..0f08303d1e 100644 --- a/Slim/Music/Artwork.pm +++ b/Slim/Music/Artwork.pm @@ -263,18 +263,34 @@ sub updateStandaloneArtwork { my $dbh = Slim::Schema->dbh; - # unflag existing unchanged artwork + # update album artwork to first track coverid for remote and embedded images. + ### I considered adding rows to scanned_pics for these images so that they'd be processed in the loop below, but I think this is more efficient. + #there's a different syntax for MySql. $dbh->do( qq{ - UPDATE scanned_pics SET status = NULL - WHERE status = 'E' - AND EXISTS (SELECT * FROM tracks WHERE tracks.coverid = scanned_pics.coverid) - } ); + UPDATE albums + SET artwork = tracks.coverid + FROM tracks + WHERE tracks.album = albums.id + AND ( tracks.cover IS NULL OR CAST(CAST(tracks.cover AS INTEGER) AS TEXT) = tracks.cover OR tracks.cover LIKE 'https%' ) + AND ( tracks.coverid <> albums.artwork OR albums.artwork IS NULL ) + } ); + + # unflag existing unchanged artwork unless we have a changed coverart pref (then we need to process all artwork) + my $prefs = preferences('server'); + my $processAllArtwork = $prefs->get('coverArt') && Slim::Music::Import->lastScanTime('lastUpdateStandaloneArtwork') < $prefs->get('_ts_coverArt'); + $dbh->do( qq{ + UPDATE scanned_pics SET status = NULL + WHERE status = 'E' + AND EXISTS (SELECT * FROM tracks WHERE tracks.coverid = scanned_pics.coverid) + } ) unless $processAllArtwork; + + Slim::Schema->forceCommit; my $sql_scanned_pics = qq{ - SELECT full_path, coverid, GROUP_CONCAT(status) + SELECT folder FROM scanned_pics WHERE status IS NOT NULL - GROUP BY full_path, coverid + GROUP BY folder }; my ($count) = $dbh->selectrow_array( qq{ @@ -297,8 +313,8 @@ sub updateStandaloneArtwork { } ); my $pic_sth = $dbh->prepare($sql_scanned_pics); - my ($picPath, $picCoverid, $status); - $pic_sth->bind_columns(\$picPath, \$picCoverid, \$status); + my $folder; + $pic_sth->bind_columns(\$folder); my $sql_tracks = qq{ SELECT tracks.id, tracks.url, @@ -319,11 +335,19 @@ sub updateStandaloneArtwork { } ); my $sth_update_tracks = $dbh->prepare( qq{ - UPDATE tracks - SET cover = ?, coverid = ?, cover_cached = NULL - WHERE id = ? + UPDATE tracks + SET cover = ?, coverid = ?, cover_cached = NULL + WHERE id = ? + } ); + + my $sth_update_albums = $dbh->prepare( qq{ + UPDATE albums + SET artwork = ? + WHERE id = ? } ); + my $previousAlbum = undef; + my $i = 0; my $t = 0; @@ -337,7 +361,7 @@ sub updateStandaloneArtwork { $t = time + 5; } - my $imageDirUrl = Slim::Utils::Misc::fileURLFromPath(dirname($picPath)); + my $imageDirUrl = Slim::Utils::Misc::fileURLFromPath($folder); my @params = ($imageDirUrl, $imageDirUrl . chr(0xff), $imageDirUrl); my @tracks = @{ @@ -355,6 +379,12 @@ sub updateStandaloneArtwork { if ( $track->{cover} ne $newCover ) { my ($newCoverid) = $dbh->selectrow_array($sth_scanned_pics, undef, $newCover); $sth_update_tracks->execute( $newCover, $newCoverid, $track->{id} ); + + if ( $previousAlbum ne $track->{albumid} && $newCoverid ne $track->{album_artwork} ) { + $progress->update( $track->{album_title} ); + $sth_update_albums->execute( $newCoverid, $track->{albumid} ); + $log->warn('Artwork has been removed for ' . $track->{album_title}) if !$newCoverid; + } } if ( ++$i % 50 == 0 ) { @@ -364,39 +394,13 @@ sub updateStandaloneArtwork { Slim::Utils::Scheduler::unpause() if !main::SCANNER; + $previousAlbum = $track->{albumid}; } return 1; } - # update album artwork to first track coverid - ### I considered adding rows to scanned_pics for remote images so that they'd be processed in the loop above, but I think this is more efficient. - #there's a different syntax for MySql. - my $sql = IS_SQLITE - ? qq{ - UPDATE albums - SET artwork = tracks.coverid - FROM tracks - WHERE tracks.album = albums.id - AND ( - tracks.coverid IS NULL AND albums.artwork IS NOT NULL - OR tracks.coverid IS NOT NULL AND albums.artwork IS NULL - OR tracks.coverid <> albums.artwork - ) - } - : qq{ - UPDATE albums JOIN tracks ON albums.id = tracks.album - SET albums.artwork = tracks.coverid - WHERE ( - tracks.coverid IS NULL AND albums.artwork IS NOT NULL - OR tracks.coverid IS NOT NULL AND albums.artwork IS NULL - OR tracks.coverid <> albums.artwork - ) - }; - - $dbh->do( $sql ); - Slim::Schema->forceCommit; $progress->final; @@ -411,6 +415,7 @@ sub updateStandaloneArtwork { # Non-async mode in scanner while ( $work->() ) { } + Slim::Music::Import->setLastScanTime('lastUpdateStandaloneArtwork'); Slim::Music::Import->endImporter('updateStandaloneArtwork'); } else { diff --git a/Slim/Schema.pm b/Slim/Schema.pm index cf684fcc67..22830ad64e 100644 --- a/Slim/Schema.pm +++ b/Slim/Schema.pm @@ -2022,10 +2022,10 @@ sub updateOrCreateBase { $attributeHash->{updated_time} = time(); my $defaultCols = { - performance => '', - grouping => '', - discsubtitle => '', - musicbrainz_id => '', + performance => undef, + grouping => undef, + discsubtitle => undef, + musicbrainz_id => undef, cover => 0, }; # Some taggers will not supply blank tags so create the attributes for columns which need to be defaulted.