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

Merge branch 'master' into storm

Conflicts:
	bin/supysonic-cli
	supysonic/api/__init__.py
	supysonic/api/albums_songs.py
	supysonic/api/annotation.py
	supysonic/api/browse.py
	supysonic/api/chat.py
	supysonic/api/media.py
	supysonic/api/playlists.py
	supysonic/api/search.py
	supysonic/api/user.py
	supysonic/db.py
	supysonic/frontend/__init__.py
	supysonic/frontend/folder.py
	supysonic/frontend/playlist.py
	supysonic/frontend/user.py
	supysonic/lastfm.py
	supysonic/managers/folder.py
	supysonic/managers/user.py
	supysonic/scanner.py
	supysonic/web.py
This commit is contained in:
spl0k 2014-06-14 17:55:52 +02:00
commit 2c41e1af64
44 changed files with 117 additions and 77 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
*.pyc *.pyc
*.swp *.swp
*~
build/
dist/
MANIFEST

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include README.md

View File

@ -20,12 +20,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys, cmd, argparse, getpass, time import sys, cmd, argparse, getpass, time
import config from supysonic import config
from db import get_store, Folder, User from supysonic.db import get_store, Folder, User
from managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from managers.user import UserManager from supysonic.managers.user import UserManager
from scanner import Scanner from supysonic.scanner import Scanner
class CLIParser(argparse.ArgumentParser): class CLIParser(argparse.ArgumentParser):
def error(self, message): def error(self, message):

View File

@ -19,12 +19,11 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys from supysonic.web import create_application
app = create_application()
if __name__ == '__main__': if __name__ == '__main__':
from web import create_application
app = create_application()
if app: if app:
import sys
app.run(host = sys.argv[1] if len(sys.argv) > 1 else None, debug = True) app.run(host = sys.argv[1] if len(sys.argv) > 1 else None, debug = True)

31
setup.py Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/python
# encoding: utf-8
from distutils.core import setup
setup(name='supysonic',
description='Python implementation of the Subsonic server API.',
keywords='subsonic music',
version='0.1',
url='https://github.com/spl0k/supysonic',
license='AGPLv3',
author='Alban Féron',
author_email='alban.feron@gmail.com',
long_description="""
Supysonic is a Python implementation of the Subsonic server API.
Current supported features are:
* browsing (by folders or tags)
* streaming of various audio file formats
* transcoding
* user or random playlists
* cover arts (cover.jpg files in the same folder as music files)
* starred tracks/albums and ratings
* Last.FM scrobbling
""",
packages=['supysonic', 'supysonic.api', 'supysonic.frontend',
'supysonic.managers'],
scripts=['bin/supysonic-cli'],
package_data={'supysonic': ['templates/*.html']}
)

View File

@ -20,7 +20,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from wsgiref.handlers import CGIHandler from wsgiref.handlers import CGIHandler
from web import create_application from supysonic.web import create_application
app = create_application() app = create_application()
if app: if app:

View File

@ -20,9 +20,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flup.server.fcgi import WSGIServer from flup.server.fcgi import WSGIServer
from web import create_application from supysonic.web import create_application
app = create_application() app = create_application()
if app: if app:
WSGIServer(app, bindaddress = '/path/to/fcgi.sock').run WSGIServer(app, bindAddress = '/path/to/fcgi.sock').run()

View File

@ -18,9 +18,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
sys.path.insert(0, '/path/to/the/supysonic/app')
from web import create_application from supysonic.web import create_application
application = create_application() application = create_application()

0
supysonic/__init__.py Normal file
View File

View File

@ -23,8 +23,8 @@ from xml.etree import ElementTree
import simplejson import simplejson
import uuid import uuid
from web import app, store from supysonic.web import app, store
from managers.user import UserManager from supysonic.managers.user import UserManager
@app.before_request @app.before_request
def set_formatter(): def set_formatter():

View File

@ -24,9 +24,9 @@ from storm.info import ClassAlias
import random import random
import uuid import uuid
from web import app, store from supysonic.web import app, store
from db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User from supysonic.db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User
from db import now from supysonic.db import now
@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
def rand_songs(): def rand_songs():

