-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
236 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
server/src/main/java/xyz/e3ndr/athena/webui/HTMLBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
25
server/src/main/java/xyz/e3ndr/athena/webui/InternalRoutes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
157
server/src/main/java/xyz/e3ndr/athena/webui/UIRoutes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |