mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06:17 +00:00
Switch to mutagen -> allow serving of more audio formats
This commit is contained in:
parent
2256f639ba
commit
22e2d351d8
@ -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
|
||||||
|
|
||||||
|
22
api/media.py
22
api/media.py
@ -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
8
db.py
@ -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()
|
||||||
|
|
||||||
|
81
scanner.py
81
scanner.py
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user