Import miniabs 1.0 source
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4fe9eed870
78 files changed
+5800
No files matched your search
@@ -0,0 +1,542 @@
|
||||
package com.miniabs;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
/** Audiobookshelf REST client (API as of server 2.x, including the 2.26+ token system). */
|
||||
final class Api {
|
||||
static final String VERSION = "1.0";
|
||||
|
||||
static class ApiException extends IOException {
|
||||
final int code;
|
||||
|
||||
ApiException(int code, String msg) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/** Sign-in is no longer valid; the user must log in again. */
|
||||
static class AuthException extends IOException {
|
||||
AuthException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object REFRESH_LOCK = new Object();
|
||||
private static SSLSocketFactory trustAllFactory;
|
||||
|
||||
static {
|
||||
// Froyo's HttpURLConnection reuses dead pooled sockets; turn pooling off.
|
||||
System.setProperty("http.keepAlive", "false");
|
||||
}
|
||||
|
||||
private Api() {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ plumbing
|
||||
|
||||
static HttpURLConnection open(Context c, String url, String method) throws IOException {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(30000);
|
||||
conn.setUseCaches(false);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
if (conn instanceof HttpsURLConnection && Prefs.get(c).trustAll()) trustAll((HttpsURLConnection) conn);
|
||||
setMethod(conn, method);
|
||||
conn.setRequestProperty("User-Agent", "MiniABS/" + VERSION + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
return conn;
|
||||
}
|
||||
|
||||
private static void setMethod(HttpURLConnection conn, String method) throws ProtocolException {
|
||||
conn.setRequestMethod(method);
|
||||
}
|
||||
|
||||
private static synchronized SSLSocketFactory trustAllFactory() throws Exception {
|
||||
if (trustAllFactory == null) {
|
||||
TrustManager tm = new X509TrustManager() {
|
||||
public void checkClientTrusted(X509Certificate[] chain, String type) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String type) {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
};
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(null, new TrustManager[]{tm}, null);
|
||||
trustAllFactory = ctx.getSocketFactory();
|
||||
}
|
||||
return trustAllFactory;
|
||||
}
|
||||
|
||||
private static void trustAll(HttpsURLConnection conn) {
|
||||
try {
|
||||
conn.setSSLSocketFactory(trustAllFactory());
|
||||
conn.setHostnameVerifier(new HostnameVerifier() {
|
||||
public boolean verify(String host, SSLSession session) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH over a plain socket. Android 2.x's HttpURLConnection only sends a body with POST or PUT,
|
||||
* and Audiobookshelf uses PATCH for "mark as played" and for saving progress without a session.
|
||||
*/
|
||||
private static Object rawPatch(Context c, String path, JSONObject body, boolean mayRefresh) throws IOException {
|
||||
Prefs p = Prefs.get(c);
|
||||
if (mayRefresh) ensureFresh(c, 120);
|
||||
String used = p.token();
|
||||
URL u = new URL(p.server() + path);
|
||||
boolean https = "https".equalsIgnoreCase(u.getProtocol());
|
||||
int port = u.getPort() > 0 ? u.getPort() : (https ? 443 : 80);
|
||||
Socket sock = new Socket();
|
||||
int code;
|
||||
try {
|
||||
sock.connect(new InetSocketAddress(u.getHost(), port), 15000);
|
||||
sock.setSoTimeout(30000);
|
||||
Socket io = sock;
|
||||
if (https) {
|
||||
boolean trust = p.trustAll();
|
||||
SSLSocketFactory f;
|
||||
try {
|
||||
f = trust ? trustAllFactory() : (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
} catch (Exception e) {
|
||||
throw new SSLException("TLS unavailable");
|
||||
}
|
||||
SSLSocket ss = (SSLSocket) f.createSocket(sock, u.getHost(), port, true);
|
||||
ss.startHandshake();
|
||||
if (!trust && !HttpsURLConnection.getDefaultHostnameVerifier().verify(u.getHost(), ss.getSession())) {
|
||||
ss.close();
|
||||
throw new SSLException("Certificate doesn't match " + u.getHost());
|
||||
}
|
||||
io = ss;
|
||||
}
|
||||
byte[] b = (body == null ? "{}" : body.toString()).getBytes("UTF-8");
|
||||
String host = u.getHost() + (u.getPort() > 0 ? ":" + u.getPort() : "");
|
||||
String head = "PATCH " + u.getFile() + " HTTP/1.1\r\n"
|
||||
+ "Host: " + host + "\r\n"
|
||||
+ "Authorization: Bearer " + used + "\r\n"
|
||||
+ "Content-Type: application/json\r\n"
|
||||
+ "Accept: application/json\r\n"
|
||||
+ "User-Agent: MiniABS/" + VERSION + "\r\n"
|
||||
+ "Content-Length: " + b.length + "\r\n"
|
||||
+ "Connection: close\r\n\r\n";
|
||||
OutputStream os = io.getOutputStream();
|
||||
os.write(head.getBytes("UTF-8"));
|
||||
os.write(b);
|
||||
os.flush();
|
||||
InputStream in = io.getInputStream();
|
||||
StringBuilder line = new StringBuilder();
|
||||
int ch;
|
||||
while ((ch = in.read()) != -1 && ch != '\n' && line.length() < 200) line.append((char) ch);
|
||||
String[] parts = line.toString().trim().split(" ");
|
||||
if (parts.length < 2 || !parts[0].startsWith("HTTP/")) throw new IOException("Unexpected reply from server");
|
||||
try {
|
||||
code = Integer.parseInt(parts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Unexpected reply from server");
|
||||
}
|
||||
byte[] rest = new byte[1024];
|
||||
int total = 0, n;
|
||||
while (total < 16384 && (n = in.read(rest)) > 0) total += n; // read the reply politely
|
||||
io.close();
|
||||
} finally {
|
||||
try {
|
||||
sock.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
if (code == 401) {
|
||||
if (mayRefresh && p.canRefresh()) {
|
||||
refreshIf(c, used, 0);
|
||||
return rawPatch(c, path, body, false);
|
||||
}
|
||||
throw new AuthException("Your sign-in has expired. Please log in again.");
|
||||
}
|
||||
if (code >= 400) throw new ApiException(code, "");
|
||||
if (code >= 300) throw new ApiException(code, "The server redirected; check the address in Settings.");
|
||||
return "OK";
|
||||
}
|
||||
|
||||
/** Old Android throws instead of returning 401 when the server sends no WWW-Authenticate header. */
|
||||
static int responseCode(HttpURLConnection conn) throws IOException {
|
||||
try {
|
||||
return conn.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
String m = e.getMessage();
|
||||
if (m != null && m.toLowerCase().contains("authentication challenge")) return 401;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeBody(HttpURLConnection conn, JSONObject body) throws IOException {
|
||||
byte[] b = (body == null ? "{}" : body.toString()).getBytes("UTF-8");
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setFixedLengthStreamingMode(b.length);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
try {
|
||||
os.write(b);
|
||||
} finally {
|
||||
os.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object readBody(HttpURLConnection conn, int maxString, String[] deepSkip) throws IOException {
|
||||
InputStream is = new BufferedInputStream(conn.getInputStream(), 8192);
|
||||
try {
|
||||
String type = conn.getContentType();
|
||||
Reader r = new InputStreamReader(is, "UTF-8");
|
||||
if (type != null && type.contains("json")) return Json.parse(r, maxString, deepSkip);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] b = new char[512];
|
||||
int n;
|
||||
while ((n = r.read(b)) > 0 && sb.length() < 4000) sb.append(b, 0, n);
|
||||
return sb.toString();
|
||||
} finally {
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String errorText(HttpURLConnection conn) {
|
||||
try {
|
||||
InputStream es = conn.getErrorStream();
|
||||
if (es == null) return "";
|
||||
Reader r = new InputStreamReader(es, "UTF-8");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] b = new char[256];
|
||||
int n;
|
||||
while ((n = r.read(b)) > 0 && sb.length() < 200) sb.append(b, 0, n);
|
||||
es.close();
|
||||
String t = sb.toString().trim();
|
||||
if (t.startsWith("<")) return ""; // an HTML error page is no use on a tiny screen
|
||||
return t.length() > 120 ? t.substring(0, 120) : t;
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static Object call(Context c, String method, String path, JSONObject body) throws IOException {
|
||||
return call(c, method, path, body, 2000, null, true);
|
||||
}
|
||||
|
||||
static Object call(Context c, String method, String path, JSONObject body, int maxString, String[] deepSkip,
|
||||
boolean mayRefresh) throws IOException {
|
||||
Prefs p = Prefs.get(c);
|
||||
if (!p.loggedIn()) throw new AuthException("Please log in.");
|
||||
if ("PATCH".equals(method)) return rawPatch(c, path, body, mayRefresh);
|
||||
if (mayRefresh) ensureFresh(c, 120);
|
||||
String used = p.token();
|
||||
HttpURLConnection conn = open(c, p.server() + path, method);
|
||||
try {
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + used);
|
||||
if (body != null || "POST".equals(method)) writeBody(conn, body);
|
||||
int code = responseCode(conn);
|
||||
if (code == 401) {
|
||||
conn.disconnect();
|
||||
if (mayRefresh && p.canRefresh()) {
|
||||
refreshIf(c, used, 0);
|
||||
return call(c, method, path, body, maxString, deepSkip, false);
|
||||
}
|
||||
throw new AuthException("Your sign-in has expired. Please log in again.");
|
||||
}
|
||||
if (code >= 400) throw new ApiException(code, errorText(conn));
|
||||
return readBody(conn, maxString, deepSkip);
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static JSONObject obj(Object o) throws IOException {
|
||||
if (o instanceof JSONObject) return (JSONObject) o;
|
||||
throw new IOException("Unexpected reply from server");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ sign-in
|
||||
|
||||
/** Checks the address points at an Audiobookshelf server. Returns its version, or "" if unknown. */
|
||||
static String status(Context c, String server) throws IOException {
|
||||
HttpURLConnection conn = open(c, server + "/status", "GET");
|
||||
try {
|
||||
int code = responseCode(conn);
|
||||
if (code == 404) throw new IOException("No Audiobookshelf server at this address.");
|
||||
if (code >= 400) throw new ApiException(code, errorText(conn));
|
||||
Object o = readBody(conn, 200, null);
|
||||
if (!(o instanceof JSONObject)) throw new IOException("No Audiobookshelf server at this address.");
|
||||
JSONObject s = (JSONObject) o;
|
||||
if (s.has("isInit") && !s.optBoolean("isInit", true)) {
|
||||
throw new IOException("This server hasn't been set up yet. Finish setup in a browser first.");
|
||||
}
|
||||
return Util.str(s, "serverVersion");
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs in with username and password. Returns the user's default library id (may be empty). */
|
||||
static String login(Context c, String server, String user, String pass) throws IOException {
|
||||
HttpURLConnection conn = open(c, server + "/login", "POST");
|
||||
try {
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestProperty("x-return-tokens", "true");
|
||||
JSONObject body = new JSONObject();
|
||||
try {
|
||||
body.put("username", user).put("password", pass);
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
writeBody(conn, body);
|
||||
int code = responseCode(conn);
|
||||
if (code == 401) throw new ApiException(401, "Wrong username or password.");
|
||||
if (code == 429) throw new ApiException(429, "Too many attempts. Wait a minute and try again.");
|
||||
if (code >= 400) throw new ApiException(code, errorText(conn));
|
||||
JSONObject o = obj(readBody(conn, 500, null));
|
||||
JSONObject u = o.optJSONObject("user");
|
||||
if (u == null) throw new IOException("Unexpected reply from server");
|
||||
String access = Util.str(u, "accessToken");
|
||||
String refresh = Util.str(u, "refreshToken");
|
||||
String legacy = Util.str(u, "token");
|
||||
Prefs p = Prefs.get(c);
|
||||
if (access.length() > 0) p.setAuth(user, Prefs.MODE_JWT, access, refresh);
|
||||
else if (legacy.length() > 0) p.setAuth(user, Prefs.MODE_LEGACY, legacy, "");
|
||||
else throw new IOException("The server didn't return a sign-in token.");
|
||||
return Util.str(o, "userDefaultLibraryId");
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** Signs in with an API key by checking it against /api/me. Returns the username. */
|
||||
static String loginWithKey(Context c, String key) throws IOException {
|
||||
Prefs p = Prefs.get(c);
|
||||
p.setAuth("", Prefs.MODE_KEY, key, "");
|
||||
try {
|
||||
JSONObject me = obj(call(c, "GET", "/api/me", null, 100, null, false));
|
||||
String name = Util.str(me, "username");
|
||||
p.setAuth(name, Prefs.MODE_KEY, key, "");
|
||||
return name;
|
||||
} catch (IOException e) {
|
||||
p.logout();
|
||||
if (e instanceof AuthException) throw new ApiException(401, "That API key wasn't accepted.");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Refreshes the access token if it has less than minSeconds left. */
|
||||
static void ensureFresh(Context c, long minSeconds) throws IOException {
|
||||
Prefs p = Prefs.get(c);
|
||||
if (!p.canRefresh()) return;
|
||||
if (p.tokenSecondsLeft() >= minSeconds) return;
|
||||
refreshIf(c, null, minSeconds);
|
||||
}
|
||||
|
||||
/** Gets a new access token now (after a 401, or before reconnecting a stream). */
|
||||
static void refresh(Context c, boolean force) throws IOException {
|
||||
refreshIf(c, force ? Prefs.get(c).token() : null, 120);
|
||||
}
|
||||
|
||||
/**
|
||||
* stale: the token the server just rejected. If another thread has already replaced it,
|
||||
* there's nothing to do. With no stale token, refreshes only if fewer than minSeconds are left.
|
||||
*/
|
||||
private static void refreshIf(Context c, String stale, long minSeconds) throws IOException {
|
||||
synchronized (REFRESH_LOCK) {
|
||||
Prefs p = Prefs.get(c);
|
||||
if (!p.canRefresh()) throw new AuthException("Your sign-in has expired. Please log in again.");
|
||||
if (stale != null) {
|
||||
if (!stale.equals(p.token())) return;
|
||||
} else if (p.tokenSecondsLeft() >= minSeconds) {
|
||||
return;
|
||||
}
|
||||
HttpURLConnection conn = open(c, p.server() + "/auth/refresh", "POST");
|
||||
try {
|
||||
conn.setRequestProperty("Accept", "application/json");
|
||||
conn.setRequestProperty("x-refresh-token", p.refreshToken());
|
||||
writeBody(conn, null);
|
||||
int code = responseCode(conn);
|
||||
if (code == 401 || code == 403) {
|
||||
throw new AuthException("Your sign-in has expired. Please log in again.");
|
||||
}
|
||||
if (code >= 400) throw new ApiException(code, errorText(conn));
|
||||
JSONObject u = obj(readBody(conn, 500, null)).optJSONObject("user");
|
||||
String access = u == null ? "" : Util.str(u, "accessToken");
|
||||
if (access.length() == 0) throw new AuthException("Your sign-in has expired. Please log in again.");
|
||||
p.setTokens(access, Util.str(u, "refreshToken"));
|
||||
} finally {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ URLs
|
||||
|
||||
static String enc(String s) {
|
||||
try {
|
||||
return URLEncoder.encode(s, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/** Appends the access token as a query parameter (MediaPlayer on Android 2.x can't send headers). */
|
||||
static String withToken(Context c, String url) {
|
||||
return url + (url.indexOf('?') >= 0 ? "&" : "?") + "token=" + enc(Prefs.get(c).token());
|
||||
}
|
||||
|
||||
/** Turns a server-relative content URL into a full URL, handling servers hosted under a sub-path. */
|
||||
static String absolute(Context c, String contentUrl) {
|
||||
if (contentUrl.startsWith("http://") || contentUrl.startsWith("https://")) return contentUrl;
|
||||
String server = Prefs.get(c).server();
|
||||
try {
|
||||
URL u = new URL(server);
|
||||
String base = u.getPath();
|
||||
if (base.length() > 1 && contentUrl.startsWith(base + "/")) {
|
||||
String origin = u.getProtocol() + "://" + u.getHost() + (u.getPort() > 0 ? ":" + u.getPort() : "");
|
||||
return origin + contentUrl;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return server + (contentUrl.startsWith("/") ? "" : "/") + contentUrl;
|
||||
}
|
||||
|
||||
static String coverUrl(Context c, String itemId, int width) {
|
||||
return withToken(c, Prefs.get(c).server() + "/api/items/" + itemId + "/cover?format=jpeg&width=" + width);
|
||||
}
|
||||
|
||||
static String[] downloadUrls(Context c, Episode e) {
|
||||
String base = Prefs.get(c).server() + "/api/items/" + e.itemId + "/file/" + e.ino;
|
||||
return new String[]{withToken(c, base + "/download"), withToken(c, base)};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ data
|
||||
|
||||
static JSONArray libraries(Context c) throws IOException {
|
||||
Object o = call(c, "GET", "/api/libraries", null);
|
||||
if (o instanceof JSONArray) return (JSONArray) o;
|
||||
JSONArray a = obj(o).optJSONArray("libraries");
|
||||
return a == null ? new JSONArray() : a;
|
||||
}
|
||||
|
||||
static JSONObject libraryItems(Context c, String lib, int page, int limit) throws IOException {
|
||||
return obj(call(c, "GET", "/api/libraries/" + lib + "/items?minified=1&sort=media.metadata.title&limit="
|
||||
+ limit + "&page=" + page, null, 300, null, true));
|
||||
}
|
||||
|
||||
static JSONObject recentEpisodes(Context c, String lib, int page, int limit) throws IOException {
|
||||
return obj(call(c, "GET", "/api/libraries/" + lib + "/recent-episodes?limit=" + limit + "&page=" + page,
|
||||
null, 1200, new String[]{"episodes", "audioFiles", "tracks"}, true));
|
||||
}
|
||||
|
||||
static JSONObject itemsInProgress(Context c) throws IOException {
|
||||
return obj(call(c, "GET", "/api/me/items-in-progress?limit=15", null, 1200,
|
||||
new String[]{"episodes"}, true));
|
||||
}
|
||||
|
||||
/** A podcast with all its episodes. Show notes are cut to keep memory low. */
|
||||
static JSONObject item(Context c, String itemId) throws IOException {
|
||||
return obj(call(c, "GET", "/api/items/" + itemId, null, 800, null, true));
|
||||
}
|
||||
|
||||
static JSONObject me(Context c) throws IOException {
|
||||
return obj(call(c, "GET", "/api/me", null, 200, null, true));
|
||||
}
|
||||
|
||||
private static JSONObject deviceInfo(Context c) throws JSONException {
|
||||
return new JSONObject()
|
||||
.put("deviceId", Prefs.get(c).deviceId())
|
||||
.put("clientName", "MiniABS")
|
||||
.put("clientVersion", VERSION)
|
||||
.put("manufacturer", Build.MANUFACTURER)
|
||||
.put("model", Build.MODEL)
|
||||
.put("sdkVersion", Build.VERSION.SDK_INT);
|
||||
}
|
||||
|
||||
/** Opens a playback session. Asks for direct play: Android 2.x can't play HLS transcodes. */
|
||||
static JSONObject startPlayback(Context c, Episode e) throws IOException {
|
||||
JSONObject body = new JSONObject();
|
||||
try {
|
||||
JSONArray mimes = new JSONArray();
|
||||
String[] ok = {"audio/mpeg", "audio/mp3", "audio/mp4", "audio/x-m4a", "audio/aac", "audio/x-aac",
|
||||
"audio/ogg", "audio/wav", "audio/x-wav", "audio/3gpp", "audio/amr"};
|
||||
for (String m : ok) mimes.put(m);
|
||||
body.put("deviceInfo", deviceInfo(c))
|
||||
.put("forceDirectPlay", true)
|
||||
.put("forceTranscode", false)
|
||||
.put("supportedMimeTypes", mimes)
|
||||
.put("mediaPlayer", "MiniABS");
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
return obj(call(c, "POST", "/api/items/" + e.itemId + "/play/" + e.id, body, 200,
|
||||
new String[]{"episodes"}, true));
|
||||
}
|
||||
|
||||
private static JSONObject syncBody(double cur, double listened, double dur) {
|
||||
JSONObject b = new JSONObject();
|
||||
try {
|
||||
b.put("currentTime", cur).put("timeListened", listened);
|
||||
if (dur > 0) b.put("duration", dur);
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static void syncSession(Context c, String sid, double cur, double listened, double dur) throws IOException {
|
||||
call(c, "POST", "/api/session/" + sid + "/sync", syncBody(cur, listened, dur));
|
||||
}
|
||||
|
||||
static void closeSession(Context c, String sid, double cur, double listened, double dur) throws IOException {
|
||||
call(c, "POST", "/api/session/" + sid + "/close", syncBody(cur, listened, dur));
|
||||
}
|
||||
|
||||
/** Saves progress without a session (offline catch-up, mark played/unplayed). */
|
||||
static void setProgress(Context c, String itemId, String epId, double cur, double dur, Boolean finished)
|
||||
throws IOException {
|
||||
JSONObject b = new JSONObject();
|
||||
try {
|
||||
if (cur >= 0) {
|
||||
b.put("currentTime", cur);
|
||||
if (dur > 0) {
|
||||
b.put("duration", dur);
|
||||
b.put("progress", Math.min(1.0, cur / dur));
|
||||
}
|
||||
}
|
||||
if (finished != null) b.put("isFinished", finished.booleanValue());
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
call(c, "PATCH", "/api/me/progress/" + itemId + "/" + epId, b);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user