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

6
db.py
View File

@ -124,7 +124,7 @@ class Folder(database.Model):
@hybrid_property
def name(self):
return self.path[self.path.rfind(os.sep) + 1:]
return os.path.basename(self.path)
def get_children(self):
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))
artist_id = Column(UUID, ForeignKey('artist.id'))
tracks = relationship('Track', backref = 'album', cascade="delete")
year = Column(String(32))
def as_subsonic_album(self, user):
info = {
@ -195,7 +196,8 @@ class Album(database.Model):
'artistId': str(self.artist_id),
'songCount': len(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:
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 time
import mutagen
import datetime
from mediafile import MediaFile
import config
import math
import sys, traceback
@ -43,7 +44,7 @@ class Scanner:
for root, subfolders, files in os.walk(root_folder.path, topdown=False):
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)
for f in files:
@ -54,10 +55,8 @@ class Scanner:
except:
app.logger.error('Problem adding file: ' + os.path.join(root,f))
app.logger.error(traceback.print_exc())
sys.exit(0)
self.__session.rollback()
pass
self.__session.add_all(self.__folders.values())
self.__session.add_all(self.__tracks.values())
root_folder.last_scan = int(time.time())
self.__session.commit()
@ -94,6 +93,7 @@ class Scanner:
tr = self.__tracks[path]
app.logger.debug('Existing File: ' + path)
if not tr.last_modification:
tr.last_modification = curmtime
@ -104,16 +104,14 @@ class Scanner:
app.logger.debug('\tFile modified, updating tag')
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))
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:
app.logger.debug('Scanning File: ' + path + '\n\tReading tag')
tag = self.__try_load_tag(path)
if not tag:
app.logger.debug('\tProblem reading tag')
try:
mf = MediaFile(path)
except:
app.logger.error('Problem reading file: ' + path)
app.logger.error(traceback.print_exc())
return False
tr = db.Track(path = path, folder = self.__find_folder(root))
@ -122,21 +120,32 @@ class Scanner:
self.__added_tracks += 1
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
tr.album = self.__find_album(self.__try_read_tag(tag, 'artist', 'Unknown'), self.__try_read_tag(tag, 'album', 'Unknown'))
# read in file tags
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
def __find_album(self, artist, album):
def __find_album(self, artist, album, yr):
# TODO : DB specific issues with single column name primary key
# for instance, case sensitivity and trailing spaces
artist = artist.rstrip()
@ -157,7 +166,7 @@ class Scanner:
return al[album]
else:
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):
@ -168,23 +177,6 @@ class Scanner:
self.__folders[path] = db.Folder(path = 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):
track.album.tracks.remove(track)
track.folder.tracks.remove(track)

View File

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