Repository: osuakatsuki/bancho.py
Issue: bug: v1 api endpoint /get_replay appears to be incomplete

### Describe the bug

replays downloaded from `v1/get_replay` cannot be processed by the client, because the replay headers are missing regardless of the `include_headers` parameter. presumably, `include_headers=True` should return a functioning `.osr` file, but the server code [seems to process it the other way around](https://github.com/osuAkatsuki/bancho.py/blob/65fabad0849c6eda3252f81ea11aa1c472cc7e28/app/api/v1/api.py#L740).

even with this in mind, specifying `include_headers=False` still provides the same incomplete replay file because the `replay_data` variable which contains the headers is never actually affixed to the raw replay file before it is sent off.

### To Reproduce

1. download the .osr file from https://api.skrungly.com/v1/get_replay?id=1447&include_headers=True (also ensure you have [the map](https://osu.ppy.sh/beatmapsets/677687#osu/1490588))
2. import into your game
3. stonks!

![image](https://github.com/osuAkatsuki/bancho.py/assets/37349466/422a81dc-de32-40e9-80ca-bd46b1d267d0)


### Expected behavior

when specifying `include_headers=True`, the `.osr` file should include headers. 

### bancho.py Version

latest

### Python Version

3.9.X (Default)

### Relevant log output

_No response_

### Additional context

i understand that the v1 api is soon to be deprecated, but to my knowledge it is currently the only way to receive certain data (such as whole replay files) from the server, and so it should at least be functional in the meantime.

Changed files:
app/api/v1/api.py

diff --git a/app/api/v1/api.py b/app/api/v1/api.py
index 7dcb475..0a820b1 100644
--- a/app/api/v1/api.py
+++ b/app/api/v1/api.py
@@ -11,8 +11,8 @@ from fastapi import APIRouter
 from fastapi import Depends
 from fastapi import status
 from fastapi.param_functions import Query
-from fastapi.responses import FileResponse
 from fastapi.responses import ORJSONResponse
+from fastapi.responses import Response
 from fastapi.security import HTTPAuthorizationCredentials as HTTPCredentials
 from fastapi.security import HTTPBearer
 
@@ -722,10 +722,9 @@ async def api_get_score_info(
 @router.get("/get_replay")
 async def api_get_replay(
     score_id: int = Query(..., alias="id", ge=0, le=9_223_372_036_854_775_807),
-    include_headers: bool = False,
+    include_headers: bool = True,
 ):
     """Return a given replay (including headers)."""
-
     # fetch replay file & make sure it exists
     replay_file = REPLAYS_PATH / f"{score_id}.osr"
     if not replay_file.exists():
@@ -733,13 +732,11 @@ async def api_get_replay(
             {"status": "Replay not found."},
             status_code=status.HTTP_404_NOT_FOUND,
         )
-
     # read replay frames from file
     raw_replay_data = replay_file.read_bytes()
-
-    if include_headers:
-        return FileResponse(
-            path=REPLAYS_PATH / f"{score_id}.osr",
+    if not include_headers:
+        return Response(
+            bytes(raw_replay_data),
             media_type="application/octet-stream",
             headers={
                 "Content-Description": "File Transfer",
@@ -747,10 +744,9 @@ async def api_get_replay(
                 # info for content-disposition for this..?
             },
         )
-
     # add replay headers from sql
     # TODO: osu_version & life graph in scores tables?
-    rec = await app.state.services.database.fetch_one(
+    row = await app.state.services.database.fetch_one(
         "SELECT u.name username, m.md5 map_md5, "
         "m.artist, m.title, m.version, "
         "s.mode, s.n300, s.n100, s.n50, s.ngeki, "
@@ -762,15 +758,12 @@ async def api_get_replay(
         "WHERE s.id = :score_id",
         {"score_id": score_id},
     )
-    row = dict(rec._mapping) if rec is not None else None
-
     if not row:
         # score not found in sql
         return ORJSONResponse(
             {"status": "Score not found."},
             status_code=status.HTTP_404_NOT_FOUND,
         )  # but replay was?
-
     # generate the replay's hash
     replay_md5 = hashlib.md5(
         "{}p{}o{}o{}t{}a{}r{}e{}y{}o{}u{}{}{}".format(
@@ -789,12 +782,14 @@ async def api_get_replay(
             "True",  # TODO: ??
         ).encode(),
     ).hexdigest()
-
     # create a buffer to construct the replay output
     replay_data = bytearray()
-
     # pack first section of headers.
-    replay_data += struct.pack("<Bi", row["mode"], 20200207)  # TODO: osuver
+    replay_data += struct.pack(
+        "<Bi",
+        GameMode(row["mode"]).as_vanilla,
+        20200207,
+    )  # TODO: osuver
     replay_data += app.packets.write_string(row["map_md5"])
     replay_data += app.packets.write_string(row["username"])
     replay_data += app.packets.write_string(replay_md5)
@@ -812,23 +807,18 @@ async def api_get_replay(
         row["mods"],
     )
     replay_data += b"\\x00"  # TODO: hp graph
-
     timestamp = int(row["play_time"].timestamp() * 1e7)
     replay_data += struct.pack("<q", timestamp + DATETIME_OFFSET)
-
     # pack the raw replay data into the buffer
     replay_data += struct.pack("<i", len(raw_replay_data))
     replay_data += raw_replay_data
-
     # pack additional info buffer.
     replay_data += struct.pack("<q", score_id)
-
     # NOTE: target practice sends extra mods, but
     # can't submit scores so should not be a problem.
-
     # stream data back to the client
-    return FileResponse(
-        path=REPLAYS_PATH / f"{score_id}.osr",
+    return Response(
+        bytes(replay_data),
         media_type="application/octet-stream",
         headers={
             "Content-Description": "File Transfer",