Import minamp 1.0 source

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-14 19:38:37 +02:00
commit 0df8124629
194 files changed
+7319

No files matched your search

+225
View File
@@ -0,0 +1,225 @@
package com.minamp.plex;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import com.minamp.util.Json;
/** Paths into one music library section, plus the playback-URL decisions. */
public final class Library {
public final Server server;
public final String section;
public Library(Server server, String section) {
this.server = server;
this.section = section;
}
private static String page(int start, int size) {
return "&X-Plex-Container-Start=" + start + "&X-Plex-Container-Size=" + size;
}
public String artistsPath(int start, int size) {
return "/library/sections/" + section + "/all?type=8&sort=titleSort" + page(start, size);
}
public String albumsPath(String sort, int start, int size) {
return "/library/sections/" + section + "/all?type=9&sort=" + sort + page(start, size);
}
public String recentlyAddedPath(int size) {
return "/library/sections/" + section + "/all?type=9&sort=addedAt:desc" + page(0, size);
}
public String recentlyPlayedPath(int size) {
return "/library/sections/" + section + "/all?type=9&sort=lastViewedAt:desc&viewCount>>=0" + page(0, size);
}
public String playlistsPath() {
return "/playlists?playlistType=audio&sectionID=" + section;
}
public String searchPath(String query) {
return "/hubs/search?query=" + Plex.enc(query) + "&sectionId=" + section + "&limit=12";
}
/** Search results grouped as [HEADER, items..., HEADER, items...] (artists, albums, tracks). */
public List<Item> search(String query) throws IOException {
Json.Obj mc = server.get(searchPath(query));
List<Item> out = new ArrayList<Item>();
Json.Arr hubs = mc == null ? null : mc.optArr("Hub");
if (hubs == null) return out;
String[] order = {"artist", "album", "track", "playlist"};
for (int o = 0; o < order.length; o++) {
for (int i = 0; i < hubs.size(); i++) {
Json.Obj hub = hubs.optObj(i);
if (hub == null || !order[o].equals(hub.optString("type"))) continue;
List<Item> items = Item.listFrom(hub);
if (items.isEmpty()) continue;
Item header = new Item();
header.type = Item.HEADER;
header.title = hub.optString("title", order[o]);
out.add(header);
out.addAll(items);
}
}
return out;
}
public static String childrenPath(Item it) {
if (it.type == Item.PLAYLIST) return "/playlists/" + it.ratingKey + "/items";
return "/library/metadata/" + it.ratingKey + "/children";
}
// ------------------------------------------------------------------ radios / mixes
/** "Library radio": a random slice of every track in the library. */
public List<Item> libraryRadio() throws IOException {
return server.items("/library/sections/" + section + "/all?type=10&sort=random" + page(0, 100));
}
/** Tracks rated four stars or better (and loved tracks), shuffled. */
public List<Item> topRatedRadio() throws IOException {
return server.items("/library/sections/" + section + "/all?type=10&userRating>>=7&sort=random" + page(0, 100));
}
/** Tracks from albums released in a random decade that the library actually has. */
public List<Item> timeTravelRadio(int decade) throws IOException {
List<Item> tracks;
try {
tracks = server.items("/library/sections/" + section + "/all?type=10&album.decade=" + decade + "&sort=random" + page(0, 100));
} catch (Plex.PlexException e) {
tracks = new ArrayList<Item>();
}
if (!tracks.isEmpty()) return tracks;
// Older servers don't filter tracks by album decade: build it from random albums instead.
List<Item> albums = server.items("/library/sections/" + section + "/all?type=9&decade=" + decade + "&sort=random" + page(0, 8));
for (int i = 0; i < albums.size(); i++) tracks.addAll(tracksOf(albums.get(i)));
shuffle(tracks);
return tracks;
}
public List<Integer> decades() throws IOException {
Json.Obj mc = server.get("/library/sections/" + section + "/decade?type=9");
List<Integer> out = new ArrayList<Integer>();
Json.Arr d = mc == null ? null : mc.optArr("Directory");
if (d == null) return out;
for (int i = 0; i < d.size(); i++) {
Json.Obj o = d.optObj(i);
if (o == null) continue;
int v = o.optInt("key", 0);
if (v > 0) out.add(Integer.valueOf(v));
}
return out;
}
/** Every track for an album, artist or playlist, in order. */
public List<Item> tracksOf(Item it) throws IOException {
String path;
switch (it.type) {
case Item.TRACK: {
List<Item> one = new ArrayList<Item>();
one.add(it);
return one;
}
case Item.ALBUM: path = "/library/metadata/" + it.ratingKey + "/children"; break;
case Item.ARTIST: path = "/library/metadata/" + it.ratingKey + "/allLeaves"; break;
case Item.PLAYLIST: path = "/playlists/" + it.ratingKey + "/items" + "?X-Plex-Container-Start=0&X-Plex-Container-Size=1000"; break;
default: return new ArrayList<Item>();
}
List<Item> all = server.items(path);
List<Item> tracks = new ArrayList<Item>();
for (int i = 0; i < all.size(); i++) if (all.get(i).type == Item.TRACK) tracks.add(all.get(i));
return tracks;
}
/** Fills in media info for a track that came from somewhere without it (e.g. search). */
public void ensureMedia(Item t) throws IOException {
if (t.partKey.length() > 0) return;
List<Item> l = server.items("/library/metadata/" + t.ratingKey);
if (!l.isEmpty()) {
Item full = l.get(0);
t.partKey = full.partKey; t.codec = full.codec; t.container = full.container;
t.bitrate = full.bitrate; t.partSize = full.partSize;
if (t.duration == 0) t.duration = full.duration;
}
if (t.partKey.length() == 0) throw new IOException("Track has no playable media");
}
public static void shuffle(List<Item> l) {
Collections.shuffle(l, new Random());
}
// ------------------------------------------------------------------ streaming
/** What Android 2.2/2.3's media framework can decode itself. */
public static boolean canDirectPlay(Item t, int maxKbps) {
String c = t.codec.toLowerCase();
String k = t.container.toLowerCase();
boolean ok = ("mp3".equals(c) && ("mp3".equals(k) || k.length() == 0))
|| ("aac".equals(c) && ("mp4".equals(k) || "m4a".equals(k) || "m4b".equals(k)))
|| ("vorbis".equals(c) && "ogg".equals(k));
if (!ok) return false;
if (maxKbps <= 0) return true;
return t.bitrate > 0 && t.bitrate <= maxKbps + 16;
}
public String directUrl(Item t) throws IOException {
return server.url(t.partKey);
}
/** MP3 transcode over plain progressive HTTP, starting at offsetMs. */
public String transcodeUrl(Item t, int kbps, long offsetMs, String session) throws IOException {
if (kbps <= 0) kbps = 320;
String profile = "add-transcode-target(type=musicProfile&context=streaming&protocol=http&container=mp3&audioCodec=mp3)"
+ "+add-limitation(scope=musicCodec&scopeName=mp3&type=upperBound&name=audio.bitrate&value=" + kbps + ")"
+ "+add-limitation(scope=musicCodec&scopeName=mp3&type=upperBound&name=audio.channels&value=2)";
String path = "/music/:/transcode/universal/start.mp3"
+ "?path=" + Plex.enc("/library/metadata/" + t.ratingKey)
+ "&mediaIndex=0&partIndex=0&protocol=http&hasMDE=1&fastSeek=1"
+ "&directPlay=0&directStream=0&directStreamAudio=0"
+ "&musicBitrate=" + kbps + "&maxAudioBitrate=" + kbps
+ "&offset=" + (offsetMs / 1000)
+ "&session=" + Plex.enc(session)
+ "&location=" + (server.isLocal() ? "lan" : "wan")
+ "&X-Plex-Session-Identifier=" + Plex.enc(session)
+ "&X-Plex-Client-Identifier=" + Plex.enc(Plex.clientId)
+ "&X-Plex-Product=" + Plex.enc(Plex.product)
+ "&X-Plex-Version=" + Plex.enc(Plex.version)
+ "&X-Plex-Platform=Generic"
+ "&X-Plex-Device=" + Plex.enc(Plex.device)
+ "&X-Plex-Client-Profile-Extra=" + Plex.enc(profile);
return server.url(path);
}
public void stopTranscode(String session) {
try {
server.fetchQuiet("/transcode/universal/stop?session=" + Plex.enc(session));
} catch (Exception ignored) {}
}
// ------------------------------------------------------------------ reporting
public void timeline(Item t, String state, long timeMs) {
try {
server.fetchQuiet("/:/timeline?ratingKey=" + t.ratingKey
+ "&key=" + Plex.enc("/library/metadata/" + t.ratingKey)
+ "&state=" + state + "&time=" + timeMs + "&playbackTime=" + timeMs
+ "&duration=" + t.duration + "&hasMDE=1");
} catch (Exception ignored) {}
}
public void scrobble(Item t) {
try {
server.fetchQuiet("/:/scrobble?identifier=com.plexapp.plugins.library&key=" + Plex.enc(t.ratingKey));
} catch (Exception ignored) {}
}
public void rate(Item t, double rating) throws IOException {
server.put("/:/rate?identifier=com.plexapp.plugins.library&key=" + Plex.enc(t.ratingKey) + "&rating=" + (int) rating);
}
}