Skip to content

Commit

Permalink
feat: serve cover art that is embedded into file
Browse files Browse the repository at this point in the history
...in case of a single item/song that does not specify an `album_id`.
Correspondingly, make the `getSong` endpoint return the song ID as `coverArt` ID when the song does not specify an `album_id`.

Extracts the cover art into a temporary file using `ffmpeg` and lets flask serve it.
Also, moves the `getCoverArt` endpoint implementation from `album.py` into a separate file since it is used to serve both album and single song cover art now.
  • Loading branch information
mgoltzsche committed Mar 25, 2024
1 parent 20b0a9e commit 1396c32
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 33 deletions.
1 change: 1 addition & 0 deletions beetsplug/beetstream/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def home():
from beetsplug.beetstream.utils import *
import beetsplug.beetstream.albums
import beetsplug.beetstream.artists
import beetsplug.beetstream.coverart
import beetsplug.beetstream.dummy
import beetsplug.beetstream.playlists
import beetsplug.beetstream.search
Expand Down
31 changes: 0 additions & 31 deletions beetsplug/beetstream/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,37 +111,6 @@ def get_album_list(version):

return Response(xml_to_string(root), mimetype='text/xml')

@app.route('/rest/getCoverArt', methods=["GET", "POST"])
@app.route('/rest/getCoverArt.view', methods=["GET", "POST"])
def cover_art_file():
query_id = int(album_subid_to_beetid(request.values.get('id')) or -1)
size = request.values.get('size')
album = g.lib.get_album(query_id)

# Fallback on item id. Some apps use this
if not album:
item = g.lib.get_item(query_id)
if item is not None and item.album_id is not None:
album = g.lib.get_album(item.album_id)
else:
flask.abort(404)

if album and album.artpath:
image_path = album.artpath.decode('utf-8')

if size is not None and int(size) > 0:
size = int(size)
with Image.open(image_path) as image:
bytes_io = io.BytesIO()
image = image.resize((size, size))
image.convert('RGB').save(bytes_io, 'PNG')
bytes_io.seek(0)
return flask.send_file(bytes_io, mimetype='image/png')

return flask.send_file(image_path)
else:
return flask.abort(404)

@app.route('/rest/getGenres', methods=["GET", "POST"])
@app.route('/rest/getGenres.view', methods=["GET", "POST"])
def genres():
Expand Down
60 changes: 60 additions & 0 deletions beetsplug/beetstream/coverart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from beetsplug.beetstream.utils import *
from beetsplug.beetstream import app
from flask import g, request
from io import BytesIO
from PIL import Image
import flask
import os
import subprocess
import tempfile

@app.route('/rest/getCoverArt', methods=["GET", "POST"])
@app.route('/rest/getCoverArt.view', methods=["GET", "POST"])
def cover_art_file():
id = request.values.get('id')
size = request.values.get('size')
album = None

if id[:len(ALBUM_ID_PREFIX)] == ALBUM_ID_PREFIX:
album_id = int(album_subid_to_beetid(id) or -1)
album = g.lib.get_album(album_id)
else:
item_id = int(song_subid_to_beetid(id) or -1)
item = g.lib.get_item(item_id)

if item is not None:
if item.album_id is not None:
album = g.lib.get_album(item.album_id)
else:
tmp_file = tempfile.NamedTemporaryFile(prefix='beetstream-coverart-', suffix='.png')
tmp_file_name = tmp_file.name
try:
tmp_file.close()
subprocess.run(['ffmpeg', '-i', item.path, '-an', '-c:v', 'copy', tmp_file_name])

return _send_image(tmp_file_name, size)
finally:
os.remove(tmp_file_name)

if album and album.artpath:
image_path = album.artpath.decode('utf-8')

if size is not None and int(size) > 0:
return _send_image(image_path, size)

return flask.send_file(image_path)
else:
return flask.abort(404)

def _send_image(path_or_bytesio, size):
converted = BytesIO()
img = Image.open(path_or_bytesio)

if size is not None and int(size) > 0:
size = int(size)
img = img.resize((size, size))

img.convert('RGB').save(converted, 'PNG')
converted.seek(0)

return flask.send_file(converted, mimetype='image/png')
9 changes: 7 additions & 2 deletions beetsplug/beetstream/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def map_song(song):
"track": song["track"],
"year": song["year"],
"genre": song["genre"],
"coverArt": album_beetid_to_subid(str(song["album_id"])) or "",
"coverArt": _cover_art_id(song),
"size": os.path.getsize(path),
"contentType": path_to_content_type(path),
"suffix": song["format"].lower(),
Expand Down Expand Up @@ -175,7 +175,7 @@ def map_song_xml(xml, song):
xml.set("track", str(song["track"]))
xml.set("year", str(song["year"]))
xml.set("genre", song["genre"])
xml.set("coverArt", album_beetid_to_subid(str(song["album_id"])) or "")
xml.set("coverArt", _cover_art_id(song)),
xml.set("size", str(os.path.getsize(path)))
xml.set("contentType", path_to_content_type(path))
xml.set("suffix", song["format"].lower())
Expand All @@ -190,6 +190,11 @@ def map_song_xml(xml, song):
if song["disc"]:
xml.set("discNumber", str(song["disc"]))

def _cover_art_id(song):
if song['album_id']:
return album_beetid_to_subid(str(song['album_id']))
return song_beetid_to_subid(str(song['id']))

def map_artist(artist_name):
return {
"id": artist_name_to_id(artist_name),
Expand Down

0 comments on commit 1396c32

Please sign in to comment.