package com.torrenttunes.client.webservice;
import static com.torrenttunes.client.db.Tables.LIBRARY;
import static com.torrenttunes.client.db.Tables.SETTINGS;
import static spark.Spark.get;
import static spark.Spark.post;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.jackson.map.JsonMappingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.frostwire.jlibtorrent.TorrentHandle;
import com.frostwire.jlibtorrent.TorrentStatus;
import com.frostwire.jlibtorrent.swig.default_storage;
import com.torrenttunes.client.LibtorrentEngine;
import com.torrenttunes.client.ScanDirectory;
import com.torrenttunes.client.ScanDirectory.ScanInfo;
import com.torrenttunes.client.db.Actions;
import com.torrenttunes.client.db.Tables.Library;
import com.torrenttunes.client.tools.DataSources;
import com.torrenttunes.client.tools.Tools;
public class Platform {
static final Logger log = LoggerFactory.getLogger(Platform.class);
public static void setup() {
get("/get_library", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
Tools.dbInit();
String json = LIBRARY.findAll().toJson(false);
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
post("/upload_music_directory", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
Map<String, String> vars = Tools.createMapFromAjaxPost(req.body());
String uploadPath = vars.get("upload_path");
log.info(uploadPath);
ScanDirectory.start(new File(uploadPath));
return "Uploading complete";
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
// Example :
// curl --data "/home/derp/Music/A Music Dir" http://localhost:4568/share_directory
post("/share_directory", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
log.info(req.body());
String path = req.body().trim();
Set<ScanInfo> scanInfos = ScanDirectory.start(new File(path));
String scanInfoReport = ScanDirectory.scanInfosReport(scanInfos) + "\n";
return scanInfoReport;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
get("/get_upload_info", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
List<ScanInfo> sis = LibtorrentEngine.INSTANCE.getScanInfosLastForty();
String json = null;
try {
json = Tools.MAPPER.writeValueAsString(sis);
} catch(JsonMappingException e1) {
json = " ";
}
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
get("/fetch_or_download_song/:infoHash", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
String json = null;
String infoHash = req.params(":infoHash");
// Fetch the song by its info hash, and return that row
Tools.dbInit();
Library track = LIBRARY.findFirst("info_hash = ?", infoHash);
Tools.dbClose();
if (track != null) {
json = track.toJson(false);
}
// If it doesn't exist, download the torrent to the cache dir
else {
if (Actions.spaceFreeInStoragePath()) {
json = Actions.downloadTorrent(infoHash);
} else {
// TODO maybe clear cache
Tools.dbInit();
Actions.clearCache();
Tools.dbClose();
throw new NoSuchElementException("Not enough storage space, "
+ "your cache has now been cleared");
}
}
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
get("/get_torrent_progress/:infoHash", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
String infoHash = req.params(":infoHash");
// log.info("progress info hash: " + infoHash);
TorrentHandle th = LibtorrentEngine.INSTANCE.getInfoHashToTorrentMap().get(infoHash);
log.info("Progress torrent: " + th.getName());
Double progress = th.getStatus().getProgressPpm() / 1E6;
// float progress = th.getStatus().getProgress();
return progress;
} catch (Exception e) {
res.status(666);
// e.printStackTrace();
return e.getMessage();
}
});
get("/get_torrent_status/:infoHash", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
String infoHash = req.params(":infoHash");
log.info("progress info hash: " + infoHash);
TorrentHandle th = LibtorrentEngine.INSTANCE.getInfoHashToTorrentMap().get(infoHash);
// th.forceRecheck();
// th.saveResumeData();
if (th == null) {
return "its null derp";
}
TorrentStatus status = th.getStatus();
Tools.printTorrentStatus(status);
return status.toString();
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
post("/clear_cache", (req, res) -> {
try {
Tools.allowOnlyLocalHeaders(req, res);
Tools.dbInit();
String json = Actions.clearCache();
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
post("/clear_database", (req, res) -> {
try {
Tools.allowOnlyLocalHeaders(req, res);
Tools.dbInit();
String json = Actions.clearDatabase();
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
get("/remove_artist/:artistMBID", (req, res) -> {
try {
Tools.allowOnlyLocalHeaders(req, res);
String artistMBID = req.params(":artistMBID");
Tools.dbInit();
String json = Actions.removeArtist(artistMBID);
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
get("/remove_song/:songMBID", (req, res) -> {
try {
Tools.allowOnlyLocalHeaders(req, res);
String songMBID = req.params(":songMBID");
Tools.dbInit();
String json = Actions.removeSong(songMBID);
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
post("/power_off", (req, res) -> {
try {
// Runtime.getRuntime().exit(0);
log.info("Powering off...");
default_storage.disk_write_access_log(false);
// LibtorrentEngine.INSTANCE.getSession().pause(); // should save all the resumeData
LibtorrentEngine.INSTANCE.getSession().abort();
System.exit(0);
return "A yellow brick road";
} catch (Exception e) {
res.status(666);
return e.getMessage();
}
});
post("/uninstall", (req, res) -> {
try {
Tools.allowOnlyLocalHeaders(req, res);
log.info("Uninstalling torrenttunes");
Tools.uninstall();
return "TorrentTunes Uninstalled";
} catch (Exception e) {
res.status(666);
return e.getMessage();
}
});
get("/error_test", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
throw new NoSuchElementException("error testing");
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
get("/get_settings", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
Tools.dbInit();
String json = SETTINGS.findFirst("id = ?", 1).toJson(false);
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
get("/get_sample_song", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
HttpServletResponse raw = res.raw();
raw.getOutputStream().write(Files.readAllBytes(Paths.get(DataSources.SAMPLE_SONG)));
raw.getOutputStream().flush();
raw.getOutputStream().close();
return res.raw();
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
get("/get_upload_download_totals", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
String json = LibtorrentEngine.INSTANCE.getUploadDownloadTotals();
return json;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
post("/save_settings", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
Tools.logRequestInfo(req);
Map<String, String> vars = Tools.createMapFromAjaxPost(req.body());
Integer maxUploadSpeed = Integer.valueOf(vars.get("max_upload_speed"));
Integer maxDownloadSpeed = Integer.valueOf(vars.get("max_download_speed"));
Integer maxCacheSize = Integer.valueOf(vars.get("max_cache_size_mb"));
String storagePath = vars.get("storage_path");
Tools.dbInit();
String message = Actions.saveSettings(storagePath,
maxDownloadSpeed,
maxUploadSpeed,
maxCacheSize);
return message;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
post("/delete_song/:infoHash", (req, res) -> {
try {
Tools.allowAllHeaders(req, res);
Tools.logRequestInfo(req);
String infoHash = req.params(":infoHash");
Tools.dbInit();
String message = Actions.deleteSong(infoHash);
return message;
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
} finally {
Tools.dbClose();
}
});
get("/get_audio_file/:encodedPath", (req, res) -> {
// res.header("Content-Disposition", "filename=\"music.mp3\"");
HttpServletResponse raw = res.raw();
try {
String origin = req.headers("Origin");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Origin", origin);
log.debug(req.params(":encodedPath"));
String correctedEncoded = req.params(":encodedPath").replaceAll("qzvkn", "%2F");
log.info("Streaming to corrected encoded = " + correctedEncoded);
String path = URLDecoder.decode(correctedEncoded, "UTF-8");
if (!path.endsWith(".mp3")) {
throw new NoSuchElementException("Not an audio file");
}
File mp3 = new File(path);
// write out the request headers:
// for (String h : req.headers()) {
// log.info("Header:" + h + " = " + req.headers(h));
// }
String range = req.headers("Range");
// Check if its a non-streaming browser, for example, firefox can't stream
Boolean nonStreamingBrowser = false;
String userAgent = req.headers("User-Agent").toLowerCase();
for (String browser : DataSources.NON_STREAMING_BROWSERS) {
if (userAgent.contains(browser.toLowerCase())) {
nonStreamingBrowser = true;
log.debug("Its a non-streaming browser.");
break;
}
}
// res.status(206);
OutputStream os = raw.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(os);
if (range == null || nonStreamingBrowser) {
res.header("Content-Length", String.valueOf(mp3.length()));
Files.copy(mp3.toPath(), os);
return res.raw();
}
int[] fromTo = fromTo(mp3, range);
// new FileInputStream(mp3).getChannel().transferTo(raw.getOutputStream().get);
int length = (int) (fromTo[1] - fromTo[0] + 1);
res.status(206);
res.type("audio/mpeg");
res.header("Accept-Ranges", "bytes");
// res.header("Content-Length", String.valueOf(mp3.length()));
res.header("Content-Range", contentRangeByteString(fromTo));
res.header("Content-Length", String.valueOf(length));
// res.header("Content-Length", String.valueOf(mp3.length()));
res.header("Content-Disposition", "attachment; filename=\"" + mp3.getName() + "\"");
res.header("Date", new java.util.Date(mp3.lastModified()).toString());
res.header("Last-Modified", new java.util.Date(mp3.lastModified()).toString());
// res.header("Server", "Apache");
res.header("X-Content-Duration", "30");
res.header("Content-Duration", "30");
res.header("Connection", "Keep-Alive");
// String etag = com.google.common.io.Files.hash(mp3, Hashing.md5()).toString();
// res.header("Etag", etag);
res.header("Cache-Control", "no-cache, private");
res.header("X-Pad","avoid browser bug");
res.header("Expires", "0");
res.header("Pragma", "no-cache");
res.header("Content-Transfer-Encoding", "binary");
res.header("Transfer-Encoding", "chunked");
res.header("Keep-Alive", "timeout=15, max=100");
res.header("If-None-Match", "webkit-no-cache");
// res.header("X-Sendfile", path);
res.header("X-Stream", "true");
// This one works, but doesn't stream
log.debug("writing random access file instead");
final RandomAccessFile raf = new RandomAccessFile(mp3, "r");
raf.seek(fromTo[0]);
writeAudioToOS(length, raf, bos);
raf.close();
bos.flush();
bos.close();
return res.raw();
} catch (Exception e) {
res.status(666);
e.printStackTrace();
return e.getMessage();
}
});
}
public static int[] fromTo(File mp3, String range) {
int[] ret = new int[3];
if (range == null || range.equals("bytes=0-")) {
// ret[0] = 0;
// ret[1] = mp3.length() -1;
// ret[2] = mp3.length();
//
// return ret;
// range = "bytes=0-";
}
String[] ranges = range.split("=")[1].split("-");
log.info(range);
log.debug("ranges[] = " + Arrays.toString(ranges));
Integer chunkSize = 512;
Integer from = Integer.parseInt(ranges[0]);
Integer to = chunkSize + from;
if (to >= mp3.length()) {
to = (int) (mp3.length() - 1);
}
if (ranges.length == 2) {
to = Integer.parseInt(ranges[1]);
}
ret[0] = from;
ret[1] = to;
ret[2] = (int) mp3.length();
// ret[2] = (int) (ret[1] - ret[0] + 1);
return ret;
}
public static String contentRangeByteString(int[] fromTo) {
String responseRange = "bytes " + fromTo[0] + "-" + fromTo[1] + "/" + fromTo[2];
log.debug("response range = " + responseRange);
return responseRange;
}
public static void writeAudioToOS(Integer length, RandomAccessFile raf, BufferedOutputStream os) throws IOException {
byte[] buf = new byte[256];
while(length != 0) {
int read = raf.read(buf, 0, buf.length > length ? length : buf.length);
os.write(buf, 0, read);
length -= read;
}
log.debug("before closing");
//
}
// public static class MediaStreamer implements StreamingOutput {
//
// private int length;
// private RandomAccessFile raf;
// final byte[] buf = new byte[4096];
//
// public MediaStreamer(int length, RandomAccessFile raf) {
// this.length = length;
// this.raf = raf;
// }
//
// @Override
// public void write(OutputStream outputStream) throws IOException, WebApplicationException {
// try {
// while( length != 0) {
// int read = raf.read(buf, 0, buf.length > length ? length : buf.length);
// outputStream.write(buf, 0, read);
// length -= read;
// }
// } finally {
// raf.close();
// }
// }
//
// public int getLenth() {
// return length;
// }
// }
//
// private static Response buildStream(final File asset, final String range) throws Exception {
// // range not requested : Firefox, Opera, IE do not send range headers
// if (range == null) {
// StreamingOutput streamer = new StreamingOutput() {
// @Override
// public void write(final OutputStream output) throws IOException, WebApplicationException {
//
// final FileChannel inputChannel = new FileInputStream(asset).getChannel();
// final WritableByteChannel outputChannel = Channels.newChannel(output);
// try {
// inputChannel.transferTo(0, inputChannel.size(), outputChannel);
// } finally {
// // closing the channels
// inputChannel.close();
// outputChannel.close();
// }
// }
// };
// return Response.ok(streamer).status(200).header(HttpHeaders.CONTENT_LENGTH, asset.length()).build();
// }
//
// String[] ranges = range.split("=")[1].split("-");
// final int from = Integer.parseInt(ranges[0]);
// /**
// * Chunk media if the range upper bound is unspecified. Chrome sends "bytes=0-"
// */
// int chunk_size = 1024;
// int to = chunk_size + from;
// if (to >= asset.length()) {
// to = (int) (asset.length() - 1);
// }
// if (ranges.length == 2) {
// to = Integer.parseInt(ranges[1]);
// }
//
// final String responseRange = String.format("bytes %d-%d/%d", from, to, asset.length());
// final RandomAccessFile raf = new RandomAccessFile(asset, "r");
// raf.seek(from);
//
// final int len = to - from + 1;
// final MediaStreamer streamer = new MediaStreamer(len, raf);
//
// Response.ResponseBuilder res = Response.ok(streamer).status(206)
// .header("Accept-Ranges", "bytes")
// .header("Content-Range", responseRange)
// .header(HttpHeaders.CONTENT_LENGTH, streamer.getLenth())
// .header(HttpHeaders.LAST_MODIFIED, new Date(asset.lastModified()));
// return res.build();
// }
//
}