Private
Public Access
Minamp 1.2
- Transcoding: send client identities Plex has a profile for (unknown ones get 400 "unable to find a matching profile"), with a fresh session id per variant - Stream proxy: fetch transcodes once into a temp file and serve every player request from it, so reconnects no longer restart the transcode (503s, tracks cut short); original files still pass Range requests through - Sign-in: remember the pending link code so it survives Minamp being closed while the code is entered elsewhere; add "I've entered it - check now" - Diagnostics: in-app connection log (DiagLog) with a streaming test and save to SD card; tokens are never logged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a2ac901bd
commit
a4b875ef34
12 files changed
+701
-196
No files matched your search
+34
-11
@@ -130,25 +130,48 @@ public class PlexTest {
|
||||
public void onError(String id, int code, String m) { refused[0] = code + " " + m; }
|
||||
public void onAccepted(String id, int index) { accepted[0] = index; }
|
||||
};
|
||||
java.io.File cache = new java.io.File(System.getProperty("java.io.tmpdir"), "minamp-test-cache");
|
||||
cache.mkdirs();
|
||||
proxy.cacheDir = cache;
|
||||
Library.preferredVariant = 0;
|
||||
String id2 = proxy.newId();
|
||||
String local2 = proxy.register(id2, lib.transcodeUrls(tracks.get(1), 192, 30500, "sess1"), "mp3");
|
||||
String local2 = proxy.register(id2, lib.transcodeUrls(tracks.get(1), 192, 30500, "sess1"), "mp3", true);
|
||||
hc = (HttpURLConnection) new URL(local2).openConnection(Proxy.NO_PROXY);
|
||||
byte[] tbody = read(hc.getInputStream());
|
||||
String ts = new String(tbody, "ISO-8859-1");
|
||||
check(hc.getResponseCode() == 200 && ts.startsWith("TRANSCODE offset=30 bitrate=192|") && hc.getHeaderField("Content-Length") == null,
|
||||
"proxy de-chunks transcode stream (" + tbody.length + "B)");
|
||||
check(accepted[0] == 1 && refused[0] == null, "refused transcode variant skipped, second accepted (index " + accepted[0] + ")");
|
||||
Library.preferredVariant = Library.variantOrder()[accepted[0]];
|
||||
check(Library.variantOrder()[0] == 1, "accepted variant is tried first next time");
|
||||
// every variant refused -> error carries the server's words
|
||||
Library.preferredVariant = 0;
|
||||
check(hc.getResponseCode() == 200 && ts.startsWith("TRANSCODE offset=30 bitrate=192|"),
|
||||
"buffered transcode stream served (" + tbody.length + "B)");
|
||||
check(accepted[0] == 0 && refused[0] == null, "web-player identity accepted first time (variant " + accepted[0] + ")");
|
||||
// the media player reconnecting must not start the transcode again (that's the 503)
|
||||
hc = (HttpURLConnection) new URL(local2).openConnection(Proxy.NO_PROXY);
|
||||
byte[] again = read(hc.getInputStream());
|
||||
check(hc.getResponseCode() == 200 && again.length == tbody.length && refused[0] == null,
|
||||
"reconnect served from the buffer, no second transcode (" + again.length + "B)");
|
||||
hc = (HttpURLConnection) new URL(local2).openConnection(Proxy.NO_PROXY);
|
||||
hc.setRequestProperty("Range", "bytes=100-");
|
||||
byte[] part = read(hc.getInputStream());
|
||||
check(hc.getResponseCode() == 206 && part.length == tbody.length - 100 && proxy.isComplete(id2),
|
||||
"seek within a finished transcode via Range (" + part.length + "B)");
|
||||
check(!lib.transcodeUrls(tracks.get(1), 192, 0, "x").get(0).contains("X-Plex-Device=")
|
||||
&& lib.transcodeUrls(tracks.get(1), 192, 0, "x").get(0).contains("session=x-v0"),
|
||||
"no phone model in transcode identity; per-variant session ids");
|
||||
String dec = lib.decision(tracks.get(1), 192, 0);
|
||||
check(dec.indexOf("Conversion OK") >= 0, "decision check reads the server's verdict: " + dec);
|
||||
check(lib.decision(tracks.get(1), 192, 2).indexOf("Conversion OK") >= 0, "speaker-profile variant also accepted by profile matching");
|
||||
// a server that refuses everything: error carries the server's own words
|
||||
String id4 = proxy.newId();
|
||||
java.util.List<String> bad = new java.util.ArrayList<String>();
|
||||
bad.add(lib.transcodeUrls(tracks.get(1), 192, 0, "s4").get(0));
|
||||
String local4 = proxy.register(id4, bad, "mp3");
|
||||
bad.add(lib.transcodeUrls(tracks.get(1), 192, 0, "s4").get(0).replace("X-Plex-Platform=Chrome", "X-Plex-Platform=Android"));
|
||||
String local4 = proxy.register(id4, bad, "mp3", true);
|
||||
hc = (HttpURLConnection) new URL(local4).openConnection(Proxy.NO_PROXY);
|
||||
int c4 = hc.getResponseCode();
|
||||
check(c4 == 400 && refused[0] != null && refused[0].indexOf("400 Bad Request") >= 0, "refusal reported with server text: " + refused[0]);
|
||||
check(c4 == 400 && refused[0] != null && refused[0].indexOf("400 Bad Request") >= 0, "unrecognised identity -> refusal with server text: " + refused[0]);
|
||||
// the pre-fix request shape (phone model as device) is exactly what a real server refuses
|
||||
String old = lib.transcodeUrls(tracks.get(1), 192, 0, "s5").get(0).replace("X-Plex-Platform=Chrome", "X-Plex-Platform=Chrome&X-Plex-Device=GT-S5570");
|
||||
Http.Response or = Http.get(old, null, 10000);
|
||||
or.readAll();
|
||||
check(or.code == 400, "mock reproduces the 400 for the old phone-model identity");
|
||||
Library.preferredVariant = 0;
|
||||
|
||||
// abort mid-stream
|
||||
String id3 = proxy.newId();
|
||||
|
||||
+24
-1
@@ -61,6 +61,7 @@ TOKEN = "server-token-xyz"
|
||||
ACCOUNT = "account-token-123"
|
||||
pins = {}
|
||||
log = []
|
||||
starts = []
|
||||
|
||||
TRACKS = [
|
||||
{"type": "track", "ratingKey": "101", "key": "/library/metadata/101", "title": "Song One", "grandparentTitle": "The Testers",
|
||||
@@ -141,7 +142,24 @@ class H(BaseHTTPRequestHandler):
|
||||
{"type": "track", "title": "Tracks", "Metadata": TRACKS[:1]}]}})
|
||||
if p.startswith("/library/parts/501"):
|
||||
return self.file(method, MP3)
|
||||
if p in ("/music/:/transcode/universal/start.mp3", "/audio/:/transcode/universal/start.mp3"):
|
||||
if p.endswith("/transcode/universal/decision"):
|
||||
if not self.profile_ok(q):
|
||||
return self.send_json({"error": "unable to find a matching profile"}, 400)
|
||||
return self.send_json({"MediaContainer": {"generalDecisionCode": 1001, "generalDecisionText": "Direct play not available; Conversion OK.",
|
||||
"transcodeDecisionCode": 1001, "transcodeDecisionText": "Transcode OK."}})
|
||||
if p in ("/music/:/transcode/universal/start.mp3", "/audio/:/transcode/universal/start.mp3", "/audio/:/transcode/universal/start"):
|
||||
# like a real server: unknown client identity -> 400; a session started twice -> 503
|
||||
if not self.profile_ok(q):
|
||||
body = b"<html><body><h1>400 Bad Request</h1></body></html>"
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
return self.wfile.write(body)
|
||||
sess = (q.get("session") or [""])[0]
|
||||
starts.append(sess)
|
||||
if starts.count(sess) > 1:
|
||||
return self.send_json({"error": "session busy"}, 503)
|
||||
extra = (q.get("X-Plex-Client-Profile-Extra") or [""])[0]
|
||||
if "scopeName=*" in extra: # pretend this server rejects that variant
|
||||
body = b"<html><head><title>Bad Request</title></head><body><h1>400 Bad Request</h1></body></html>"
|
||||
@@ -157,6 +175,11 @@ class H(BaseHTTPRequestHandler):
|
||||
return self.send_json({})
|
||||
self.send_json({"error": "not found " + p}, 404)
|
||||
|
||||
def profile_ok(self, q):
|
||||
platform = (q.get("X-Plex-Platform") or [""])[0]
|
||||
device = (q.get("X-Plex-Device") or [""])[0]
|
||||
return platform in ("Chrome", "Linux") and device in ("", "Sonos")
|
||||
|
||||
def file(self, method, data, ctype="audio/mpeg"):
|
||||
rng = self.headers.get("Range")
|
||||
start, end = 0, len(data) - 1
|
||||
|
||||
Reference in new issue
Block a user