View File

@ -21,12 +21,12 @@
import time import time
import uuid import uuid
from flask import request from flask import request
from web import app, store from supysonic.web import app, store
from . import get_entity from . import get_entity
from lastfm import LastFm from supysonic.lastfm import LastFm
from db import Track, Album, Artist, Folder from supysonic.db import Track, Album, Artist, Folder
from db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder from supysonic.db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder
from db import RatingTrack, RatingFolder from supysonic.db import RatingTrack, RatingFolder
@app.route('/rest/star.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/star.view', methods = [ 'GET', 'POST' ])
def star(): def star():

View File

@ -19,10 +19,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from web import app, store from supysonic.web import app, store
from db import Folder, Artist, Album, Track from supysonic.db import Folder, Artist, Album, Track
from . import get_entity from . import get_entity
import uuid, time, string import uuid, string
@app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ])
def list_folders(): def list_folders():

View File

@ -19,8 +19,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from web import app, store from supysonic.web import app, store
from db import ChatMessage from supysonic.db import ChatMessage
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
def get_chat(): def get_chat():

View File

@ -26,9 +26,9 @@ import subprocess
import codecs import codecs
from xml.etree import ElementTree from xml.etree import ElementTree
import config, scanner from supysonic import config, scanner
from web import app, store from supysonic.web import app, store
from db import Track, Album, Artist, Folder, User, ClientPrefs, now from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import get_entity from . import get_entity
def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate):

View File

@ -21,8 +21,8 @@
from flask import request from flask import request
from storm.expr import Or from storm.expr import Or
import uuid import uuid
from web import app, store from supysonic.web import app, store
from db import Playlist, User, Track from supysonic.db import Playlist, User, Track
from . import get_entity from . import get_entity
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])

View File

@ -20,8 +20,8 @@
from flask import request from flask import request
from storm.info import ClassAlias from storm.info import ClassAlias
from web import app, store from supysonic.web import app, store
from db import Folder, Track, Artist, Album from supysonic.db import Folder, Track, Artist, Album
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
def old_search(): def old_search():

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from web import app from supysonic.web import app
@app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/ping.view', methods = [ 'GET', 'POST' ])
def ping(): def ping():

View File

@ -19,9 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import request from flask import request
from web import app, store from supysonic.web import app, store
from db import User from supysonic.db import User
from managers.user import UserManager from supysonic.managers.user import UserManager
@app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ])
def user_info(): def user_info():

View File

@ -19,9 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import session from flask import session
from web import app, store from supysonic.web import app, store
from db import Artist, Album, Track from supysonic.db import Artist, Album, Track
from managers.user import UserManager from supysonic.managers.user import UserManager
app.add_template_filter(str) app.add_template_filter(str)

View File

@ -22,11 +22,11 @@ from flask import request, flash, render_template, redirect, url_for, session
import os.path import os.path
import uuid import uuid
from web import app, store from supysonic.web import app, store
from db import Folder from supysonic.db import Folder
from scanner import Scanner from supysonic.scanner import Scanner
from managers.user import UserManager from supysonic.managers.user import UserManager
from managers.folder import FolderManager from supysonic.managers.folder import FolderManager
@app.before_request @app.before_request
def check_admin(): def check_admin():

View File

@ -20,8 +20,8 @@
from flask import request, session, flash, render_template, redirect, url_for from flask import request, session, flash, render_template, redirect, url_for
import uuid import uuid
from web import app, store from supysonic.web import app, store
from db import Playlist from supysonic.db import Playlist
@app.route('/playlist') @app.route('/playlist')
def playlist_index(): def playlist_index():

View File

