/*
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 .
Copyright 2016 (C) Airsonic Authors
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
*/
package org.airsonic.player.service;
import org.airsonic.player.domain.*;
import org.airsonic.player.domain.MusicIndex.SortableArtist;
import org.airsonic.player.util.FileUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.Serializable;
import java.text.Collator;
import java.util.*;
/**
* Provides services for grouping artists by index.
*
* @author Sindre Mehus
*/
@Service
public class MusicIndexService {
@Autowired
private SettingsService settingsService;
@Autowired
private MediaFileService mediaFileService;
/**
* Returns a map from music indexes to sorted lists of artists that are direct children of the given music folders.
*
* @param folders The music folders.
* @param refresh Whether to look for updates by checking the last-modified timestamp of the music folders.
* @return A map from music indexes to sets of artists that are direct children of this music file.
*/
public SortedMap> getIndexedArtists(List folders, boolean refresh) {
List artists = createSortableArtists(folders, refresh);
return sortArtists(artists);
}
public SortedMap> getIndexedArtists(List artists) {
List sortableArtists = createSortableArtists(artists);
return sortArtists(sortableArtists);
}
public MusicFolderContent getMusicFolderContent(List musicFoldersToUse, boolean refresh) {
SortedMap> indexedArtists = getIndexedArtists(musicFoldersToUse, refresh);
List singleSongs = getSingleSongs(musicFoldersToUse, refresh);
return new MusicFolderContent(indexedArtists, singleSongs);
}
private List getSingleSongs(List folders, boolean refresh) {
List result = new ArrayList();
for (MusicFolder folder : folders) {
MediaFile parent = mediaFileService.getMediaFile(folder.getPath(), !refresh);
result.addAll(mediaFileService.getChildrenOf(parent, true, false, true, !refresh));
}
return result;
}
public List getShortcuts(List musicFoldersToUse) {
List result = new ArrayList();
for (String shortcut : settingsService.getShortcutsAsArray()) {
for (MusicFolder musicFolder : musicFoldersToUse) {
File file = new File(musicFolder.getPath(), shortcut);
if (FileUtil.exists(file)) {
result.add(mediaFileService.getMediaFile(file, true));
}
}
}
return result;
}
private SortedMap> sortArtists(List artists) {
List indexes = createIndexesFromExpression(settingsService.getIndexString());
Comparator indexComparator = new MusicIndexComparator(indexes);
SortedMap> result = new TreeMap>(indexComparator);
for (T artist : artists) {
MusicIndex index = getIndex(artist, indexes);
List artistSet = result.computeIfAbsent(index, k -> new ArrayList());
artistSet.add(artist);
}
for (List artistList : result.values()) {
Collections.sort(artistList);
}
return result;
}
/**
* Creates a new instance by parsing the given expression. The expression consists of an index name, followed by
* an optional list of one-character prefixes. For example:
*
* The expression "A" will create the index "A" -> ["A"]
* The expression "The" will create the index "The" -> ["The"]
* The expression "A(AÅÆ)" will create the index "A" -> ["A", "Å", "Æ"]
* The expression "X-Z(XYZ)" will create the index "X-Z" -> ["X", "Y", "Z"]
*
* @param expr The expression to parse.
* @return A new instance.
*/
protected MusicIndex createIndexFromExpression(String expr) {
int separatorIndex = expr.indexOf('(');
if (separatorIndex == -1) {
MusicIndex index = new MusicIndex(expr);
index.addPrefix(expr);
return index;
}
MusicIndex index = new MusicIndex(expr.substring(0, separatorIndex));
String prefixString = expr.substring(separatorIndex + 1, expr.length() - 1);
for (int i = 0; i < prefixString.length(); i++) {
index.addPrefix(prefixString.substring(i, i + 1));
}
return index;
}
/**
* Creates a list of music indexes by parsing the given expression. The expression is a space-separated list of
* sub-expressions, for which the rules described in {@link #createIndexFromExpression} apply.
*
* @param expr The expression to parse.
* @return A list of music indexes.
*/
protected List createIndexesFromExpression(String expr) {
List result = new ArrayList();
StringTokenizer tokenizer = new StringTokenizer(expr, " ");
while (tokenizer.hasMoreTokens()) {
MusicIndex index = createIndexFromExpression(tokenizer.nextToken());
result.add(index);
}
return result;
}
private List createSortableArtists(List folders, boolean refresh) {
String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray();
String[] shortcuts = settingsService.getShortcutsAsArray();
SortedMap artistMap = new TreeMap();
Set shortcutSet = new HashSet(Arrays.asList(shortcuts));
Collator collator = createCollator();
for (MusicFolder folder : folders) {
MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh);
List children = mediaFileService.getChildrenOf(root, false, true, true, !refresh);
for (MediaFile child : children) {
if (shortcutSet.contains(child.getName())) {
continue;
}
String sortableName = createSortableName(child.getName(), ignoredArticles);
MusicIndex.SortableArtistWithMediaFiles artist = artistMap.get(sortableName);
if (artist == null) {
artist = new MusicIndex.SortableArtistWithMediaFiles(child.getName(), sortableName, collator);
artistMap.put(sortableName, artist);
}
artist.addMediaFile(child);
}
}
return new ArrayList(artistMap.values());
}
private List createSortableArtists(List artists) {
List result = new ArrayList();
String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray();
Collator collator = createCollator();
for (Artist artist : artists) {
String sortableName = createSortableName(artist.getName(), ignoredArticles);
result.add(new MusicIndex.SortableArtistWithArtist(artist.getName(), sortableName, artist, collator));
}
return result;
}
/**
* Returns a collator to be used when sorting artists.
*/
private Collator createCollator() {
return Collator.getInstance(settingsService.getLocale());
}
private String createSortableName(String name, String[] ignoredArticles) {
String uppercaseName = name.toUpperCase();
for (String article : ignoredArticles) {
if (uppercaseName.startsWith(article.toUpperCase() + " ")) {
return name.substring(article.length() + 1) + ", " + article;
}
}
return name;
}
/**
* Returns the music index to which the given artist belongs.
*
* @param artist The artist in question.
* @param indexes List of available indexes.
* @return The music index to which this music file belongs, or {@link MusicIndex#OTHER} if no index applies.
*/
private MusicIndex getIndex(SortableArtist artist, List indexes) {
String sortableName = artist.getSortableName().toUpperCase();
for (MusicIndex index : indexes) {
for (String prefix : index.getPrefixes()) {
if (sortableName.startsWith(prefix.toUpperCase())) {
return index;
}
}
}
return MusicIndex.OTHER;
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService;
}
private static class MusicIndexComparator implements Comparator, Serializable {
private List indexes;
public MusicIndexComparator(List indexes) {
this.indexes = indexes;
}
public int compare(MusicIndex a, MusicIndex b) {
int indexA = indexes.indexOf(a);
int indexB = indexes.indexOf(b);
if (indexA == -1) {
indexA = Integer.MAX_VALUE;
}
if (indexB == -1) {
indexB = Integer.MAX_VALUE;
}
return Integer.compare(indexA, indexB);
}
}
}