1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-20 19:31:04 +00:00

Don't restrict cover art to 'cover.jpg' files

Ref #81
This commit is contained in:
spl0k 2018-05-21 16:16:06 +02:00
parent 918cd11262
commit 405a26a20a
13 changed files with 160 additions and 19 deletions

View File

@ -11,7 +11,7 @@ Current supported features are:
* streaming of various audio file formats
* [transcoding]
* user or random playlists
* cover arts (`cover.jpg` files in the same folder as music files)
* cover arts (as image files in the same folder as music files)
* starred tracks/albums and ratings
* [Last.FM][lastfm] scrobbling

View File

@ -0,0 +1,12 @@
START TRANSACTION;
ALTER TABLE folder ADD cover_art VARCHAR(256) AFTER has_cover_art;
UPDATE folder
SET cover_art = 'cover.jpg'
WHERE has_cover_art;
ALTER TABLE folder DROP COLUMN has_cover_art;
COMMIT;

View File

@ -0,0 +1,12 @@
START TRANSACTION;
ALTER TABLE folder ADD cover_art VARCHAR(256);
UPDATE folder
SET cover_art = 'cover.jpg'
WHERE has_cover_art;
ALTER TABLE folder DROP COLUMN has_cover_art;
COMMIT;

View File

@ -0,0 +1,27 @@
BEGIN TRANSACTION;
DROP INDEX index_folder_path;
ALTER TABLE folder RENAME TO folder_old;
CREATE TABLE folder (
id CHAR(36) PRIMARY KEY,
root BOOLEAN NOT NULL,
name VARCHAR(256) NOT NULL COLLATE NOCASE,
path VARCHAR(4096) NOT NULL,
path_hash BLOB NOT NULL,
created DATETIME NOT NULL,
cover_art VARCHAR(256),
last_scan INTEGER NOT NULL,
parent_id CHAR(36) REFERENCES folder
);
CREATE UNIQUE INDEX index_folder_path ON folder(path_hash);
INSERT INTO folder(id, root, name, path, path_hash, created, cover_art, last_scan, parent_id)
SELECT id, root, name, path, path_hash, created, CASE WHEN has_cover_art THEN 'cover.jpg' ELSE NULL END, last_scan, parent_id
FROM folder_old;
DROP TABLE folder_old;
COMMIT;
VACUUM;

View File

@ -5,7 +5,7 @@ CREATE TABLE folder (
path VARCHAR(4096) NOT NULL,
path_hash BINARY(20) NOT NULL,
created DATETIME NOT NULL,
has_cover_art BOOLEAN NOT NULL,
cover_art VARCHAR(256),
last_scan INTEGER NOT NULL,
parent_id BINARY(16) REFERENCES folder
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -5,7 +5,7 @@ CREATE TABLE folder (
path VARCHAR(4096) NOT NULL,
path_hash BYTEA NOT NULL,
created TIMESTAMP NOT NULL,
has_cover_art BOOLEAN NOT NULL,
cover_art VARCHAR(256),
last_scan INTEGER NOT NULL,
parent_id UUID REFERENCES folder
);

View File

@ -5,7 +5,7 @@ CREATE TABLE folder (
path VARCHAR(4096) NOT NULL,
path_hash BLOB NOT NULL,
created DATETIME NOT NULL,
has_cover_art BOOLEAN NOT NULL,
cover_art VARCHAR(256),
last_scan INTEGER NOT NULL,
parent_id CHAR(36) REFERENCES folder
);

View File

@ -132,29 +132,30 @@ def download_media():
@api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ])
def cover_art():
res = get_entity(Folder)
if not res.has_cover_art or not os.path.isfile(os.path.join(res.path, 'cover.jpg')):
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)
size = request.values.get('size')
if size:
size = int(size)
else:
return send_file(os.path.join(res.path, 'cover.jpg'))
return send_file(cover_path)
im = Image.open(os.path.join(res.path, 'cover.jpg'))
if size > im.size[0] and size > im.size[1]:
return send_file(os.path.join(res.path, 'cover.jpg'))
im = Image.open(cover_path)
if size > im.width and size > im.height:
return send_file(cover_path)
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)))
if os.path.exists(path):
return send_file(path, mimetype = 'image/jpeg')
return send_file(path, mimetype = 'image/' + im.format.lower())
if not os.path.exists(size_path):
os.makedirs(size_path)
im.thumbnail([size, size], Image.ANTIALIAS)
im.save(path, 'JPEG')
return send_file(path, mimetype = 'image/jpeg')
im.save(path, im.format)
return send_file(path, mimetype = 'image/' + im.format.lower())
@api.route('/getLyrics.view', methods = [ 'GET', 'POST' ])
def lyrics():

