1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 17:06:17 +00:00

Merge remote-tracking branch 'TaizoSimpson/master'

This commit is contained in:
spl0k 2018-10-20 15:59:38 +02:00
commit 889b83b761
16 changed files with 153 additions and 19 deletions

View File

@ -131,11 +131,28 @@ def download_media():
@api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ]) @api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ])
def cover_art(): def cover_art():
res = get_entity(Folder) eid = request.values['id']
if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): if Folder.exists(id=eid):
raise NotFound('Cover art') 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')
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
if not os.path.exists(embed_cache):
os.makedirs(embed_cache)
with open(cover_path, 'wb') as cover_file:
cover_file.write(art)
else:
raise NotFound('Entity')
cover_path = os.path.join(res.path, res.cover_art)
size = request.values.get('size') size = request.values.get('size')
if size: if size:
size = int(size) size = int(size)
@ -147,7 +164,7 @@ def cover_art():
return send_file(cover_path) return send_file(cover_path)
size_path = os.path.join(current_app.config['WEBAPP']['cache_dir'], str(size)) size_path = os.path.join(current_app.config['WEBAPP']['cache_dir'], str(size))
path = os.path.abspath(os.path.join(size_path, str(res.id))) path = os.path.abspath(os.path.join(size_path, eid))
if os.path.exists(path): if os.path.exists(path):
return send_file(path, mimetype = 'image/' + im.format.lower()) return send_file(path, mimetype = 'image/' + im.format.lower())
if not os.path.exists(size_path): if not os.path.exists(size_path):
@ -215,4 +232,3 @@ def read_file_as_unicode(path):
# Fallback to ASCII # Fallback to ASCII
current_app.logger.debug('Reading file {} with ascii encoding'.format(path)) current_app.logger.debug('Reading file {} with ascii encoding'.format(path))
return unicode(open(path, 'r').read()) return unicode(open(path, 'r').read())

View File

@ -7,8 +7,10 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
import base64
import importlib import importlib
import mimetypes import mimetypes
import mutagen
import os.path import os.path
import pkg_resources import pkg_resources
import time import time
@ -29,7 +31,7 @@ try:
except ImportError: except ImportError:
from urlparse import urlparse, parse_qsl from urlparse import urlparse, parse_qsl
SCHEMA_VERSION = '20180829' SCHEMA_VERSION = '20181010'
def now(): def now():
return datetime.now().replace(microsecond = 0) return datetime.now().replace(microsecond = 0)
@ -101,6 +103,11 @@ class Folder(PathMixin, db.Entity):
info['artist'] = self.parent.name info['artist'] = self.parent.name
if self.cover_art: if self.cover_art:
info['coverArt'] = str(self.id) info['coverArt'] = str(self.id)
else:
for track in self.tracks:
if track.has_art:
info['coverArt'] = str(track.id)
break
try: try:
starred = StarredFolder[user.id, self.id] starred = StarredFolder[user.id, self.id]
@ -183,6 +190,10 @@ class Album(db.Entity):
track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first() track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first()
if track_with_cover is not None: if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.folder.id) info['coverArt'] = str(track_with_cover.folder.id)
else:
track_with_cover = self.tracks.select(lambda t: t.has_art).first()
if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.id)
try: try:
starred = StarredAlbum[user.id, self.id] starred = StarredAlbum[user.id, self.id]
@ -209,6 +220,7 @@ class Track(PathMixin, db.Entity):
year = Optional(int) year = Optional(int)
genre = Optional(str, nullable = True) genre = Optional(str, nullable = True)
duration = Required(int) duration = Required(int)
has_art = Required(bool, default=False)
album = Required(Album, column = 'album_id') album = Required(Album, column = 'album_id')
artist = Required(Artist, column = 'artist_id') artist = Required(Artist, column = 'artist_id')
@ -259,7 +271,9 @@ class Track(PathMixin, db.Entity):
info['year'] = self.year info['year'] = self.year
if self.genre: if self.genre:
info['genre'] = self.genre info['genre'] = self.genre
if self.folder.cover_art: if self.has_art:
info['coverArt'] = str(self.id)
elif self.folder.cover_art:
info['coverArt'] = str(self.folder.id) info['coverArt'] = str(self.folder.id)
try: try:
@ -293,6 +307,23 @@ class Track(PathMixin, db.Entity):
def sort_key(self): def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
def extract_cover_art(self):
return Track._extract_cover_art(self.path)
@staticmethod
def _extract_cover_art(path):
if os.path.exists(path):
metadata = mutagen.File(path)
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): class User(db.Entity):
_table_ = 'user' _table_ = 'user'

