Split SearchService
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import org.airsonic.player.service.PlayerService;
|
||||
import org.airsonic.player.service.SearchService;
|
||||
import org.airsonic.player.service.SecurityService;
|
||||
import org.airsonic.player.service.SettingsService;
|
||||
import org.airsonic.player.service.search.IndexType;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -87,13 +88,13 @@ public class SearchController {
|
||||
criteria.setCount(MATCH_COUNT);
|
||||
criteria.setQuery(query);
|
||||
|
||||
SearchResult artists = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST);
|
||||
SearchResult artists = searchService.search(criteria, musicFolders, IndexType.ARTIST);
|
||||
command.setArtists(artists.getMediaFiles());
|
||||
|
||||
SearchResult albums = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM);
|
||||
SearchResult albums = searchService.search(criteria, musicFolders, IndexType.ALBUM);
|
||||
command.setAlbums(albums.getMediaFiles());
|
||||
|
||||
SearchResult songs = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG);
|
||||
SearchResult songs = searchService.search(criteria, musicFolders, IndexType.SONG);
|
||||
command.setSongs(songs.getMediaFiles());
|
||||
|
||||
command.setPlayer(playerService.getPlayer(request, response));
|
||||
|
||||
+8
-7
@@ -32,6 +32,7 @@ import org.airsonic.player.domain.Bookmark;
|
||||
import org.airsonic.player.domain.PlayQueue;
|
||||
import org.airsonic.player.i18n.LocaleResolver;
|
||||
import org.airsonic.player.service.*;
|
||||
import org.airsonic.player.service.search.IndexType;
|
||||
import org.airsonic.player.util.Pair;
|
||||
import org.airsonic.player.util.StringUtil;
|
||||
import org.airsonic.player.util.Util;
|
||||
@@ -685,7 +686,7 @@ public class SubsonicRESTController {
|
||||
criteria.setOffset(getIntParameter(request, "offset", 0));
|
||||
List<org.airsonic.player.domain.MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username);
|
||||
|
||||
org.airsonic.player.domain.SearchResult result = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG);
|
||||
org.airsonic.player.domain.SearchResult result = searchService.search(criteria, musicFolders, IndexType.SONG);
|
||||
org.subsonic.restapi.SearchResult searchResult = new org.subsonic.restapi.SearchResult();
|
||||
searchResult.setOffset(result.getOffset());
|
||||
searchResult.setTotalHits(result.getTotalHits());
|
||||
@@ -713,21 +714,21 @@ public class SubsonicRESTController {
|
||||
criteria.setQuery(StringUtils.trimToEmpty(query));
|
||||
criteria.setCount(getIntParameter(request, "artistCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "artistOffset", 0));
|
||||
org.airsonic.player.domain.SearchResult artists = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST);
|
||||
org.airsonic.player.domain.SearchResult artists = searchService.search(criteria, musicFolders, IndexType.ARTIST);
|
||||
for (MediaFile mediaFile : artists.getMediaFiles()) {
|
||||
searchResult.getArtist().add(createJaxbArtist(mediaFile, username));
|
||||
}
|
||||
|
||||
criteria.setCount(getIntParameter(request, "albumCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "albumOffset", 0));
|
||||
org.airsonic.player.domain.SearchResult albums = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM);
|
||||
org.airsonic.player.domain.SearchResult albums = searchService.search(criteria, musicFolders, IndexType.ALBUM);
|
||||
for (MediaFile mediaFile : albums.getMediaFiles()) {
|
||||
searchResult.getAlbum().add(createJaxbChild(player, mediaFile, username));
|
||||
}
|
||||
|
||||
criteria.setCount(getIntParameter(request, "songCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "songOffset", 0));
|
||||
org.airsonic.player.domain.SearchResult songs = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG);
|
||||
org.airsonic.player.domain.SearchResult songs = searchService.search(criteria, musicFolders, IndexType.SONG);
|
||||
for (MediaFile mediaFile : songs.getMediaFiles()) {
|
||||
searchResult.getSong().add(createJaxbChild(player, mediaFile, username));
|
||||
}
|
||||
@@ -752,21 +753,21 @@ public class SubsonicRESTController {
|
||||
criteria.setQuery(StringUtils.trimToEmpty(query));
|
||||
criteria.setCount(getIntParameter(request, "artistCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "artistOffset", 0));
|
||||
org.airsonic.player.domain.SearchResult result = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST_ID3);
|
||||
org.airsonic.player.domain.SearchResult result = searchService.search(criteria, musicFolders, IndexType.ARTIST_ID3);
|
||||
for (org.airsonic.player.domain.Artist artist : result.getArtists()) {
|
||||
searchResult.getArtist().add(createJaxbArtist(new ArtistID3(), artist, username));
|
||||
}
|
||||
|
||||
criteria.setCount(getIntParameter(request, "albumCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "albumOffset", 0));
|
||||
result = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM_ID3);
|
||||
result = searchService.search(criteria, musicFolders, IndexType.ALBUM_ID3);
|
||||
for (Album album : result.getAlbums()) {
|
||||
searchResult.getAlbum().add(createJaxbAlbum(new AlbumID3(), album, username));
|
||||
}
|
||||
|
||||
criteria.setCount(getIntParameter(request, "songCount", 20));
|
||||
criteria.setOffset(getIntParameter(request, "songOffset", 0));
|
||||
result = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG);
|
||||
result = searchService.search(criteria, musicFolders, IndexType.SONG);
|
||||
for (MediaFile song : result.getMediaFiles()) {
|
||||
searchResult.getSong().add(createJaxbChild(player, song, username));
|
||||
}
|
||||
|
||||
@@ -1,666 +1,86 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.airsonic.player.service;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.airsonic.player.dao.AlbumDao;
|
||||
import org.airsonic.player.dao.ArtistDao;
|
||||
import org.airsonic.player.domain.*;
|
||||
import org.airsonic.player.util.FileUtil;
|
||||
import org.apache.lucene.analysis.*;
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.analysis.standard.StandardFilter;
|
||||
import org.apache.lucene.analysis.standard.StandardTokenizer;
|
||||
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.NumericField;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.queryParser.MultiFieldQueryParser;
|
||||
import org.apache.lucene.queryParser.QueryParser;
|
||||
import org.apache.lucene.search.*;
|
||||
import org.apache.lucene.search.spans.SpanOrQuery;
|
||||
import org.apache.lucene.search.spans.SpanQuery;
|
||||
import org.apache.lucene.search.spans.SpanTermQuery;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
import org.apache.lucene.util.Version;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.util.*;
|
||||
|
||||
import static org.airsonic.player.service.SearchService.IndexType.*;
|
||||
|
||||
/**
|
||||
* Performs Lucene-based searching and indexing.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
* @see MediaScannerService
|
||||
*/
|
||||
@Service
|
||||
public class SearchService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SearchService.class);
|
||||
|
||||
private static final String FIELD_ID = "id";
|
||||
private static final String FIELD_TITLE = "title";
|
||||
private static final String FIELD_ALBUM = "album";
|
||||
private static final String FIELD_ARTIST = "artist";
|
||||
private static final String FIELD_GENRE = "genre";
|
||||
private static final String FIELD_YEAR = "year";
|
||||
private static final String FIELD_MEDIA_TYPE = "mediaType";
|
||||
private static final String FIELD_FOLDER = "folder";
|
||||
private static final String FIELD_FOLDER_ID = "folderId";
|
||||
|
||||
private static final Version LUCENE_VERSION = Version.LUCENE_30;
|
||||
private static final String LUCENE_DIR = "lucene2";
|
||||
|
||||
@Autowired
|
||||
private MediaFileService mediaFileService;
|
||||
@Autowired
|
||||
private ArtistDao artistDao;
|
||||
@Autowired
|
||||
private AlbumDao albumDao;
|
||||
|
||||
private IndexWriter artistWriter;
|
||||
private IndexWriter artistId3Writer;
|
||||
private IndexWriter albumWriter;
|
||||
private IndexWriter albumId3Writer;
|
||||
private IndexWriter songWriter;
|
||||
|
||||
public SearchService() {
|
||||
removeLocks();
|
||||
}
|
||||
|
||||
public void startIndexing() {
|
||||
try {
|
||||
artistWriter = createIndexWriter(ARTIST);
|
||||
artistId3Writer = createIndexWriter(ARTIST_ID3);
|
||||
albumWriter = createIndexWriter(ALBUM);
|
||||
albumId3Writer = createIndexWriter(ALBUM_ID3);
|
||||
songWriter = createIndexWriter(SONG);
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index.", x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(MediaFile mediaFile) {
|
||||
try {
|
||||
if (mediaFile.isFile()) {
|
||||
songWriter.addDocument(SONG.createDocument(mediaFile));
|
||||
} else if (mediaFile.isAlbum()) {
|
||||
albumWriter.addDocument(ALBUM.createDocument(mediaFile));
|
||||
} else {
|
||||
artistWriter.addDocument(ARTIST.createDocument(mediaFile));
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + mediaFile, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(Artist artist, MusicFolder musicFolder) {
|
||||
try {
|
||||
artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist, musicFolder));
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + artist, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(Album album) {
|
||||
try {
|
||||
albumId3Writer.addDocument(ALBUM_ID3.createDocument(album));
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + album, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopIndexing() {
|
||||
try {
|
||||
artistWriter.optimize();
|
||||
artistId3Writer.optimize();
|
||||
albumWriter.optimize();
|
||||
albumId3Writer.optimize();
|
||||
songWriter.optimize();
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(artistId3Writer);
|
||||
FileUtil.closeQuietly(artistWriter);
|
||||
FileUtil.closeQuietly(albumWriter);
|
||||
FileUtil.closeQuietly(albumId3Writer);
|
||||
FileUtil.closeQuietly(songWriter);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders, IndexType indexType) {
|
||||
SearchResult result = new SearchResult();
|
||||
int offset = criteria.getOffset();
|
||||
int count = criteria.getCount();
|
||||
result.setOffset(offset);
|
||||
|
||||
if (count <= 0) return result;
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = createIndexReader(indexType);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
Analyzer analyzer = new CustomAnalyzer();
|
||||
|
||||
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(LUCENE_VERSION, indexType.getFields(), analyzer, indexType.getBoosts());
|
||||
|
||||
BooleanQuery query = new BooleanQuery();
|
||||
query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST);
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId()))));
|
||||
} else {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
}
|
||||
query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST);
|
||||
|
||||
TopDocs topDocs = searcher.search(query, null, offset + count);
|
||||
result.setTotalHits(topDocs.totalHits);
|
||||
|
||||
int start = Math.min(offset, topDocs.totalHits);
|
||||
int end = Math.min(start + count, topDocs.totalHits);
|
||||
for (int i = start; i < end; i++) {
|
||||
Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
|
||||
switch (indexType) {
|
||||
case SONG:
|
||||
case ARTIST:
|
||||
case ALBUM:
|
||||
MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(mediaFile, result.getMediaFiles());
|
||||
break;
|
||||
case ARTIST_ID3:
|
||||
Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(artist, result.getArtists());
|
||||
break;
|
||||
case ALBUM_ID3:
|
||||
Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(album, result.getAlbums());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable x) {
|
||||
LOG.error("Failed to execute Lucene search.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String analyzeQuery(String query) throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
ASCIIFoldingFilter filter = new ASCIIFoldingFilter(new StandardTokenizer(LUCENE_VERSION, new StringReader(query)));
|
||||
TermAttribute termAttribute = filter.getAttribute(TermAttribute.class);
|
||||
while (filter.incrementToken()) {
|
||||
result.append(termAttribute.term()).append("* ");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of random songs.
|
||||
*
|
||||
* @param criteria Search criteria.
|
||||
* @return List of random songs.
|
||||
*/
|
||||
public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) {
|
||||
List<MediaFile> result = new ArrayList<MediaFile>();
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = createIndexReader(SONG);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
|
||||
BooleanQuery query = new BooleanQuery();
|
||||
query.add(new TermQuery(new Term(FIELD_MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), BooleanClause.Occur.MUST);
|
||||
if (criteria.getGenre() != null) {
|
||||
String genre = normalizeGenre(criteria.getGenre());
|
||||
query.add(new TermQuery(new Term(FIELD_GENRE, genre)), BooleanClause.Occur.MUST);
|
||||
}
|
||||
if (criteria.getFromYear() != null || criteria.getToYear() != null) {
|
||||
NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FIELD_YEAR, criteria.getFromYear(), criteria.getToYear(), true, true);
|
||||
query.add(rangeQuery, BooleanClause.Occur.MUST);
|
||||
}
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : criteria.getMusicFolders()) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST);
|
||||
|
||||
TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
|
||||
List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
|
||||
Random random = new Random(System.currentTimeMillis());
|
||||
|
||||
while (!scoreDocs.isEmpty() && result.size() < criteria.getCount()) {
|
||||
int index = random.nextInt(scoreDocs.size());
|
||||
Document doc = searcher.doc(scoreDocs.remove(index).doc);
|
||||
int id = Integer.valueOf(doc.get(FIELD_ID));
|
||||
try {
|
||||
addIfNotNull(mediaFileService.getMediaFile(id), result);
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to get media file " + id);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable x) {
|
||||
LOG.error("Failed to search or random songs.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String normalizeGenre(String genre) {
|
||||
return genre.toLowerCase().replace(" ", "").replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of random albums.
|
||||
*
|
||||
* @param count Number of albums to return.
|
||||
* @param musicFolders Only return albums from these folders.
|
||||
* @return List of random albums.
|
||||
*/
|
||||
public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) {
|
||||
List<MediaFile> result = new ArrayList<MediaFile>();
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = createIndexReader(ALBUM);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));
|
||||
|
||||
TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
|
||||
List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
|
||||
Random random = new Random(System.currentTimeMillis());
|
||||
|
||||
while (!scoreDocs.isEmpty() && result.size() < count) {
|
||||
int index = random.nextInt(scoreDocs.size());
|
||||
Document doc = searcher.doc(scoreDocs.remove(index).doc);
|
||||
int id = Integer.valueOf(doc.get(FIELD_ID));
|
||||
try {
|
||||
addIfNotNull(mediaFileService.getMediaFile(id), result);
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to get media file " + id, x);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable x) {
|
||||
LOG.error("Failed to search for random albums.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of random albums, using ID3 tag.
|
||||
*
|
||||
* @param count Number of albums to return.
|
||||
* @param musicFolders Only return albums from these folders.
|
||||
* @return List of random albums.
|
||||
*/
|
||||
public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) {
|
||||
List<Album> result = new ArrayList<Album>();
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = createIndexReader(ALBUM_ID3);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId()))));
|
||||
}
|
||||
Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));
|
||||
TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
|
||||
List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
|
||||
Random random = new Random(System.currentTimeMillis());
|
||||
|
||||
while (!scoreDocs.isEmpty() && result.size() < count) {
|
||||
int index = random.nextInt(scoreDocs.size());
|
||||
Document doc = searcher.doc(scoreDocs.remove(index).doc);
|
||||
int id = Integer.valueOf(doc.get(FIELD_ID));
|
||||
try {
|
||||
addIfNotNull(albumDao.getAlbum(id), result);
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to get album file " + id, x);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable x) {
|
||||
LOG.error("Failed to search for random albums.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public <T> ParamSearchResult<T> searchByName(String name, int offset, int count, List<MusicFolder> folderList, Class<T> clazz) {
|
||||
IndexType indexType = null;
|
||||
String field = null;
|
||||
if (clazz.isAssignableFrom(Album.class)) {
|
||||
indexType = IndexType.ALBUM_ID3;
|
||||
field = FIELD_ALBUM;
|
||||
} else if (clazz.isAssignableFrom(Artist.class)) {
|
||||
indexType = IndexType.ARTIST_ID3;
|
||||
field = FIELD_ARTIST;
|
||||
} else if (clazz.isAssignableFrom(MediaFile.class)) {
|
||||
indexType = IndexType.SONG;
|
||||
field = FIELD_TITLE;
|
||||
}
|
||||
ParamSearchResult<T> result = new ParamSearchResult<T>();
|
||||
// we only support album, artist, and song for now
|
||||
if (indexType == null || field == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.setOffset(offset);
|
||||
|
||||
IndexReader reader = null;
|
||||
|
||||
try {
|
||||
reader = createIndexReader(indexType);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
Analyzer analyzer = new CustomAnalyzer();
|
||||
QueryParser queryParser = new QueryParser(LUCENE_VERSION, field, analyzer);
|
||||
|
||||
Query q = queryParser.parse(name + "*");
|
||||
|
||||
Sort sort = new Sort(new SortField(field, SortField.STRING));
|
||||
TopDocs topDocs = searcher.search(q, null, offset + count, sort);
|
||||
result.setTotalHits(topDocs.totalHits);
|
||||
|
||||
int start = Math.min(offset, topDocs.totalHits);
|
||||
int end = Math.min(start + count, topDocs.totalHits);
|
||||
for (int i = start; i < end; i++) {
|
||||
Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
|
||||
switch (indexType) {
|
||||
case SONG:
|
||||
MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(clazz.cast(mediaFile), result.getItems());
|
||||
break;
|
||||
case ARTIST_ID3:
|
||||
Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(clazz.cast(artist), result.getItems());
|
||||
break;
|
||||
case ALBUM_ID3:
|
||||
Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID)));
|
||||
addIfNotNull(clazz.cast(album), result.getItems());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Throwable x) {
|
||||
LOG.error("Failed to execute Lucene search.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private <T> void addIfNotNull(T value, List<T> list) {
|
||||
if (value != null) {
|
||||
list.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private IndexWriter createIndexWriter(IndexType indexType) throws IOException {
|
||||
File dir = getIndexDirectory(indexType);
|
||||
return new IndexWriter(FSDirectory.open(dir), new CustomAnalyzer(), true, new IndexWriter.MaxFieldLength(10));
|
||||
}
|
||||
|
||||
private IndexReader createIndexReader(IndexType indexType) throws IOException {
|
||||
File dir = getIndexDirectory(indexType);
|
||||
return IndexReader.open(FSDirectory.open(dir), true);
|
||||
}
|
||||
|
||||
private File getIndexRootDirectory() {
|
||||
return new File(SettingsService.getAirsonicHome(), LUCENE_DIR);
|
||||
}
|
||||
|
||||
private File getIndexDirectory(IndexType indexType) {
|
||||
return new File(getIndexRootDirectory(), indexType.toString().toLowerCase());
|
||||
}
|
||||
|
||||
private void removeLocks() {
|
||||
for (IndexType indexType : IndexType.values()) {
|
||||
Directory dir = null;
|
||||
try {
|
||||
dir = FSDirectory.open(getIndexDirectory(indexType));
|
||||
if (IndexWriter.isLocked(dir)) {
|
||||
IndexWriter.unlock(dir);
|
||||
LOG.info("Removed Lucene lock file in " + dir);
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to remove Lucene lock file in " + dir, x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setMediaFileService(MediaFileService mediaFileService) {
|
||||
this.mediaFileService = mediaFileService;
|
||||
}
|
||||
|
||||
public void setArtistDao(ArtistDao artistDao) {
|
||||
this.artistDao = artistDao;
|
||||
}
|
||||
|
||||
public void setAlbumDao(AlbumDao albumDao) {
|
||||
this.albumDao = albumDao;
|
||||
}
|
||||
|
||||
public static enum IndexType {
|
||||
|
||||
SONG(new String[]{FIELD_TITLE, FIELD_ARTIST}, FIELD_TITLE) {
|
||||
@Override
|
||||
public Document createDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
|
||||
doc.add(new Field(FIELD_MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, Field.Index.ANALYZED_NO_NORMS));
|
||||
|
||||
if (mediaFile.getTitle() != null) {
|
||||
doc.add(new Field(FIELD_TITLE, mediaFile.getTitle(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getGenre() != null) {
|
||||
doc.add(new Field(FIELD_GENRE, normalizeGenre(mediaFile.getGenre()), Field.Store.NO, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getYear() != null) {
|
||||
doc.add(new NumericField(FIELD_YEAR, Field.Store.NO, true).setIntValue(mediaFile.getYear()));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
},
|
||||
|
||||
ALBUM(new String[]{FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER}, FIELD_ALBUM) {
|
||||
@Override
|
||||
public Document createDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
|
||||
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getAlbumName() != null) {
|
||||
doc.add(new Field(FIELD_ALBUM, mediaFile.getAlbumName(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
},
|
||||
|
||||
ALBUM_ID3(new String[]{FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER_ID}, FIELD_ALBUM) {
|
||||
@Override
|
||||
public Document createDocument(Album album) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(album.getId()));
|
||||
|
||||
if (album.getArtist() != null) {
|
||||
doc.add(new Field(FIELD_ARTIST, album.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (album.getName() != null) {
|
||||
doc.add(new Field(FIELD_ALBUM, album.getName(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (album.getFolderId() != null) {
|
||||
doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(album.getFolderId()));
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
},
|
||||
|
||||
ARTIST(new String[]{FIELD_ARTIST, FIELD_FOLDER}, null) {
|
||||
@Override
|
||||
public Document createDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
|
||||
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
},
|
||||
|
||||
ARTIST_ID3(new String[]{FIELD_ARTIST}, null) {
|
||||
@Override
|
||||
public Document createDocument(Artist artist, MusicFolder musicFolder) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(artist.getId()));
|
||||
doc.add(new Field(FIELD_ARTIST, artist.getName(), Field.Store.YES, Field.Index.ANALYZED));
|
||||
doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(musicFolder.getId()));
|
||||
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
|
||||
private final String[] fields;
|
||||
private final Map<String, Float> boosts;
|
||||
|
||||
private IndexType(String[] fields, String boostedField) {
|
||||
this.fields = fields;
|
||||
boosts = new HashMap<String, Float>();
|
||||
if (boostedField != null) {
|
||||
boosts.put(boostedField, 2.0F);
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
protected Document createDocument(MediaFile mediaFile) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
protected Document createDocument(Artist artist, MusicFolder musicFolder) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
protected Document createDocument(Album album) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public Map<String, Float> getBoosts() {
|
||||
return boosts;
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomAnalyzer extends StandardAnalyzer {
|
||||
private CustomAnalyzer() {
|
||||
super(LUCENE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenStream tokenStream(String fieldName, Reader reader) {
|
||||
TokenStream result = super.tokenStream(fieldName, reader);
|
||||
return new ASCIIFoldingFilter(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException {
|
||||
class SavedStreams {
|
||||
StandardTokenizer tokenStream;
|
||||
TokenStream filteredTokenStream;
|
||||
}
|
||||
|
||||
SavedStreams streams = (SavedStreams) getPreviousTokenStream();
|
||||
if (streams == null) {
|
||||
streams = new SavedStreams();
|
||||
setPreviousTokenStream(streams);
|
||||
streams.tokenStream = new StandardTokenizer(LUCENE_VERSION, reader);
|
||||
streams.filteredTokenStream = new StandardFilter(streams.tokenStream);
|
||||
streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream);
|
||||
streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream, STOP_WORDS_SET);
|
||||
streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream);
|
||||
} else {
|
||||
streams.tokenStream.reset(reader);
|
||||
}
|
||||
streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH);
|
||||
|
||||
return streams.filteredTokenStream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service;
|
||||
|
||||
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.ParamSearchResult;
|
||||
import org.airsonic.player.domain.RandomSearchCriteria;
|
||||
import org.airsonic.player.domain.SearchCriteria;
|
||||
import org.airsonic.player.domain.SearchResult;
|
||||
import org.airsonic.player.service.search.IndexType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Performs Lucene-based searching and indexing.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
* @see MediaScannerService
|
||||
*/
|
||||
public interface SearchService {
|
||||
|
||||
void startIndexing();
|
||||
|
||||
void index(MediaFile mediaFile);
|
||||
|
||||
void index(Artist artist, MusicFolder musicFolder);
|
||||
|
||||
void index(Album album);
|
||||
|
||||
void stopIndexing();
|
||||
|
||||
SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders,
|
||||
IndexType indexType);
|
||||
|
||||
/**
|
||||
* Returns a number of random songs.
|
||||
*
|
||||
* @param criteria Search criteria.
|
||||
* @return List of random songs.
|
||||
*/
|
||||
List<MediaFile> getRandomSongs(RandomSearchCriteria criteria);
|
||||
|
||||
/**
|
||||
* Returns a number of random albums.
|
||||
*
|
||||
* @param count Number of albums to return.
|
||||
* @param musicFolders Only return albums from these folders.
|
||||
* @return List of random albums.
|
||||
*/
|
||||
List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders);
|
||||
|
||||
/**
|
||||
* Returns a number of random albums, using ID3 tag.
|
||||
*
|
||||
* @param count Number of albums to return.
|
||||
* @param musicFolders Only return albums from these folders.
|
||||
* @return List of random albums.
|
||||
*/
|
||||
List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders);
|
||||
|
||||
<T> ParamSearchResult<T> searchByName(
|
||||
String name, int offset, int count, List<MusicFolder> folderList, Class<T> clazz);
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.airsonic.player.domain.AlbumListType;
|
||||
import org.airsonic.player.domain.MediaFile;
|
||||
import org.airsonic.player.domain.Playlist;
|
||||
import org.airsonic.player.domain.User;
|
||||
import org.airsonic.player.service.search.IndexType;
|
||||
import org.airsonic.player.service.sonos.SonosHelper;
|
||||
import org.airsonic.player.service.sonos.SonosServiceRegistration;
|
||||
import org.airsonic.player.service.sonos.SonosSoapFault;
|
||||
@@ -264,13 +265,13 @@ public class SonosService implements SonosSoap {
|
||||
public SearchResponse search(Search parameters) {
|
||||
String id = parameters.getId();
|
||||
|
||||
SearchService.IndexType indexType;
|
||||
IndexType indexType;
|
||||
if (ID_SEARCH_ARTISTS.equals(id)) {
|
||||
indexType = SearchService.IndexType.ARTIST;
|
||||
indexType = IndexType.ARTIST;
|
||||
} else if (ID_SEARCH_ALBUMS.equals(id)) {
|
||||
indexType = SearchService.IndexType.ALBUM;
|
||||
indexType = IndexType.ALBUM;
|
||||
} else if (ID_SEARCH_SONGS.equals(id)) {
|
||||
indexType = SearchService.IndexType.SONG;
|
||||
indexType = IndexType.SONG;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid search category: " + id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import org.apache.lucene.analysis.ASCIIFoldingFilter;
|
||||
import org.apache.lucene.analysis.Analyzer;
|
||||
import org.apache.lucene.analysis.LowerCaseFilter;
|
||||
import org.apache.lucene.analysis.StopFilter;
|
||||
import org.apache.lucene.analysis.TokenStream;
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.analysis.standard.StandardFilter;
|
||||
import org.apache.lucene.analysis.standard.StandardTokenizer;
|
||||
import org.apache.lucene.util.Version;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
|
||||
/**
|
||||
* Analyzer provider.
|
||||
* This class is a division of what was once part of SearchService and added functionality.
|
||||
* This class provides Analyzer which is used at index generation
|
||||
* and QueryAnalyzer which analyzes the specified query at search time.
|
||||
* Analyzer can be closed but is a reuse premise.
|
||||
* It is held in this class.
|
||||
*/
|
||||
@Component
|
||||
public final class AnalyzerFactory {
|
||||
|
||||
private Analyzer analyzer;
|
||||
|
||||
private Analyzer queryAnalyzer;
|
||||
|
||||
/**
|
||||
* Return analyzer.
|
||||
*
|
||||
* @return analyzer for index
|
||||
*/
|
||||
public Analyzer getAnalyzer() {
|
||||
if (null == analyzer) {
|
||||
analyzer = new CustomAnalyzer();
|
||||
}
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return analyzer.
|
||||
*
|
||||
* @return analyzer for index
|
||||
*/
|
||||
public Analyzer getQueryAnalyzer() {
|
||||
if (null == queryAnalyzer) {
|
||||
queryAnalyzer = new CustomAnalyzer();
|
||||
}
|
||||
return queryAnalyzer;
|
||||
}
|
||||
|
||||
/*
|
||||
* The legacy CustomAnalyzer implementation is kept as it is.
|
||||
*/
|
||||
private class CustomAnalyzer extends StandardAnalyzer {
|
||||
private CustomAnalyzer() {
|
||||
/*
|
||||
* Version.LUCENE_30
|
||||
* It is a transient description and will be deleted when upgrading the version.
|
||||
* SearchService variables are not used because the reference direction conflicts.
|
||||
*/
|
||||
super(Version.LUCENE_30);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenStream tokenStream(String fieldName, Reader reader) {
|
||||
TokenStream result = super.tokenStream(fieldName, reader);
|
||||
return new ASCIIFoldingFilter(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException {
|
||||
class SavedStreams {
|
||||
StandardTokenizer tokenStream;
|
||||
TokenStream filteredTokenStream;
|
||||
}
|
||||
|
||||
SavedStreams streams = (SavedStreams) getPreviousTokenStream();
|
||||
if (streams == null) {
|
||||
streams = new SavedStreams();
|
||||
setPreviousTokenStream(streams);
|
||||
streams.tokenStream = new StandardTokenizer(Version.LUCENE_30, reader);
|
||||
streams.filteredTokenStream = new StandardFilter(streams.tokenStream);
|
||||
streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream);
|
||||
streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream,
|
||||
STOP_WORDS_SET);
|
||||
streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream);
|
||||
} else {
|
||||
streams.tokenStream.reset(reader);
|
||||
}
|
||||
streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH);
|
||||
|
||||
return streams.filteredTokenStream;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
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.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.NumericField;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* A factory that generates the documents to be stored in the index.
|
||||
*/
|
||||
@Component
|
||||
public class DocumentFactory {
|
||||
|
||||
/**
|
||||
* Normalize the genre string.
|
||||
*
|
||||
* @param genre genre string
|
||||
* @return genre string normalized
|
||||
* @deprecated should be resolved with tokenizer or filter
|
||||
*/
|
||||
@Deprecated
|
||||
private String normalizeGenre(String genre) {
|
||||
return genre.toLowerCase().replace(" ", "").replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param mediaFile target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createAlbumDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false)
|
||||
.setIntValue(mediaFile.getId()));
|
||||
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getAlbumName() != null) {
|
||||
doc.add(new Field(FieldNames.ALBUM, mediaFile.getAlbumName(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO,
|
||||
Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param mediaFile target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createArtistDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false)
|
||||
.setIntValue(mediaFile.getId()));
|
||||
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO,
|
||||
Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param album target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createAlbumId3Document(Album album) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false).setIntValue(album.getId()));
|
||||
|
||||
if (album.getArtist() != null) {
|
||||
doc.add(new Field(FieldNames.ARTIST, album.getArtist(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (album.getName() != null) {
|
||||
doc.add(new Field(FieldNames.ALBUM, album.getName(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (album.getFolderId() != null) {
|
||||
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true)
|
||||
.setIntValue(album.getFolderId()));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param artist target of document
|
||||
* @param musicFolder target folder exists
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createArtistId3Document(Artist artist, MusicFolder musicFolder) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false)
|
||||
.setIntValue(artist.getId()));
|
||||
doc.add(new Field(FieldNames.ARTIST, artist.getName(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
doc.add(new NumericField(FieldNames.FOLDER_ID, Field.Store.NO, true)
|
||||
.setIntValue(musicFolder.getId()));
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a document.
|
||||
*
|
||||
* @param mediaFile target of document
|
||||
* @return document
|
||||
* @since legacy
|
||||
*/
|
||||
public Document createSongDocument(MediaFile mediaFile) {
|
||||
Document doc = new Document();
|
||||
doc.add(new NumericField(FieldNames.ID, Field.Store.YES, false)
|
||||
.setIntValue(mediaFile.getId()));
|
||||
doc.add(new Field(FieldNames.MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO,
|
||||
Field.Index.ANALYZED_NO_NORMS));
|
||||
if (mediaFile.getTitle() != null) {
|
||||
doc.add(new Field(FieldNames.TITLE, mediaFile.getTitle(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getArtist() != null) {
|
||||
doc.add(new Field(FieldNames.ARTIST, mediaFile.getArtist(), Field.Store.YES,
|
||||
Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getGenre() != null) {
|
||||
doc.add(new Field(FieldNames.GENRE, normalizeGenre(mediaFile.getGenre()),
|
||||
Field.Store.NO, Field.Index.ANALYZED));
|
||||
}
|
||||
if (mediaFile.getYear() != null) {
|
||||
doc.add(new NumericField(FieldNames.YEAR, Field.Store.NO, true)
|
||||
.setIntValue(mediaFile.getYear()));
|
||||
}
|
||||
if (mediaFile.getFolder() != null) {
|
||||
doc.add(new Field(FieldNames.FOLDER, mediaFile.getFolder(), Field.Store.NO,
|
||||
Field.Index.NOT_ANALYZED_NO_NORMS));
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
/**
|
||||
* Enum that symbolizes the field name used for lucene index.
|
||||
* This class is a division of what was once part of SearchService and added functionality.
|
||||
*/
|
||||
class FieldNames {
|
||||
|
||||
private FieldNames() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, id field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String ID = "id";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, id field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String FOLDER_ID = "folderId";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, numeric field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String YEAR = "year";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, key field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String GENRE = "genre";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, key field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String MEDIA_TYPE = "mediaType";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, key field.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String FOLDER = "folder";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, usually with common word parsing.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String ARTIST = "artist";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, usually with common word parsing.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String ALBUM = "album";
|
||||
|
||||
/**
|
||||
* A field same to a legacy server, usually with common word parsing.
|
||||
*
|
||||
* @since legacy
|
||||
**/
|
||||
public static final String TITLE = "title";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
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.service.SettingsService;
|
||||
import org.airsonic.player.util.FileUtil;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.airsonic.player.service.search.IndexType.ALBUM;
|
||||
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3;
|
||||
import static org.airsonic.player.service.search.IndexType.ARTIST;
|
||||
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3;
|
||||
import static org.airsonic.player.service.search.IndexType.SONG;
|
||||
|
||||
/**
|
||||
* Function class that is strongly linked to the lucene index implementation.
|
||||
* Legacy has an implementation in SearchService.
|
||||
*
|
||||
* If the index CRUD and search functionality are in the same class,
|
||||
* there is often a dependency conflict on the class used.
|
||||
* Although the interface of SearchService is left to maintain the legacy implementation,
|
||||
* it is desirable that methods of index operations other than search essentially use this class directly.
|
||||
*/
|
||||
@Component
|
||||
public class IndexManager {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IndexManager.class);
|
||||
|
||||
@Autowired
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
@Autowired
|
||||
private DocumentFactory documentFactory;
|
||||
|
||||
private IndexWriter artistWriter;
|
||||
private IndexWriter artistId3Writer;
|
||||
private IndexWriter albumWriter;
|
||||
private IndexWriter albumId3Writer;
|
||||
private IndexWriter songWriter;
|
||||
|
||||
public void index(Album album) {
|
||||
try {
|
||||
albumId3Writer.addDocument(documentFactory.createAlbumId3Document(album));
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + album, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(Artist artist, MusicFolder musicFolder) {
|
||||
try {
|
||||
artistId3Writer
|
||||
.addDocument(documentFactory.createArtistId3Document(artist, musicFolder));
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + artist, x);
|
||||
}
|
||||
}
|
||||
|
||||
public void index(MediaFile mediaFile) {
|
||||
try {
|
||||
if (mediaFile.isFile()) {
|
||||
songWriter.addDocument(documentFactory.createSongDocument(mediaFile));
|
||||
} else if (mediaFile.isAlbum()) {
|
||||
albumWriter.addDocument(documentFactory.createAlbumDocument(mediaFile));
|
||||
} else {
|
||||
artistWriter.addDocument(documentFactory.createArtistDocument(mediaFile));
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index for " + mediaFile, x);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String LUCENE_DIR = "lucene2";
|
||||
|
||||
public IndexReader createIndexReader(IndexType indexType) throws IOException {
|
||||
File dir = getIndexDirectory(indexType);
|
||||
return IndexReader.open(FSDirectory.open(dir), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* It is static as an intermediate response of the transition period.
|
||||
* (It is called before injection because it is called by SearchService constructor)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static File getIndexRootDirectory() {
|
||||
return new File(SettingsService.getAirsonicHome(), LUCENE_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make it public as an interim response of the transition period.
|
||||
* (It is called before the injection because it is called in the SearchService constructor.)
|
||||
*
|
||||
* @param indexType
|
||||
* @return
|
||||
* @deprecated It should not be called from outside.
|
||||
*/
|
||||
@Deprecated
|
||||
public static File getIndexDirectory(IndexType indexType) {
|
||||
return new File(getIndexRootDirectory(), indexType.toString().toLowerCase());
|
||||
}
|
||||
|
||||
private IndexWriter createIndexWriter(IndexType indexType) throws IOException {
|
||||
File dir = getIndexDirectory(indexType);
|
||||
return new IndexWriter(FSDirectory.open(dir), analyzerFactory.getAnalyzer(), true,
|
||||
new IndexWriter.MaxFieldLength(10));
|
||||
}
|
||||
|
||||
public final void startIndexing() {
|
||||
try {
|
||||
artistWriter = createIndexWriter(ARTIST);
|
||||
artistId3Writer = createIndexWriter(ARTIST_ID3);
|
||||
albumWriter = createIndexWriter(ALBUM);
|
||||
albumId3Writer = createIndexWriter(ALBUM_ID3);
|
||||
songWriter = createIndexWriter(SONG);
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index.", x);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopIndexing() {
|
||||
try {
|
||||
artistWriter.optimize();
|
||||
artistId3Writer.optimize();
|
||||
albumWriter.optimize();
|
||||
albumId3Writer.optimize();
|
||||
songWriter.optimize();
|
||||
} catch (Exception x) {
|
||||
LOG.error("Failed to create search index.", x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(artistId3Writer);
|
||||
FileUtil.closeQuietly(artistWriter);
|
||||
FileUtil.closeQuietly(albumWriter);
|
||||
FileUtil.closeQuietly(albumId3Writer);
|
||||
FileUtil.closeQuietly(songWriter);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Enum that symbolizes the each lucene index entity.
|
||||
* This class is a division of what was once part of SearchService and added functionality.
|
||||
* @since legacy
|
||||
*/
|
||||
public enum IndexType {
|
||||
|
||||
/*
|
||||
* Boosts is a factor for search scores, which is 1 by default.
|
||||
*/
|
||||
SONG(
|
||||
fieldNames(
|
||||
FieldNames.TITLE,
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.TITLE, 2F))),
|
||||
|
||||
ALBUM(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER),
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ALBUM_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ALBUM,
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER_ID),
|
||||
boosts(
|
||||
entry(FieldNames.ALBUM, 2F))),
|
||||
|
||||
ARTIST(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST,
|
||||
FieldNames.FOLDER),
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 1F))),
|
||||
|
||||
ARTIST_ID3(
|
||||
fieldNames(
|
||||
FieldNames.ARTIST),
|
||||
boosts(
|
||||
entry(FieldNames.ARTIST, 2F))),
|
||||
|
||||
;
|
||||
|
||||
@SafeVarargs
|
||||
private static final Map<String, Float> boosts(SimpleEntry<String, Float>... entry) {
|
||||
Map<String, Float> m = new HashMap<>();
|
||||
Arrays.stream(entry).forEach(kv -> m.put(kv.getKey(), kv.getValue()));
|
||||
return Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
/*
|
||||
* The current state is implemented to set the same value as legacy.
|
||||
* However unlike legacy, it has been changed
|
||||
* so that different values can be set for each field.
|
||||
* When setting two or more boost values,
|
||||
* it is desirable to differentiate the values.
|
||||
*/
|
||||
private static final SimpleEntry<String, Float> entry(String k, float v) {
|
||||
return new AbstractMap.SimpleEntry<>(k, v);
|
||||
}
|
||||
|
||||
private static final String[] fieldNames(String... names) {
|
||||
return Arrays.stream(names).toArray(String[]::new);
|
||||
}
|
||||
|
||||
private final Map<String, Float> boosts;
|
||||
|
||||
private final String[] fields;
|
||||
|
||||
private IndexType(String[] fieldNames, Map<String, Float> boosts) {
|
||||
this.fields = fieldNames;
|
||||
this.boosts = boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of fields and boost values.
|
||||
*
|
||||
* @return map of fields and boost values
|
||||
* @since legacy
|
||||
*/
|
||||
/*
|
||||
* See the lucene documentation for boost specifications.
|
||||
*/
|
||||
public Map<String, Float> getBoosts() {
|
||||
return boosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return some of the fields defined in the index.
|
||||
*
|
||||
* @return Fields mainly used in multi-field search
|
||||
* @since legacy
|
||||
*/
|
||||
/*
|
||||
* It maintains a fairly early implementation
|
||||
* and can be considered as an argument of MultiFieldQueryParser.
|
||||
* In fact, the fields and boosts used in the search are difficult topics
|
||||
* that can be determined by the search requirements.
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import org.airsonic.player.domain.MediaFile;
|
||||
import org.airsonic.player.domain.MusicFolder;
|
||||
import org.airsonic.player.domain.RandomSearchCriteria;
|
||||
import org.airsonic.player.domain.SearchCriteria;
|
||||
import org.apache.lucene.analysis.ASCIIFoldingFilter;
|
||||
import org.apache.lucene.analysis.standard.StandardTokenizer;
|
||||
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.queryParser.MultiFieldQueryParser;
|
||||
import org.apache.lucene.queryParser.ParseException;
|
||||
import org.apache.lucene.queryParser.QueryParser;
|
||||
import org.apache.lucene.search.BooleanClause;
|
||||
import org.apache.lucene.search.BooleanQuery;
|
||||
import org.apache.lucene.search.NumericRangeQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.TermQuery;
|
||||
import org.apache.lucene.search.spans.SpanOrQuery;
|
||||
import org.apache.lucene.search.spans.SpanQuery;
|
||||
import org.apache.lucene.search.spans.SpanTermQuery;
|
||||
import org.apache.lucene.util.NumericUtils;
|
||||
import org.apache.lucene.util.Version;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.airsonic.player.service.search.IndexType.ALBUM_ID3;
|
||||
import static org.airsonic.player.service.search.IndexType.ARTIST_ID3;
|
||||
|
||||
/**
|
||||
* Factory class of Lucene Query.
|
||||
* This class is an extract of the functionality that was once part of SearchService.
|
||||
* It is for maintainability and verification.
|
||||
* Each corresponds to the SearchService method.
|
||||
* The API syntax for query generation depends on the lucene version.
|
||||
* verification with query grammar is possible.
|
||||
* On the other hand, the generated queries are relatively small by version.
|
||||
* Therefore, test cases of this class are useful for large version upgrades.
|
||||
**/
|
||||
@Component
|
||||
public class QueryFactory {
|
||||
|
||||
@Autowired
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
private String analyzeQuery(String query) throws IOException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
/*
|
||||
* Version.LUCENE_30
|
||||
* It is a transient description and will be deleted when upgrading the version.
|
||||
* SearchService variables are not used because the reference direction conflicts.
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
ASCIIFoldingFilter filter = new ASCIIFoldingFilter(
|
||||
new StandardTokenizer(Version.LUCENE_30, new StringReader(query)));
|
||||
TermAttribute termAttribute = filter.getAttribute(TermAttribute.class);
|
||||
while (filter.incrementToken()) {
|
||||
result.append(termAttribute.term()).append("* ");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the genre string.
|
||||
*
|
||||
* @param genre genre string
|
||||
* @return genre string normalized
|
||||
* @deprecated should be resolved with tokenizer or filter
|
||||
*/
|
||||
@Deprecated
|
||||
private String normalizeGenre(String genre) {
|
||||
return genre.toLowerCase().replace(" ", "").replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#search(SearchCriteria, List, IndexType)}.
|
||||
*
|
||||
* @param criteria criteria
|
||||
* @param musicFolders musicFolders
|
||||
* @param indexType {@link IndexType}
|
||||
* @return Query
|
||||
* @throws IOException When parsing of MultiFieldQueryParser fails
|
||||
* @throws ParseException When parsing of MultiFieldQueryParser fails
|
||||
*/
|
||||
public Query search(SearchCriteria criteria, List<MusicFolder> musicFolders,
|
||||
IndexType indexType) throws ParseException, IOException {
|
||||
/*
|
||||
* Version.LUCENE_30
|
||||
* It is a transient description and will be deleted when upgrading the version.
|
||||
* SearchService variables are not used because the reference direction conflicts.
|
||||
*/
|
||||
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_30,
|
||||
indexType.getFields(), analyzerFactory.getQueryAnalyzer(), indexType.getBoosts());
|
||||
|
||||
BooleanQuery query = new BooleanQuery();
|
||||
query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST);
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID,
|
||||
NumericUtils.intToPrefixCoded(musicFolder.getId()))));
|
||||
} else {
|
||||
musicFolderQueries.add(new SpanTermQuery(
|
||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
}
|
||||
query.add(
|
||||
new SpanOrQuery(
|
||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])),
|
||||
BooleanClause.Occur.MUST);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#getRandomSongs(RandomSearchCriteria)}.
|
||||
*
|
||||
* @param criteria criteria
|
||||
* @return Query
|
||||
*/
|
||||
public Query getRandomSongs(RandomSearchCriteria criteria) {
|
||||
BooleanQuery query = new BooleanQuery();
|
||||
query.add(new TermQuery(
|
||||
new Term(FieldNames.MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())),
|
||||
BooleanClause.Occur.MUST);
|
||||
if (criteria.getGenre() != null) {
|
||||
String genre = normalizeGenre(criteria.getGenre());
|
||||
query.add(new TermQuery(new Term(FieldNames.GENRE, genre)), BooleanClause.Occur.MUST);
|
||||
}
|
||||
if (criteria.getFromYear() != null || criteria.getToYear() != null) {
|
||||
NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FieldNames.YEAR,
|
||||
criteria.getFromYear(), criteria.getToYear(), true, true);
|
||||
query.add(rangeQuery, BooleanClause.Occur.MUST);
|
||||
}
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : criteria.getMusicFolders()) {
|
||||
musicFolderQueries.add(new SpanTermQuery(
|
||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
query.add(
|
||||
new SpanOrQuery(
|
||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])),
|
||||
BooleanClause.Occur.MUST);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#searchByName( String, String, int, int, List, Class)}.
|
||||
*
|
||||
* @param fieldName {@link FieldNames}
|
||||
* @return Query
|
||||
* @throws ParseException When parsing of QueryParser fails
|
||||
*/
|
||||
public Query searchByName(String fieldName, String name) throws ParseException {
|
||||
/*
|
||||
* Version.LUCENE_30
|
||||
* It is a transient description and will be deleted when upgrading the version.
|
||||
* SearchService variables are not used because the reference direction conflicts.
|
||||
*/
|
||||
QueryParser queryParser = new QueryParser(Version.LUCENE_30, fieldName,
|
||||
analyzerFactory.getQueryAnalyzer());
|
||||
Query query = queryParser.parse(name + "*");
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#getRandomAlbums(int, List)}.
|
||||
*
|
||||
* @param musicFolders musicFolders
|
||||
* @return Query
|
||||
*/
|
||||
public Query getRandomAlbums(List<MusicFolder> musicFolders) {
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
musicFolderQueries.add(new SpanTermQuery(
|
||||
new Term(FieldNames.FOLDER, musicFolder.getPath().getPath())));
|
||||
}
|
||||
Query query = new SpanOrQuery(
|
||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generation expression extracted from
|
||||
* {@link org.airsonic.player.service.SearchService#getRandomAlbumsId3(int, List)}.
|
||||
*
|
||||
* @param musicFolders musicFolders
|
||||
* @return Query
|
||||
*/
|
||||
public Query getRandomAlbumsId3(List<MusicFolder> musicFolders) {
|
||||
|
||||
List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
|
||||
for (MusicFolder musicFolder : musicFolders) {
|
||||
musicFolderQueries.add(new SpanTermQuery(new Term(FieldNames.FOLDER_ID,
|
||||
NumericUtils.intToPrefixCoded(musicFolder.getId()))));
|
||||
}
|
||||
Query query = new SpanOrQuery(
|
||||
musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import org.airsonic.player.domain.*;
|
||||
import org.airsonic.player.service.SearchService;
|
||||
import org.airsonic.player.util.FileUtil;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.queryParser.ParseException;
|
||||
import org.apache.lucene.search.*;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.airsonic.player.service.search.IndexType.*;
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
@Service
|
||||
public class SearchServiceImpl implements SearchService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SearchServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private QueryFactory queryFactory;
|
||||
@Autowired
|
||||
private IndexManager indexManager;
|
||||
@Autowired
|
||||
private SearchServiceUtilities util;
|
||||
|
||||
// TODO Should be changed to SecureRandom?
|
||||
private final Random random = new Random(System.currentTimeMillis());
|
||||
|
||||
public SearchServiceImpl() {
|
||||
removeLocks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startIndexing() {
|
||||
indexManager.startIndexing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void index(MediaFile mediaFile) {
|
||||
indexManager.index(mediaFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void index(Artist artist, MusicFolder musicFolder) {
|
||||
indexManager.index(artist, musicFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void index(Album album) {
|
||||
indexManager.index(album);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopIndexing() {
|
||||
indexManager.stopIndexing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(SearchCriteria criteria, List<MusicFolder> musicFolders,
|
||||
IndexType indexType) {
|
||||
|
||||
SearchResult result = new SearchResult();
|
||||
int offset = criteria.getOffset();
|
||||
int count = criteria.getCount();
|
||||
result.setOffset(offset);
|
||||
|
||||
if (count <= 0)
|
||||
return result;
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
|
||||
reader = indexManager.createIndexReader(indexType);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
Query query = queryFactory.search(criteria, musicFolders, indexType);
|
||||
|
||||
TopDocs topDocs = searcher.search(query, null, offset + count);
|
||||
result.setTotalHits(topDocs.totalHits);
|
||||
int start = Math.min(offset, topDocs.totalHits);
|
||||
int end = Math.min(start + count, topDocs.totalHits);
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
util.addIfAnyMatch(result, indexType, searcher.doc(topDocs.scoreDocs[i].doc));
|
||||
}
|
||||
|
||||
} catch (IOException | ParseException e) {
|
||||
LOG.error("Failed to execute Lucene search.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common processing of random method.
|
||||
*
|
||||
* @param count Number of albums to return.
|
||||
* @param searcher
|
||||
* @param query
|
||||
* @param id2ListCallBack Callback to get D from id and store it in List
|
||||
* @return result
|
||||
* @throws IOException
|
||||
*/
|
||||
private final <D> List<D> createRandomDocsList(
|
||||
int count, Searcher searcher, Query query, BiConsumer<List<D>, Integer> id2ListCallBack)
|
||||
throws IOException {
|
||||
|
||||
List<Integer> docs = Arrays
|
||||
.stream(searcher.search(query, Integer.MAX_VALUE).scoreDocs)
|
||||
.map(sd -> sd.doc)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<D> result = new ArrayList<>();
|
||||
while (!docs.isEmpty() && result.size() < count) {
|
||||
int randomPos = random.nextInt(docs.size());
|
||||
Document document = searcher.doc(docs.get(randomPos));
|
||||
id2ListCallBack.accept(result, util.getId.apply(document));
|
||||
docs.remove(randomPos);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) {
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = indexManager.createIndexReader(SONG);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
if (isEmpty(searcher)) {
|
||||
// At first start
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Query query = queryFactory.getRandomSongs(criteria);
|
||||
return createRandomDocsList(criteria.getCount(), searcher, query,
|
||||
(dist, id) -> util.addIgnoreNull(dist, SONG, id));
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to search or random songs.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) {
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = indexManager.createIndexReader(ALBUM);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
if (isEmpty(searcher)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Query query = queryFactory.getRandomAlbums(musicFolders);
|
||||
return createRandomDocsList(count, searcher, query,
|
||||
(dist, id) -> util.addIgnoreNull(dist, ALBUM, id));
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to search for random albums.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) {
|
||||
|
||||
IndexReader reader = null;
|
||||
try {
|
||||
reader = indexManager.createIndexReader(ALBUM_ID3);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
if (isEmpty(searcher)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Query query = queryFactory.getRandomAlbumsId3(musicFolders);
|
||||
return createRandomDocsList(count, searcher, query,
|
||||
(dist, id) -> util.addIgnoreNull(dist, ALBUM_ID3, id));
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to search for random albums.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ParamSearchResult<T> searchByName(String name, int offset, int count,
|
||||
List<MusicFolder> folderList, Class<T> assignableClass) {
|
||||
|
||||
// we only support album, artist, and song for now
|
||||
@Nullable
|
||||
IndexType indexType = util.getIndexType.apply(assignableClass);
|
||||
@Nullable
|
||||
String fieldName = util.getFieldName.apply(assignableClass);
|
||||
|
||||
ParamSearchResult<T> result = new ParamSearchResult<T>();
|
||||
result.setOffset(offset);
|
||||
|
||||
if (isEmpty(indexType) || isEmpty(fieldName) || count <= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
IndexReader reader = null;
|
||||
|
||||
try {
|
||||
reader = indexManager.createIndexReader(indexType);
|
||||
Searcher searcher = new IndexSearcher(reader);
|
||||
Query query = queryFactory.searchByName(fieldName, name);
|
||||
|
||||
Sort sort = new Sort(new SortField(fieldName, SortField.STRING));
|
||||
TopDocs topDocs = searcher.search(query, null, offset + count, sort);
|
||||
|
||||
result.setTotalHits(topDocs.totalHits);
|
||||
int start = Math.min(offset, topDocs.totalHits);
|
||||
int end = Math.min(start + count, topDocs.totalHits);
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
|
||||
util.addIgnoreNull(result, indexType, util.getId.apply(doc), assignableClass);
|
||||
}
|
||||
|
||||
} catch (IOException | ParseException e) {
|
||||
LOG.error("Failed to execute Lucene search.", e);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(reader);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks are managed automatically by the framework.
|
||||
*
|
||||
* @deprecated It becomes unnecessary at the time of version upgrade.
|
||||
*/
|
||||
@Deprecated
|
||||
public void removeLocks() {
|
||||
for (IndexType indexType : IndexType.values()) {
|
||||
Directory dir = null;
|
||||
try {
|
||||
/*
|
||||
* Static access to the accompanying method is performed as a transition period.
|
||||
* (Unnecessary processing after updating Lucene.)
|
||||
*/
|
||||
dir = FSDirectory.open(IndexManager.getIndexDirectory(indexType));
|
||||
if (IndexWriter.isLocked(dir)) {
|
||||
IndexWriter.unlock(dir);
|
||||
LOG.info("Removed Lucene lock file in " + dir);
|
||||
}
|
||||
} catch (Exception x) {
|
||||
LOG.warn("Failed to remove Lucene lock file in " + dir, x);
|
||||
} finally {
|
||||
FileUtil.closeQuietly(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
This file is part of Airsonic.
|
||||
|
||||
Airsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Airsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2016 (C) Airsonic Authors
|
||||
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
|
||||
package org.airsonic.player.service.search;
|
||||
|
||||
import org.airsonic.player.dao.AlbumDao;
|
||||
import org.airsonic.player.dao.ArtistDao;
|
||||
import org.airsonic.player.domain.Album;
|
||||
import org.airsonic.player.domain.Artist;
|
||||
import org.airsonic.player.domain.MediaFile;
|
||||
import org.airsonic.player.domain.ParamSearchResult;
|
||||
import org.airsonic.player.domain.SearchResult;
|
||||
import org.airsonic.player.service.MediaFileService;
|
||||
import org.airsonic.player.service.SettingsService;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
|
||||
/**
|
||||
* Termination used by SearchService.
|
||||
*
|
||||
* Since SearchService operates as a proxy for storage (DB) using lucene,
|
||||
* there are many redundant descriptions different from essential data processing.
|
||||
* This class is a transfer class for saving those redundant descriptions.
|
||||
*
|
||||
* Exception handling is not termination,
|
||||
* so do not include exception handling in this class.
|
||||
*/
|
||||
@Component
|
||||
public class SearchServiceUtilities {
|
||||
|
||||
/* Search by id only. */
|
||||
@Autowired
|
||||
private ArtistDao artistDao;
|
||||
|
||||
/* Search by id only. */
|
||||
@Autowired
|
||||
private AlbumDao albumDao;
|
||||
|
||||
/*
|
||||
* Search by id only.
|
||||
* Although there is no influence at present,
|
||||
* mediaFileService has a caching mechanism.
|
||||
* Service is used instead of Dao until you are sure you need to use mediaFileDao.
|
||||
*/
|
||||
@Autowired
|
||||
private MediaFileService mediaFileService;
|
||||
|
||||
public final Function<Long, Integer> round = (i) -> {
|
||||
// return
|
||||
// NumericUtils.floatToSortableInt(i);
|
||||
return i.intValue();
|
||||
};
|
||||
|
||||
public final Function<Document, Integer> getId = d -> {
|
||||
return Integer.valueOf(d.get(FieldNames.ID));
|
||||
};
|
||||
|
||||
public final BiConsumer<List<MediaFile>, Integer> addMediaFileIfAnyMatch = (dist, id) -> {
|
||||
if (!dist.stream().anyMatch(m -> id == m.getId())) {
|
||||
MediaFile mediaFile = mediaFileService.getMediaFile(id);
|
||||
if (!isEmpty(mediaFile)) {
|
||||
dist.add(mediaFile);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public final BiConsumer<List<Artist>, Integer> addArtistId3IfAnyMatch = (dist, id) -> {
|
||||
if (!dist.stream().anyMatch(a -> id == a.getId())) {
|
||||
Artist artist = artistDao.getArtist(id);
|
||||
if (!isEmpty(artist)) {
|
||||
dist.add(artist);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public final Function<Class<?>, @Nullable IndexType> getIndexType = (assignableClass) -> {
|
||||
IndexType indexType = null;
|
||||
if (assignableClass.isAssignableFrom(Album.class)) {
|
||||
indexType = IndexType.ALBUM_ID3;
|
||||
} else if (assignableClass.isAssignableFrom(Artist.class)) {
|
||||
indexType = IndexType.ARTIST_ID3;
|
||||
} else if (assignableClass.isAssignableFrom(MediaFile.class)) {
|
||||
indexType = IndexType.SONG;
|
||||
}
|
||||
return indexType;
|
||||
};
|
||||
|
||||
public final Function<Class<?>, @Nullable String> getFieldName = (assignableClass) -> {
|
||||
String fieldName = null;
|
||||
if (assignableClass.isAssignableFrom(Album.class)) {
|
||||
fieldName = FieldNames.ALBUM;
|
||||
} else if (assignableClass.isAssignableFrom(Artist.class)) {
|
||||
fieldName = FieldNames.ARTIST;
|
||||
} else if (assignableClass.isAssignableFrom(MediaFile.class)) {
|
||||
fieldName = FieldNames.TITLE;
|
||||
}
|
||||
return fieldName;
|
||||
};
|
||||
|
||||
public final BiConsumer<List<Album>, Integer> addAlbumId3IfAnyMatch = (dist, subjectId) -> {
|
||||
if (!dist.stream().anyMatch(a -> subjectId == a.getId())) {
|
||||
Album album = albumDao.getAlbum(subjectId);
|
||||
if (!isEmpty(album)) {
|
||||
dist.add(album);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Function<String, File> getRootDirectory = (version) -> {
|
||||
return new File(SettingsService.getAirsonicHome(), version);
|
||||
};
|
||||
|
||||
public final BiFunction<String, IndexType, File> getDirectory = (version, indexType) -> {
|
||||
return new File(getRootDirectory.apply(version), indexType.toString().toLowerCase());
|
||||
};
|
||||
|
||||
public final Term createPrimarykey(Album album) {
|
||||
return new Term(FieldNames.ID, Integer.toString(album.getId()));
|
||||
};
|
||||
|
||||
public final Term createPrimarykey(Artist artist) {
|
||||
return new Term(FieldNames.ID, Integer.toString(artist.getId()));
|
||||
};
|
||||
|
||||
public final Term createPrimarykey(MediaFile mediaFile) {
|
||||
return new Term(FieldNames.ID, Integer.toString(mediaFile.getId()));
|
||||
};
|
||||
|
||||
public final boolean addIgnoreNull(Collection<?> collection, Object object) {
|
||||
return CollectionUtils.addIgnoreNull(collection, object);
|
||||
}
|
||||
|
||||
public final boolean addIgnoreNull(Collection<?> collection, IndexType indexType,
|
||||
int subjectId) {
|
||||
if (indexType == IndexType.ALBUM | indexType == IndexType.SONG) {
|
||||
return addIgnoreNull(collection, mediaFileService.getMediaFile(subjectId));
|
||||
} else if (indexType == IndexType.ALBUM_ID3) {
|
||||
return addIgnoreNull(collection, albumDao.getAlbum(subjectId));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public final <T> void addIgnoreNull(ParamSearchResult<T> dist, IndexType indexType,
|
||||
int subjectId, Class<T> subjectClass) {
|
||||
if (indexType == IndexType.SONG) {
|
||||
MediaFile mediaFile = mediaFileService.getMediaFile(subjectId);
|
||||
addIgnoreNull(dist.getItems(), subjectClass.cast(mediaFile));
|
||||
} else if (indexType == IndexType.ARTIST_ID3) {
|
||||
Artist artist = artistDao.getArtist(subjectId);
|
||||
addIgnoreNull(dist.getItems(), subjectClass.cast(artist));
|
||||
} else if (indexType == IndexType.ALBUM_ID3) {
|
||||
Album album = albumDao.getAlbum(subjectId);
|
||||
addIgnoreNull(dist.getItems(), subjectClass.cast(album));
|
||||
}
|
||||
}
|
||||
|
||||
public final void addIfAnyMatch(SearchResult dist, IndexType subjectIndexType,
|
||||
Document subject) {
|
||||
int documentId = getId.apply(subject);
|
||||
if (subjectIndexType == IndexType.ARTIST | subjectIndexType == IndexType.ALBUM
|
||||
| subjectIndexType == IndexType.SONG) {
|
||||
addMediaFileIfAnyMatch.accept(dist.getMediaFiles(), documentId);
|
||||
} else if (subjectIndexType == IndexType.ARTIST_ID3) {
|
||||
addArtistId3IfAnyMatch.accept(dist.getArtists(), documentId);
|
||||
} else if (subjectIndexType == IndexType.ALBUM_ID3) {
|
||||
addAlbumId3IfAnyMatch.accept(dist.getAlbums(), documentId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.airsonic.player.controller.CoverArtController;
|
||||
import org.airsonic.player.dao.MediaFileDao;
|
||||
import org.airsonic.player.domain.*;
|
||||
import org.airsonic.player.service.*;
|
||||
import org.airsonic.player.service.search.IndexType;
|
||||
import org.airsonic.player.util.StringUtil;
|
||||
import org.airsonic.player.util.Util;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -523,7 +524,7 @@ public class SonosHelper {
|
||||
return Arrays.asList(artists, albums, songs);
|
||||
}
|
||||
|
||||
public MediaList forSearch(String query, int offset, int count, SearchService.IndexType indexType, String username, HttpServletRequest request) {
|
||||
public MediaList forSearch(String query, int offset, int count, IndexType indexType, String username, HttpServletRequest request) {
|
||||
|
||||
SearchCriteria searchCriteria = new SearchCriteria();
|
||||
searchCriteria.setCount(count);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<property name="playerService" ref="playerService"/>
|
||||
<property name="transcodingService" ref="transcodingService"/>
|
||||
<property name="musicIndexService" ref="musicIndexService"/>
|
||||
<property name="searchService" ref="searchService"/>
|
||||
<property name="searchService" ref="searchServiceImpl"/>
|
||||
<property name="ratingService" ref="ratingService"/>
|
||||
<property name="lastFmService" ref="lastFmService"/>
|
||||
<property name="podcastService" ref="podcastService"/>
|
||||
|
||||
Reference in New Issue
Block a user