Add /playlist/{id}
- Shows requested playlist, works similarly to /search
This commit is contained in:
parent
b3859eb5be
commit
9de7d20a18
7 changed files with 1016 additions and 645 deletions
|
@ -174,6 +174,31 @@ async def search(
|
||||||
QueryValues=QueryValues,
|
QueryValues=QueryValues,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.get("/playlist/{playlist_id}", response_class=HTMLResponse)
|
||||||
|
async def showList(request: starlette.requests.Request, playlist_id: int):
|
||||||
|
template = template_env.get_template("playlist.html")
|
||||||
|
try:
|
||||||
|
rq = httpx.get(BASE_URL + "/api/v1/list/" + str(playlist_id), timeout=10)
|
||||||
|
if rq.status_code == 503:
|
||||||
|
return error_template.render(reason="Server is unavailable right now")
|
||||||
|
response = rq.json()
|
||||||
|
# searchResults = response["Maps"]
|
||||||
|
print(rq.url)
|
||||||
|
except httpx.ReadTimeout:
|
||||||
|
return error_template.render(reason="Server timed out")
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
return error_template.render(
|
||||||
|
reason="Failed to parse server response.", details=rq.text
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return error_template.render(reason="Uncaught exception", details=exc)
|
||||||
|
|
||||||
|
return template.render(
|
||||||
|
response=response,
|
||||||
|
QueryValues=request.query_params,
|
||||||
|
THUMB_URL=THUMB_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/level/{level_id}", response_class=HTMLResponse)
|
@app.get("/level/{level_id}", response_class=HTMLResponse)
|
||||||
async def showLevel(level_id: int):
|
async def showLevel(level_id: int):
|
||||||
|
|
|
@ -86,17 +86,20 @@ code {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link, a:visited {
|
a, a:link, a:visited {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: initial;
|
text-decoration: initial;
|
||||||
cursor: auto;
|
transition: color 200ms ease;
|
||||||
|
transition: text-shadow 100ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
outline: 2px dotted blue;
|
color: cyan;
|
||||||
|
/* outline: 2px dotted blue; */
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link:active {
|
a:link:active {
|
||||||
color: inherit;
|
color: cyan;
|
||||||
outline: 2px dotted blue;
|
text-shadow: 5px 5px 15px cyan;
|
||||||
|
/* outline: 2px dotted blue; */
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullResponse table, .fullResponse table td {
|
.fullResponse table, .fullResponse table td {
|
||||||
border: 1px solid gray;
|
border: 2px solid transparent;
|
||||||
|
background-color: #30303a55;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .fullResponse table tr { */
|
||||||
|
/* transition: background-color 100ms cubic-bezier(0.19, 1, 0.22, 1); */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
.fullResponse table thead {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullResponse table tr:hover {
|
||||||
|
background-color: #FFFFFF55;
|
||||||
|
}
|
||||||
|
|
||||||
.searchResultCard {
|
.searchResultCard {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
|
@ -91,21 +104,21 @@
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.levelTitle a:link, a:visited {
|
/* .levelTitle a:link, a:visited { */
|
||||||
color: inherit;
|
/* color: inherit; */
|
||||||
text-decoration: initial;
|
/* text-decoration: initial; */
|
||||||
cursor: auto;
|
/* cursor: auto; */
|
||||||
}
|
/* } */
|
||||||
|
/**/
|
||||||
.levelTitle a:hover {
|
/* .levelTitle a:hover { */
|
||||||
outline: 2px dotted blue;
|
/* outline: 2px dotted blue; */
|
||||||
}
|
/* } */
|
||||||
|
/**/
|
||||||
.levelTitle a:link:active {
|
/* .levelTitle a:link:active { */
|
||||||
color: inherit;
|
/* color: inherit; */
|
||||||
outline: 2px dotted blue;
|
/* outline: 2px dotted blue; */
|
||||||
}
|
/* } */
|
||||||
|
/**/
|
||||||
.creatorName {
|
.creatorName {
|
||||||
justify-self: left;
|
justify-self: left;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -152,15 +165,18 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
border: 1px solid #505050;
|
border: 1px solid #505050;
|
||||||
|
transition: background-color 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.levelCode input:focus {
|
.levelCode input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #00FFFF10;
|
background-color: #00FFFF10;
|
||||||
|
transition: background-color 1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.levelCode:active, .levelCode input:active {
|
.levelCode input:active {
|
||||||
animation: pulse 0.5s 1 ease-out;
|
/* animation: pulse 0.5s 1 ease-out; */
|
||||||
|
background-color: lime;
|
||||||
}
|
}
|
||||||
|
|
||||||
.illegal {
|
.illegal {
|
||||||
|
@ -179,3 +195,16 @@
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
/* Playlist only */
|
||||||
|
.playlist-details {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-details h2 {
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-details p {
|
||||||
|
margin: 0em;
|
||||||
|
}
|
||||||
|
|
86
iwm_browser/templates/playlist.html
Normal file
86
iwm_browser/templates/playlist.html
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- {{ QueryValues }} -->
|
||||||
|
<title>IWM Browser</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="{{ combineCSS(
|
||||||
|
'/static/index.css',
|
||||||
|
'/static/main.css',
|
||||||
|
'/static/search/search.css',
|
||||||
|
'/static/search/searchResults.css'
|
||||||
|
) }}">
|
||||||
|
<!-- Optional features -->
|
||||||
|
<script>
|
||||||
|
function copy(obj) {
|
||||||
|
// copies the contents of the
|
||||||
|
// input field to clipboard, and
|
||||||
|
// adds the "copied" css class
|
||||||
|
navigator.clipboard.writeText(obj.value);
|
||||||
|
//alert("Level code copied")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="Search">
|
||||||
|
<div class="contentBox_">
|
||||||
|
<div class="playlist-details">
|
||||||
|
<h1>{{ QueryValues.name if QueryValues.name}}{{ "Playlist" if not QueryValues.name}} </h1>
|
||||||
|
<h2>by <a href="/user/{{ response.CreatorID }}">{{ response.CreatorName }}</a></h2>
|
||||||
|
<p>ID: {{ response.ID }}</p>
|
||||||
|
<p>Maps: {{ response.MapCount }}</p>
|
||||||
|
</div>
|
||||||
|
{% if response.Maps %}
|
||||||
|
<div class="searchResults">
|
||||||
|
{% for map in response.Maps %}
|
||||||
|
<div class="searchResultCard">
|
||||||
|
<div class="thumbnail"><img aria-hidden="true" src="{{ generate_thumbnail_url(map['ID'], THUMB_URL) }}"></div>
|
||||||
|
<div class="details">
|
||||||
|
<div class="basic">
|
||||||
|
<div class="levelTitle"><a href="/level/{{ map['ID'] }}">{{ map['Name'] }}</a></div>
|
||||||
|
<div class="levelDifficulty"><span aria-label="Difficulty: {{ map['AverageUserDifficulty']|int }}">Difficulty: {{ '▲' * map['AverageUserDifficulty']|int }}</span></div>
|
||||||
|
<div class="creatorName">by <a href="/user/{{ map['CreatorId'] }}"><b>{{ map['CreatorName'] }}</b></a></div>
|
||||||
|
<div class="levelRating">{{ map['NumThumbsUp'] }} 👍 {{ map['NumThumbsDown'] }} 👎</div>
|
||||||
|
<div class="replayVisibility">Replays visible: {{ "always" if map['ReplayVisibility'] == 2 }}{{ "after clear" if map['ReplayVisibility'] == 0 }}</div>
|
||||||
|
<div class="dragonCoins">Gems : {{ map['DragonCoins'] }}</div>
|
||||||
|
{% if map['Description'] != ''%}<div class="levelDescription">{{ map['Description'] }}</div>{% endif %}
|
||||||
|
<div class="levelCode"><label for="levelCode-input-{{ map['ID'] }}">Code:</label> <input id="levelCode-input-{{ map['ID'] }}" type="text" onclick="copy(this)" readonly=1 value="{{ map['MapCode'] }}" /></div>
|
||||||
|
<div class="fullResponse">
|
||||||
|
<details>
|
||||||
|
<summary>Full server response</summary>
|
||||||
|
<table>
|
||||||
|
<thead><td>Key</td><td>Value</td></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for value in map %}
|
||||||
|
<tr>
|
||||||
|
<td>{{value}}</td><td>{{map[value]}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<!-- <pre>{{ json_dumps(map, indent=4) }}</pre> -->
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="times">
|
||||||
|
<div class="FirstClear">First Clear: <a href="/user/{{map['FirstClearUserID']}}"><b>{{ map['FirstClearUsername'] }}</b></a></div>
|
||||||
|
{% set BestTime = convert_times(map['BestTimePlaytime']) %}
|
||||||
|
{% set BestFullTime = convert_times(map['BestFullTimePlaytime']) %}
|
||||||
|
<div class="BestTime">Best Time: <a href="/user/{{ map['BestFullTimeUserID'] }}"><b>{{ map['BestTimeUsername'] }}</b></a> [{{ BestTime[0] }}:{{ BestTime[1] }}:{{ BestTime[2] }}]</div>
|
||||||
|
{% if map['BestFullTimeUserID'] != 0 %}<div class="BestTime {{ 'illegal' if not map['DragonCoins']}}">Best 100% Time: <a href="/user/{{map['BestFullTimeUserID']}}"><b>{{ map['BestFullTimeUsername'] }}</b></a> [{{ BestFullTime[0] }}:{{ BestFullTime[1] }}:{{ BestFullTime[2] }}]{{ "(illegal time)" if not map['DragonCoins'] }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -17,7 +17,7 @@ def convert_times(time):
|
||||||
|
|
||||||
return (hours, minutes, seconds)
|
return (hours, minutes, seconds)
|
||||||
|
|
||||||
def config_value(key: str, default_value=None):
|
def config_value(key: str, default_value=""):
|
||||||
"""Searches for environ value for the specified key,
|
"""Searches for environ value for the specified key,
|
||||||
or returns the default value.
|
or returns the default value.
|
||||||
|
|
||||||
|
|
1465
poetry.lock
generated
1465
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,6 @@ start = "iwm_browser.main:start"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.8"
|
||||||
pymongo = "^4.3.2"
|
|
||||||
Jinja2 = "^3.1.2"
|
Jinja2 = "^3.1.2"
|
||||||
uvicorn = "^0.19.0"
|
uvicorn = "^0.19.0"
|
||||||
aiohttp = {extras = ["speedups"], version = "^3.8.3"}
|
aiohttp = {extras = ["speedups"], version = "^3.8.3"}
|
||||||
|
|
Loading…
Reference in a new issue