Private
Public Access
Minamp 1.3
- Fix NoSuchMethodError on Android 2.x: Dalvik before 4.0 can't link Object methods (toString/equals/...) called through an interface. Patch the bundled Bouncy Castle (tools/patch_bc_dalvik.py) and our own CertVerifier, and make build.sh fail if tools/scan_dalvik.py finds any such call - Treat a manually entered private address (10.x, 172.16-31.x, 192.168.x, .local) as home, so LAN servers that allow access without sign-in work - Send Plex client headers on media requests (files and transcodes) - Explain a 503 on a file as the server being unable to open it (drive or folder not available) - Connection log: home/away, token presence, Plex's file check and a direct file request in the streaming test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4b875ef34
commit
58d21b6d68
14 files changed
+218
-13
No files matched your search
@@ -499,7 +499,22 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
}
|
||||
lib.ensureMedia(t);
|
||||
com.minamp.net.DiagLog.log("track: " + t.title + " [" + t.codec + "/" + t.container + " " + t.bitrate + " kbps] via "
|
||||
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind)));
|
||||
+ (lib.server.active() == null ? "?" : Server.kindLabel(lib.server.active().kind))
|
||||
+ (lib.server.isLocal() ? " (home)" : " (away)"));
|
||||
String tok = lib.server.token;
|
||||
com.minamp.net.DiagLog.log("token: " + (tok == null || tok.length() == 0 ? "none (relying on the server allowing this network without sign-in)" : "set, " + tok.length() + " chars"));
|
||||
com.minamp.net.DiagLog.log("server file check: " + lib.checkPart(t));
|
||||
com.minamp.net.Http.Response fr = null;
|
||||
try {
|
||||
java.util.Map<String, String> h = lib.server.mediaHeaders();
|
||||
h.put("Range", "bytes=0-15");
|
||||
fr = com.minamp.net.Http.get(lib.directUrl(t), h, 20000);
|
||||
com.minamp.net.DiagLog.log("original file request: " + fr.code + " " + (fr.code >= 400 ? com.minamp.plex.Plex.snippet(fr.readString()) : fr.header("content-type")));
|
||||
} catch (Exception e) {
|
||||
com.minamp.net.DiagLog.log("original file request failed: " + com.minamp.net.Threads.describe(e));
|
||||
} finally {
|
||||
com.minamp.net.Http.abort(fr);
|
||||
}
|
||||
int kbps = app.currentMaxKbps();
|
||||
if (kbps <= 0) kbps = 320;
|
||||
for (int v = 0; v < Library.VARIANTS; v++) {
|
||||
@@ -508,7 +523,7 @@ public abstract class BaseActivity extends Activity implements PlayerService.Lis
|
||||
String url = lib.transcodeUrl(t, kbps, 0, session, v);
|
||||
com.minamp.net.Http.Response r = null;
|
||||
try {
|
||||
r = com.minamp.net.Http.get(url, null, 30000);
|
||||
r = com.minamp.net.Http.get(url, lib.server.mediaHeaders(), 30000);
|
||||
if (r.code < 400) {
|
||||
byte[] buf = new byte[4096];
|
||||
int n = r.body.read(buf);
|
||||
|
||||
@@ -161,7 +161,7 @@ public final class ImageLoader {
|
||||
if (data == null) {
|
||||
if (app.server == null) return null;
|
||||
String url = app.server.imageUrl(j.thumb, j.size);
|
||||
Http.Response r = Http.get(url, null, 20000);
|
||||
Http.Response r = Http.get(url, app.server.mediaHeaders(), 20000);
|
||||
if (r.code != 200) { r.close(); return null; }
|
||||
data = r.readAll();
|
||||
try {
|
||||
|
||||
@@ -320,7 +320,7 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
Http.Response r = null;
|
||||
String why = null;
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
r = Http.get(urls.get(i), Plex.headers(null), 30000);
|
||||
r = Http.get(urls.get(i), App.get().server != null ? App.get().server.mediaHeaders() : Plex.headers(null), 30000);
|
||||
if (r.code == 200) {
|
||||
if (order != null) {
|
||||
final int idx = i;
|
||||
@@ -397,7 +397,8 @@ public class PlayerService extends Service implements MediaPlayer.OnPreparedList
|
||||
refusalCode = 0;
|
||||
acceptedVariant = -1;
|
||||
// transcodes are fetched once and buffered, so player reconnects can't restart them
|
||||
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding));
|
||||
mp.setDataSource(proxy.register(routeId, urls, ext, transcoding,
|
||||
App.get().server != null ? App.get().server.mediaHeaders() : null));
|
||||
}
|
||||
setState(BUFFERING, "Buffering…");
|
||||
mp.prepareAsync();
|
||||
|
||||
@@ -160,7 +160,8 @@ public final class CertVerifier {
|
||||
GeneralName n = arr[i];
|
||||
if (n.getTagNo() == GeneralName.dNSName) {
|
||||
sawDns = true;
|
||||
if (!ip && dnsMatch(n.getName().toString(), host)) return true;
|
||||
// String.valueOf, not getName().toString(): Dalvik < 4.0 can't call Object methods via an interface
|
||||
if (!ip && dnsMatch(String.valueOf(n.getName()), host)) return true;
|
||||
} else if (n.getTagNo() == GeneralName.iPAddress && ip) {
|
||||
byte[] a = ASN1OctetString.getInstance(n.getName()).getOctets();
|
||||
if (ipString(a).equals(host)) return true;
|
||||
|
||||
@@ -50,6 +50,7 @@ public final class StreamProxy implements Runnable {
|
||||
final String id;
|
||||
final List<String> urls;
|
||||
final boolean buffered;
|
||||
Map<String, String> headers; // sent upstream with every request
|
||||
int chosen = -1; // index that worked
|
||||
final List<Http.Response> live = new ArrayList<Http.Response>();
|
||||
// buffered state (guarded by this)
|
||||
@@ -95,8 +96,14 @@ public final class StreamProxy implements Runnable {
|
||||
|
||||
/** @return the local URL to give MediaPlayer, ending in an extension it may sniff. */
|
||||
public String register(String id, List<String> remoteUrls, String ext, boolean buffered) {
|
||||
return register(id, remoteUrls, ext, buffered, null);
|
||||
}
|
||||
|
||||
public String register(String id, List<String> remoteUrls, String ext, boolean buffered, Map<String, String> headers) {
|
||||
Route r = new Route(id, new ArrayList<String>(remoteUrls), buffered);
|
||||
r.headers = headers;
|
||||
synchronized (routes) {
|
||||
routes.put(id, new Route(id, new ArrayList<String>(remoteUrls), buffered));
|
||||
routes.put(id, r);
|
||||
}
|
||||
return "http://127.0.0.1:" + port() + "/s/" + id + "/stream." + ext;
|
||||
}
|
||||
@@ -198,6 +205,7 @@ public final class StreamProxy implements Runnable {
|
||||
/** Tries the route's URLs in order; returns the accepted response or throws with the server's words. */
|
||||
private Http.Response openUpstream(Route route, String method, String range, int first, int last) throws IOException {
|
||||
Map<String, String> h = new LinkedHashMap<String, String>();
|
||||
if (route.headers != null) h.putAll(route.headers);
|
||||
h.put("Accept", "*/*");
|
||||
if (range != null) h.put("Range", range);
|
||||
String refusal = null;
|
||||
@@ -216,6 +224,9 @@ public final class StreamProxy implements Runnable {
|
||||
up.close();
|
||||
code = up.code;
|
||||
refusal = "Server refused the stream (" + up.code + ")" + (body.trim().length() > 0 ? ": " + com.minamp.plex.Plex.snippet(body) : "");
|
||||
if (up.code == 503 && route.urls.get(i).indexOf("/library/parts/") >= 0) {
|
||||
refusal = "Plex couldn't open this file (503). Is the drive or folder with your music available to the server?";
|
||||
}
|
||||
DiagLog.log("stream variant " + i + " refused: " + refusal);
|
||||
}
|
||||
throw new RefusedException(code, refusal);
|
||||
|
||||
@@ -251,12 +251,30 @@ public final class Library {
|
||||
return server.url(base + "?" + q);
|
||||
}
|
||||
|
||||
/** Whether the server can actually read a track's file (Plex's checkFiles), for diagnostics. */
|
||||
public String checkPart(Item t) {
|
||||
try {
|
||||
Json.Obj mc = server.get("/library/metadata/" + t.ratingKey + "?checkFiles=1");
|
||||
Json.Arr md = mc == null ? null : mc.optArr("Metadata");
|
||||
Json.Obj m = md == null ? null : md.optObj(0);
|
||||
Json.Arr media = m == null ? null : m.optArr("Media");
|
||||
Json.Obj me = media == null ? null : media.optObj(0);
|
||||
Json.Arr parts = me == null ? null : me.optArr("Part");
|
||||
Json.Obj p = parts == null ? null : parts.optObj(0);
|
||||
if (p == null) return "no file information returned";
|
||||
return "exists=" + p.optString("exists", "?") + " accessible=" + p.optString("accessible", "?")
|
||||
+ " file=" + p.optString("file", "?");
|
||||
} catch (Exception e) {
|
||||
return com.minamp.net.Threads.describe(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ask the server what it would do with a transcode request (for the diagnostics screen). */
|
||||
public String decision(Item t, int kbps, int variant) {
|
||||
try {
|
||||
String url = transcodeUrl(t, kbps, 0, "minamp-check-" + System.currentTimeMillis(), variant);
|
||||
url = url.replace("/start.mp3?", "/decision?").replace("/universal/start?", "/universal/decision?");
|
||||
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, Plex.headers(null), 20000);
|
||||
com.minamp.net.Http.Response r = com.minamp.net.Http.get(url, server.mediaHeaders(), 20000);
|
||||
String body = r.readString();
|
||||
if (r.code != 200) return "HTTP " + r.code + (Plex.snippet(body).length() > 0 ? ": " + Plex.snippet(body) : "");
|
||||
Json.Obj mc = Json.parseObj(body).optObj("MediaContainer");
|
||||
|
||||
@@ -73,7 +73,28 @@ public final class Server {
|
||||
|
||||
public boolean isLocal() {
|
||||
Conn c = active;
|
||||
return c != null && (c.kind == LOCAL || c.kind == LOCAL_HTTP);
|
||||
if (c == null) return false;
|
||||
if (c.kind == LOCAL || c.kind == LOCAL_HTTP) return true;
|
||||
return c.kind == MANUAL && isPrivateAddress(c.uri);
|
||||
}
|
||||
|
||||
/** A typed-in address on the home network (10.x, 172.16-31.x, 192.168.x, .local) counts as home. */
|
||||
static boolean isPrivateAddress(String uri) {
|
||||
String h = uri;
|
||||
int s = h.indexOf("://");
|
||||
if (s >= 0) h = h.substring(s + 3);
|
||||
int end = h.length();
|
||||
for (int i = 0; i < h.length(); i++) { char ch = h.charAt(i); if (ch == ':' || ch == '/') { end = i; break; } }
|
||||
h = h.substring(0, end).toLowerCase();
|
||||
if (h.endsWith(".local") || h.equals("localhost")) return true;
|
||||
if (h.startsWith("10.") || h.startsWith("192.168.") || h.startsWith("127.")) return true;
|
||||
if (h.startsWith("172.")) {
|
||||
try {
|
||||
int second = Integer.parseInt(h.substring(4, h.indexOf('.', 4)));
|
||||
return second >= 16 && second <= 31;
|
||||
} catch (Exception e) { return false; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isRelay() {
|
||||
@@ -202,9 +223,17 @@ public final class Server {
|
||||
|
||||
/** Absolute URL with the token in the query string (for the stream proxy and images). */
|
||||
public String url(String path) throws IOException {
|
||||
if (token == null || token.length() == 0) return base() + path; // never send an empty token
|
||||
return base() + path + (path.indexOf('?') >= 0 ? "&" : "?") + "X-Plex-Token=" + Plex.enc(token);
|
||||
}
|
||||
|
||||
/** Headers for media requests (files, transcodes): Plex wants to know which client is asking. */
|
||||
public java.util.Map<String, String> mediaHeaders() {
|
||||
java.util.Map<String, String> h = Plex.headers(token);
|
||||
h.put("Accept", "*/*");
|
||||
return h;
|
||||
}
|
||||
|
||||
public String imageUrl(String thumb, int size) throws IOException {
|
||||
if (thumb == null || thumb.length() == 0) return null;
|
||||
return url("/photo/:/transcode?width=" + size + "&height=" + size + "&minSize=1&upscale=1&url=" + Plex.enc(thumb));
|
||||
|
||||
Reference in new issue
Block a user