Skip to content

Commit b2fba4c

Browse files
authored
Merge pull request #232 from pSpitzner/filemeta_cleanup
Restyling of asis candidate in inbox view
2 parents 0f6568e + 03aa359 commit b2fba4c

File tree

24 files changed

+849
-930
lines changed

24 files changed

+849
-930
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Import Bootleg Button now works as expected [#218](https://github.com/pSpitzner/beets-flask/issues/218)
2222
- Startup script was not executed correctly if placed in `/config/beets-flask/startup.sh` [#227](https://github.com/pSpitzner/beets-flask/pull/227)
2323
- Another state-related bug around Searching for Candidates [#225](https://github.com/pSpitzner/beets-flask/issues/225). We now no longer require a certaint type of state before allowing to add candidates.
24+
- Asis candidates have been restyled to be more consistent with other candidate types. They now also include a cover art preview if available.
2425
- Fixed a typo in our opiniated beets config [#235](https://github.com/pSpitzner/beets-flask/issues/235)
2526

2627
### Added
@@ -35,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3536
- Fixed typing issues in `./tests` folder and enabled mypy check for it.
3637
- Ruff now has the F401 (imported but unused) check enabled.
3738
- Ruff now had the UP checks enabled to enforce modern python syntax.
39+
- Unified coverart components in the frontend, we now use common styling for external and internal coverart.
40+
- Moved inbox metadata fetching into the library api routes.
3841

3942
### Dependencies
4043

backend/beets_flask/server/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import os
55
from dataclasses import asdict, is_dataclass
6-
from datetime import datetime
6+
from datetime import date, datetime
77
from typing import TYPE_CHECKING, Any
88

99
from quart import Quart
@@ -72,7 +72,7 @@ def default(self, o):
7272
# Might yield strange results for other byte objects
7373
return o.decode("utf-8")
7474

75-
if isinstance(o, datetime):
75+
if isinstance(o, (datetime, date)):
7676
return o.isoformat()
7777

7878
# Dataclasses are not serializable by default

backend/beets_flask/server/routes/inbox.py

Lines changed: 1 addition & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import os
22
import shutil
33
from datetime import datetime
4-
from io import BytesIO
54
from pathlib import Path
6-
from typing import TypedDict, cast
5+
from typing import TypedDict
76

87
from cachetools import Cache
9-
from mediafile import Image, MediaFile # comes with the beets install
108
from quart import Blueprint, jsonify, request
119
from sqlalchemy import func, select
12-
from tinytag import TinyTag
1310

1411
from beets_flask.database import db_session_factory
1512
from beets_flask.database.models.states import FolderInDb, SessionStateInDb
@@ -24,10 +21,8 @@
2421
from beets_flask.importer.progress import Progress
2522
from beets_flask.logger import log
2623
from beets_flask.server.exceptions import InvalidUsageException, NotFoundException
27-
from beets_flask.server.routes.library.artwork import send_image
2824
from beets_flask.server.utility import (
2925
pop_folder_params,
30-
pop_paths_param,
3126
)
3227
from beets_flask.server.websocket.status import (
3328
trigger_clear_cache,
@@ -208,70 +203,6 @@ async def delete():
208203
)
209204

210205

211-
@inbox_bp.route("/metadata", methods=["POST"])
212-
async def get_multiple_filemeta():
213-
params = await request.get_json()
214-
215-
file_paths = pop_paths_param(params, "file_paths", default=[])
216-
217-
if len(file_paths) == 0:
218-
raise InvalidUsageException("No file paths provided", status_code=400)
219-
220-
tags = []
221-
222-
for p in file_paths:
223-
if not p.is_file():
224-
raise InvalidUsageException(f"Invalid file path: {p}", status_code=400)
225-
226-
tag = _get_filemeta(p)
227-
tags.append(tag)
228-
229-
return jsonify(tags)
230-
231-
232-
def _get_filemeta(path: str | Path):
233-
"""Get the file metadata for a given audio file."""
234-
235-
tag = TinyTag.get(path).as_dict()
236-
for k, v in tag.items():
237-
# TODO: we cant just omit if there are multiple values...
238-
if isinstance(v, list):
239-
tag[k] = v[0]
240-
241-
if "filename" in tag:
242-
tag["filename"] = str(tag["filename"]).split("/")[-1]
243-
244-
return tag
245-
246-
247-
# TODO: consolidate with the file artwork route from artwork.py
248-
@inbox_bp.route("/metadata_art/<path:query>", methods=["GET"])
249-
async def file_art(query: str):
250-
"""Get the cover art for a given audio file.
251-
252-
Parameters
253-
----------
254-
query : str
255-
The path to the file to get the cover art for.
256-
"""
257-
path = Path("/" + query)
258-
if not path.is_file():
259-
raise NotFoundException(f"File not found: '{path}'.")
260-
261-
return await send_image(_file_art(path))
262-
263-
264-
def _file_art(path: Path):
265-
"""Get the cover art for a given audio file."""
266-
267-
mediafile = MediaFile(path)
268-
if not mediafile.images or len(mediafile.images) < 1:
269-
raise NotFoundException(f"File has no cover art: '{path}'.")
270-
271-
im: Image = cast(Image, mediafile.images[0])
272-
return BytesIO(im.data)
273-
274-
275206
# ------------------------------------------------------------------------------------ #
276207
# Stats #
277208
# ------------------------------------------------------------------------------------ #

backend/beets_flask/server/routes/library/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .artists import artists_bp
1212
from .artwork import artwork_pb
1313
from .audio import audio_bp
14+
from .metadata import metadata_bp
1415
from .resources import resource_bp
1516
from .stats import stats_bp
1617

@@ -20,7 +21,7 @@
2021
library_bp.register_blueprint(resource_bp)
2122
library_bp.register_blueprint(stats_bp)
2223
library_bp.register_blueprint(artists_bp)
23-
24+
library_bp.register_blueprint(metadata_bp)
2425

2526
from typing import TYPE_CHECKING
2627

backend/beets_flask/server/routes/library/artwork.py

Lines changed: 61 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
from io import BytesIO
33
from typing import TYPE_CHECKING, cast
4-
from urllib.parse import unquote_plus
54

65
from beets import util as beets_util
76
from mediafile import Image, MediaFile # comes with the beets install
@@ -54,6 +53,42 @@ def parse_size(size_key: str) -> tuple[int, int] | None:
5453
return preset
5554

5655

56+
def parse_art_params() -> tuple[int, tuple[int, int] | None]:
57+
"""Parse common artwork parameters from request."""
58+
idx = int(request.args.get("index", 0))
59+
size_key = request.args.get("size", "small")
60+
try:
61+
size = parse_size(size_key)
62+
except KeyError:
63+
raise InvalidUsageException(
64+
f"Invalid size key '{size_key}' provided. Supported keys: {', '.join(SIZE_PRESETS.keys())}"
65+
)
66+
return idx, size
67+
68+
69+
def get_image_data_from_file(filepath: str, index: int = 0) -> BytesIO:
70+
"""Get image data from a file path."""
71+
if not os.path.exists(filepath):
72+
raise IntegrityException(f"File '{filepath}' does not exist.")
73+
74+
mediafile = MediaFile(filepath)
75+
images = mediafile.images
76+
if not images or len(images) <= index:
77+
raise NotFoundException(
78+
f"File has no cover art at index {index}: '{filepath}'."
79+
)
80+
81+
im: Image = cast(Image, images[index])
82+
return BytesIO(im.data)
83+
84+
85+
def get_image_count_from_file(filepath: str) -> int:
86+
"""Get number of images from a file path."""
87+
mediafile = MediaFile(filepath)
88+
images = mediafile.images
89+
return len(images) if images else 0
90+
91+
5792
async def send_image(img_data: BytesIO, size: tuple[int, int] | None = None):
5893
# Resize if preset provided
5994
if size:
@@ -63,90 +98,51 @@ async def send_image(img_data: BytesIO, size: tuple[int, int] | None = None):
6398
return response
6499

65100

101+
# -------------------------------- Item Routes ------------------------------- #
102+
103+
66104
@artwork_pb.route("/item/<int:item_id>/nArtworks", methods=["GET"])
67105
async def item_art_idx(item_id: int):
68-
"""Get the number of images for an item.
69-
70-
This is a HEAD request to check the number of images available for an item.
71-
"""
106+
"""Get the number of images for an item."""
72107
log.debug(f"Item art index query for id:'{item_id}'")
73108

74-
# Item from beets library
75109
item = g.lib.get_item(item_id)
76110
if not item:
77111
raise NotFoundException(
78112
f"Item with beets_id:'{item_id}' not found in beets db."
79113
)
80114

81-
# File
82115
item_path = beets_util.syspath(item.path)
83-
if not os.path.exists(item_path):
84-
raise IntegrityException(
85-
f"Item file '{item_path}' does not exist for item beets_id:'{item_id}'."
86-
)
87-
88-
# Get image with mediafile library (comes with beets)
89-
mediafile = MediaFile(item_path)
90-
images = mediafile.images
91-
if not images or len(images) < 1:
92-
return jsonify({"count": 0}), 200
93-
94-
return jsonify({"count": len(images)}), 200
116+
count = get_image_count_from_file(item_path)
117+
return jsonify({"count": count}), 200
95118

96119

97120
@artwork_pb.route("/item/<int:item_id>/art", methods=["GET"])
98121
async def item_art(item_id: int):
99122
log.debug(f"Item art query for id:'{item_id}'")
100123

101-
# Allow selecting image index and size via query params
102-
idx = int(request.args.get("index", 0))
103-
size_key = request.args.get("size", "small")
104-
try:
105-
size = parse_size(size_key)
106-
except KeyError:
107-
raise InvalidUsageException(
108-
f"Invalid size key '{size_key}' provided. Supported keys: {', '.join(SIZE_PRESETS.keys())}"
109-
)
124+
idx, size = parse_art_params()
110125

111-
# Item from beets library
112126
item = g.lib.get_item(item_id)
113127
if not item:
114128
raise NotFoundException(
115129
f"Item with beets_id:'{item_id}' not found in beets db."
116130
)
117131

118-
# File
119132
item_path = beets_util.syspath(item.path)
120-
if not os.path.exists(item_path):
121-
raise IntegrityException(
122-
f"Item file '{item_path}' does not exist for item beets_id:'{item_id}'."
123-
)
133+
img_data = get_image_data_from_file(item_path, idx)
134+
return await send_image(img_data, size)
124135

125-
# Get image with mediafile library (comes with beets)
126-
mediafile = MediaFile(item_path)
127-
images = mediafile.images
128-
if not images or len(images) < 1:
129-
raise NotFoundException(f"Item has no cover art: '{item_id}'.")
130136

131-
im: Image = cast(Image, images[idx])
132-
return await send_image(BytesIO(im.data), size)
137+
# ------------------------------- Album Routes ------------------------------- #
133138

134139

135140
@artwork_pb.route("/album/<int:album_id>/art", methods=["GET"])
136141
async def album_art(album_id: int):
137142
log.debug(f"Album art query for id:'{album_id}'")
138143

139-
# Allow selecting image index and size via query params
140-
idx = int(request.args.get("index", 0))
141-
size_key = request.args.get("size", "small")
142-
try:
143-
size = parse_size(size_key)
144-
except KeyError:
145-
raise InvalidUsageException(
146-
f"Invalid size key '{size_key}' provided. Supported keys: {', '.join(SIZE_PRESETS.keys())}"
147-
)
144+
idx, size = parse_art_params()
148145

149-
# Album from beets library
150146
album = g.lib.get_album(album_id)
151147
if not album:
152148
raise NotFoundException(
@@ -172,37 +168,28 @@ async def album_art(album_id: int):
172168
".item_art",
173169
item_id=items[0].id,
174170
index=idx,
175-
size=size_key,
171+
size=request.args.get("size", "small"),
176172
)
177173
)
178174

179175

180-
@artwork_pb.route("/file/<string:filepath>/art", methods=["GET"])
181-
async def file_art(filepath: str):
182-
# Decode url encoded filepath
183-
filepath = unquote_plus(filepath)
184-
filepath = beets_util.syspath(filepath)
176+
# -------------------------------- File Routes ------------------------------- #
185177

186-
# Allow selecting image index and size via query params
187-
idx = int(request.args.get("index", 0))
188-
size_key = request.args.get("size", "small")
189-
try:
190-
size = parse_size(size_key)
191-
except KeyError:
192-
raise InvalidUsageException(
193-
f"Invalid size key '{size_key}' provided. Supported keys: {', '.join(SIZE_PRESETS.keys())}"
194-
)
195178

196-
if not os.path.exists(filepath):
197-
raise IntegrityException(f"File '{filepath}' does not exist.")
179+
@artwork_pb.route("/files/<string:filepath>/nArtworks", methods=["GET"])
180+
async def file_art_idx(filepath: str):
181+
"""Get the number of images for a file."""
182+
filepath = bytes.fromhex(filepath).decode("utf-8")
183+
count = get_image_count_from_file(filepath)
184+
return jsonify({"count": count}), 200
198185

199-
mediafile = MediaFile(filepath)
200-
images = mediafile.images
201-
if not images or len(images) <= idx:
202-
raise NotFoundException(f"File has no cover art at index {idx}: '{filepath}'.")
203186

204-
im: Image = cast(Image, images[idx])
205-
return await send_image(BytesIO(im.data), size)
187+
@artwork_pb.route("/file/<string:filepath>/art", methods=["GET"])
188+
async def file_art(filepath: str):
189+
filepath = bytes.fromhex(filepath).decode("utf-8")
190+
idx, size = parse_art_params()
191+
img_data = get_image_data_from_file(filepath, idx)
192+
return await send_image(img_data, size)
206193

207194

208195
# ---------------------------------- Utils ----------------------------------- #

0 commit comments

Comments
 (0)