diff --git a/app/api/send_file.py b/app/api/send_file.py index bbcd7c05..72fe8b37 100644 --- a/app/api/send_file.py +++ b/app/api/send_file.py @@ -4,7 +4,7 @@ Contains all the track routes. import os -from flask import Blueprint, send_file, request +from flask import Blueprint, send_file, request, Response from flask_openapi3 import APIBlueprint, Tag from pydantic import BaseModel, Field from app.api.apischemas import TrackHashSchema @@ -37,7 +37,7 @@ def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery): ext = filename.rsplit(".", maxsplit=1)[-1] return f"audio/{ext}" - # If filepath is provide, try to send that + # If filepath is provided, try to send that if filepath is not None: try: track = TrackStore.get_tracks_by_filepaths([filepath])[0] @@ -48,7 +48,7 @@ def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery): if track_exists: audio_type = get_mime(filepath) - return send_file(filepath, mimetype=audio_type) + return send_file_as_chunks(track.filepath, audio_type) # Else, find file by trackhash tracks = TrackStore.get_tracks_by_trackhashes([trackhash]) @@ -60,13 +60,59 @@ def send_track_file(path: TrackHashSchema, query: SendTrackFileQuery): audio_type = get_mime(track.filepath) try: - return send_file(track.filepath, mimetype=audio_type) + return send_file_as_chunks(track.filepath, audio_type) except (FileNotFoundError, OSError) as e: return msg, 404 return msg, 404 +def send_file_as_chunks(filepath: str, audio_type: str) -> Response: + file_size = os.path.getsize(filepath) + start = 0 + end = file_size - 1 + + range_header = request.headers.get("Range") + if range_header: + start, end = parse_range_header(range_header, file_size) + + chunk_size = 1024 * 1024 # 1MB chunk size (adjust as needed) + + def generate_chunks(): + with open(filepath, "rb") as file: + file.seek(start) + remaining_bytes = end - start + 1 + + while remaining_bytes > 0: + chunk = file.read(min(chunk_size, remaining_bytes)) + yield chunk + remaining_bytes -= len(chunk) + + response = Response( + generate_chunks(), + 206, # Partial Content status code + mimetype=audio_type, + content_type=audio_type, + direct_passthrough=True, + ) + response.headers.add("Content-Range", f"bytes {start}-{end}/{file_size}") + response.headers.add("Accept-Ranges", "bytes") + response.headers.add("Content-Length", str(end - start + 1)) + + return response + + +def parse_range_header(range_header: str, file_size: int) -> tuple[int, int]: + try: + range_start, range_end = range_header.strip().split("=")[1].split("-") + start = int(range_start) + end = min(int(range_end), file_size - 1) + except ValueError: + return 0, file_size - 1 + + return start, end + + class GetAudioSilenceBody(BaseModel): ending_file: str = Field( description="The ending file's path", diff --git a/app/lib/folderslib.py b/app/lib/folderslib.py index 12b60f8b..160b53af 100644 --- a/app/lib/folderslib.py +++ b/app/lib/folderslib.py @@ -10,7 +10,7 @@ from app.utils.wintools import win_replace_slash from app.store.tracks import TrackStore -def create_folder(path: str, count=0) -> Folder: +def create_folder(path: str, trackcount=0, foldercount=0) -> Folder: """ Creates a folder object from a path. """ @@ -20,7 +20,8 @@ def create_folder(path: str, count=0) -> Folder: name=folder.name, path=win_replace_slash(str(folder)), is_sym=folder.is_symlink(), - count=count, + trackcount=trackcount, + foldercount=foldercount, ) @@ -29,15 +30,36 @@ def get_folders(paths: list[str]): Filters out folders that don't have any tracks and returns a list of folder objects. """ - count_dict = {path: 0 for path in paths} + count_dict = { + "tracks": {path: 0 for path in paths}, + # folders are immediate children of the root folder + "folders": {path: 0 for path in paths}, + } for track in TrackStore.tracks: for path in paths: if track.folder.startswith(path): - count_dict[path] += 1 + count_dict["tracks"][path] += 1 + # increment folder count by counting the number of slashes - folders = [{"path": path, "count": count_dict[path]} for path in paths] - return [create_folder(f["path"], f["count"]) for f in folders if f["count"] > 0] + print("slash count", track.folder.count("/"), path.count("/")) + if track.folder.count("/") == path.count("/"): + count_dict["folders"][path] += 1 + + folders = [ + { + "path": path, + "trackcount": count_dict["tracks"][path], + "foldercount": count_dict["folders"][path], + } + for path in paths + ] + + return [ + create_folder(f["path"], f["trackcount"], f["foldercount"]) + for f in folders + if f["trackcount"] > 0 + ] class GetFilesAndDirs: diff --git a/app/models/folder.py b/app/models/folder.py index 920542b7..cd92b0bc 100644 --- a/app/models/folder.py +++ b/app/models/folder.py @@ -6,4 +6,5 @@ class Folder: name: str path: str is_sym: bool = False - count: int = 0 + trackcount: int = 0 + foldercount: int = 0