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

Switch to mutagen -> allow serving of more audio formats

This commit is contained in:
spl0k 2013-10-14 18:36:45 +02:00
parent 2256f639ba
commit 22e2d351d8
4 changed files with 75 additions and 44 deletions

View File

@ -4,9 +4,9 @@ Supysonic
Supysonic is a Python implementation of the [Subsonic](http://www.subsonic.org/) server API. Supysonic is a Python implementation of the [Subsonic](http://www.subsonic.org/) server API.
Current supported features are: Current supported features are:
* browsing (by folders or ID3 tags) * browsing (by folders or tags)
* streaming (obviously, the collection scanner only looks for MP3s though) * streaming of various audio file formats
* random playlists * user or random playlists
* cover arts (`cover.jpg` files in the same folder as music files) * cover arts (`cover.jpg` files in the same folder as music files)
* starred tracks/albums and ratings * starred tracks/albums and ratings
* [Last.FM](http://www.last.fm/) scrobbling * [Last.FM](http://www.last.fm/) scrobbling
@ -27,7 +27,7 @@ or as a WSGI application (on Apache for instance). But first:
* Python Imaging Library (`apt-get install python-imaging`) * Python Imaging Library (`apt-get install python-imaging`)
* simplejson (`apt-get install python-simplejson`) * simplejson (`apt-get install python-simplejson`)
* [requests](http://docs.python-requests.org/) >= 0.12.1 (`pip install requests`) * [requests](http://docs.python-requests.org/) >= 0.12.1 (`pip install requests`)
* [eyeD3](http://eyed3.nicfit.net/) >= 0.7 (`pip install eyed3`) * [mutagen](https://code.google.com/p/mutagen/) (`apt-get install python-mutagen`)
### Configuration ### Configuration

View File

@ -16,21 +16,23 @@ def stream_media():
return res return res
maxBitRate, format, timeOffset, size, estimateContentLength = map(request.args.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ]) maxBitRate, format, timeOffset, size, estimateContentLength = map(request.args.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ])
format = format.lower()
if maxBitRate: if format != 'raw':
try: if maxBitRate:
maxBitRate = int(maxBitRate) try:
except: maxBitRate = int(maxBitRate)
return request.error_formatter(0, 'Invalid bitrate value') except:
return request.error_formatter(0, 'Invalid bitrate value')
if res.bitrate > maxBitRate: if res.bitrate > maxBitRate:
# TODO transcode
pass
if format and format != res.suffix():
# TODO transcode # TODO transcode
pass pass
if format != 'mp3':
# TODO transcode
pass
res.play_count = res.play_count + 1 res.play_count = res.play_count + 1
res.last_play = now() res.last_play = now()
request.user.last_play = res request.user.last_play = res

8
db.py
View File

@ -204,6 +204,7 @@ class Track(Base):
bitrate = Column(Integer) bitrate = Column(Integer)
path = Column(String(4096)) # should be unique, but mysql don't like such large columns path = Column(String(4096)) # should be unique, but mysql don't like such large columns
content_type = Column(String(32))
created = Column(DateTime, default = now) created = Column(DateTime, default = now)
last_modification = Column(Integer) last_modification = Column(Integer)
@ -225,8 +226,8 @@ class Track(Base):
'artist': self.album.artist.name, 'artist': self.album.artist.name,
'track': self.number, 'track': self.number,
'size': os.path.getsize(self.path), 'size': os.path.getsize(self.path),
'contentType': 'audio/mpeg', # we only know how to read mp3s 'contentType': self.content_type,
'suffix': 'mp3', # same as above 'suffix': self.suffix(),
'duration': self.duration, 'duration': self.duration,
'bitRate': self.bitrate, 'bitRate': self.bitrate,
'path': self.path[len(self.root_folder.path) + 1:], 'path': self.path[len(self.root_folder.path) + 1:],
@ -267,6 +268,9 @@ class Track(Base):
ret = '%02i:%s' % (self.duration / 3600, ret) ret = '%02i:%s' % (self.duration / 3600, ret)
return ret return ret
def suffix(self):
return os.path.splitext(self.path)[1][1:].lower()
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()

View File

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
import os, os.path import os, os.path
import time import time, mimetypes
import eyed3.id3, eyed3.mp3 import mutagen
import db import db
class Scanner: class Scanner:
@ -22,20 +22,12 @@ class Scanner:
def scan(self, folder): def scan(self, folder):
for root, subfolders, files in os.walk(folder.path): for root, subfolders, files in os.walk(folder.path):
for f in files: for f in files:
if f.endswith('.mp3'): self.__scan_file(os.path.join(root, f), folder)
self.__scan_file(os.path.join(root, f), folder)
folder.last_scan = int(time.time()) folder.last_scan = int(time.time())
def prune(self, folder): def prune(self, folder):
for track in [ t for t in self.__tracks if t.root_folder.id == folder.id and not os.path.exists(t.path) ]: for track in [ t for t in self.__tracks if t.root_folder.id == folder.id and not os.path.exists(t.path) ]:
track.album.tracks.remove(track) self.__remove_track(track)
track.folder.tracks.remove(track)
# As we don't have a track -> playlists relationship, SQLAlchemy doesn't know it has to remove tracks
# from playlists as well, so let's help it
for playlist in db.Playlist.query.filter(db.Playlist.tracks.contains(track)):
playlist.tracks.remove(track)
self.__session.delete(track)
self.__deleted_tracks += 1
for album in [ album for artist in self.__artists for album in artist.albums if len(album.tracks) == 0 ]: for album in [ album for artist in self.__artists for album in artist.albums if len(album.tracks) == 0 ]:
album.artist.albums.remove(album) album.artist.albums.remove(album)
@ -55,27 +47,33 @@ class Scanner:
def __scan_file(self, path, folder): def __scan_file(self, path, folder):
tr = filter(lambda t: t.path == path, self.__tracks) tr = filter(lambda t: t.path == path, self.__tracks)
if not tr: if tr:
tr = db.Track(path = path, root_folder = folder, folder = self.__find_folder(path, folder))
self.__tracks.append(tr)
self.__added_tracks += 1
else:
tr = tr[0] tr = tr[0]
if not os.path.getmtime(path) > tr.last_modification: if not os.path.getmtime(path) > tr.last_modification:
return return
tag = eyed3.id3.Tag() tag = self.__try_load_tag(path)
tag.parse(path) if not tag:
info = eyed3.mp3.Mp3AudioFile(path).info self.__remove_track(tr)
return
else:
tag = self.__try_load_tag(path)
if not tag:
return
tr.disc = tag.disc_num[0] or 1 tr = db.Track(path = path, root_folder = folder, folder = self.__find_folder(path, folder))
tr.number = tag.track_num[0] or 1 self.__tracks.append(tr)
tr.title = tag.title self.__added_tracks += 1
tr.year = tag.best_release_date.year if tag.best_release_date else None
tr.genre = tag.genre.name if tag.genre else None tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
tr.duration = info.time_secs tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
tr.album = self.__find_album(tag.artist, tag.album) tr.title = self.__try_read_tag(tag, 'title')
tr.bitrate = info.bit_rate[1] 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)
tr.album = self.__find_album(self.__try_read_tag(tag, 'artist'), self.__try_read_tag(tag, 'album'))
tr.bitrate = tag.info.bitrate / 1000
tr.content_type = mimetypes.guess_type(path, False)[0] or tag.mime[0]
tr.last_modification = os.path.getmtime(path) tr.last_modification = os.path.getmtime(path)
def __find_album(self, artist, album): def __find_album(self, artist, album):
@ -121,6 +119,33 @@ class Scanner:
return folder return folder
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)
# As we don't have a track -> playlists relationship, SQLAlchemy doesn't know it has to remove tracks
# from playlists as well, so let's help it
for playlist in db.Playlist.query.filter(db.Playlist.tracks.contains(track)):
playlist.tracks.remove(track)
self.__session.delete(track)
self.__deleted_tracks += 1
def __cleanup_folder(self, folder): def __cleanup_folder(self, folder):
for f in folder.children: for f in folder.children:
self.__cleanup_folder(f) self.__cleanup_folder(f)