@ -20,12 +20,12 @@
from flask import request, session, flash, render_template, redirect, url_for, make_response from flask import request, session, flash, render_template, redirect, url_for, make_response
from web import app, store from supysonic.web import app, store
from managers.user import UserManager from supysonic.managers.user import UserManager
from db import User, ClientPrefs from supysonic.db import User, ClientPrefs
import uuid, csv import uuid, csv
import config from supysonic import config
from lastfm import LastFm from supysonic.lastfm import LastFm
@app.before_request @app.before_request
def check_admin(): def check_admin():

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests, hashlib import requests, hashlib
import config import supysonic.config
class LastFm: class LastFm:
def __init__(self, user, logger): def __init__(self, user, logger):
@ -34,7 +34,9 @@ class LastFm:
return False, 'No API key set' return False, 'No API key set'
res = self.__api_request(False, method = 'auth.getSession', token = token) res = self.__api_request(False, method = 'auth.getSession', token = token)
if 'error' in res: if not res:
return False, 'Error connecting to LastFM'
elif 'error' in res:
return False, 'Error %i: %s' % (res['error'], res['message']) return False, 'Error %i: %s' % (res['error'], res['message'])
else: else:
self.__user.lastfm_session = res['session']['key'] self.__user.lastfm_session = res['session']['key']
@ -49,14 +51,14 @@ class LastFm:
if not self.__enabled: if not self.__enabled:
return return
res = self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name,
trackNumber = track.number, duration = track.duration) trackNumber = track.number, duration = track.duration)
def scrobble(self, track, ts): def scrobble(self, track, ts):
if not self.__enabled: if not self.__enabled:
return return
res = self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name,
timestamp = ts, trackNumber = track.number, duration = track.duration) timestamp = ts, trackNumber = track.number, duration = track.duration)
def __api_request(self, write, **kwargs): def __api_request(self, write, **kwargs):
@ -81,15 +83,20 @@ class LastFm:
kwargs['api_sig'] = sig kwargs['api_sig'] = sig
kwargs['format'] = 'json' kwargs['format'] = 'json'
if write: try:
r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs) if write:
else: r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs)
r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs) else:
r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs)
except requests.exceptions.RequestException, e:
self.__logger.warn('Error while connecting to LastFM: ' + str(e))
return None
if 'error' in r.json: json = r.json()
if r.json['error'] in (9, '9'): if 'error' in json:
if json['error'] in (9, '9'):
self.__user.lastfm_status = False self.__user.lastfm_status = False
self.__logger.warn('LastFM error %i: %s' % (r.json['error'], r.json['message'])) self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message']))
return r.json return json

View File

@ -19,7 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path, uuid import os.path, uuid
from db import Folder, Artist, Album, Track from supysonic.db import Folder, Artist, Album, Track
class FolderManager: class FolderManager:
SUCCESS = 0 SUCCESS = 0

View File

@ -21,7 +21,7 @@
import string, random, hashlib import string, random, hashlib
import uuid import uuid
from db import User from supysonic.db import User
class UserManager: class UserManager:
SUCCESS = 0 SUCCESS = 0

View File

@ -21,8 +21,8 @@
import os, os.path import os, os.path
import time, mimetypes import time, mimetypes
import mutagen import mutagen
import config from supysonic import config
from db import Folder, Artist, Album, Track from supysonic.db import Folder, Artist, Album, Track
def get_mime(ext): def get_mime(ext):
return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream' return mimetypes.guess_type('dummy.' + ext, False)[0] or config.get('mimetypes', ext) or 'application/octet-stream'
@ -90,7 +90,7 @@ class Scanner:
tr = self.__store.find(Track, Track.path == path).one() tr = self.__store.find(Track, Track.path == path).one()
add = False add = False
if tr: if tr:
if not os.path.getmtime(path) > tr.last_modification: if not int(os.path.getmtime(path)) > tr.last_modification:
return return
tag = self.__try_load_tag(path) tag = self.__try_load_tag(path)

View File

@ -22,8 +22,8 @@ import os.path
from flask import Flask, g from flask import Flask, g
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
import config from supysonic import config
from db import get_store from supysonic.db import get_store
def get_db_store(): def get_db_store():
store = getattr(g, 'store', None) store = getattr(g, 'store', None)
@ -60,8 +60,8 @@ def create_application():
handler.setLevel(logging.WARNING) handler.setLevel(logging.WARNING)
app.logger.addHandler(handler) app.logger.addHandler(handler)
import frontend from supysonic import frontend
import api from supysonic import api
return app return app