View File

@ -140,6 +140,7 @@ class Scanner:
trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0])) trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
trdict['genre'] = self.__try_read_tag(tag, 'genre') trdict['genre'] = self.__try_read_tag(tag, 'genre')
trdict['duration'] = int(tag.info.length) trdict['duration'] = int(tag.info.length)
trdict['has_art'] = bool(Track._extract_cover_art(path))
trdict['bitrate'] = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) // 1000 trdict['bitrate'] = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) // 1000
trdict['content_type'] = mimetypes.guess_type(path, False)[0] or 'application/octet-stream' trdict['content_type'] = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'

View File

@ -0,0 +1,6 @@
START TRANSACTION;
ALTER TABLE track ADD has_art BOOLEAN DEFAULT false NOT NULL;
COMMIT;

View File

@ -0,0 +1,6 @@
START TRANSACTION;
ALTER TABLE track ADD has_art BOOLEAN DEFAULT false NOT NULL;
COMMIT;

View File

@ -0,0 +1,3 @@
ALTER TABLE track ADD has_art BOOLEAN DEFAULT false NOT NULL;
COMMIT;

View File

@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS track (
year INTEGER, year INTEGER,
genre VARCHAR(256), genre VARCHAR(256),
duration INTEGER NOT NULL, duration INTEGER NOT NULL,
has_art BOOLEAN NOT NULL DEFAULT false,
album_id BINARY(16) NOT NULL REFERENCES album, album_id BINARY(16) NOT NULL REFERENCES album,
artist_id BINARY(16) NOT NULL REFERENCES artist, artist_id BINARY(16) NOT NULL REFERENCES artist,
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,

View File

@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS track (
year INTEGER, year INTEGER,
genre VARCHAR(256), genre VARCHAR(256),
duration INTEGER NOT NULL, duration INTEGER NOT NULL,
has_art BOOLEAN NOT NULL DEFAULT false,
album_id UUID NOT NULL REFERENCES album, album_id UUID NOT NULL REFERENCES album,
artist_id UUID NOT NULL REFERENCES artist, artist_id UUID NOT NULL REFERENCES artist,
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,

View File

@ -31,6 +31,7 @@ CREATE TABLE IF NOT EXISTS track (
year INTEGER, year INTEGER,
genre VARCHAR(256), genre VARCHAR(256),
duration INTEGER NOT NULL, duration INTEGER NOT NULL,
has_art BOOLEAN NOT NULL DEFAULT false,
album_id CHAR(36) NOT NULL REFERENCES album, album_id CHAR(36) NOT NULL REFERENCES album,
artist_id CHAR(36) NOT NULL REFERENCES artist, artist_id CHAR(36) NOT NULL REFERENCES artist,
bitrate INTEGER NOT NULL, bitrate INTEGER NOT NULL,

View File

@ -51,6 +51,24 @@ class MediaTestCase(ApiTestBase):
) )
self.trackid = track.id self.trackid = track.id
self.formats = [('mp3','mpeg'), ('flac','flac'), ('ogg','ogg')]
for i in range(len(self.formats)):
track_embeded_art = Track(
title = '[silence]',
number = 1,
disc = 1,
artist = artist,
album = album,
path = os.path.abspath('tests/assets/formats/silence.{0}'.format(self.formats[i][0])),
root_folder = folder,
folder = folder,
duration = 2,
bitrate = 320,
content_type = 'audio/{0}'.format(self.formats[i][1]),
last_modification = 0
)
self.formats[i] = track_embeded_art.id
def test_stream(self): def test_stream(self):
self._make_request('stream', error = 10) self._make_request('stream', error = 10)
self._make_request('stream', { 'id': 'string' }, error = 0) self._make_request('stream', { 'id': 'string' }, error = 0)
@ -121,6 +139,15 @@ class MediaTestCase(ApiTestBase):
# TODO test non square covers # TODO test non square covers
# Test extracting cover art from embeded media
for args['id'] in self.formats:
rv = self.client.get('/rest/getCoverArt.view', query_string = args)
self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'image/png')
im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'PNG')
self.assertEqual(im.size, (120, 120))
def test_get_lyrics(self): def test_get_lyrics(self):
self._make_request('getLyrics', error = 10) self._make_request('getLyrics', error = 10)
self._make_request('getLyrics', { 'artist': 'artist' }, error = 10) self._make_request('getLyrics', { 'artist': 'artist' }, error = 10)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
../folder/silence.mp3

