1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-11-14 22:22:18 +00:00

use mediafile library from beets to read tags and coverart from songs

This commit is contained in:
Emory P 2014-02-04 03:36:59 -05:00
parent 8570884e6e
commit f313ff5369
6 changed files with 1651 additions and 48 deletions

View File

@ -5,9 +5,10 @@ import os.path
from PIL import Image from PIL import Image
import subprocess import subprocess
import shlex import shlex
import mutagen
import fnmatch import fnmatch
import mimetypes import mimetypes
from mediafile import MediaFile
import mutagen
import config, scanner import config, scanner
from web import app from web import app
@ -174,6 +175,8 @@ def download_media():
@app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ])
def cover_art(): def cover_art():
# Speed up the file transfer
@after_this_request @after_this_request
def add_header(response): def add_header(response):
if 'X-Sendfile' in response.headers: if 'X-Sendfile' in response.headers:
@ -184,23 +187,37 @@ def cover_art():
app.logger.debug('X-Accel-Redirect: ' + xsendfile + redirect) app.logger.debug('X-Accel-Redirect: ' + xsendfile + redirect)
return response return response
# retrieve folder from database
status, res = get_entity(request, Folder) status, res = get_entity(request, Folder)
if not status: if not status:
return res return res
# Check the folder id given for jpgs
app.logger.debug('Cover Art Check: ' + res.path + '/*.jp*g') app.logger.debug('Cover Art Check: ' + res.path + '/*.jp*g')
coverfile = os.listdir(res.path) coverfile = os.listdir(res.path)
coverfile = fnmatch.filter(coverfile, '*.jp*g') coverfile = fnmatch.filter(coverfile, '*.jp*g')
app.logger.debug('Found Images: ' + str(coverfile)) app.logger.debug('Found Images: ' + str(coverfile))
# when there is not a jpeg in the folder check files for embedded art
if not coverfile: if not coverfile:
app.logger.debug('No Art Found!') app.logger.debug('No Art Found in Folder, Checking Files!')
res.has_cover_art = False
session.commit() for tr in res.tracks:
app.logger.debug('Checking ' + tr.path + ' For Artwork')
try:
mf = MediaFile(tr.path)
coverfile = getattr(mf, 'art')
if coverfile is not None:
return coverfile
except:
app.logger.debug('Problem reading embedded art')
return request.error_formatter(70, 'Cover art not found'), 404 return request.error_formatter(70, 'Cover art not found'), 404
# pick the first image found
# TODO: prefer cover
coverfile = coverfile[0] coverfile = coverfile[0]
size = request.args.get('size') size = request.args.get('size')

6
db.py
View File

@ -124,7 +124,7 @@ class Folder(database.Model):
@hybrid_property @hybrid_property
def name(self): def name(self):
return self.path[self.path.rfind(os.sep) + 1:] return os.path.basename(self.path)
def get_children(self): def get_children(self):
return Folder.query.filter(Folder.path.like(self.path + '/%%')).filter(~Folder.path.like(self.path + '/%%/%%')) return Folder.query.filter(Folder.path.like(self.path + '/%%')).filter(~Folder.path.like(self.path + '/%%/%%'))
@ -186,6 +186,7 @@ class Album(database.Model):
name = Column(String(255)) name = Column(String(255))
artist_id = Column(UUID, ForeignKey('artist.id')) artist_id = Column(UUID, ForeignKey('artist.id'))
tracks = relationship('Track', backref = 'album', cascade="delete") tracks = relationship('Track', backref = 'album', cascade="delete")
year = Column(String(32))
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = { info = {
@ -195,7 +196,8 @@ class Album(database.Model):
'artistId': str(self.artist_id), 'artistId': str(self.artist_id),
'songCount': len(self.tracks), 'songCount': len(self.tracks),
'duration': sum(map(lambda t: t.duration, self.tracks)), 'duration': sum(map(lambda t: t.duration, self.tracks)),
'created': min(map(lambda t: t.created, self.tracks)).isoformat() 'created': min(map(lambda t: t.created, self.tracks)).isoformat(),
'year': self.year
} }
if self.tracks[0].folder.has_cover_art: if self.tracks[0].folder.has_cover_art:
info['coverArt'] = str(self.tracks[0].folder_id) info['coverArt'] = str(self.tracks[0].folder_id)

178
enumeration.py Executable file
View File

