Skip to content

Commit

Permalink
Server: Bring in the Simple UI!
Browse files Browse the repository at this point in the history
  • Loading branch information
e3ndr committed Sep 22, 2023
1 parent d5d657c commit 7f5f91b
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 31 deletions.
6 changes: 6 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.unbescape</groupId>
<artifactId>unbescape</artifactId>
<version>1.1.6.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.e3ndr</groupId>
<artifactId>FastLoggingFramework</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public void close() throws IOException {} // Already handled in MediaSession#sta
@SneakyThrows
@HttpEndpoint(uri = "/api/media/:mediaId/stream/hls")
public HttpResponse onStreamHLS(SoraHttpSession session) {
String playlist = this.generateHLSPlaylist(session, "/api/media/%s/stream/raw");
return HttpResponse.newFixedLengthResponse(StandardHttpStatus.OK, playlist)
.setMimeType("application/vnd.apple.mpegurl");
}

public String generateHLSPlaylist(SoraHttpSession session, String uriBase) {
final double DURATION = 8;
String mediaId = session.getUriParameters().get("mediaId");

Expand All @@ -136,12 +142,11 @@ public HttpResponse onStreamHLS(SoraHttpSession session) {
+ "#EXT-X-MEDIA-SEQUENCE:0\r\n";

// TODO
playlist += String.format("#EXTINF:%.1f,\r\n/api/media/%s/stream/raw%s&skipTo=%d\r\n", DURATION, mediaId, session.getQueryString(), -1);
playlist += String.format("#EXTINF:%.1f,\r\n%s%s&skipTo=%d\r\n", DURATION, uriBase, mediaId, session.getQueryString(), -1);

playlist += "#EXT-X-ENDLIST";

return HttpResponse.newFixedLengthResponse(StandardHttpStatus.OK, playlist)
.setMimeType("application/vnd.apple.mpegurl");
return playlist;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static class AthenaSoraAdapter extends SoraPlugin {
@Override
public void onInit(Sora sora) {
sora.addProvider(this, new UIRoutes());
sora.addProvider(this, new InternalRoutes());
}

@Override
Expand Down
67 changes: 67 additions & 0 deletions server/src/main/java/xyz/e3ndr/athena/webui/HTMLBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package xyz.e3ndr.athena.webui;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.unbescape.html.HtmlEscape;

import co.casterlabs.rakurai.io.http.HttpStatus;
import co.casterlabs.rakurai.io.http.server.HttpResponse;

/**
* This class is chainable.
*/
public class HTMLBuilder {
private List<String> lines = new LinkedList<>();

public HTMLBuilder f(String format, Object... args) {
// Escape the args.
for (int idx = 0; idx < args.length; idx++) {
if (args[idx] instanceof String) {
args[idx] = HtmlEscape.escapeHtml5(String.valueOf(args[idx]));
}
}
this.lines.add(String.format(format, args)); // Format and add to body.
return this; // Chain
}

@Override
public String toString() {
List<String> lines = new LinkedList<>();
lines.addAll(
Arrays.asList(
"<!DOCTYPE html>",
"<html>",
" <head>",
" <meta charset=\"utf-8\" />",
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />",
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />",
" <title>Athena</title>",
" </head>",
" <body>"
)
);
lines.addAll(this.lines);
lines.addAll(
Arrays.asList(
" </body>",
"</html>"
)
);
return String.join("\r\n", lines);
}

public HttpResponse toResponse(HttpStatus status) {
return HttpResponse
.newFixedLengthResponse(status, this.toString())
.setMimeType("text/html");
}

public static HTMLBuilder of(String... lines) {
HTMLBuilder instance = new HTMLBuilder();
for (String line : lines) instance.lines.add(line);
return instance;
}

}
25 changes: 25 additions & 0 deletions server/src/main/java/xyz/e3ndr/athena/webui/InternalRoutes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package xyz.e3ndr.athena.webui;

import co.casterlabs.rakurai.io.http.StandardHttpStatus;
import co.casterlabs.rakurai.io.http.server.HttpResponse;
import co.casterlabs.sora.api.http.HttpProvider;
import co.casterlabs.sora.api.http.SoraHttpSession;
import co.casterlabs.sora.api.http.annotations.HttpEndpoint;
import xyz.e3ndr.athena.server.http.StreamRoutes;

class InternalRoutes implements HttpProvider {
private StreamRoutes streamRoutes = new StreamRoutes();

@HttpEndpoint(uri = "/_internal/media/:mediaId/stream/raw")
public HttpResponse onStreamRAW(SoraHttpSession session) {
return this.streamRoutes.onStreamRAW(session);
}

@HttpEndpoint(uri = "/_internal/media/:mediaId/stream/hls")
public HttpResponse onStreamHLS(SoraHttpSession session) {
String playlist = this.streamRoutes.generateHLSPlaylist(session, "/_internal/media/%s/stream/raw");
return HttpResponse.newFixedLengthResponse(StandardHttpStatus.OK, playlist)
.setMimeType("application/vnd.apple.mpegurl");
}

}
157 changes: 129 additions & 28 deletions server/src/main/java/xyz/e3ndr/athena/webui/UIRoutes.java
Original file line number Diff line number Diff line change
@@ -1,49 +1,150 @@
package xyz.e3ndr.athena.webui;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import co.casterlabs.rakurai.io.http.StandardHttpStatus;
import co.casterlabs.rakurai.io.http.server.HttpResponse;
import co.casterlabs.sora.api.http.HttpProvider;
import co.casterlabs.sora.api.http.SoraHttpSession;
import co.casterlabs.sora.api.http.annotations.HttpEndpoint;
import xyz.e3ndr.athena.Athena;
import xyz.e3ndr.athena.types.AudioCodec;
import xyz.e3ndr.athena.types.ContainerFormat;
import xyz.e3ndr.athena.types.VideoCodec;
import xyz.e3ndr.athena.types.VideoQuality;
import xyz.e3ndr.athena.types.media.Media;

class UIRoutes implements HttpProvider {

@HttpEndpoint(uri = "/*")
public HttpResponse onViewIndex(SoraHttpSession session) {
return html(
return HTMLBuilder.of(
"<h1>Welcome to Athena!</h1>",
);
"<p>Continue to your <a href=\"/media\">media library</a>.</p>"
).toResponse(StandardHttpStatus.OK);
}

private static HttpResponse html(String... body) {
List<String> lines = new LinkedList<>();
lines.addAll(
Arrays.asList(
"<!DOCTYPE html>",
"<html>",
" <head>",
" <meta charset=\"utf-8\" />",
" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />",
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />",
" <title>Athena</title>",
" </head>",
" <body>"
)
);
lines.addAll(Arrays.asList(body));
lines.addAll(
Arrays.asList(
" </body>",
"</html>"
@HttpEndpoint(uri = "/media")
public HttpResponse onViewMedia(SoraHttpSession session) {
final int itemsPerPage = 20;
int page = Integer.parseInt(session.getQueryParameters().getOrDefault("page", "1"));
int total = Athena.totalMedia();

// Constrain.
if (page < 1) {
page = 1;
} else if ((page - 1) * itemsPerPage > total) {
page = 1;
}

List<Media> listOfMedia = Athena.listMedia((page - 1) * itemsPerPage, itemsPerPage);

HTMLBuilder html = new HTMLBuilder();

for (Media media : listOfMedia) {
html.f("<a");
html.f(" style=\"margin: 10px; display: inline-block\"");
html.f(" href=\"/media/%s\"", media.getId());
html.f(" title=\"%s (%d)\"", media.getInfo().getTitle(), media.getInfo().getYear());
html.f(">");
html.f(" <img");
html.f(" style=\"width: 200px; height: 320px; border-radius: 20px; object-fit: cover\"");
html.f(" src=\"%s\"", media.getFiles().getImages().getPosterUrl());
html.f(" alt=\"%s Poster\"", media.getInfo().getTitle());
html.f(" />");
html.f(" <h1 style=\"font-size: small; font-weight: 500\">%s</h1>", media.getInfo().getTitle());
html.f("</a>");
}

html.f("<br />");
html.f("<br />");

html.f("You are on page %d.", page);
if (page > 1) {
html.f("<a href=\"?page=%d\">Previous</a>", page - 1);
}
if ((page) * itemsPerPage < total) {
html.f("<a href=\"?page=%d\">Next</a>", page + 1);
}

return html.toResponse(StandardHttpStatus.OK);
}

@HttpEndpoint(uri = "/media/:mediaId")
public HttpResponse onViewSpecificMedia(SoraHttpSession session) {
Media media = Athena.getMedia(session.getUriParameters().get("mediaId"));

if (media == null) {
return HttpResponse.newFixedLengthResponse(StandardHttpStatus.NOT_FOUND);
}

String genres = String.join(", ", media.getInfo().getGenres());
String studios = String.join(", ", media.getInfo().getStudios().stream().limit(2).map((s) -> s.getName()).collect(Collectors.toList()));
String directors = String.join(", ", media.getInfo().getDirectors().stream().map((p) -> p.getName()).collect(Collectors.toList()));
String actors = String.join(", ", media.getInfo().getActors().stream().limit(4).map((p) -> p.getName()).collect(Collectors.toList()));

return new HTMLBuilder()
.f("<table>")
.f(" <tr>")
.f(" <td>")
.f(" <img")
.f(" style=\"width: 192px; height: 307px; object-fit: cover; border-radius: 5px; display: inline-block;\"")
.f(" alt=\"\"")
.f(" src=\"%s\"", media.getFiles().getImages().getPosterUrl())
.f(" />")
.f(" </td>")
.f(" <td style=\"vertical-align: top; padding-left: 15px;\">")
.f(" <div>")
.f(" <h1 style=\"margin: 0px;\">%s (%d)</h1>", media.getInfo().getTitle(), media.getInfo().getYear())
.f(" <h2 style=\"margin: 0px; font-size: large;\">%s</h2>", genres)
.f(" </div>")
.f("")
.f(" <p>%s</p>", media.getInfo().getSummary())
.f("")
.f(" <table>")
.f(" <tr>")
.f(" <td>Directors:</td>")
.f(" <td>%s</td>", directors)
.f(" </tr>")
.f(" <tr>")
.f(" <td>Starring:</td>")
.f(" <td>%s</td>", actors)
.f(" </tr>")
.f(" <tr>")
.f(" <td>Studios:</td>")
.f(" <td>%s</td>", studios)
.f(" </tr>")
.f(" </table>")
.f("")
.f(" <br />")
.f(" <a href=\"/media/%s/watch\">Watch</a>", media.getId())
.f(" </td>")
.f(" </tr>")
.f("</table>")
.toResponse(StandardHttpStatus.OK);
}

@HttpEndpoint(uri = "/media/:mediaId/watch")
public HttpResponse onWatchSpecificMedia(SoraHttpSession session) {
Media media = Athena.getMedia(session.getUriParameters().get("mediaId"));

if (media == null) {
return HttpResponse.newFixedLengthResponse(StandardHttpStatus.NOT_FOUND);
}

ContainerFormat container = ContainerFormat.MKV;
VideoCodec vCodec = VideoCodec.SOURCE;
AudioCodec aCodec = AudioCodec.SOURCE;
VideoQuality quality = VideoQuality.UHD;

return new HTMLBuilder()
.f(
"<video src=\"/api/media/%s/stream/raw?format=%s&videoCodec=%s&audioCodec=%s&quality=%s\" controls fullscreen style=\"width: 100%%; height: 100%%;\" />",
media.getId(),
container, vCodec, aCodec, quality
)
);
return HttpResponse
.newFixedLengthResponse(StandardHttpStatus.OK, String.join("\r\n", lines))
.setMimeType("text/html");
.toResponse(StandardHttpStatus.OK);
}

}

0 comments on commit 7f5f91b

Please sign in to comment.