Audinaut-subsonic-app-android/app/src/main/java/net/nullsum/audinaut/service/OfflineMusicService.java

545 lines
21 KiB
Java

/*
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.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<Artist> artists = new ArrayList<>();
List<Entry> 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<String> 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<MusicFolder> 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<Artist> artists = new ArrayList<>();
List<Entry> albums = new ArrayList<>();
List<Entry> 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<Entry> albums, List<Entry> 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<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
List<Playlist> 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<File> 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<Entry> 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<Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException();
}
@Override
public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
throw new OfflineException();
}
@Override
public void overwritePlaylist(String id, String name, int toRemove, List<Entry> 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<Genre> 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<File> 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<File> children) {
for (File file : FileUtil.listMediaFiles(parent)) {
if (file.isFile()) {
children.add(file);
} else {
listFilesRecursively(file, children);
}
}
}
}