80
supysonic/covers.py Normal file
View File

@ -0,0 +1,80 @@
# coding: utf-8
#
# This file is part of Supysonic.
# Supysonic is a Python implementation of the Subsonic server API.
#
# Copyright (C) 2018 Alban 'spl0k' Féron
#
# Distributed under terms of the GNU AGPLv3 license.
import os, os.path
import re
from PIL import Image
EXTENSIONS = ('.jpg', '.jpeg', '.png', '.bmp')
NAMING_SCORE_RULES = (
('cover', 5),
('albumart', 5),
('folder', 5),
('front', 10),
('back', -10),
('large', 2),
('small', -2)
)
class CoverFile(object):
__clean_regex = re.compile(r'[^a-z]')
@staticmethod
def __clean_name(name):
return CoverFile.__clean_regex.sub('', name.lower())
def __init__(self, name, album_name = None):
self.name = name
self.score = 0
for part, score in NAMING_SCORE_RULES:
if part in name.lower():
self.score += score
if album_name:
basename, _ = os.path.splitext(name)
clean = CoverFile.__clean_name(basename)
album_name = CoverFile.__clean_name(album_name)
if clean in album_name or album_name in clean:
self.score += 20
def is_valid_cover(path):
if not os.path.isfile(path):
return False
_, ext = os.path.splitext(path)
if ext.lower() not in EXTENSIONS:
return False
try: # Ensure the image can be read
with Image.open(path):
return True
except IOError:
return False
def find_cover_in_folder(path, album_name = None):
if not os.path.isdir(path):
raise ValueError('Invalid path')
candidates = []
for f in os.listdir(path):
file_path = os.path.join(path, f)
if not is_valid_cover(file_path):
continue
cover = CoverFile(f, album_name)
candidates.append(cover)
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]
return sorted(candidates, key = lambda c: c.score, reverse = True)[0]

View File

@ -59,7 +59,7 @@ class Folder(PathMixin, db.Entity):
path = Required(str, 4096) # unique
_path_hash = Required(buffer, column = 'path_hash')
created = Required(datetime, precision = 0, default = now)
has_cover_art = Required(bool, default = False)
cover_art = Optional(str, nullable = True)
last_scan = Required(int, default = 0)
parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id')
@ -82,7 +82,7 @@ class Folder(PathMixin, db.Entity):
if not self.root:
info['parent'] = str(self.parent.id)
info['artist'] = self.parent.name
if self.has_cover_art:
if self.cover_art:
info['coverArt'] = str(self.id)
try:
@ -163,7 +163,7 @@ class Album(db.Entity):
created = min(self.tracks.created).isoformat()
)
track_with_cover = self.tracks.select(lambda t: t.folder.has_cover_art).first()
track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first()
if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.folder.id)
@ -242,7 +242,7 @@ class Track(PathMixin, db.Entity):
info['year'] = self.year
if self.genre:
info['genre'] = self.genre
if self.folder.has_cover_art:
if self.folder.cover_art:
info['coverArt'] = str(self.folder.id)
try:

View File

@ -14,6 +14,7 @@ import time
from pony.orm import db_session
from .covers import find_cover_in_folder
from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack
@ -84,7 +85,15 @@ class Scanner:
folders = [ folder ]
while folders:
f = folders.pop()
f.has_cover_art = os.path.isfile(os.path.join(f.path, 'cover.jpg'))
album_name = None
track = f.tracks.select().first()
if track is not None:
album_name = track.album.name
cover = find_cover_in_folder(f.path, album_name)
f.cover_art = cover.name if cover is not None else None
folders += f.children
folder.last_scan = int(time.time())

View File

@ -28,7 +28,7 @@ class MediaTestCase(ApiTestBase):
name = 'Root',
path = os.path.abspath('tests/assets'),
root = True,
has_cover_art = True # 420x420 PNG
cover_art = 'cover.jpg'
)
self.folderid = folder.id

View File

@ -42,7 +42,7 @@ class DbTestCase(unittest.TestCase):
root = False,
name = 'Child folder',
path = 'tests/assets',
has_cover_art = True,
cover_art = 'cover.jpg',
parent = root_folder
)