diff --git a/supysonic/api/media.py b/supysonic/api/media.py index c958e0c..ddbc226 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -7,15 +7,12 @@ # # Distributed under terms of the GNU AGPLv3 license. -import base64 import codecs import mimetypes -import mutagen import os.path import requests import shlex import subprocess -import tempfile from flask import request, Response, send_file from flask import current_app @@ -134,27 +131,27 @@ def download_media(): @api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ]) def cover_art(): - res = get_entity(Folder) - if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): - # Check for embeded metadata in songs - temp_cover = tempfile.NamedTemporaryFile() - cover_path = temp_cover.name - for track in res.tracks: - song = mutagen.File(track.path) - if isinstance(song.tags, mutagen.id3.ID3Tags) and len(song.tags.getall('APIC')) > 0: - temp_cover.write(song.tags.getall('APIC')[0].data) - break - elif isinstance(song, mutagen.flac.FLAC) and len(song.pictures): - temp_cover.write(song.pictures[0].data) - break - elif isinstance(song.tags, mutagen._vorbis.VCommentDict) and 'METADATA_BLOCK_PICTURE' in song.tags and len(song.tags['METADATA_BLOCK_PICTURE']) > 0: - picture = mutagen.flac.Picture(base64.b64decode(song.tags['METADATA_BLOCK_PICTURE'][0])) - temp_cover.write(picture.data) - break - else: + eid = request.values['id'] + if Folder.exists(id=eid): + res = get_entity(Folder) + if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): raise NotFound('Cover art') - else: cover_path = os.path.join(res.path, res.cover_art) + elif Track.exists(id=eid): + embed_cache = os.path.join(current_app.config['WEBAPP']['cache_dir'], 'embeded_art') + cover_path = os.path.join(embed_cache, eid) + if not os.path.exists(cover_path): + res = get_entity(Track) + art = res.extract_cover_art() + if not art: + raise NotFound('Cover art') + #Art found, save to cache + os.makedirs(embed_cache, exist_ok=True) + with open(cover_path, 'wb') as cover_file: + cover_file.write(art) + else: + raise NotFound('Entity') + size = request.values.get('size') if size: size = int(size) @@ -234,4 +231,3 @@ def read_file_as_unicode(path): # Fallback to ASCII current_app.logger.debug('Reading file {} with ascii encoding'.format(path)) return unicode(open(path, 'r').read()) - diff --git a/supysonic/db.py b/supysonic/db.py index 78d56f1..1db68a3 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -7,8 +7,10 @@ # # Distributed under terms of the GNU AGPLv3 license. +import base64 import importlib import mimetypes +import mutagen import os.path import pkg_resources import time @@ -101,6 +103,11 @@ class Folder(PathMixin, db.Entity): info['artist'] = self.parent.name if self.cover_art: info['coverArt'] = str(self.id) + else: + for track in self.tracks: + if track.extract_cover_art(): + info['coverArt'] = str(track.id) + break try: starred = StarredFolder[user.id, self.id] @@ -259,7 +266,9 @@ class Track(PathMixin, db.Entity): info['year'] = self.year if self.genre: info['genre'] = self.genre - if self.folder.cover_art: + if self.extract_cover_art(): + info['coverArt'] = str(self.id) + elif self.folder.cover_art: info['coverArt'] = str(self.folder.id) try: @@ -293,6 +302,20 @@ class Track(PathMixin, db.Entity): def sort_key(self): return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() + + def extract_cover_art(self): + if os.path.exists(self.path): + metadata = mutagen.File(self.path) + data = None + if metadata: + if isinstance(metadata.tags, mutagen.id3.ID3Tags) and len(metadata.tags.getall('APIC')) > 0: + return metadata.tags.getall('APIC')[0].data + elif isinstance(metadata, mutagen.flac.FLAC) and len(metadata.pictures): + return metadata.pictures[0].data + elif isinstance(metadata.tags, mutagen._vorbis.VCommentDict) and 'METADATA_BLOCK_PICTURE' in metadata.tags and len(metadata.tags['METADATA_BLOCK_PICTURE']) > 0: + picture = mutagen.flac.Picture(base64.b64decode(metadata.tags['METADATA_BLOCK_PICTURE'][0])) + return picture.data + return None class User(db.Entity): _table_ = 'user' diff --git a/tests/api/test_media.py b/tests/api/test_media.py index 7ef77a6..688b4be 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -32,13 +32,6 @@ class MediaTestCase(ApiTestBase): ) self.folderid = folder.id - folder_embeded_art = Folder( - name = 'Root', - path = os.path.abspath('tests/assets/folder'), - root = True, - ) - self.folderid_embeded = folder_embeded_art.id - artist = Artist(name = 'Artist') album = Album(artist = artist, name = 'Album') @@ -64,13 +57,14 @@ class MediaTestCase(ApiTestBase): artist = artist, album = album, path = os.path.abspath('tests/assets/folder/silence.mp3'), - root_folder = folder_embeded_art, - folder = folder_embeded_art, + root_folder = folder, + folder = folder, duration = 2, bitrate = 320, content_type = 'audio/mpeg', last_modification = 0 ) + self.trackid_embeded_art = track_embeded_art.id def test_stream(self): self._make_request('stream', error = 10) @@ -143,7 +137,7 @@ class MediaTestCase(ApiTestBase): # TODO test non square covers # Test extracting cover art from embeded media - args['id'] = str(self.folderid_embeded) + args['id'] = str(self.trackid_embeded_art) rv = self.client.get('/rest/getCoverArt.view', query_string = args) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.mimetype, 'image/png')