From 437d8ce94715e343bb2b4cc775299b3e8b86b997 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Thu, 12 Sep 2019 22:26:29 -0600 Subject: [PATCH] Move index meta information Signed-off-by: Andrew DeMaria --- .../player/controller/LeftController.java | 5 +- .../MusicFolderSettingsController.java | 16 ++-- .../player/domain/MediaLibraryStatistics.java | 88 +++++++++++-------- .../player/service/MediaScannerService.java | 54 +++++------- .../player/service/SettingsService.java | 25 +----- .../player/service/search/IndexManager.java | 66 +++++++++++--- .../upnp/FolderBasedContentDirectory.java | 5 +- .../service/upnp/RootUpnpProcessor.java | 8 +- .../java/org/airsonic/player/util/Util.java | 41 ++++++++- .../service/MediaScannerServiceTestCase.java | 6 ++ .../service/MediaScannerServiceUnitTest.java | 31 +++++++ .../service/search/IndexManagerTestCase.java | 2 +- .../org/airsonic/player/util/UtilTest.java | 88 +++++++++++++++++++ 13 files changed, 313 insertions(+), 122 deletions(-) create mode 100644 airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java create mode 100644 airsonic-main/src/test/java/org/airsonic/player/util/UtilTest.java diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/LeftController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/LeftController.java index 79c67ee7..e58fbadd 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/LeftController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/LeftController.java @@ -21,6 +21,7 @@ package org.airsonic.player.controller; import org.airsonic.player.domain.*; import org.airsonic.player.service.*; +import org.airsonic.player.service.search.IndexManager; import org.airsonic.player.util.FileUtil; import org.airsonic.player.util.StringUtil; import org.springframework.beans.factory.annotation.Autowired; @@ -56,6 +57,8 @@ public class LeftController { @Autowired private MediaScannerService mediaScannerService; @Autowired + private IndexManager indexManager; + @Autowired private SettingsService settingsService; @Autowired private SecurityService securityService; @@ -116,7 +119,7 @@ public class LeftController { boolean musicFolderChanged = saveSelectedMusicFolder(request); Map map = new HashMap<>(); - MediaLibraryStatistics statistics = mediaScannerService.getStatistics(); + MediaLibraryStatistics statistics = indexManager.getStatistics(); Locale locale = RequestContextUtils.getLocale(request); boolean refresh = ServletRequestUtils.getBooleanParameter(request, "refresh", false); diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java index ec675ab6..7182f133 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java @@ -23,6 +23,7 @@ import org.airsonic.player.command.MusicFolderSettingsCommand; import org.airsonic.player.dao.AlbumDao; import org.airsonic.player.dao.ArtistDao; import org.airsonic.player.dao.MediaFileDao; +import org.airsonic.player.domain.MediaLibraryStatistics; import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.service.MediaScannerService; import org.airsonic.player.service.SettingsService; @@ -103,11 +104,16 @@ public class MusicFolderSettingsController { private void expunge() { // to be before dao#expunge - LOG.debug("Cleaning search index..."); - indexManager.startIndexing(); - indexManager.expunge(); - indexManager.stopIndexing(); - LOG.debug("Search index cleanup complete."); + MediaLibraryStatistics statistics = indexManager.getStatistics(); + if (statistics != null) { + LOG.debug("Cleaning search index..."); + indexManager.startIndexing(); + indexManager.expunge(); + indexManager.stopIndexing(statistics); + LOG.debug("Search index cleanup complete."); + } else { + LOG.warn("Missing index statistics - index probably hasn't been created yet. Not expunging index."); + } LOG.debug("Cleaning database..."); LOG.debug("Deleting non-present artists..."); diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/MediaLibraryStatistics.java b/airsonic-main/src/main/java/org/airsonic/player/domain/MediaLibraryStatistics.java index 584e3eb7..ac5b5a5a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/MediaLibraryStatistics.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/MediaLibraryStatistics.java @@ -19,9 +19,10 @@ */ package org.airsonic.player.domain; -import org.airsonic.player.util.StringUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import javax.validation.constraints.NotNull; + +import java.util.Date; +import java.util.Objects; /** * Contains media libaray statistics, including the number of artists, albums and songs. @@ -31,31 +32,37 @@ import org.slf4j.LoggerFactory; */ public class MediaLibraryStatistics { - private static final Logger LOG = LoggerFactory.getLogger(MediaLibraryStatistics.class); + @NotNull + private Integer artistCount; + @NotNull + private Integer albumCount; + @NotNull + private Integer songCount; + @NotNull + private Long totalLengthInBytes; + @NotNull + private Long totalDurationInSeconds; + @NotNull + private Date scanDate; - private int artistCount; - private int albumCount; - private int songCount; - private long totalLengthInBytes; - private long totalDurationInSeconds; + public MediaLibraryStatistics() { - public MediaLibraryStatistics(int artistCount, int albumCount, int songCount, long totalLengthInBytes, long totalDurationInSeconds) { - this.artistCount = artistCount; - this.albumCount = albumCount; - this.songCount = songCount; - this.totalLengthInBytes = totalLengthInBytes; - this.totalDurationInSeconds = totalDurationInSeconds; } - public MediaLibraryStatistics() { + public MediaLibraryStatistics(Date scanDate) { + if (scanDate == null) { + throw new IllegalArgumentException(); + } + this.scanDate = scanDate; + reset(); } - public void reset() { + protected void reset() { artistCount = 0; albumCount = 0; songCount = 0; - totalLengthInBytes = 0; - totalDurationInSeconds = 0; + totalLengthInBytes = 0L; + totalDurationInSeconds = 0L; } public void incrementArtists(int n) { @@ -78,42 +85,45 @@ public class MediaLibraryStatistics { totalDurationInSeconds += n; } - public int getArtistCount() { + public Integer getArtistCount() { return artistCount; } - public int getAlbumCount() { + public Integer getAlbumCount() { return albumCount; } - public int getSongCount() { + public Integer getSongCount() { return songCount; } - public long getTotalLengthInBytes() { + public Long getTotalLengthInBytes() { return totalLengthInBytes; } - public long getTotalDurationInSeconds() { + public Long getTotalDurationInSeconds() { return totalDurationInSeconds; } - public String format() { - return artistCount + " " + albumCount + " " + songCount + " " + totalLengthInBytes + " " + totalDurationInSeconds; + public Date getScanDate() { + return scanDate; } - public static MediaLibraryStatistics parse(String s) { - try { - String[] strings = StringUtil.split(s); - return new MediaLibraryStatistics( - Integer.parseInt(strings[0]), - Integer.parseInt(strings[1]), - Integer.parseInt(strings[2]), - Long.parseLong(strings[3]), - Long.parseLong(strings[4])); - } catch (Exception e) { - LOG.warn("Failed to parse media library statistics: " + s); - return new MediaLibraryStatistics(); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaLibraryStatistics that = (MediaLibraryStatistics) o; + return Objects.equals(artistCount, that.artistCount) && + Objects.equals(albumCount, that.albumCount) && + Objects.equals(songCount, that.songCount) && + Objects.equals(totalLengthInBytes, that.totalLengthInBytes) && + Objects.equals(totalDurationInSeconds, that.totalDurationInSeconds) && + Objects.equals(scanDate, that.scanDate); + } + + @Override + public int hashCode() { + return Objects.hash(artistCount, albumCount, songCount, totalLengthInBytes, totalDurationInSeconds, scanDate); } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java b/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java index e0b94806..00c674be 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java @@ -46,8 +46,6 @@ public class MediaScannerService { private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class); - private MediaLibraryStatistics statistics; - private boolean scanning; private Timer timer; @Autowired @@ -69,13 +67,11 @@ public class MediaScannerService { @PostConstruct public void init() { indexManager.initializeIndexDirectory(); - statistics = settingsService.getMediaLibraryStatistics(); schedule(); } public void initNoSchedule() { indexManager.deleteOldIndexFiles(); - statistics = settingsService.getMediaLibraryStatistics(); } /** @@ -120,12 +116,16 @@ public class MediaScannerService { LOG.info("Automatic media library scanning scheduled to run every " + daysBetween + " day(s), starting at " + firstTime); // In addition, create index immediately if it doesn't exist on disk. - if (settingsService.getLastScanned() == null) { + if (neverScanned()) { LOG.info("Media library never scanned. Doing it now."); scanLibrary(); } } + boolean neverScanned() { + return indexManager.getStatistics() == null; + } + /** * Returns whether the media library is currently being scanned. */ @@ -165,8 +165,9 @@ public class MediaScannerService { private void doScanLibrary() { LOG.info("Starting to scan media library."); - Date lastScanned = DateUtils.truncate(new Date(), Calendar.SECOND); - LOG.debug("New last scan date is " + lastScanned); + MediaLibraryStatistics statistics = new MediaLibraryStatistics( + DateUtils.truncate(new Date(), Calendar.SECOND)); + LOG.debug("New last scan date is " + statistics.getScanDate()); try { @@ -175,7 +176,6 @@ public class MediaScannerService { Genres genres = new Genres(); scanCount = 0; - statistics.reset(); mediaFileService.setMemoryCacheEnabled(false); indexManager.startIndexing(); @@ -185,24 +185,24 @@ public class MediaScannerService { // Recurse through all files on disk. for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); - scanFile(root, musicFolder, lastScanned, albumCount, genres, false); + scanFile(root, musicFolder, statistics, albumCount, genres, false); } // Scan podcast folder. File podcastFolder = new File(settingsService.getPodcastFolder()); if (podcastFolder.exists()) { scanFile(mediaFileService.getMediaFile(podcastFolder), new MusicFolder(podcastFolder, null, true, null), - lastScanned, albumCount, genres, true); + statistics, albumCount, genres, true); } LOG.info("Scanned media library with " + scanCount + " entries."); LOG.info("Marking non-present files."); - mediaFileDao.markNonPresent(lastScanned); + mediaFileDao.markNonPresent(statistics.getScanDate()); LOG.info("Marking non-present artists."); - artistDao.markNonPresent(lastScanned); + artistDao.markNonPresent(statistics.getScanDate()); LOG.info("Marking non-present albums."); - albumDao.markNonPresent(lastScanned); + albumDao.markNonPresent(statistics.getScanDate()); // Update statistics statistics.incrementArtists(albumCount.size()); @@ -213,21 +213,18 @@ public class MediaScannerService { // Update genres mediaFileDao.updateGenres(genres.getGenres()); - settingsService.setMediaLibraryStatistics(statistics); - settingsService.setLastScanned(lastScanned); - settingsService.save(false); LOG.info("Completed media library scan."); } catch (Throwable x) { LOG.error("Failed to scan media library.", x); } finally { mediaFileService.setMemoryCacheEnabled(true); - indexManager.stopIndexing(); + indexManager.stopIndexing(statistics); scanning = false; } } - private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, + private void scanFile(MediaFile file, MusicFolder musicFolder, MediaLibraryStatistics statistics, Map albumCount, Genres genres, boolean isPodcast) { scanCount++; if (scanCount % 250 == 0) { @@ -246,22 +243,22 @@ public class MediaScannerService { if (file.isDirectory()) { for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) { - scanFile(child, musicFolder, lastScanned, albumCount, genres, isPodcast); + scanFile(child, musicFolder, statistics, albumCount, genres, isPodcast); } for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) { - scanFile(child, musicFolder, lastScanned, albumCount, genres, isPodcast); + scanFile(child, musicFolder, statistics, albumCount, genres, isPodcast); } } else { if (!isPodcast) { - updateAlbum(file, musicFolder, lastScanned, albumCount); - updateArtist(file, musicFolder, lastScanned, albumCount); + updateAlbum(file, musicFolder, statistics.getScanDate(), albumCount); + updateArtist(file, musicFolder, statistics.getScanDate(), albumCount); } statistics.incrementSongs(1); } updateGenres(file, genres); - mediaFileDao.markPresent(file.getPath(), lastScanned); - artistDao.markPresent(file.getAlbumArtist(), lastScanned); + mediaFileDao.markPresent(file.getPath(), statistics.getScanDate()); + artistDao.markPresent(file.getAlbumArtist(), statistics.getScanDate()); if (file.getDurationSeconds() != null) { statistics.incrementTotalDurationInSeconds(file.getDurationSeconds()); @@ -373,15 +370,6 @@ public class MediaScannerService { } } - /** - * Returns media library statistics, including the number of artists, albums and songs. - * - * @return Media library statistics. - */ - public MediaLibraryStatistics getStatistics() { - return statistics; - } - public void setSettingsService(SettingsService settingsService) { this.settingsService = settingsService; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java index 6f8fd32f..6c4160ef 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java @@ -96,10 +96,8 @@ public class SettingsService { private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing"; private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled"; private static final String KEY_SETTINGS_CHANGED = "SettingsChanged"; - private static final String KEY_LAST_SCANNED = "LastScanned"; private static final String KEY_ORGANIZE_BY_FOLDER_STRUCTURE = "OrganizeByFolderStructure"; private static final String KEY_SORT_ALBUMS_BY_YEAR = "SortAlbumsByYear"; - private static final String KEY_MEDIA_LIBRARY_STATISTICS = "MediaLibraryStatistics"; private static final String KEY_DLNA_ENABLED = "DlnaEnabled"; private static final String KEY_DLNA_SERVER_NAME = "DlnaServerName"; private static final String KEY_DLNA_BASE_LAN_URL = "DlnaBaseLANURL"; @@ -182,7 +180,6 @@ public class SettingsService { private static final long DEFAULT_SETTINGS_CHANGED = 0L; private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true; private static final boolean DEFAULT_SORT_ALBUMS_BY_YEAR = true; - private static final String DEFAULT_MEDIA_LIBRARY_STATISTICS = "0 0 0 0 0"; private static final boolean DEFAULT_DLNA_ENABLED = false; private static final String DEFAULT_DLNA_SERVER_NAME = "Airsonic"; private static final String DEFAULT_DLNA_BASE_LAN_URL = null; @@ -220,6 +217,7 @@ public class SettingsService { "CoverArtFileTypes", "UrlRedirectCustomHost", "CoverArtLimit", "StreamPort", "PortForwardingEnabled", "RewriteUrl", "UrlRedirectCustomUrl", "UrlRedirectContextPath", "UrlRedirectFrom", "UrlRedirectionEnabled", "UrlRedirectType", "Port", "HttpsPort", + "MediaLibraryStatistics", "LastScanned", // Database settings renamed "database.varchar.maxlength", "database.config.type", "database.config.embed.driver", "database.config.embed.url", "database.config.embed.username", "database.config.embed.password", @@ -726,19 +724,6 @@ public class SettingsService { return getLong(KEY_SETTINGS_CHANGED, DEFAULT_SETTINGS_CHANGED); } - public Date getLastScanned() { - String lastScanned = getProperty(KEY_LAST_SCANNED, null); - return lastScanned == null ? null : new Date(Long.parseLong(lastScanned)); - } - - void setLastScanned(Date date) { - if (date == null) { - setProperty(KEY_LAST_SCANNED, null); - } else { - setLong(KEY_LAST_SCANNED, date.getTime()); - } - } - public boolean isOrganizeByFolderStructure() { return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE); } @@ -787,14 +772,6 @@ public class SettingsService { return excludePattern; } - public MediaLibraryStatistics getMediaLibraryStatistics() { - return MediaLibraryStatistics.parse(getString(KEY_MEDIA_LIBRARY_STATISTICS, DEFAULT_MEDIA_LIBRARY_STATISTICS)); - } - - void setMediaLibraryStatistics(MediaLibraryStatistics statistics) { - setString(KEY_MEDIA_LIBRARY_STATISTICS, statistics.format()); - } - /** * Returns whether we are running in Development mode. * diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/search/IndexManager.java b/airsonic-main/src/main/java/org/airsonic/player/service/search/IndexManager.java index 6f47ec30..b3bfc8b3 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/search/IndexManager.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/search/IndexManager.java @@ -23,17 +23,13 @@ package org.airsonic.player.service.search; import org.airsonic.player.dao.AlbumDao; import org.airsonic.player.dao.ArtistDao; import org.airsonic.player.dao.MediaFileDao; -import org.airsonic.player.domain.Album; -import org.airsonic.player.domain.Artist; -import org.airsonic.player.domain.MediaFile; -import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.domain.*; import org.airsonic.player.service.SettingsService; import org.airsonic.player.util.FileUtil; +import org.airsonic.player.util.Util; import org.apache.commons.io.FileUtils; import org.apache.lucene.document.Document; -import org.apache.lucene.index.IndexWriter; -import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.index.Term; +import org.apache.lucene.index.*; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.store.FSDirectory; @@ -47,6 +43,8 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -83,6 +81,8 @@ public class IndexManager { */ private static final String INDEX_ROOT_DIR_NAME = "index"; + private static final String MEDIA_STATISTICS_KEY = "stats"; + /** * File supplier for index directory. */ @@ -221,26 +221,29 @@ public class IndexManager { * Close Writer of all indexes and update SearcherManager. * Called at the end of the Scan flow. */ - public void stopIndexing() { - Arrays.asList(IndexType.values()).forEach(this::stopIndexing); + public void stopIndexing(MediaLibraryStatistics statistics) { + Arrays.asList(IndexType.values()).forEach(indexType -> stopIndexing(indexType, statistics)); } /** * Close Writer of specified index and refresh SearcherManager. */ - private void stopIndexing(IndexType type) { + private void stopIndexing(IndexType type, MediaLibraryStatistics statistics) { boolean isUpdate = false; // close + IndexWriter indexWriter = writers.get(type); try { - isUpdate = -1 != writers.get(type).commit(); - writers.get(type).close(); + Map userData = Util.objectToStringMap(statistics); + indexWriter.setLiveCommitData(userData.entrySet()); + isUpdate = -1 != indexWriter.commit(); + indexWriter.close(); writers.remove(type); LOG.trace("Success to create or update search index : [" + type + "]"); } catch (IOException e) { LOG.error("Failed to create search index.", e); } finally { - FileUtil.closeQuietly(writers.get(type)); + FileUtil.closeQuietly(indexWriter); } // refresh reader as index may have been written @@ -256,6 +259,43 @@ public class IndexManager { } + /** + * Return the MediaLibraryStatistics saved on commit in the index. Ensures that each index reports the same data. + * On invalid indices, returns null. + */ + public @Nullable MediaLibraryStatistics getStatistics() { + MediaLibraryStatistics stats = null; + for (IndexType indexType : IndexType.values()) { + IndexSearcher searcher = getSearcher(indexType); + if (searcher == null) { + LOG.trace("No index for type " + indexType); + return null; + } + IndexReader indexReader = searcher.getIndexReader(); + if (!(indexReader instanceof DirectoryReader)) { + LOG.warn("Unexpected index type " + indexReader.getClass()); + return null; + } + try { + Map userData = ((DirectoryReader) indexReader).getIndexCommit().getUserData(); + MediaLibraryStatistics currentStats = Util.stringMapToValidObject(MediaLibraryStatistics.class, + userData); + if (stats == null) { + stats = currentStats; + } else { + if (!Objects.equals(stats, currentStats)) { + LOG.warn("Index type " + indexType + " had differing stats data"); + return null; + } + } + } catch (IOException | IllegalArgumentException e) { + LOG.warn("Exception encountered while fetching index commit data", e); + return null; + } + } + return stats; + } + /** * Return the IndexSearcher of the specified index. * At initial startup, it may return null diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java index 4b72a474..2090358e 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java @@ -22,6 +22,7 @@ package org.airsonic.player.service.upnp; import org.airsonic.player.domain.*; import org.airsonic.player.service.MediaFileService; import org.airsonic.player.service.PlaylistService; +import org.airsonic.player.service.search.IndexManager; import org.airsonic.player.util.Util; import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; @@ -57,6 +58,8 @@ public class FolderBasedContentDirectory extends CustomContentDirectory { private MediaFileService mediaFileService; @Autowired private PlaylistService playlistService; + @Autowired + private IndexManager indexManager; @Override public BrowseResult browse(String objectId, BrowseFlag browseFlag, String filter, long firstResult, @@ -98,7 +101,7 @@ public class FolderBasedContentDirectory extends CustomContentDirectory { root.setId(CONTAINER_ID_ROOT); root.setParentID("-1"); - MediaLibraryStatistics statistics = settingsService.getMediaLibraryStatistics(); + MediaLibraryStatistics statistics = indexManager.getStatistics(); root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); root.setTitle("Airsonic Media"); root.setRestricted(true); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java index daca3fe5..b2ef31c4 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java @@ -20,10 +20,12 @@ package org.airsonic.player.service.upnp; import org.airsonic.player.domain.MediaLibraryStatistics; +import org.airsonic.player.service.search.IndexManager; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.WriteStatus; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.StorageFolder; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; @@ -35,12 +37,16 @@ import java.util.List; */ @Component public class RootUpnpProcessor extends UpnpContentProcessor { + + @Autowired + IndexManager indexManager; + public Container createRootContainer() { StorageFolder root = new StorageFolder(); root.setId(DispatchingContentDirectory.CONTAINER_ID_ROOT); root.setParentID("-1"); - MediaLibraryStatistics statistics = getDispatchingContentDirectory().getSettingsService().getMediaLibraryStatistics(); + MediaLibraryStatistics statistics = indexManager.getStatistics(); // returning large storageUsed values doesn't play nicely with // some upnp clients //root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); diff --git a/airsonic-main/src/main/java/org/airsonic/player/util/Util.java b/airsonic-main/src/main/java/org/airsonic/player/util/Util.java index 3d5e4aa8..35fcc728 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/util/Util.java +++ b/airsonic-main/src/main/java/org/airsonic/player/util/Util.java @@ -20,6 +20,8 @@ package org.airsonic.player.util; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.SystemUtils; import org.slf4j.Logger; @@ -30,10 +32,12 @@ import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * Miscellaneous general utility methods. @@ -113,7 +117,11 @@ public final class Util { return result; } - static ObjectMapper objectMapper = new ObjectMapper(); + private static ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + public static String debugObject(Object object) { try { return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); @@ -169,4 +177,29 @@ public final class Util { return false; } } + + public static Map objectToStringMap(Object object) { + TypeReference> typeReference = new TypeReference>() {}; + return objectMapper.convertValue(object, typeReference); + } + + public static T stringMapToObject(Class clazz, Map data) { + return objectMapper.convertValue(data, clazz); + } + + private static Validator validator; + static { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + public static T stringMapToValidObject(Class clazz, Map data) { + T object = stringMapToObject(clazz, data); + Set> validate = validator.validate(object); + if (validate.isEmpty()) { + return object; + } else { + throw new IllegalArgumentException("Created object was not valid"); + } + } } diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java index e8d5d197..5375780a 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java @@ -178,6 +178,12 @@ public class MediaScannerServiceTestCase { assertNotNull(mediaFile); } + @Test + public void testNeverScanned() { + + mediaScannerService.neverScanned(); + } + @Test public void testMusicBrainzReleaseIdTag() { diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java new file mode 100644 index 00000000..6359ec6b --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java @@ -0,0 +1,31 @@ +package org.airsonic.player.service; + +import org.airsonic.player.domain.MediaLibraryStatistics; +import org.airsonic.player.service.search.IndexManager; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class MediaScannerServiceUnitTest { + + @InjectMocks + MediaScannerService mediaScannerService; + + @Mock + IndexManager indexManager; + + @Test + public void neverScanned() { + when(indexManager.getStatistics()).thenReturn(null); + assertTrue(mediaScannerService.neverScanned()); + + when(indexManager.getStatistics()).thenReturn(new MediaLibraryStatistics()); + assertFalse(mediaScannerService.neverScanned()); + } +} \ No newline at end of file diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/search/IndexManagerTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/service/search/IndexManagerTestCase.java index 0d74838b..44e15550 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/search/IndexManagerTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/search/IndexManagerTestCase.java @@ -172,7 +172,7 @@ public class IndexManagerTestCase extends AbstractAirsonicHomeTest { /* Does not scan, only expunges the index. */ indexManager.startIndexing(); indexManager.expunge(); - indexManager.stopIndexing(); + indexManager.stopIndexing(indexManager.getStatistics()); /* * Subsequent search results. diff --git a/airsonic-main/src/test/java/org/airsonic/player/util/UtilTest.java b/airsonic-main/src/test/java/org/airsonic/player/util/UtilTest.java new file mode 100644 index 00000000..afb701cf --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/util/UtilTest.java @@ -0,0 +1,88 @@ +package org.airsonic.player.util; + +import org.airsonic.player.domain.MediaLibraryStatistics; +import org.junit.Test; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class UtilTest { + + @Test + public void objectToStringMapNull() { + MediaLibraryStatistics statistics = null; + Map stringStringMap = Util.objectToStringMap(statistics); + assertNull(stringStringMap); + } + + @Test + public void objectToStringMap() { + Date date = new Date(1568350960725L); + MediaLibraryStatistics statistics = new MediaLibraryStatistics(date); + statistics.incrementAlbums(5); + statistics.incrementSongs(4); + statistics.incrementArtists(910823); + statistics.incrementTotalDurationInSeconds(30); + statistics.incrementTotalLengthInBytes(2930491082L); + Map stringStringMap = Util.objectToStringMap(statistics); + assertEquals("5", stringStringMap.get("albumCount")); + assertEquals("4", stringStringMap.get("songCount")); + assertEquals("910823", stringStringMap.get("artistCount")); + assertEquals("30", stringStringMap.get("totalDurationInSeconds")); + assertEquals("2930491082", stringStringMap.get("totalLengthInBytes")); + assertEquals("1568350960725", stringStringMap.get("scanDate")); + } + + @Test + public void stringMapToObject() { + Map stringStringMap = new HashMap<>(); + stringStringMap.put("albumCount", "5"); + stringStringMap.put("songCount", "4"); + stringStringMap.put("artistCount", "910823"); + stringStringMap.put("totalDurationInSeconds", "30"); + stringStringMap.put("totalLengthInBytes", "2930491082"); + stringStringMap.put("scanDate", "1568350960725"); + MediaLibraryStatistics statistics = Util.stringMapToObject(MediaLibraryStatistics.class, stringStringMap); + assertEquals(new Integer(5), statistics.getAlbumCount()); + assertEquals(new Integer(4), statistics.getSongCount()); + assertEquals(new Integer(910823), statistics.getArtistCount()); + assertEquals(new Long(30L), statistics.getTotalDurationInSeconds()); + assertEquals(new Long(2930491082L), statistics.getTotalLengthInBytes()); + assertEquals(new Date(1568350960725L), statistics.getScanDate()); + } + + @Test + public void stringMapToObjectWithExtraneousData() { + Map stringStringMap = new HashMap<>(); + stringStringMap.put("albumCount", "5"); + stringStringMap.put("songCount", "4"); + stringStringMap.put("artistCount", "910823"); + stringStringMap.put("totalDurationInSeconds", "30"); + stringStringMap.put("totalLengthInBytes", "2930491082"); + stringStringMap.put("scanDate", "1568350960725"); + stringStringMap.put("extraneousData", "nothingHereToLookAt"); + MediaLibraryStatistics statistics = Util.stringMapToObject(MediaLibraryStatistics.class, stringStringMap); + assertEquals(new Integer(5), statistics.getAlbumCount()); + assertEquals(new Integer(4), statistics.getSongCount()); + assertEquals(new Integer(910823), statistics.getArtistCount()); + assertEquals(new Long(30L), statistics.getTotalDurationInSeconds()); + assertEquals(new Long(2930491082L), statistics.getTotalLengthInBytes()); + assertEquals(new Date(1568350960725L), statistics.getScanDate()); + } + + public void stringMapToObjectWithNoData() { + Map stringStringMap = new HashMap<>(); + MediaLibraryStatistics statistics = Util.stringMapToObject(MediaLibraryStatistics.class, stringStringMap); + assertNotNull(statistics); + } + + @Test(expected = IllegalArgumentException.class) + public void stringMapToValidObjectWithNoData() { + Map stringStringMap = new HashMap<>(); + MediaLibraryStatistics statistics = Util.stringMapToValidObject(MediaLibraryStatistics.class, stringStringMap); + } + +} \ No newline at end of file