diff --git a/supysonic/__init__.py b/supysonic/__init__.py index e69de29..b10767a 100644 --- a/supysonic/__init__.py +++ b/supysonic/__init__.py @@ -0,0 +1,25 @@ +# coding: utf-8 + +# This file is part of Supysonic. +# +# Supysonic is a Python implementation of the Subsonic server API. +# Copyright (C) 2013-2017 Alban 'spl0k' FĂ©ron +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import mimetypes + +def get_mime(ext): + return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream' + diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index 994b8b1..bf9fcb8 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -75,6 +75,22 @@ def authorize(): request.username = username request.user = user +@app.before_request +def get_client_prefs(): + if not request.path.startswith('/rest/'): + return + + client = request.values.get('c') + prefs = store.get(ClientPrefs, (request.user.id, client)) + if not prefs: + prefs = ClientPrefs() + prefs.user_id = request.user.id + prefs.client_name = client + store.add(prefs) + store.commit() + + request.prefs = prefs + @app.after_request def set_headers(response): if not request.path.startswith('/rest/'): diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index 2fc8dcf..9bc94ba 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -63,7 +63,7 @@ def rand_songs(): return request.formatter({ 'randomSongs': { - 'song': [ t.as_subsonic_child(request.user) for t in tracks ] + 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in tracks ] } }) @@ -171,7 +171,7 @@ def now_playing(): return request.formatter({ 'nowPlaying': { 'entry': [ dict( - u.last_play.as_subsonic_child(request.user).items() + + u.last_play.as_subsonic_child(request.user, request.prefs).items() + { 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items() ) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ] } @@ -185,7 +185,7 @@ def get_starred(): 'starred': { 'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in folders.find(Folder.parent_id == StarredFolder.starred_id, Track.folder_id == Folder.id).config(distinct = True) ], 'album': [ sf.starred.as_subsonic_child(request.user) for sf in folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ], - 'song': [ st.starred.as_subsonic_child(request.user) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] + 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] } }) @@ -195,7 +195,7 @@ def get_starred_id3(): 'starred2': { 'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in store.find(StarredArtist, StarredArtist.user_id == User.id, User.name == request.username) ], 'album': [ sa.starred.as_subsonic_album(request.user) for sa in store.find(StarredAlbum, StarredAlbum.user_id == User.id, User.name == request.username) ], - 'song': [ st.starred.as_subsonic_child(request.user) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] + 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] } }) diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index e05ec32..aaa431c 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -97,7 +97,7 @@ def list_indexes(): 'name': a.name } for a in sorted(v, key = lambda a: a.name.lower()) ] } for k, v in sorted(indexes.iteritems()) ], - 'child': [ c.as_subsonic_child(request.user) for c in sorted(childs, key = lambda t: t.sort_key()) ] + 'child': [ c.as_subsonic_child(request.user, request.prefs) for c in sorted(childs, key = lambda t: t.sort_key()) ] } }) @@ -110,7 +110,7 @@ def show_directory(): directory = { 'id': str(res.id), 'name': res.name, - 'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + 'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] } if not res.root: directory['parent'] = str(res.parent_id) @@ -162,7 +162,7 @@ def album_info(): return res info = res.as_subsonic_album(request.user) - info['song'] = [ t.as_subsonic_child(request.user) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + info['song'] = [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] return request.formatter({ 'album': info }) @@ -172,7 +172,7 @@ def track_info(): if not status: return res - return request.formatter({ 'song': res.as_subsonic_child(request.user) }) + return request.formatter({ 'song': res.as_subsonic_child(request.user, request.prefs) }) @app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ]) def list_videos(): diff --git a/supysonic/api/media.py b/supysonic/api/media.py index d231ad6..f308377 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -45,7 +45,7 @@ def stream_media(): if not status: return res - maxBitRate, format, timeOffset, size, estimateContentLength, client = map(request.values.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength', 'c' ]) + maxBitRate, format, timeOffset, size, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ]) if format: format = format.lower() @@ -54,18 +54,10 @@ def stream_media(): dst_bitrate = res.bitrate dst_mimetype = res.content_type - if client: - prefs = store.get(ClientPrefs, (request.user.id, client)) - if not prefs: - prefs = ClientPrefs() - prefs.user_id = request.user.id - prefs.client_name = client - store.add(prefs) - - if prefs.format: - dst_suffix = prefs.format - if prefs.bitrate and prefs.bitrate < dst_bitrate: - dst_bitrate = prefs.bitrate + if request.prefs.format: + dst_suffix = request.prefs.format + if request.prefs.bitrate and request.prefs.bitrate < dst_bitrate: + dst_bitrate = request.prefs.bitrate if maxBitRate: try: @@ -87,7 +79,9 @@ def stream_media(): if not transcoder and (not decoder or not encoder): transcoder = config.get('transcoding', 'transcoder') if not transcoder: - return request.error_formatter(0, 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix)) + message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix) + app.logger.info(message) + return request.error_formatter(0, message) transcoder, decoder, encoder = map(lambda x: prepare_transcoding_cmdline(x, res.path, src_suffix, dst_suffix, dst_bitrate), [ transcoder, decoder, encoder ]) try: diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py index 535de8f..133a0e6 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -45,7 +45,7 @@ def show_playlist(): return res info = res.as_subsonic_playlist(request.user) - info['entry'] = [ t.as_subsonic_child(request.user) for t in res.tracks ] + info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.tracks ] return request.formatter({ 'playlist': info }) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) diff --git a/supysonic/api/search.py b/supysonic/api/search.py index e999b27..9bba11d 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -52,7 +52,7 @@ def old_search(): return request.formatter({ 'searchResult': { 'totalHits': folders.count() + tracks.count(), 'offset': offset, - 'match': [ r.as_subsonic_child(request.user) for r in res ] + 'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in res ] }}) else: return request.error_formatter(10, 'Missing search parameter') @@ -60,7 +60,7 @@ def old_search(): return request.formatter({ 'searchResult': { 'totalHits': query.count(), 'offset': offset, - 'match': [ r.as_subsonic_child(request.user) for r in query[offset : offset + count] ] + 'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in query[offset : offset + count] ] }}) @app.route('/rest/search2.view', methods = [ 'GET', 'POST' ]) @@ -89,7 +89,7 @@ def new_search(): return request.formatter({ 'searchResult2': { 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ], 'album': [ f.as_subsonic_child(request.user) for f in album_query ], - 'song': [ t.as_subsonic_child(request.user) for t in song_query ] + 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ] }}) @app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) @@ -117,6 +117,6 @@ def search_id3(): return request.formatter({ 'searchResult3': { 'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ], 'album': [ a.as_subsonic_album(request.user) for a in album_query ], - 'song': [ t.as_subsonic_child(request.user) for t in song_query ] + 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ] }}) diff --git a/supysonic/db.py b/supysonic/db.py index 0f45882..34f10e6 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -27,6 +27,8 @@ from storm.variables import Variable import uuid, datetime, time import os.path +from supysonic import get_mime + def now(): return datetime.datetime.now().replace(microsecond = 0) @@ -168,7 +170,7 @@ class Track(object): folder_id = UUID() folder = Reference(folder_id, Folder.id) - def as_subsonic_child(self, user): + def as_subsonic_child(self, user, prefs): info = { 'id': str(self.id), 'parent': str(self.folder_id), @@ -209,8 +211,9 @@ class Track(object): if avgRating: info['averageRating'] = avgRating - # transcodedContentType - # transcodedSuffix + if prefs and prefs.format and prefs.format != self.suffix(): + info['transcodedSuffix'] = prefs.format + info['transcodedContentType'] = get_mime(prefs.format) return info diff --git a/supysonic/scanner.py b/supysonic/scanner.py index 783ae31..3a3683a 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -19,20 +19,17 @@ # along with this program. If not, see . import os, os.path -import time, mimetypes +import time import mutagen from storm.expr import ComparableExpr, compile, Like from storm.exceptions import NotSupportedError -from supysonic import config +from supysonic import config, get_mime from supysonic.db import Folder, Artist, Album, Track, User, PlaylistTrack from supysonic.db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack from supysonic.db import RatingFolder, RatingTrack -def get_mime(ext): - return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream' - # Hacking in support for a concatenation expression class Concat(ComparableExpr): __slots__ = ("left", "right", "db")