@ -0,0 +1,178 @@
# This file is part of beets.
# Copyright 2013, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""A metaclass for enumerated types that really are types.
You can create enumerations with `enum(values, [name])` and they work
how you would expect them to.
>>> from enumeration import enum
>>> Direction = enum('north east south west', name='Direction')
>>> Direction.west
Direction.west
>>> Direction.west == Direction.west
True
>>> Direction.west == Direction.east
False
>>> isinstance(Direction.west, Direction)
True
>>> Direction[3]
Direction.west
>>> Direction['west']
Direction.west
>>> Direction.west.name
'west'
>>> Direction.north < Direction.west
True
Enumerations are classes; their instances represent the possible values
of the enumeration. Because Python classes must have names, you may
provide a `name` parameter to `enum`; if you don't, a meaningless one
will be chosen for you.
"""
import random
class Enumeration(type):
"""A metaclass whose classes are enumerations.
The `values` attribute of the class is used to populate the
enumeration. Values may either be a list of enumerated names or a
string containing a space-separated list of names. When the class
is created, it is instantiated for each name value in `values`.
Each such instance is the name of the enumerated item as the sole
argument.
The `Enumerated` class is a good choice for a superclass.
"""
def __init__(cls, name, bases, dic):
super(Enumeration, cls).__init__(name, bases, dic)
if 'values' not in dic:
# Do nothing if no values are provided (i.e., with
# Enumerated itself).
return
# May be called with a single string, in which case we split on
# whitespace for convenience.
values = dic['values']
if isinstance(values, basestring):
values = values.split()
# Create the Enumerated instances for each value. We have to use
# super's __setattr__ here because we disallow setattr below.
super(Enumeration, cls).__setattr__('_items_dict', {})
super(Enumeration, cls).__setattr__('_items_list', [])
for value in values:
item = cls(value, len(cls._items_list))
cls._items_dict[value] = item
cls._items_list.append(item)
def __getattr__(cls, key):
try:
return cls._items_dict[key]
except KeyError:
raise AttributeError("enumeration '" + cls.__name__ +
"' has no item '" + key + "'")
def __setattr__(cls, key, val):
raise TypeError("enumerations do not support attribute assignment")
def __getitem__(cls, key):
if isinstance(key, int):
return cls._items_list[key]
else:
return getattr(cls, key)
def __len__(cls):
return len(cls._items_list)
def __iter__(cls):
return iter(cls._items_list)
def __nonzero__(cls):
# Ensures that __len__ doesn't get called before __init__ by
# pydoc.
return True
class Enumerated(object):
"""An item in an enumeration.
Contains instance methods inherited by enumerated objects. The
metaclass is preset to `Enumeration` for your convenience.
Instance attributes:
name -- The name of the item.
index -- The index of the item in its enumeration.
>>> from enumeration import Enumerated
>>> class Garment(Enumerated):
... values = 'hat glove belt poncho lederhosen suspenders'
... def wear(self):
... print('now wearing a ' + self.name)
...
>>> Garment.poncho.wear()
now wearing a poncho
"""
__metaclass__ = Enumeration
def __init__(self, name, index):
self.name = name
self.index = index
def __str__(self):
return type(self).__name__ + '.' + self.name
def __repr__(self):
return str(self)
def __cmp__(self, other):
if type(self) is type(other):
# Note that we're assuming that the items are direct
# instances of the same Enumeration (i.e., no fancy
# subclassing), which is probably okay.
return cmp(self.index, other.index)
else:
return NotImplemented
def enum(*values, **kwargs):
"""Shorthand for creating a new Enumeration class.
Call with enumeration values as a list, a space-delimited string, or
just an argument list. To give the class a name, pass it as the
`name` keyword argument. Otherwise, a name will be chosen for you.
The following are all equivalent:
enum('pinkie ring middle index thumb')
enum('pinkie', 'ring', 'middle', 'index', 'thumb')
enum(['pinkie', 'ring', 'middle', 'index', 'thumb'])
"""
if ('name' not in kwargs) or kwargs['name'] is None:
# Create a probably-unique name. It doesn't really have to be
# unique, but getting distinct names each time helps with
# identification in debugging.
name = 'Enumeration' + hex(random.randint(0,0xfffffff))[2:].upper()
else:
name = kwargs['name']
if len(values) == 1:
# If there's only one value, we have a couple of alternate calling
# styles.
if isinstance(values[0], basestring) or hasattr(values[0], '__iter__'):
values = values[0]
return type(name, (Enumerated,), {'values': values})

1414
mediafile.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,8 @@
import os, os.path import os, os.path
import time import time
import mutagen import datetime
from mediafile import MediaFile
import config import config
import math import math
import sys, traceback import sys, traceback
@ -43,7 +44,7 @@ class Scanner:
for root, subfolders, files in os.walk(root_folder.path, topdown=False): for root, subfolders, files in os.walk(root_folder.path, topdown=False):
if(root not in self.__folders): if(root not in self.__folders):
app.logger.debug('Adding folder (empty): ' + root) app.logger.debug('Adding folder: ' + root)
self.__folders[root] = db.Folder(path = root) self.__folders[root] = db.Folder(path = root)
for f in files: for f in files:
@ -54,10 +55,8 @@ class Scanner:
except: except:
app.logger.error('Problem adding file: ' + os.path.join(root,f)) app.logger.error('Problem adding file: ' + os.path.join(root,f))
app.logger.error(traceback.print_exc()) app.logger.error(traceback.print_exc())
sys.exit(0) pass
self.__session.rollback()
self.__session.add_all(self.__folders.values())
self.__session.add_all(self.__tracks.values()) self.__session.add_all(self.__tracks.values())
root_folder.last_scan = int(time.time()) root_folder.last_scan = int(time.time())
self.__session.commit() self.__session.commit()
@ -94,6 +93,7 @@ class Scanner:
tr = self.__tracks[path] tr = self.__tracks[path]
app.logger.debug('Existing File: ' + path) app.logger.debug('Existing File: ' + path)
if not tr.last_modification: if not tr.last_modification:
tr.last_modification = curmtime tr.last_modification = curmtime
@ -104,16 +104,14 @@ class Scanner:
app.logger.debug('\tFile modified, updating tag') app.logger.debug('\tFile modified, updating tag')
app.logger.debug('\tcurmtime %s / last_mod %s', curmtime, tr.last_modification) app.logger.debug('\tcurmtime %s / last_mod %s', curmtime, tr.last_modification)
app.logger.debug('\t\t%s Seconds Newer\n\t\t', str(curmtime - tr.last_modification)) app.logger.debug('\t\t%s Seconds Newer\n\t\t', str(curmtime - tr.last_modification))
tag = self.__try_load_tag(path)
if not tag:
app.logger.debug('\tError retrieving tags, removing track from DB')
self.__remove_track(tr)
return False
else: else:
app.logger.debug('Scanning File: ' + path + '\n\tReading tag') app.logger.debug('Scanning File: ' + path + '\n\tReading tag')
tag = self.__try_load_tag(path)
if not tag: try:
app.logger.debug('\tProblem reading tag') mf = MediaFile(path)
except:
app.logger.error('Problem reading file: ' + path)
app.logger.error(traceback.print_exc())
return False return False
tr = db.Track(path = path, folder = self.__find_folder(root)) tr = db.Track(path = path, folder = self.__find_folder(root))
@ -122,21 +120,32 @@ class Scanner:
self.__added_tracks += 1 self.__added_tracks += 1
tr.last_modification = curmtime tr.last_modification = curmtime
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
tr.title = self.__try_read_tag(tag, 'title', '')
tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
tr.genre = self.__try_read_tag(tag, 'genre')
tr.duration = int(tag.info.length)
# TODO: use album artist if available, then artist, then unknown # read in file tags
tr.album = self.__find_album(self.__try_read_tag(tag, 'artist', 'Unknown'), self.__try_read_tag(tag, 'album', 'Unknown')) tr.disc = getattr(mf, 'disc')
tr.number = getattr(mf, 'track')
tr.title = getattr(mf, 'title')
tr.year = getattr(mf, 'year')
tr.genre = getattr(mf, 'genre')
tr.artist = getattr(mf, 'artist')
tr.bitrate = getattr(mf, 'bitrate')/1000
tr.duration = getattr(mf, 'length')
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000 albumartist = getattr(mf, 'albumartist')
if (albumartist == u''):
# Use folder name two levels up if no albumartist tag found
# Assumes structure main -> artist -> album -> song.file
# That way the songs in compilations will show up in the same album
albumartist = os.path.basename(os.path.dirname(tr.folder.path))
tr.created = datetime.datetime.fromtimestamp(curmtime)
# album year is the same as year of first track found from album, might be inaccurate
tr.album = self.__find_album(albumartist, getattr(mf, 'album'), tr.year)
return True return True
def __find_album(self, artist, album): def __find_album(self, artist, album, yr):
# TODO : DB specific issues with single column name primary key # TODO : DB specific issues with single column name primary key
# for instance, case sensitivity and trailing spaces # for instance, case sensitivity and trailing spaces
artist = artist.rstrip() artist = artist.rstrip()
@ -157,7 +166,7 @@ class Scanner:
return al[album] return al[album]
else: else:
self.__added_albums += 1 self.__added_albums += 1
return db.Album(name = album, artist = ar) return db.Album(name = album, artist = ar, year = yr)
def __find_folder(self, path): def __find_folder(self, path):
@ -168,23 +177,6 @@ class Scanner:
self.__folders[path] = db.Folder(path = path) self.__folders[path] = db.Folder(path = path)
return self.__folders[path] return self.__folders[path]
def __try_load_tag(self, path):
try:
return mutagen.File(path, easy = True)
except:
return None
def __try_read_tag(self, metadata, field, default = None, transform = lambda x: x[0]):
try:
value = metadata[field]
if not value:
return default
if transform:
value = transform(value)
return value if value else default
except:
return default
def __remove_track(self, track): def __remove_track(self, track):
track.album.tracks.remove(track) track.album.tracks.remove(track)
track.folder.tracks.remove(track) track.folder.tracks.remove(track)

View File

@ -9,6 +9,6 @@
touch-reload = /tmp/supysonic.reload touch-reload = /tmp/supysonic.reload
enable-threads = true enable-threads = true
processes = 8 processes = 8
harakiri = 120 harakiri = 280
daemonize = uwsgi.log daemonize = uwsgi.log
close-on-exec = true close-on-exec = true