1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-23 01:16:18 +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 * streaming of various audio file formats
* [transcoding] * [transcoding]
* user or random playlists * 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 * starred tracks/albums and ratings
* [Last.FM][lastfm] scrobbling * [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 VARCHAR(4096) NOT NULL,
path_hash BINARY(20) NOT NULL, path_hash BINARY(20) NOT NULL,
created DATETIME NOT NULL, created DATETIME NOT NULL,
has_cover_art BOOLEAN NOT NULL, cover_art VARCHAR(256),
last_scan INTEGER NOT NULL, last_scan INTEGER NOT NULL,
parent_id BINARY(16) REFERENCES folder parent_id BINARY(16) REFERENCES folder
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

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

View File

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

View File

@ -132,29 +132,30 @@ 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) 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') raise NotFound('Cover art')
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)
else: 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')) im = Image.open(cover_path)
if size > im.size[0] and size > im.size[1]: if size > im.width and size > im.height:
return send_file(os.path.join(res.path, 'cover.jpg')) 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, str(res.id)))
if os.path.exists(path): 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): if not os.path.exists(size_path):
os.makedirs(size_path) os.makedirs(size_path)
im.thumbnail([size, size], Image.ANTIALIAS) im.thumbnail([size, size], Image.ANTIALIAS)
im.save(path, 'JPEG') im.save(path, im.format)
return send_file(path, mimetype = 'image/jpeg') return send_file(path, mimetype = 'image/' + im.format.lower())
@api.route('/getLyrics.view', methods = [ 'GET', 'POST' ]) @api.route('/getLyrics.view', methods = [ 'GET', 'POST' ])
def lyrics(): 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 = Required(str, 4096) # unique
_path_hash = Required(buffer, column = 'path_hash') _path_hash = Required(buffer, column = 'path_hash')
created = Required(datetime, precision = 0, default = now) 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) last_scan = Required(int, default = 0)
parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id')
@ -82,7 +82,7 @@ class Folder(PathMixin, db.Entity):
if not self.root: if not self.root:
info['parent'] = str(self.parent.id) info['parent'] = str(self.parent.id)
info['artist'] = self.parent.name info['artist'] = self.parent.name
if self.has_cover_art: if self.cover_art:
info['coverArt'] = str(self.id) info['coverArt'] = str(self.id)
try: try:
@ -163,7 +163,7 @@ class Album(db.Entity):
created = min(self.tracks.created).isoformat() 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: if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.folder.id) info['coverArt'] = str(track_with_cover.folder.id)
@ -242,7 +242,7 @@ 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.has_cover_art: if self.folder.cover_art:
info['coverArt'] = str(self.folder.id) info['coverArt'] = str(self.folder.id)
try: try:

View File

@ -14,6 +14,7 @@ import time
from pony.orm import db_session from pony.orm import db_session
from .covers import find_cover_in_folder
from .db import Folder, Artist, Album, Track, User from .db import Folder, Artist, Album, Track, User
from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from .db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from .db import RatingFolder, RatingTrack from .db import RatingFolder, RatingTrack
@ -84,7 +85,15 @@ class Scanner:
folders = [ folder ] folders = [ folder ]
while folders: while folders:
f = folders.pop() 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 folders += f.children
folder.last_scan = int(time.time()) folder.last_scan = int(time.time())

View File

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

View File

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