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,
|
||||
)
|
||||
|
||||
@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)
|
||||
async def showLevel(level_id: int):
|
||||
|
|
|
@ -86,17 +86,20 @@ code {
|
|||
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: inherit;
|
||||
text-decoration: initial;
|
||||
cursor: auto;
|
||||
a, a:link, a:visited {
|
||||
color: inherit;
|
||||
text-decoration: initial;
|
||||
transition: color 200ms ease;
|
||||
transition: text-shadow 100ms ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
outline: 2px dotted blue;
|
||||
color: cyan;
|
||||
/* outline: 2px dotted blue; */
|
||||
}
|
||||
|
||||
a:link:active {
|
||||
color: inherit;
|
||||
outline: 2px dotted blue;
|
||||
color: cyan;
|
||||
text-shadow: 5px 5px 15px cyan;
|
||||
/* outline: 2px dotted blue; */
|
||||
}
|
||||
|
|
|
@ -11,10 +11,23 @@
|
|||
}
|
||||
|
||||
.fullResponse table, .fullResponse table td {
|
||||
border: 1px solid gray;
|
||||
border: 2px solid transparent;
|
||||
background-color: #30303a55;
|
||||
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 {
|
||||
display: grid;
|
||||
margin: 4px;
|
||||
|
@ -91,21 +104,21 @@
|
|||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.levelTitle a:link, a:visited {
|
||||
color: inherit;
|
||||
text-decoration: initial;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.levelTitle a:hover {
|
||||
outline: 2px dotted blue;
|
||||
}
|
||||
|
||||
.levelTitle a:link:active {
|
||||
color: inherit;
|
||||
outline: 2px dotted blue;
|
||||
}
|
||||
|
||||
/* .levelTitle a:link, a:visited { */
|
||||
/* color: inherit; */
|
||||
/* text-decoration: initial; */
|
||||
/* cursor: auto; */
|
||||
/* } */
|
||||
/**/
|
||||
/* .levelTitle a:hover { */
|
||||
/* outline: 2px dotted blue; */
|
||||
/* } */
|
||||
/**/
|
||||
/* .levelTitle a:link:active { */
|
||||
/* color: inherit; */
|
||||
/* outline: 2px dotted blue; */
|
||||
/* } */
|
||||
/**/
|
||||
.creatorName {
|
||||
justify-self: left;
|
||||
text-align: left;
|
||||
|
@ -152,15 +165,18 @@
|
|||
text-align: center;
|
||||
|
||||
border: 1px solid #505050;
|
||||
transition: background-color 200ms ease;
|
||||
}
|
||||
|
||||
.levelCode input:focus {
|
||||
outline: none;
|
||||
background-color: #00FFFF10;
|
||||
transition: background-color 1s ease;
|
||||
}
|
||||
|
||||
.levelCode:active, .levelCode input:active {
|
||||
animation: pulse 0.5s 1 ease-out;
|
||||
.levelCode input:active {
|
||||
/* animation: pulse 0.5s 1 ease-out; */
|
||||
background-color: lime;
|
||||
}
|
||||
|
||||
.illegal {
|
||||
|
@ -179,3 +195,16 @@
|
|||
font-size: 2em;
|
||||
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)
|
||||
|
||||
def config_value(key: str, default_value=None):
|
||||
def config_value(key: str, default_value=""):
|
||||
"""Searches for environ value for the specified key,
|
||||
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]
|
||||
python = "^3.8"
|
||||
pymongo = "^4.3.2"
|
||||
Jinja2 = "^3.1.2"
|
||||
uvicorn = "^0.19.0"
|
||||
aiohttp = {extras = ["speedups"], version = "^3.8.3"}
|
||||
|
|
Loading…
Reference in a new issue