Merge remote-tracking branch 'origin/pr/1262'

master
Andrew DeMaria 5 years ago
commit eda6406865
No known key found for this signature in database
GPG Key ID: 0A3F5E91F8364EDF
  1. 5
      airsonic-main/src/main/java/org/airsonic/player/controller/LeftController.java
  2. 16
      airsonic-main/src/main/java/org/airsonic/player/controller/MusicFolderSettingsController.java
  3. 88
      airsonic-main/src/main/java/org/airsonic/player/domain/MediaLibraryStatistics.java
  4. 54
      airsonic-main/src/main/java/org/airsonic/player/service/MediaScannerService.java
  5. 25
      airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java
  6. 66
      airsonic-main/src/main/java/org/airsonic/player/service/search/IndexManager.java
  7. 5
      airsonic-main/src/main/java/org/airsonic/player/service/upnp/FolderBasedContentDirectory.java
  8. 8
      airsonic-main/src/main/java/org/airsonic/player/service/upnp/RootUpnpProcessor.java
  9. 41
      airsonic-main/src/main/java/org/airsonic/player/util/Util.java
  10. 6
      airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java
  11. 31
      airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceUnitTest.java
  12. 2
      airsonic-main/src/test/java/org/airsonic/player/service/search/IndexManagerTestCase.java
  13. 88
      airsonic-main/src/test/java/org/airsonic/player/util/UtilTest.java