Binary file not shown.

View File

@ -46,10 +46,17 @@ class DbTestCase(unittest.TestCase):
parent = root_folder parent = root_folder
) )
return root_folder, child_folder child_2 = db.Folder(
root = False,
name = 'Child folder (No Art)',
path = 'tests/formats',
parent = root_folder
)
return root_folder, child_folder, child_2
def create_some_tracks(self, artist = None, album = None): def create_some_tracks(self, artist = None, album = None):
root, child = self.create_some_folders() root, child, child_2 = self.create_some_folders()
if not artist: if not artist:
artist = db.Artist(name = 'Test artist') artist = db.Artist(name = 'Test artist')
@ -64,9 +71,10 @@ class DbTestCase(unittest.TestCase):
disc = 1, disc = 1,
number = 1, number = 1,
duration = 3, duration = 3,
has_art = True,
bitrate = 320, bitrate = 320,
path = 'tests/assets/empty', path = 'tests/assets/formats/silence.ogg',
content_type = 'audio/mpeg', content_type = 'audio/ogg',
last_modification = 1234, last_modification = 1234,
root_folder = root, root_folder = root,
folder = child folder = child
@ -89,6 +97,25 @@ class DbTestCase(unittest.TestCase):
return track1, track2 return track1, track2
def create_track_in(self, folder, root, artist = None, album = None, has_art = True):
artist = artist or db.Artist(name = 'Snazzy Artist')
album = album or db.Album(artist = artist, name = 'Rockin\' Album')
return db.Track(
title = 'Nifty Number',
album = album,
artist = artist,
disc = 1,
number = 1,
duration = 5,
has_art = has_art,
bitrate = 96,
path = 'tests/assets/formats/silence.flac',
content_type = 'audio/flac',
last_modification = 1234,
root_folder = root,
folder = folder
)
def create_user(self, name = 'Test User'): def create_user(self, name = 'Test User'):
return db.User( return db.User(
name = name, name = name,
@ -107,7 +134,8 @@ class DbTestCase(unittest.TestCase):
@db_session @db_session
def test_folder_base(self): def test_folder_base(self):
root_folder, child_folder = self.create_some_folders() root_folder, child_folder, child_noart = self.create_some_folders()
track_embededart = self.create_track_in(child_noart, root_folder)
MockUser = namedtuple('User', [ 'id' ]) MockUser = namedtuple('User', [ 'id' ])
user = MockUser(uuid.uuid4()) user = MockUser(uuid.uuid4())
@ -132,9 +160,13 @@ class DbTestCase(unittest.TestCase):
self.assertEqual(child['artist'], root_folder.name) self.assertEqual(child['artist'], root_folder.name)
self.assertEqual(child['coverArt'], child['id']) self.assertEqual(child['coverArt'], child['id'])
noart = child_noart.as_subsonic_child(user)
self.assertIn('coverArt', noart)
self.assertEqual(noart['coverArt'], str(track_embededart.id))
@db_session @db_session
def test_folder_annotation(self): def test_folder_annotation(self):
root_folder, child_folder = self.create_some_folders() root_folder, child_folder, _ = self.create_some_folders()
user = self.create_user() user = self.create_user()
star = db.StarredFolder( star = db.StarredFolder(
@ -202,7 +234,8 @@ class DbTestCase(unittest.TestCase):
# No tracks, shouldn't be stored under normal circumstances # No tracks, shouldn't be stored under normal circumstances
self.assertRaises(ValueError, album.as_subsonic_album, user) self.assertRaises(ValueError, album.as_subsonic_album, user)
self.create_some_tracks(artist, album) root_folder, folder_art, folder_noart = self.create_some_folders()
track1 = self.create_track_in(root_folder, folder_noart, artist = artist, album = album)
album_dict = album.as_subsonic_album(user) album_dict = album.as_subsonic_album(user)
self.assertIsInstance(album_dict, dict) self.assertIsInstance(album_dict, dict)
@ -214,11 +247,13 @@ class DbTestCase(unittest.TestCase):
self.assertIn('duration', album_dict) self.assertIn('duration', album_dict)
self.assertIn('created', album_dict) self.assertIn('created', album_dict)
self.assertIn('starred', album_dict) self.assertIn('starred', album_dict)
self.assertIn('coverArt', album_dict)
self.assertEqual(album_dict['name'], album.name) self.assertEqual(album_dict['name'], album.name)
self.assertEqual(album_dict['artist'], artist.name) self.assertEqual(album_dict['artist'], artist.name)
self.assertEqual(album_dict['artistId'], str(artist.id)) self.assertEqual(album_dict['artistId'], str(artist.id))
self.assertEqual(album_dict['songCount'], 2) self.assertEqual(album_dict['songCount'], 1)
self.assertEqual(album_dict['duration'], 8) self.assertEqual(album_dict['duration'], 5)
self.assertEqual(album_dict['coverArt'], str(track1.id))
self.assertRegex(album_dict['created'], date_regex) self.assertRegex(album_dict['created'], date_regex)
self.assertRegex(album_dict['starred'], date_regex) self.assertRegex(album_dict['starred'], date_regex)
@ -237,6 +272,11 @@ class DbTestCase(unittest.TestCase):
self.assertIn('isDir', track1_dict) self.assertIn('isDir', track1_dict)
self.assertIn('title', track1_dict) self.assertIn('title', track1_dict)
self.assertFalse(track1_dict['isDir']) self.assertFalse(track1_dict['isDir'])
self.assertIn('coverArt', track1_dict)
self.assertEqual(track1_dict['coverArt'], track1_dict['id'])
track2_dict = track2.as_subsonic_child(user, None)
self.assertEqual(track2_dict['coverArt'], track2_dict['parent'])
# ... we'll test the rest against the API XSD. # ... we'll test the rest against the API XSD.
@db_session @db_session

View File

@ -26,7 +26,7 @@ class ScannerTestCase(unittest.TestCase):
db.init_database('sqlite:') db.init_database('sqlite:')
with db_session: with db_session:
folder = FolderManager.add('folder', os.path.abspath('tests/assets')) folder = FolderManager.add('folder', os.path.abspath('tests/assets/folder'))
self.assertIsNotNone(folder) self.assertIsNotNone(folder)
self.folderid = folder.id self.folderid = folder.id
@ -126,7 +126,7 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
track = db.Track.select().first() track = db.Track.select().first()
new_path = os.path.abspath(os.path.join(os.path.dirname(track.path), '..', 'silence.mp3')) new_path = track.path.replace('silence','silence_moved')
self.scanner.move_file(track.path, new_path) self.scanner.move_file(track.path, new_path)
commit() commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)