/* 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); } } }