285 lines
12 KiB
Java
285 lines
12 KiB
Java
package f.f.freezer;
|
|
|
|
import android.content.pm.PackageManager;
|
|
import android.util.Log;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FilterInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.RandomAccessFile;
|
|
import java.net.URL;
|
|
import java.util.HashMap;
|
|
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import fi.iki.elonen.NanoHTTPD;
|
|
|
|
public class StreamServer {
|
|
|
|
public HashMap<String, StreamInfo> streams = new HashMap<>();
|
|
|
|
private WebServer server;
|
|
private String host = "127.0.0.1";
|
|
private int port = 36958;
|
|
private String offlinePath;
|
|
|
|
//Shared log & API
|
|
private DownloadLog logger;
|
|
private Deezer deezer;
|
|
|
|
StreamServer(String arl, String offlinePath) {
|
|
//Initialize shared variables
|
|
logger = new DownloadLog();
|
|
deezer = new Deezer();
|
|
deezer.init(logger, arl);
|
|
this.offlinePath = offlinePath;
|
|
}
|
|
|
|
//Create server
|
|
void start() {
|
|
try {
|
|
server = new WebServer(host, port);
|
|
server.start();
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
if (server != null)
|
|
server.stop();
|
|
}
|
|
|
|
//Information about streamed audio - for showing in UI
|
|
public class StreamInfo {
|
|
String format;
|
|
long size;
|
|
//"Stream" or "Offline"
|
|
String source;
|
|
|
|
StreamInfo(String format, long size, String source) {
|
|
this.format = format;
|
|
this.size = size;
|
|
this.source = source;
|
|
}
|
|
|
|
//For passing into UI
|
|
public HashMap<String, Object> toJSON() {
|
|
HashMap<String, Object> out = new HashMap<>();
|
|
out.put("format", format);
|
|
out.put("size", size);
|
|
out.put("source", source);
|
|
return out;
|
|
}
|
|
|
|
}
|
|
|
|
private class WebServer extends NanoHTTPD {
|
|
public WebServer(String hostname, int port) {
|
|
super(hostname, port);
|
|
}
|
|
|
|
@Override
|
|
public Response serve(IHTTPSession session) {
|
|
//Must be only GET
|
|
if (session.getMethod() != Method.GET)
|
|
return newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED, MIME_PLAINTEXT, "Only GET request supported!");
|
|
|
|
//Parse range header
|
|
String rangeHeader = session.getHeaders().get("range");
|
|
int startBytes = 0;
|
|
boolean isRanged = false;
|
|
int end = -1;
|
|
if (rangeHeader != null && rangeHeader.startsWith("bytes")) {
|
|
isRanged = true;
|
|
String[] ranges = rangeHeader.split("=")[1].split("-");
|
|
startBytes = Integer.parseInt(ranges[0]);
|
|
if (ranges.length > 1 && !ranges[1].equals(" ")) {
|
|
end = Integer.parseInt(ranges[1]);
|
|
}
|
|
}
|
|
|
|
//Check query parameters
|
|
if (session.getParameters().keySet().size() < 4) {
|
|
//Play offline
|
|
if (session.getParameters().get("id") != null) {
|
|
return offlineStream(session, startBytes, end, isRanged);
|
|
}
|
|
//Missing QP
|
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Invalid / Missing QP");
|
|
}
|
|
|
|
//Stream
|
|
return deezerStream(session, startBytes, end, isRanged);
|
|
|
|
}
|
|
|
|
private Response offlineStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
|
|
//Get path
|
|
String trackId = session.getParameters().get("id").get(0);
|
|
File file = new File(offlinePath, trackId);
|
|
long size = file.length();
|
|
//Read header
|
|
boolean isFlac = false;
|
|
try {
|
|
InputStream inputStream = new FileInputStream(file);
|
|
byte[] buffer = new byte[4];
|
|
inputStream.read(buffer, 0, 4);
|
|
inputStream.close();
|
|
if (new String(buffer).equals("fLaC"))
|
|
isFlac = true;
|
|
} catch (Exception e) {
|
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Invalid file!");
|
|
}
|
|
//Open file
|
|
RandomAccessFile randomAccessFile;
|
|
try {
|
|
randomAccessFile = new RandomAccessFile(file, "r");
|
|
randomAccessFile.seek(startBytes);
|
|
} catch (Exception e) {
|
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Failed getting data!");
|
|
}
|
|
|
|
//Generate response
|
|
Response response = newFixedLengthResponse(
|
|
isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
|
|
isFlac ? "audio/flac" : "audio/mpeg",
|
|
new InputStream() {
|
|
@Override
|
|
public int read() throws IOException {
|
|
return 0;
|
|
}
|
|
//Pass thru
|
|
@Override
|
|
public int read(byte[] b, int off, int len) throws IOException {
|
|
return randomAccessFile.read(b, off, len);
|
|
}
|
|
},
|
|
((end == -1) ? size : end) - startBytes
|
|
);
|
|
//Ranged header
|
|
if (isRanged) {
|
|
String range = "bytes " + Integer.toString(startBytes) + "-" + Long.toString((end == -1) ? size - 1 : end);
|
|
range += "/" + Long.toString(size);
|
|
response.addHeader("Content-Range", range);
|
|
}
|
|
response.addHeader("Accept-Ranges", "bytes");
|
|
|
|
//Save stream info
|
|
streams.put(trackId, new StreamInfo((isFlac ? "FLAC" : "MP3"), size, "Offline"));
|
|
|
|
return response;
|
|
}
|
|
|
|
private Response deezerStream(IHTTPSession session, int startBytes, int end, boolean isRanged) {
|
|
//Get QP into Quality Info
|
|
Deezer.QualityInfo qualityInfo = new Deezer.QualityInfo(
|
|
Integer.parseInt(session.getParameters().get("q").get(0)),
|
|
session.getParameters().get("id").get(0),
|
|
session.getParameters().get("md5origin").get(0),
|
|
session.getParameters().get("mv").get(0),
|
|
logger
|
|
);
|
|
//Fallback
|
|
try {
|
|
boolean res = qualityInfo.fallback(deezer);
|
|
if (!res)
|
|
throw new Exception("No more to fallback!");
|
|
} catch (Exception e) {
|
|
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Fallback failed!");
|
|
}
|
|
|
|
//Calculate Deezer offsets
|
|
int deezerStart = startBytes - (startBytes % 2048);
|
|
int dropBytes = startBytes % 2048;
|
|
//Start download
|
|
String sURL = Deezer.getTrackUrl(qualityInfo.trackId, qualityInfo.md5origin, qualityInfo.mediaVersion, qualityInfo.quality);
|
|
try {
|
|
URL url = new URL(sURL);
|
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
|
//Set headers
|
|
connection.setConnectTimeout(30000);
|
|
connection.setRequestMethod("GET");
|
|
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36");
|
|
connection.setRequestProperty("Accept-Language", "*");
|
|
connection.setRequestProperty("Accept", "*/*");
|
|
connection.setRequestProperty("Range", "bytes=" + Integer.toString(deezerStart) + "-" + ((end == -1) ? "" : Integer.toString(end)));
|
|
connection.connect();
|
|
|
|
//Get decryption key
|
|
final byte[] key = Deezer.getKey(qualityInfo.trackId);
|
|
|
|
//Write response headers
|
|
Response outResponse = newFixedLengthResponse(
|
|
isRanged ? Response.Status.PARTIAL_CONTENT : Response.Status.OK,
|
|
(qualityInfo.quality == 9) ? "audio/flac" : "audio/mpeg",
|
|
new BufferedInputStream(new FilterInputStream(connection.getInputStream()) {
|
|
|
|
int counter = deezerStart / 2048;
|
|
int drop = dropBytes;
|
|
|
|
//Decryption stream
|
|
@Override
|
|
public int read(byte[] b, int off, int len) throws IOException {
|
|
//Read 2048b or EOF
|
|
byte[] buffer = new byte[2048];
|
|
int read = 0;
|
|
int totalRead = 0;
|
|
while (read != -1 && totalRead != 2048) {
|
|
read = in.read(buffer, totalRead, 2048 - totalRead);
|
|
if (read != -1)
|
|
totalRead += read;
|
|
}
|
|
if (totalRead == 0)
|
|
return -1;
|
|
|
|
//Not full chunk return unencrypted
|
|
if (totalRead != 2048) {
|
|
System.arraycopy(buffer, 0, b, off, totalRead);
|
|
return totalRead;
|
|
}
|
|
//Decrypt
|
|
if ((counter % 3) == 0) {
|
|
buffer = Deezer.decryptChunk(key, buffer);
|
|
}
|
|
//Drop bytes from rounding to 2048
|
|
if (drop > 0) {
|
|
int output = 2048 - drop;
|
|
System.arraycopy(buffer, drop, b, off, output);
|
|
drop = 0;
|
|
counter++;
|
|
return output;
|
|
}
|
|
//Copy
|
|
System.arraycopy(buffer, 0, b, off, 2048);
|
|
counter++;
|
|
return 2048;
|
|
}
|
|
}, 2048),
|
|
connection.getContentLength() - dropBytes
|
|
);
|
|
//Ranged header
|
|
if (isRanged) {
|
|
String range = "bytes " + Integer.toString(startBytes) + "-" + Integer.toString((end == -1) ? (connection.getContentLength() + deezerStart) - 1 : end);
|
|
range += "/" + Integer.toString(connection.getContentLength() + deezerStart);
|
|
outResponse.addHeader("Content-Range", range);
|
|
}
|
|
outResponse.addHeader("Accept-Ranges", "bytes");
|
|
|
|
//Save stream info, use original track id
|
|
streams.put(session.getParameters().get("id").get(0), new StreamInfo(
|
|
((qualityInfo.quality == 9) ? "FLAC" : "MP3"),
|
|
deezerStart + connection.getContentLength(),
|
|
"Stream"
|
|
));
|
|
|
|
return outResponse;
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Failed getting data!");
|
|
}
|
|
}
|
|
} |