/* 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 . Copyright 2009 (C) Sindre Mehus */ package net.nullsum.audinaut.service; import android.content.Context; import android.graphics.Bitmap; import android.util.Log; import net.nullsum.audinaut.domain.Artist; import net.nullsum.audinaut.domain.Genre; import net.nullsum.audinaut.domain.Indexes; import net.nullsum.audinaut.domain.MusicDirectory; import net.nullsum.audinaut.domain.MusicDirectory.Entry; import net.nullsum.audinaut.domain.MusicFolder; import net.nullsum.audinaut.domain.Playlist; import net.nullsum.audinaut.domain.SearchCritera; import net.nullsum.audinaut.domain.SearchResult; import net.nullsum.audinaut.domain.User; import net.nullsum.audinaut.util.Constants; import net.nullsum.audinaut.util.FileUtil; import net.nullsum.audinaut.util.ProgressListener; import net.nullsum.audinaut.util.SilentBackgroundTask; import net.nullsum.audinaut.util.Util; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.Set; import java.util.SortedSet; import okhttp3.Response; /** * @author Sindre Mehus */ public class OfflineMusicService implements MusicService { public static final String ERRORMSG = "Not available in offline mode"; private static final String TAG = OfflineMusicService.class.getSimpleName(); private static final Random random = new Random(); @Override public void ping(Context context, ProgressListener progressListener) throws Exception { } @Override public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { List artists = new ArrayList<>(); List entries = new ArrayList<>(); File root = FileUtil.getMusicDirectory(context); for (File file : FileUtil.listFiles(root)) { if (file.isDirectory()) { Artist artist = new Artist(); artist.setId(file.getPath()); artist.setIndex(file.getName().substring(0, 1)); artist.setName(file.getName()); artists.add(artist); } else if (!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) { entries.add(createEntry(context, file)); } } return new Indexes(Collections.emptyList(), artists, entries); } @Override public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception { return getMusicDirectory(id, context); } private MusicDirectory getMusicDirectory(String id, Context context) throws Exception { File dir = new File(id); MusicDirectory result = new MusicDirectory(); result.setName(dir.getName()); Set names = new HashSet<>(); for (File file : FileUtil.listMediaFiles(dir)) { String name = getName(file); if (name != null & !names.contains(name)) { names.add(name); result.addChild(createEntry(context, file, name, true)); } } result.sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true)); return result; } @Override public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } private String getName(File file) { String name = file.getName(); if (file.isDirectory()) { return name; } if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) { return null; } name = name.replace(".complete", ""); return FileUtil.getBaseName(name); } private Entry createEntry(Context context, File file) { return createEntry(context, file, getName(file)); } private Entry createEntry(Context context, File file, String name) { return createEntry(context, file, name, true); } private Entry createEntry(Context context, File file, String name, boolean load) { Entry entry; entry = new Entry(); entry.setDirectory(file.isDirectory()); entry.setId(file.getPath()); entry.setParent(file.getParent()); String root = FileUtil.getMusicDirectory(context).getPath(); entry.setPath(file.getPath().replaceFirst("^" + root + "/", "")); String title = name; if (file.isFile()) { File artistFolder = file.getParentFile().getParentFile(); File albumFolder = file.getParentFile(); if (artistFolder.getPath().equals(root)) { entry.setArtist(albumFolder.getName()); } else { entry.setArtist(artistFolder.getName()); } entry.setAlbum(albumFolder.getName()); int index = name.indexOf('-'); if (index != -1) { try { entry.setTrack(Integer.parseInt(name.substring(0, index))); title = title.substring(index + 1); } catch (Exception e) { // Failed parseInt, just means track filled out } } if (load) { entry.loadMetadata(file); } } entry.setTitle(title); entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", ""))); File albumArt = FileUtil.getAlbumArtFile(context, entry); if (albumArt.exists()) { entry.setCoverArt(albumArt.getPath()); } return entry; } @Override public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { try { return FileUtil.getAlbumArtBitmap(context, entry, size); } catch (Exception e) { return null; } } @Override public Response getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { throw new OfflineException(); } @Override public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception { List artists = new ArrayList<>(); List albums = new ArrayList<>(); List songs = new ArrayList<>(); File root = FileUtil.getMusicDirectory(context); int closeness; for (File artistFile : FileUtil.listFiles(root)) { String artistName = artistFile.getName(); if (artistFile.isDirectory()) { if ((closeness = matchCriteria(criteria, artistName)) > 0) { Artist artist = new Artist(); artist.setId(artistFile.getPath()); artist.setIndex(artistFile.getName().substring(0, 1)); artist.setName(artistName); artist.setCloseness(closeness); artists.add(artist); } recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs); } } Collections.sort(artists, (lhs, rhs) -> { if (lhs.getCloseness() == rhs.getCloseness()) { return 0; } else if (lhs.getCloseness() > rhs.getCloseness()) { return -1; } else { return 1; } }); Collections.sort(albums, (lhs, rhs) -> { if (lhs.getCloseness() == rhs.getCloseness()) { return 0; } else if (lhs.getCloseness() > rhs.getCloseness()) { return -1; } else { return 1; } }); Collections.sort(songs, (lhs, rhs) -> { if (lhs.getCloseness() == rhs.getCloseness()) { return 0; } else if (lhs.getCloseness() > rhs.getCloseness()) { return -1; } else { return 1; } }); // Respect counts in search criteria int artistCount = Math.min(artists.size(), criteria.getArtistCount()); int albumCount = Math.min(albums.size(), criteria.getAlbumCount()); int songCount = Math.min(songs.size(), criteria.getSongCount()); artists = artists.subList(0, artistCount); albums = albums.subList(0, albumCount); songs = songs.subList(0, songCount); return new SearchResult(artists, albums, songs); } private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List albums, List songs) { int closeness; for (File albumFile : FileUtil.listMediaFiles(file)) { if (albumFile.isDirectory()) { String albumName = getName(albumFile); if ((closeness = matchCriteria(criteria, albumName)) > 0) { Entry album = createEntry(context, albumFile, albumName); album.setArtist(artistName); album.setCloseness(closeness); albums.add(album); } for (File songFile : FileUtil.listMediaFiles(albumFile)) { String songName = getName(songFile); if (songName == null) { continue; } if (songFile.isDirectory()) { recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs); } else if ((closeness = matchCriteria(criteria, songName)) > 0) { Entry song = createEntry(context, albumFile, songName); song.setArtist(artistName); song.setAlbum(albumName); song.setCloseness(closeness); songs.add(song); } } } else { String songName = getName(albumFile); if ((closeness = matchCriteria(criteria, songName)) > 0) { Entry song = createEntry(context, albumFile, songName); song.setArtist(artistName); song.setAlbum(songName); song.setCloseness(closeness); songs.add(song); } } } } private int matchCriteria(SearchCritera criteria, String name) { if (criteria.getPattern().matcher(name).matches()) { return Util.getStringDistance( criteria.getQuery().toLowerCase(), name.toLowerCase()); } else { return 0; } } @Override public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { List playlists = new ArrayList<>(); File root = FileUtil.getPlaylistDirectory(context); String lastServer = null; boolean removeServer = true; for (File folder : FileUtil.listFiles(root)) { if (folder.isDirectory()) { String server = folder.getName(); SortedSet fileList = FileUtil.listFiles(folder); for (File file : fileList) { if (FileUtil.isPlaylistFile(file)) { String id = file.getName(); String filename = FileUtil.getBaseName(id); String name = server + ": " + filename; Playlist playlist = new Playlist(server, name); playlist.setComment(filename); Reader reader = null; BufferedReader buffer = null; int songCount = 0; try { reader = new FileReader(file); buffer = new BufferedReader(reader); String line = buffer.readLine(); while ((line = buffer.readLine()) != null) { // No matter what, end file can't have .complete in it line = line.replace(".complete", ""); File entryFile = new File(line); // Don't add file to playlist if it doesn't exist as cached or pinned! File checkFile = entryFile; if (!checkFile.exists()) { // If normal file doens't exist, check if .complete version does checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName()) + ".complete." + FileUtil.getExtension(entryFile.getName())); } String entryName = getName(entryFile); if (checkFile.exists() && entryName != null) { songCount++; } } playlist.setSongCount(Integer.toString(songCount)); } catch (Exception e) { Log.w(TAG, "Failed to count songs in playlist", e); } finally { Util.close(buffer); Util.close(reader); } if (songCount > 0) { playlists.add(playlist); } } } if (!server.equals(lastServer) && fileList.size() > 0) { if (lastServer != null) { removeServer = false; } lastServer = server; } } else { // Delete legacy playlist files try { folder.delete(); } catch (Exception e) { Log.w(TAG, "Failed to delete old playlist file: " + folder.getName()); } } } if (removeServer) { for (Playlist playlist : playlists) { playlist.setName(playlist.getName().substring(playlist.getId().length() + 2)); } } return playlists; } @Override public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { DownloadService downloadService = DownloadService.getInstance(); if (downloadService == null) { return new MusicDirectory(); } Reader reader = null; BufferedReader buffer = null; try { int firstIndex = name.indexOf(id); if (firstIndex != -1) { name = name.substring(id.length() + 2); } File playlistFile = FileUtil.getPlaylistFile(context, id, name); reader = new FileReader(playlistFile); buffer = new BufferedReader(reader); MusicDirectory playlist = new MusicDirectory(); String line = buffer.readLine(); if (!"#EXTM3U".equals(line)) return playlist; while ((line = buffer.readLine()) != null) { // No matter what, end file can't have .complete in it line = line.replace(".complete", ""); File entryFile = new File(line); // Don't add file to playlist if it doesn't exist as cached or pinned! File checkFile = entryFile; if (!checkFile.exists()) { // If normal file doens't exist, check if .complete version does checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName()) + ".complete." + FileUtil.getExtension(entryFile.getName())); } String entryName = getName(entryFile); if (checkFile.exists() && entryName != null) { playlist.addChild(createEntry(context, entryFile, entryName, false)); } } return playlist; } finally { Util.close(buffer); Util.close(reader); } } @Override public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void addToPlaylist(String id, List toAdd, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void removeFromPlaylist(String id, List toRemove, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void overwritePlaylist(String id, String name, int toRemove, List toAdd, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { File root = FileUtil.getMusicDirectory(context); List children = new LinkedList<>(); listFilesRecursively(root, children); MusicDirectory result = new MusicDirectory(); if (children.isEmpty()) { return result; } for (int i = 0; i < size; i++) { File file = children.get(random.nextInt(children.size())); result.addChild(createEntry(context, file, getName(file))); } return result; } @Override public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { throw new OfflineException(); } @Override public void setInstance(Integer instance) throws Exception { throw new OfflineException(); } private void listFilesRecursively(File parent, List children) { for (File file : FileUtil.listMediaFiles(parent)) { if (file.isFile()) { children.add(file); } else { listFilesRecursively(file, children); } } } }