/* This file is part of Subsonic. Subsonic 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. Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>. Copyright 2009 (C) Sindre Mehus */ package net.sourceforge.subsonic.service; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.TreeSet; import org.jfree.util.Log; import net.sourceforge.subsonic.domain.Album; import net.sourceforge.subsonic.domain.Artist; import net.sourceforge.subsonic.domain.MediaFile; import net.sourceforge.subsonic.domain.MusicFolder; import net.sourceforge.subsonic.domain.MusicIndex; import net.sourceforge.subsonic.domain.MusicIndex.SortableArtist; /** * Provides services for grouping artists by index. * * @author Sindre Mehus */ public class MusicIndexService { private SettingsService settingsService; private MediaFileService mediaFileService; /** * Returns a map from music indexes to sets 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. * @param index * @return A map from music indexes to sets of artists that are direct children of this music file. * @throws IOException If an I/O error occurs. */ public SortedMap<MusicIndex, SortedSet<MusicIndex.SortableArtistWithMediaFiles>> getIndexedArtists(List<MusicFolder> folders, boolean refresh, int index) throws IOException { SortedSet<MusicIndex.SortableArtistWithMediaFiles> artists = createSortableArtists(folders, refresh); return sortArtists(artists, index); } public SortedMap<MusicIndex, SortedSet<MusicIndex.SortableArtistforGenre>> getIndexedArtistsforGenre(List<MusicFolder> folders, boolean refresh, String genre, int index) throws IOException { SortedSet<MusicIndex.SortableArtistforGenre> sortableArtists = createSortableArtistsforGenre(folders, genre, refresh); return sortArtists(sortableArtists, index); } public SortedMap<MusicIndex, SortedSet<MusicIndex.SortableArtistWithArtist>> getIndexedArtists(List<Artist> artists, int index) throws IOException { SortedSet<MusicIndex.SortableArtistWithArtist> sortableArtists = createSortableArtists(artists); return sortArtists(sortableArtists, index); } public SortedMap<MusicIndex, SortedSet<MusicIndex.SortableArtistWithData>> getIndexedArtistsData(List<MusicFolder> folders, boolean refresh, String index) throws IOException { SortedSet<MusicIndex.SortableArtistWithData> artists = createSortableArtistsData(folders, true, index); return filterArtists(artists, index); } public SortedMap<MusicIndex, SortedSet<MusicIndex.SortableArtistWithAlbums>> getIndexedArtistsHub(List<MusicFolder> folders, String index) throws IOException { SortedSet<MusicIndex.SortableArtistWithAlbums> sortableArtists = createSortableArtistsWithAlbums(folders, index); return filterArtists(sortableArtists, index); } private <T extends SortableArtist> SortedMap<MusicIndex, SortedSet<T>> filterArtists(SortedSet<T> artists, String index) { List<MusicIndex> indexes; String ArtistIndex = "#(!#) 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"; indexes = createIndexesFromExpression(ArtistIndex); Comparator<MusicIndex> indexComparator = new MusicIndexComparator(indexes); SortedMap<MusicIndex, SortedSet<T>> result = new TreeMap<MusicIndex, SortedSet<T>>(indexComparator); for (T artist : artists) { MusicIndex indextmp = getIndex(artist, indexes); SortedSet<T> artistSet = result.get(indextmp); if (artistSet == null) { artistSet = new TreeSet<T>(); result.put(indextmp, artistSet); } artistSet.add(artist); } return result; } private <T extends SortableArtist> SortedMap<MusicIndex, SortedSet<T>> sortArtists(SortedSet<T> artists, int index) { List<MusicIndex> indexes; switch(index) { case 4: indexes = createIndexesFromExpression(settingsService.getIndex4String()); break; case 3: indexes = createIndexesFromExpression(settingsService.getIndex3String()); break; case 2: indexes = createIndexesFromExpression(settingsService.getIndex2String()); break; default: indexes = createIndexesFromExpression(settingsService.getIndexString()); } Comparator<MusicIndex> indexComparator = new MusicIndexComparator(indexes); SortedMap<MusicIndex, SortedSet<T>> result = new TreeMap<MusicIndex, SortedSet<T>>(indexComparator); for (T artist : artists) { MusicIndex indextmp = getIndex(artist, indexes); SortedSet<T> artistSet = result.get(indextmp); if (artistSet == null) { artistSet = new TreeSet<T>(); result.put(indextmp, artistSet); } artistSet.add(artist); } 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:<p/> * <p/> * The expression <em>"A"</em> will create the index <em>"A" -> ["A"]</em><br/> * The expression <em>"The"</em> will create the index <em>"The" -> ["The"]</em><br/> * The expression <em>"A(AÅÆ)"</em> will create the index <em>"A" -> ["A", "Å", "Æ"]</em><br/> * The expression <em>"X-Z(XYZ)"</em> will create the index <em>"X-Z" -> ["X", "Y", "Z"]</em> * * @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<MusicIndex> createIndexesFromExpression(String expr) { List<MusicIndex> result = new ArrayList<MusicIndex>(); StringTokenizer tokenizer = new StringTokenizer(expr, " "); while (tokenizer.hasMoreTokens()) { MusicIndex index = createIndexFromExpression(tokenizer.nextToken()); result.add(index); } return result; } private SortedSet<MusicIndex.SortableArtistforGenre> createSortableArtistsforGenre(List<MusicFolder> folders, String genre, boolean refresh) throws IOException { String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); String[] shortcuts = settingsService.getShortcutsAsArray(); SortedMap<String, MusicIndex.SortableArtistforGenre> artistMap = new TreeMap<String, MusicIndex.SortableArtistforGenre>(); Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); for (MusicFolder folder : folders) { MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh); List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, !refresh); for (MediaFile child : children) { if (shortcutSet.contains(child.getName())) { continue; } String _genre = child.getGenre(); if (_genre == null ) { _genre = "unknown genre"; } if (genre == null ) { genre = ""; } if (genre == "unknown genre" || genre != null || genre == "") { if (_genre.toLowerCase().contains(genre.toLowerCase())) { String sortableName = createSortableName(child.getName(), ignoredArticles); MusicIndex.SortableArtistforGenre artist = artistMap.get(sortableName); if (artist == null) { artist = new MusicIndex.SortableArtistforGenre(child.getName(), sortableName, null, _genre); } artistMap.put(sortableName, artist); artist.addMediaFile(child); } } } } return new TreeSet<MusicIndex.SortableArtistforGenre>(artistMap.values()); } private SortedSet<MusicIndex.SortableArtistWithMediaFiles> createSortableArtists(List<MusicFolder> folders, boolean refresh) throws IOException { String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); String[] shortcuts = settingsService.getShortcutsAsArray(); SortedMap<String, MusicIndex.SortableArtistWithMediaFiles> artistMap = new TreeMap<String, MusicIndex.SortableArtistWithMediaFiles>(); Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); for (MusicFolder folder : folders) { MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh); List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, !refresh); for (MediaFile child : children) { if (shortcutSet.contains(child.getName())) { continue; } String sortableName = createSortableName(child.getName(), ignoredArticles); //ignoredArticles MusicIndex.SortableArtistWithMediaFiles artist = artistMap.get(sortableName); if (artist == null) { artist = new MusicIndex.SortableArtistWithMediaFiles(child.getName(), sortableName); artistMap.put(sortableName, artist); } artist.addMediaFile(child); } } return new TreeSet<MusicIndex.SortableArtistWithMediaFiles>(artistMap.values()); } private SortedSet<MusicIndex.SortableArtistWithArtist> createSortableArtists(List<Artist> artists) { TreeSet<MusicIndex.SortableArtistWithArtist> result = new TreeSet<MusicIndex.SortableArtistWithArtist>(); String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); for (Artist artist : artists) { String sortableName = createSortableName(artist.getName(), ignoredArticles); result.add(new MusicIndex.SortableArtistWithArtist(artist.getName(), sortableName, artist)); } return result; } private SortedSet<MusicIndex.SortableArtistWithData> createSortableArtistsData(List<MusicFolder> folders, boolean refresh, String index) throws IOException { String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); String[] shortcuts = settingsService.getShortcutsAsArray(); SortedMap<String, MusicIndex.SortableArtistWithData> artistMap = new TreeMap<String, MusicIndex.SortableArtistWithData>(); Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); for (MusicFolder folder : folders) { MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh); List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, !refresh); for (MediaFile child : children) { if (shortcutSet.contains(child.getName())) { continue; } if (!child.getName().startsWith(index)) { continue; } String sortableName = createSortableName(child.getName(), ignoredArticles); MusicIndex.SortableArtistWithData artist = artistMap.get(sortableName); if (artist == null) { Artist a = mediaFileService.getArtistforName(child.getAlbumSetName()); artist = new MusicIndex.SortableArtistWithData(child.getName(), sortableName); if (a != null) { artist.setAlbumCount(a.getAlbumCount()); artist.setPlayCount(a.getPlayCount()); artist.setSongCount(a.getSongCount()); } // artist.setAlbumCount(mediaFileService.getalbumCount(child.getName())); // artist.setSongCount(mediaFileService.getsongCount(child.getName())); // artist.setPlayCount(mediaFileService.getplayCount(child.getName())); artistMap.put(sortableName, artist); } artist.addMediaFile(child); List<MediaFile> childs = mediaFileService.getChildrenOf(child, false, true, true, !refresh); int count = 0; // boolean countMore = false; for (MediaFile topchild : childs) { count++; if (artist.getMediaFilesChildren().size() < 3) { if (artist.getMediaFilesChildren().size() < 4) { if (count < 4 ){ artist.addMediaFilesChildren(topchild); } else { continue; } } else { break; } } } } } return new TreeSet<MusicIndex.SortableArtistWithData>(artistMap.values()); } private SortedSet<MusicIndex.SortableArtistWithAlbums> createSortableArtistsWithAlbums(List<MusicFolder> folders, String index ) { boolean refresh = false; String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); String[] shortcuts = settingsService.getShortcutsAsArray(); SortedMap<String, MusicIndex.SortableArtistWithAlbums> artistMap = new TreeMap<String, MusicIndex.SortableArtistWithAlbums>(); Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); for (MusicFolder folder : folders) { MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh); List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, !refresh); for (MediaFile child : children) { if (shortcutSet.contains(child.getName())) { continue; } if (!createSortableName(child.getName(), ignoredArticles).toUpperCase().startsWith(index.toUpperCase())) { continue; } String sortableName = createSortableName(child.getName(), ignoredArticles); MusicIndex.SortableArtistWithAlbums artist = artistMap.get(sortableName); if (artist == null) { artist = new MusicIndex.SortableArtistWithAlbums(child.getName(), sortableName); artistMap.put(sortableName, artist); } ////// add IDs artist.addMediaFile(child); ////// add albums List<String> candidates = mediaFileService.getArtistFolder(child.getName()); Set<String> albumSetNames = new HashSet<String>(); for (Album a : artist.getAlbums()) { albumSetNames.add(a.getName().toLowerCase()); } for (String artistName : candidates){ List<Album> artistAlbums = mediaFileService.getAlbumsForArtist(artistName); for (Album album : artistAlbums) { if (album.getSetName() != null && album.getName() != null ){ if (albumSetNames.contains(album.getSetName().toLowerCase())) { continue; } if (albumSetNames.size() < 100) { albumSetNames.add(album.getName().toLowerCase()); // album.setId(mediaFileService.getIdsForAlbums(artistName, album.getSetName())); artist.addAlbum(album); } } } } } } return new TreeSet<MusicIndex.SortableArtistWithAlbums>(artistMap.values()); } 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<MusicIndex> 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<MusicIndex>, Serializable { private List<MusicIndex> indexes; public MusicIndexComparator(List<MusicIndex> 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; } if (indexA < indexB) { return -1; } if (indexA > indexB) { return 1; } return 0; } } }