@ -21,6 +21,7 @@ package org.airsonic.player.controller;
import org.airsonic.player.domain.*; import org.airsonic.player.domain.*;
import org.airsonic.player.service.*; import org.airsonic.player.service.*;
import org.airsonic.player.service.search.IndexManager;
import org.airsonic.player.util.FileUtil; import org.airsonic.player.util.FileUtil;
import org.airsonic.player.util.StringUtil; import org.airsonic.player.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -56,6 +57,8 @@ public class LeftController {
@Autowired @Autowired
private MediaScannerService mediaScannerService; private MediaScannerService mediaScannerService;
@Autowired @Autowired
private IndexManager indexManager;
@Autowired
private SettingsService settingsService; private SettingsService settingsService;
@Autowired @Autowired
private SecurityService securityService; private SecurityService securityService;
@ -116,7 +119,7 @@ public class LeftController {
boolean musicFolderChanged = saveSelectedMusicFolder(request); boolean musicFolderChanged = saveSelectedMusicFolder(request);
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
MediaLibraryStatistics statistics = mediaScannerService.getStatistics(); MediaLibraryStatistics statistics = indexManager.getStatistics();
Locale locale = RequestContextUtils.getLocale(request); Locale locale = RequestContextUtils.getLocale(request);
boolean refresh = ServletRequestUtils.getBooleanParameter(request, "refresh", false); boolean refresh = ServletRequestUtils.getBooleanParameter(request, "refresh", false);

@ -23,6 +23,7 @@ import org.airsonic.player.command.MusicFolderSettingsCommand;
import org.airsonic.player.dao.AlbumDao; import org.airsonic.player.dao.AlbumDao;
import org.airsonic.player.dao.ArtistDao; import org.airsonic.player.dao.ArtistDao;
import org.airsonic.player.dao.MediaFileDao; import org.airsonic.player.dao.MediaFileDao;
import org.airsonic.player.domain.MediaLibraryStatistics;
import org.airsonic.player.domain.MusicFolder; import org.airsonic.player.domain.MusicFolder;
import org.airsonic.player.service.MediaScannerService; import org.airsonic.player.service.MediaScannerService;
import org.airsonic.player.service.SettingsService; import org.airsonic.player.service.SettingsService;
@ -103,11 +104,16 @@ public class MusicFolderSettingsController {
private void expunge() { private void expunge() {
// to be before dao#expunge // to be before dao#expunge
LOG.debug("Cleaning search index..."); MediaLibraryStatistics statistics = indexManager.getStatistics();
indexManager.startIndexing(); if (statistics != null) {
indexManager.expunge(); LOG.debug("Cleaning search index...");
indexManager.stopIndexing(); indexManager.startIndexing();
LOG.debug("Search index cleanup complete."); 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("Cleaning database...");
LOG.debug("Deleting non-present artists..."); LOG.debug("Deleting non-present artists...");

@ -19,9 +19,10 @@
*/ */
package org.airsonic.player.domain; package org.airsonic.player.domain;
import org.airsonic.player.util.StringUtil; import javax.validation.constraints.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.Date;
import java.util.Objects;
/** /**
* Contains media libaray statistics, including the number of artists, albums and songs. * Contains media libaray statistics, including the number of artists, albums and songs.
@ -31,31 +32,37 @@ import org.slf4j.LoggerFactory;
*/ */
public class MediaLibraryStatistics { 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; public MediaLibraryStatistics() {
private int albumCount;
private int songCount;
private long totalLengthInBytes;
private long totalDurationInSeconds;
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; artistCount = 0;
albumCount = 0; albumCount = 0;
songCount = 0; songCount = 0;
totalLengthInBytes = 0; totalLengthInBytes = 0L;
totalDurationInSeconds = 0; totalDurationInSeconds = 0L;
} }
public void incrementArtists(int n) { public void incrementArtists(int n) {
@ -78,42 +85,45 @@ public class MediaLibraryStatistics {
totalDurationInSeconds += n; totalDurationInSeconds += n;
} }
public int getArtistCount() { public Integer getArtistCount() {
return artistCount; return artistCount;
} }
public int getAlbumCount() { public Integer getAlbumCount() {
return albumCount; return albumCount;
} }
public int getSongCount() { public Integer getSongCount() {
return songCount; return songCount;
} }
public long getTotalLengthInBytes() { public Long getTotalLengthInBytes() {
return totalLengthInBytes; return totalLengthInBytes;
} }
public long getTotalDurationInSeconds() { public Long getTotalDurationInSeconds() {
return totalDurationInSeconds; return totalDurationInSeconds;
} }
public String format() { public Date getScanDate() {
return artistCount + " " + albumCount + " " + songCount + " " + totalLengthInBytes + " " + totalDurationInSeconds; return scanDate;
} }
public static MediaLibraryStatistics parse(String s) { @Override
try { public boolean equals(Object o) {
String[] strings = StringUtil.split(s); if (this == o) return true;
return new MediaLibraryStatistics( if (o == null || getClass() != o.getClass()) return false;
Integer.parseInt(strings[0]), MediaLibraryStatistics that = (MediaLibraryStatistics) o;
Integer.parseInt(strings[1]), return Objects.equals(artistCount, that.artistCount) &&
Integer.parseInt(strings[2]), Objects.equals(albumCount, that.albumCount) &&
Long.parseLong(strings[3]), Objects.equals(songCount, that.songCount) &&
Long.parseLong(strings[4])); Objects.equals(totalLengthInBytes, that.totalLengthInBytes) &&
} catch (Exception e) { Objects.equals(totalDurationInSeconds, that.totalDurationInSeconds) &&
LOG.warn("Failed to parse media library statistics: " + s); Objects.equals(scanDate, that.scanDate);
return new MediaLibraryStatistics(); }
}
@Override
public int hashCode() {
return Objects.hash(artistCount, albumCount, songCount, totalLengthInBytes, totalDurationInSeconds, scanDate);
} }
} }

@ -51,8 +51,6 @@ public class MediaScannerService {
private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class); private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class);
private MediaLibraryStatistics statistics;
private boolean scanning; private boolean scanning;
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
@ -76,13 +74,11 @@ public class MediaScannerService {
@PostConstruct @PostConstruct
public void init() { public void init() {
indexManager.initializeIndexDirectory(); indexManager.initializeIndexDirectory();
statistics = settingsService.getMediaLibraryStatistics();
schedule(); schedule();
} }
public void initNoSchedule() { public void initNoSchedule() {
indexManager.deleteOldIndexFiles(); indexManager.deleteOldIndexFiles();
statistics = settingsService.getMediaLibraryStatistics();
} }
/** /**
@ -115,12 +111,16 @@ public class MediaScannerService {
LOG.info("Automatic media library scanning scheduled to run every {} day(s), starting at {}", daysBetween, nextRun); LOG.info("Automatic media library scanning scheduled to run every {} day(s), starting at {}", daysBetween, nextRun);
// In addition, create index immediately if it doesn't exist on disk. // 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."); LOG.info("Media library never scanned. Doing it now.");
scanLibrary(); scanLibrary();
} }
} }
boolean neverScanned() {
return indexManager.getStatistics() == null;
}
/** /**
* Returns whether the media library is currently being scanned. * Returns whether the media library is currently being scanned.
*/ */
@ -160,8 +160,9 @@ public class MediaScannerService {
private void doScanLibrary() { private void doScanLibrary() {
LOG.info("Starting to scan media library."); LOG.info("Starting to scan media library.");
Date lastScanned = DateUtils.truncate(new Date(), Calendar.SECOND); MediaLibraryStatistics statistics = new MediaLibraryStatistics(
LOG.debug("New last scan date is " + lastScanned); DateUtils.truncate(new Date(), Calendar.SECOND));
LOG.debug("New last scan date is " + statistics.getScanDate());
try { try {
@ -170,7 +171,6 @@ public class MediaScannerService {
Genres genres = new Genres(); Genres genres = new Genres();
scanCount = 0; scanCount = 0;
statistics.reset();
mediaFileService.setMemoryCacheEnabled(false); mediaFileService.setMemoryCacheEnabled(false);
indexManager.startIndexing(); indexManager.startIndexing();
@ -180,24 +180,24 @@ public class MediaScannerService {
// Recurse through all files on disk. // Recurse through all files on disk.
for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) {
MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false);
scanFile(root, musicFolder, lastScanned, albumCount, genres, false); scanFile(root, musicFolder, statistics, albumCount, genres, false);
} }
// Scan podcast folder. // Scan podcast folder.
File podcastFolder = new File(settingsService.getPodcastFolder()); File podcastFolder = new File(settingsService.getPodcastFolder());
if (podcastFolder.exists()) { if (podcastFolder.exists()) {
scanFile(mediaFileService.getMediaFile(podcastFolder), new MusicFolder(podcastFolder, null, true, null), 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("Scanned media library with " + scanCount + " entries.");
LOG.info("Marking non-present files."); LOG.info("Marking non-present files.");
mediaFileDao.markNonPresent(lastScanned); mediaFileDao.markNonPresent(statistics.getScanDate());
LOG.info("Marking non-present artists."); LOG.info("Marking non-present artists.");
artistDao.markNonPresent(lastScanned); artistDao.markNonPresent(statistics.getScanDate());
LOG.info("Marking non-present albums."); LOG.info("Marking non-present albums.");
albumDao.markNonPresent(lastScanned); albumDao.markNonPresent(statistics.getScanDate());
// Update statistics // Update statistics
statistics.incrementArtists(albumCount.size()); statistics.incrementArtists(albumCount.size());
@ -208,21 +208,18 @@ public class MediaScannerService {
// Update genres // Update genres
mediaFileDao.updateGenres(genres.getGenres()); mediaFileDao.updateGenres(genres.getGenres());
settingsService.setMediaLibraryStatistics(statistics);
settingsService.setLastScanned(lastScanned);
settingsService.save(false);
LOG.info("Completed media library scan."); LOG.info("Completed media library scan.");
} catch (Throwable x) { } catch (Throwable x) {
LOG.error("Failed to scan media library.", x); LOG.error("Failed to scan media library.", x);
} finally { } finally {
mediaFileService.setMemoryCacheEnabled(true); mediaFileService.setMemoryCacheEnabled(true);
indexManager.stopIndexing(); indexManager.stopIndexing(statistics);
scanning = false; scanning = false;
} }
} }
private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, private void scanFile(MediaFile file, MusicFolder musicFolder, MediaLibraryStatistics statistics,
Map<String, Integer> albumCount, Genres genres, boolean isPodcast) { Map<String, Integer> albumCount, Genres genres, boolean isPodcast) {
scanCount++; scanCount++;
if (scanCount % 250 == 0) { if (scanCount % 250 == 0) {
@ -241,22 +238,22 @@ public class MediaScannerService {
if (file.isDirectory()) { if (file.isDirectory()) {
for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) { 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)) { 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 { } else {
if (!isPodcast) { if (!isPodcast) {
updateAlbum(file, musicFolder, lastScanned, albumCount); updateAlbum(file, musicFolder, statistics.getScanDate(), albumCount);
updateArtist(file, musicFolder, lastScanned, albumCount); updateArtist(file, musicFolder, statistics.getScanDate(), albumCount);
} }
statistics.incrementSongs(1); statistics.incrementSongs(1);
} }
updateGenres(file, genres); updateGenres(file, genres);
mediaFileDao.markPresent(file.getPath(), lastScanned); mediaFileDao.markPresent(file.getPath(), statistics.getScanDate());
artistDao.markPresent(file.getAlbumArtist(), lastScanned); artistDao.markPresent(file.getAlbumArtist(), statistics.getScanDate());
if (file.getDurationSeconds() != null) { if (file.getDurationSeconds() != null) {
statistics.incrementTotalDurationInSeconds(file.getDurationSeconds()); statistics.incrementTotalDurationInSeconds(file.getDurationSeconds());
@ -368,15 +365,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) { public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService; this.settingsService = settingsService;
} }

@ -96,10 +96,8 @@ public class SettingsService {
private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing"; private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing";
private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled"; private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled";
private static final String KEY_SETTINGS_CHANGED = "SettingsChanged"; 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_ORGANIZE_BY_FOLDER_STRUCTURE = "OrganizeByFolderStructure";
private static final String KEY_SORT_ALBUMS_BY_YEAR = "SortAlbumsByYear"; 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_ENABLED = "DlnaEnabled";
private static final String KEY_DLNA_SERVER_NAME = "DlnaServerName"; private static final String KEY_DLNA_SERVER_NAME = "DlnaServerName";
private static final String KEY_DLNA_BASE_LAN_URL = "DlnaBaseLANURL"; 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 long DEFAULT_SETTINGS_CHANGED = 0L;
private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true; private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true;
private static final boolean DEFAULT_SORT_ALBUMS_BY_YEAR = 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 boolean DEFAULT_DLNA_ENABLED = false;
private static final String DEFAULT_DLNA_SERVER_NAME = "Airsonic"; private static final String DEFAULT_DLNA_SERVER_NAME = "Airsonic";
private static final String DEFAULT_DLNA_BASE_LAN_URL = null; private static final String DEFAULT_DLNA_BASE_LAN_URL = null;
@ -220,6 +217,7 @@ public class SettingsService {
"CoverArtFileTypes", "UrlRedirectCustomHost", "CoverArtLimit", "StreamPort", "CoverArtFileTypes", "UrlRedirectCustomHost", "CoverArtLimit", "StreamPort",
"PortForwardingEnabled", "RewriteUrl", "UrlRedirectCustomUrl", "UrlRedirectContextPath", "PortForwardingEnabled", "RewriteUrl", "UrlRedirectCustomUrl", "UrlRedirectContextPath",
"UrlRedirectFrom", "UrlRedirectionEnabled", "UrlRedirectType", "Port", "HttpsPort", "UrlRedirectFrom", "UrlRedirectionEnabled", "UrlRedirectType", "Port", "HttpsPort",
"MediaLibraryStatistics", "LastScanned",
// Database settings renamed // Database settings renamed
"database.varchar.maxlength", "database.config.type", "database.config.embed.driver", "database.varchar.maxlength", "database.config.type", "database.config.embed.driver",
"database.config.embed.url", "database.config.embed.username", "database.config.embed.password", "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); 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() { public boolean isOrganizeByFolderStructure() {
return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE); return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE);
} }
@ -787,14 +772,6 @@ public class SettingsService {
return excludePattern; 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. * Returns whether we are running in Development mode.
* *

@ -23,17 +23,13 @@ package org.airsonic.player.service.search;
import org.airsonic.player.dao.AlbumDao; import org.airsonic.player.dao.AlbumDao;
import org.airsonic.player.dao.ArtistDao; import org.airsonic.player.dao.ArtistDao;
import org.airsonic.player.dao.MediaFileDao; import org.airsonic.player.dao.MediaFileDao;
import org.airsonic.player.domain.Album; import org.airsonic.player.domain.*;
import org.airsonic.player.domain.Artist;
import org.airsonic.player.domain.MediaFile;
import org.airsonic.player.domain.MusicFolder;
import org.airsonic.player.service.SettingsService; import org.airsonic.player.service.SettingsService;
import org.airsonic.player.util.FileUtil; import org.airsonic.player.util.FileUtil;
import org.airsonic.player.util.Util;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.lucene.document.Document; import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.*;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SearcherManager; import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.FSDirectory;
@ -47,6 +43,8 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Pattern; 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 INDEX_ROOT_DIR_NAME = "index";
private static final String MEDIA_STATISTICS_KEY = "stats";
/** /**
* File supplier for index directory. * File supplier for index directory.
*/ */
@ -221,26 +221,29 @@ public class IndexManager {
* Close Writer of all indexes and update SearcherManager. * Close Writer of all indexes and update SearcherManager.
* Called at the end of the Scan flow. * Called at the end of the Scan flow.
*/ */
public void stopIndexing() { public void stopIndexing(MediaLibraryStatistics statistics) {
Arrays.asList(IndexType.values()).forEach(this::stopIndexing); Arrays.asList(IndexType.values()).forEach(indexType -> stopIndexing(indexType, statistics));
} }
/** /**
* Close Writer of specified index and refresh SearcherManager. * Close Writer of specified index and refresh SearcherManager.
*/ */
private void stopIndexing(IndexType type) { private void stopIndexing(IndexType type, MediaLibraryStatistics statistics) {
boolean isUpdate = false; boolean isUpdate = false;
// close // close
IndexWriter indexWriter = writers.get(type);
try { try {
isUpdate = -1 != writers.get(type).commit(); Map<String,String> userData = Util.objectToStringMap(statistics);
writers.get(type).close(); indexWriter.setLiveCommitData(userData.entrySet());
isUpdate = -1 != indexWriter.commit();
indexWriter.close();
writers.remove(type); writers.remove(type);
LOG.trace("Success to create or update search index : [" + type + "]"); LOG.trace("Success to create or update search index : [" + type + "]");
} catch (IOException e) { } catch (IOException e) {
LOG.error("Failed to create search index.", e); LOG.error("Failed to create search index.", e);
} finally { } finally {
FileUtil.closeQuietly(writers.get(type)); FileUtil.closeQuietly(indexWriter);
} }
// refresh reader as index may have been written // 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<String, String> 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. * Return the IndexSearcher of the specified index.
* At initial startup, it may return null * At initial startup, it may return null

@ -22,6 +22,7 @@ package org.airsonic.player.service.upnp;
import org.airsonic.player.domain.*; import org.airsonic.player.domain.*;
import org.airsonic.player.service.MediaFileService; import org.airsonic.player.service.MediaFileService;
import org.airsonic.player.service.PlaylistService; import org.airsonic.player.service.PlaylistService;
import org.airsonic.player.service.search.IndexManager;
import org.airsonic.player.util.Util; import org.airsonic.player.util.Util;
import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode;
import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; import org.fourthline.cling.support.contentdirectory.ContentDirectoryException;
@ -57,6 +58,8 @@ public class FolderBasedContentDirectory extends CustomContentDirectory {
private MediaFileService mediaFileService; private MediaFileService mediaFileService;
@Autowired @Autowired
private PlaylistService playlistService; private PlaylistService playlistService;
@Autowired
private IndexManager indexManager;
@Override @Override
public BrowseResult browse(String objectId, BrowseFlag browseFlag, String filter, long firstResult, 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.setId(CONTAINER_ID_ROOT);
root.setParentID("-1"); root.setParentID("-1");
MediaLibraryStatistics statistics = settingsService.getMediaLibraryStatistics(); MediaLibraryStatistics statistics = indexManager.getStatistics();
root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes());
root.setTitle("Airsonic Media"); root.setTitle("Airsonic Media");
root.setRestricted(true); root.setRestricted(true);

@ -20,10 +20,12 @@
package org.airsonic.player.service.upnp; package org.airsonic.player.service.upnp;
import org.airsonic.player.domain.MediaLibraryStatistics; 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.DIDLContent;
import org.fourthline.cling.support.model.WriteStatus; import org.fourthline.cling.support.model.WriteStatus;
import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.Container;
import org.fourthline.cling.support.model.container.StorageFolder; import org.fourthline.cling.support.model.container.StorageFolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.ArrayList;
@ -35,12 +37,16 @@ import java.util.List;
*/ */
@Component @Component
public class RootUpnpProcessor extends UpnpContentProcessor <Container, Container> { public class RootUpnpProcessor extends UpnpContentProcessor <Container, Container> {
@Autowired
IndexManager indexManager;
public Container createRootContainer() { public Container createRootContainer() {
StorageFolder root = new StorageFolder(); StorageFolder root = new StorageFolder();
root.setId(DispatchingContentDirectory.CONTAINER_ID_ROOT); root.setId(DispatchingContentDirectory.CONTAINER_ID_ROOT);
root.setParentID("-1"); root.setParentID("-1");
MediaLibraryStatistics statistics = getDispatchingContentDirectory().getSettingsService().getMediaLibraryStatistics(); MediaLibraryStatistics statistics = indexManager.getStatistics();
// returning large storageUsed values doesn't play nicely with // returning large storageUsed values doesn't play nicely with
// some upnp clients // some upnp clients
//root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); //root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes());

@ -20,6 +20,8 @@
package org.airsonic.player.util; package org.airsonic.player.util;
import com.fasterxml.jackson.core.JsonProcessingException; 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 com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -30,10 +32,12 @@ import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; 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.*;
import java.util.Collections;
import java.util.List;
/** /**
* Miscellaneous general utility methods. * Miscellaneous general utility methods.
@ -113,7 +117,11 @@ public final class Util {
return result; 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) { public static String debugObject(Object object) {
try { try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
@ -169,4 +177,29 @@ public final class Util {
return false; return false;
} }
} }
public static Map<String, String> objectToStringMap(Object object) {
TypeReference<HashMap<String, String>> typeReference = new TypeReference<HashMap<String, String>>() {};
return objectMapper.convertValue(object, typeReference);
}
public static <T> T stringMapToObject(Class<T> clazz, Map<String, String> data) {
return objectMapper.convertValue(data, clazz);
}
private static Validator validator;
static {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
public static <T> T stringMapToValidObject(Class<T> clazz, Map<String, String> data) {
T object = stringMapToObject(clazz, data);
Set<ConstraintViolation<T>> validate = validator.validate(object);
if (validate.isEmpty()) {
return object;
} else {
throw new IllegalArgumentException("Created object was not valid");
}
}
} }

@ -178,6 +178,12 @@ public class MediaScannerServiceTestCase {
assertNotNull(mediaFile); assertNotNull(mediaFile);
} }
@Test
public void testNeverScanned() {
mediaScannerService.neverScanned();
}
@Test @Test
public void testMusicBrainzReleaseIdTag() { public void testMusicBrainzReleaseIdTag() {

@ -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());
}
}

@ -172,7 +172,7 @@ public class IndexManagerTestCase extends AbstractAirsonicHomeTest {
/* Does not scan, only expunges the index. */ /* Does not scan, only expunges the index. */
indexManager.startIndexing(); indexManager.startIndexing();
indexManager.expunge(); indexManager.expunge();
indexManager.stopIndexing(); indexManager.stopIndexing(indexManager.getStatistics());
/* /*
* Subsequent search results. * Subsequent search results.

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> stringStringMap = new HashMap<>();
MediaLibraryStatistics statistics = Util.stringMapToObject(MediaLibraryStatistics.class, stringStringMap);
assertNotNull(statistics);
}
@Test(expected = IllegalArgumentException.class)
public void stringMapToValidObjectWithNoData() {
Map<String, String> stringStringMap = new HashMap<>();
MediaLibraryStatistics statistics = Util.stringMapToValidObject(MediaLibraryStatistics.class, stringStringMap);
}
}
Loading…
Cancel
Save