diff --git a/.gitignore b/.gitignore index c9b568f..ce11010 100755 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.pyc *.swp +*~ +build/ +dist/ +MANIFEST diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bb3ec5f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md diff --git a/cli.py b/bin/supysonic-cli similarity index 97% rename from cli.py rename to bin/supysonic-cli index d9f8078..829156f 100755 --- a/cli.py +++ b/bin/supysonic-cli @@ -20,12 +20,12 @@ # along with this program. If not, see . import sys, cmd, argparse, getpass, time -import config +from supysonic import config -from db import get_store, Folder, User -from managers.folder import FolderManager -from managers.user import UserManager -from scanner import Scanner +from supysonic.db import get_store, Folder, User +from supysonic.managers.folder import FolderManager +from supysonic.managers.user import UserManager +from supysonic.scanner import Scanner class CLIParser(argparse.ArgumentParser): def error(self, message): diff --git a/debug_server.py b/server.py similarity index 91% rename from debug_server.py rename to server.py index 955c7e3..0ca3efd 100755 --- a/debug_server.py +++ b/server.py @@ -19,12 +19,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import sys +from supysonic.web import create_application +app = create_application() if __name__ == '__main__': - from web import create_application - - app = create_application() if app: + import sys app.run(host = sys.argv[1] if len(sys.argv) > 1 else None, debug = True) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e8d85ea --- /dev/null +++ b/setup.py @@ -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']} + ) diff --git a/supysonic.cgi b/supysonic.cgi index f7e7549..9cadacc 100755 --- a/supysonic.cgi +++ b/supysonic.cgi @@ -20,7 +20,7 @@ # along with this program. If not, see . from wsgiref.handlers import CGIHandler -from web import create_application +from supysonic.web import create_application app = create_application() if app: diff --git a/supysonic.fcgi b/supysonic.fcgi index 25540ad..81fd551 100755 --- a/supysonic.fcgi +++ b/supysonic.fcgi @@ -20,9 +20,9 @@ # along with this program. If not, see . from flup.server.fcgi import WSGIServer -from web import create_application +from supysonic.web import create_application app = create_application() if app: - WSGIServer(app, bindaddress = '/path/to/fcgi.sock').run + WSGIServer(app, bindAddress = '/path/to/fcgi.sock').run() diff --git a/supysonic.wsgi b/supysonic.wsgi index 5437096..f19151a 100755 --- a/supysonic.wsgi +++ b/supysonic.wsgi @@ -18,9 +18,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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() diff --git a/supysonic/__init__.py b/supysonic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/__init__.py b/supysonic/api/__init__.py similarity index 98% rename from api/__init__.py rename to supysonic/api/__init__.py index 7cdd795..15b1084 100644 --- a/api/__init__.py +++ b/supysonic/api/__init__.py @@ -23,8 +23,8 @@ from xml.etree import ElementTree import simplejson import uuid -from web import app, store -from managers.user import UserManager +from supysonic.web import app, store +from supysonic.managers.user import UserManager @app.before_request def set_formatter(): diff --git a/api/albums_songs.py b/supysonic/api/albums_songs.py similarity index 97% rename from api/albums_songs.py rename to supysonic/api/albums_songs.py index 06caef2..fc80bbd 100644 --- a/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -24,9 +24,9 @@ from storm.info import ClassAlias import random import uuid -from web import app, store -from db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User -from db import now +from supysonic.web import app, store +from supysonic.db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User +from supysonic.db import now @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) def rand_songs(): diff --git a/api/annotation.py b/supysonic/api/annotation.py similarity index 94% rename from api/annotation.py rename to supysonic/api/annotation.py index 4bea837..1aeb751 100644 --- a/api/annotation.py +++ b/supysonic/api/annotation.py @@ -21,12 +21,12 @@ import time import uuid from flask import request -from web import app, store +from supysonic.web import app, store from . import get_entity -from lastfm import LastFm -from db import Track, Album, Artist, Folder -from db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder -from db import RatingTrack, RatingFolder +from supysonic.lastfm import LastFm +from supysonic.db import Track, Album, Artist, Folder +from supysonic.db import StarredTrack, StarredAlbum, StarredArtist, StarredFolder +from supysonic.db import RatingTrack, RatingFolder @app.route('/rest/star.view', methods = [ 'GET', 'POST' ]) def star(): diff --git a/api/browse.py b/supysonic/api/browse.py similarity index 97% rename from api/browse.py rename to supysonic/api/browse.py index bb579c4..610803a 100644 --- a/api/browse.py +++ b/supysonic/api/browse.py @@ -19,10 +19,10 @@ # along with this program. If not, see . from flask import request -from web import app, store -from db import Folder, Artist, Album, Track +from supysonic.web import app, store +from supysonic.db import Folder, Artist, Album, Track from . import get_entity -import uuid, time, string +import uuid, string @app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) def list_folders(): diff --git a/api/chat.py b/supysonic/api/chat.py similarity index 95% rename from api/chat.py rename to supysonic/api/chat.py index 0e67b61..38184ed 100644 --- a/api/chat.py +++ b/supysonic/api/chat.py @@ -19,8 +19,8 @@ # along with this program. If not, see . from flask import request -from web import app, store -from db import ChatMessage +from supysonic.web import app, store +from supysonic.db import ChatMessage @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) def get_chat(): diff --git a/api/media.py b/supysonic/api/media.py similarity index 98% rename from api/media.py rename to supysonic/api/media.py index 19e451a..4554824 100644 --- a/api/media.py +++ b/supysonic/api/media.py @@ -26,9 +26,9 @@ import subprocess import codecs from xml.etree import ElementTree -import config, scanner -from web import app, store -from db import Track, Album, Artist, Folder, User, ClientPrefs, now +from supysonic import config, scanner +from supysonic.web import app, store +from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now from . import get_entity def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): diff --git a/api/playlists.py b/supysonic/api/playlists.py similarity index 98% rename from api/playlists.py rename to supysonic/api/playlists.py index 62abdd8..7e1ed45 100644 --- a/api/playlists.py +++ b/supysonic/api/playlists.py @@ -21,8 +21,8 @@ from flask import request from storm.expr import Or import uuid -from web import app, store -from db import Playlist, User, Track +from supysonic.web import app, store +from supysonic.db import Playlist, User, Track from . import get_entity @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) diff --git a/api/search.py b/supysonic/api/search.py similarity index 98% rename from api/search.py rename to supysonic/api/search.py index 4a5932d..77be0eb 100644 --- a/api/search.py +++ b/supysonic/api/search.py @@ -20,8 +20,8 @@ from flask import request from storm.info import ClassAlias -from web import app, store -from db import Folder, Track, Artist, Album +from supysonic.web import app, store +from supysonic.db import Folder, Track, Artist, Album @app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) def old_search(): diff --git a/api/system.py b/supysonic/api/system.py similarity index 97% rename from api/system.py rename to supysonic/api/system.py index 7d83e69..3f8871c 100644 --- a/api/system.py +++ b/supysonic/api/system.py @@ -19,7 +19,7 @@ # along with this program. If not, see . from flask import request -from web import app +from supysonic.web import app @app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) def ping(): diff --git a/api/user.py b/supysonic/api/user.py similarity index 96% rename from api/user.py rename to supysonic/api/user.py index 39c296a..ddb9ecb 100644 --- a/api/user.py +++ b/supysonic/api/user.py @@ -19,9 +19,9 @@ # along with this program. If not, see . from flask import request -from web import app, store -from db import User -from managers.user import UserManager +from supysonic.web import app, store +from supysonic.db import User +from supysonic.managers.user import UserManager @app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) def user_info(): diff --git a/config.py b/supysonic/config.py similarity index 100% rename from config.py rename to supysonic/config.py diff --git a/db.py b/supysonic/db.py similarity index 100% rename from db.py rename to supysonic/db.py diff --git a/frontend/__init__.py b/supysonic/frontend/__init__.py similarity index 92% rename from frontend/__init__.py rename to supysonic/frontend/__init__.py index cd6aef0..73e526a 100644 --- a/frontend/__init__.py +++ b/supysonic/frontend/__init__.py @@ -19,9 +19,9 @@ # along with this program. If not, see . from flask import session -from web import app, store -from db import Artist, Album, Track -from managers.user import UserManager +from supysonic.web import app, store +from supysonic.db import Artist, Album, Track +from supysonic.managers.user import UserManager app.add_template_filter(str) diff --git a/frontend/folder.py b/supysonic/frontend/folder.py similarity index 93% rename from frontend/folder.py rename to supysonic/frontend/folder.py index 2b990ad..898ddf8 100644 --- a/frontend/folder.py +++ b/supysonic/frontend/folder.py @@ -22,11 +22,11 @@ from flask import request, flash, render_template, redirect, url_for, session import os.path import uuid -from web import app, store -from db import Folder -from scanner import Scanner -from managers.user import UserManager -from managers.folder import FolderManager +from supysonic.web import app, store +from supysonic.db import Folder +from supysonic.scanner import Scanner +from supysonic.managers.user import UserManager +from supysonic.managers.folder import FolderManager @app.before_request def check_admin(): diff --git a/frontend/playlist.py b/supysonic/frontend/playlist.py similarity index 97% rename from frontend/playlist.py rename to supysonic/frontend/playlist.py index f53ce73..1e6d950 100644 --- a/frontend/playlist.py +++ b/supysonic/frontend/playlist.py @@ -20,8 +20,8 @@ from flask import request, session, flash, render_template, redirect, url_for import uuid -from web import app, store -from db import Playlist +from supysonic.web import app, store +from supysonic.db import Playlist @app.route('/playlist') def playlist_index(): diff --git a/frontend/user.py b/supysonic/frontend/user.py similarity index 97% rename from frontend/user.py rename to supysonic/frontend/user.py index ee5fa08..dfb2213 100644 --- a/frontend/user.py +++ b/supysonic/frontend/user.py @@ -20,12 +20,12 @@ from flask import request, session, flash, render_template, redirect, url_for, make_response -from web import app, store -from managers.user import UserManager -from db import User, ClientPrefs +from supysonic.web import app, store +from supysonic.managers.user import UserManager +from supysonic.db import User, ClientPrefs import uuid, csv -import config -from lastfm import LastFm +from supysonic import config +from supysonic.lastfm import LastFm @app.before_request def check_admin(): diff --git a/lastfm.py b/supysonic/lastfm.py similarity index 74% rename from lastfm.py rename to supysonic/lastfm.py index c49d67d..3a5f484 100644 --- a/lastfm.py +++ b/supysonic/lastfm.py @@ -19,7 +19,7 @@ # along with this program. If not, see . import requests, hashlib -import config +import supysonic.config class LastFm: def __init__(self, user, logger): @@ -34,7 +34,9 @@ class LastFm: return False, 'No API key set' 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']) else: self.__user.lastfm_session = res['session']['key'] @@ -49,14 +51,14 @@ class LastFm: if not self.__enabled: 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) def scrobble(self, track, ts): if not self.__enabled: 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) def __api_request(self, write, **kwargs): @@ -81,15 +83,20 @@ class LastFm: kwargs['api_sig'] = sig kwargs['format'] = 'json' - if write: - r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs) - else: - r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs) + try: + if write: + r = requests.post('http://ws.audioscrobbler.com/2.0/', data = 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: - if r.json['error'] in (9, '9'): + json = r.json() + if 'error' in json: + if json['error'] in (9, '9'): 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 diff --git a/managers/__init__.py b/supysonic/managers/__init__.py similarity index 100% rename from managers/__init__.py rename to supysonic/managers/__init__.py diff --git a/managers/folder.py b/supysonic/managers/folder.py similarity index 98% rename from managers/folder.py rename to supysonic/managers/folder.py index fa934c9..9ac31b3 100644 --- a/managers/folder.py +++ b/supysonic/managers/folder.py @@ -19,7 +19,7 @@ # along with this program. If not, see . import os.path, uuid -from db import Folder, Artist, Album, Track +from supysonic.db import Folder, Artist, Album, Track class FolderManager: SUCCESS = 0 diff --git a/managers/user.py b/supysonic/managers/user.py similarity index 99% rename from managers/user.py rename to supysonic/managers/user.py index f1ac41b..362eb75 100644 --- a/managers/user.py +++ b/supysonic/managers/user.py @@ -21,7 +21,7 @@ import string, random, hashlib import uuid -from db import User +from supysonic.db import User class UserManager: SUCCESS = 0 diff --git a/scanner.py b/supysonic/scanner.py similarity index 97% rename from scanner.py rename to supysonic/scanner.py index 5ea59c4..c1e2bd2 100644 --- a/scanner.py +++ b/supysonic/scanner.py @@ -21,8 +21,8 @@ import os, os.path import time, mimetypes import mutagen -import config -from db import Folder, Artist, Album, Track +from supysonic import config +from supysonic.db import Folder, Artist, Album, Track def get_mime(ext): 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() add = False if tr: - if not os.path.getmtime(path) > tr.last_modification: + if not int(os.path.getmtime(path)) > tr.last_modification: return tag = self.__try_load_tag(path) diff --git a/templates/addfolder.html b/supysonic/templates/addfolder.html similarity index 100% rename from templates/addfolder.html rename to supysonic/templates/addfolder.html diff --git a/templates/adduser.html b/supysonic/templates/adduser.html similarity index 100% rename from templates/adduser.html rename to supysonic/templates/adduser.html diff --git a/templates/change_mail.html b/supysonic/templates/change_mail.html similarity index 100% rename from templates/change_mail.html rename to supysonic/templates/change_mail.html diff --git a/templates/change_pass.html b/supysonic/templates/change_pass.html similarity index 100% rename from templates/change_pass.html rename to supysonic/templates/change_pass.html diff --git a/templates/folders.html b/supysonic/templates/folders.html similarity index 100% rename from templates/folders.html rename to supysonic/templates/folders.html diff --git a/templates/home.html b/supysonic/templates/home.html similarity index 100% rename from templates/home.html rename to supysonic/templates/home.html diff --git a/templates/importusers.html b/supysonic/templates/importusers.html similarity index 100% rename from templates/importusers.html rename to supysonic/templates/importusers.html diff --git a/templates/layout.html b/supysonic/templates/layout.html similarity index 100% rename from templates/layout.html rename to supysonic/templates/layout.html diff --git a/templates/login.html b/supysonic/templates/login.html similarity index 100% rename from templates/login.html rename to supysonic/templates/login.html diff --git a/templates/playlist.html b/supysonic/templates/playlist.html similarity index 100% rename from templates/playlist.html rename to supysonic/templates/playlist.html diff --git a/templates/playlists.html b/supysonic/templates/playlists.html similarity index 100% rename from templates/playlists.html rename to supysonic/templates/playlists.html diff --git a/templates/profile.html b/supysonic/templates/profile.html similarity index 100% rename from templates/profile.html rename to supysonic/templates/profile.html diff --git a/templates/users.html b/supysonic/templates/users.html similarity index 100% rename from templates/users.html rename to supysonic/templates/users.html diff --git a/web.py b/supysonic/web.py similarity index 93% rename from web.py rename to supysonic/web.py index 1be9527..167cca8 100644 --- a/web.py +++ b/supysonic/web.py @@ -22,8 +22,8 @@ import os.path from flask import Flask, g from werkzeug.local import LocalProxy -import config -from db import get_store +from supysonic import config +from supysonic.db import get_store def get_db_store(): store = getattr(g, 'store', None) @@ -60,8 +60,8 @@ def create_application(): handler.setLevel(logging.WARNING) app.logger.addHandler(handler) - import frontend - import api + from supysonic import frontend + from supysonic import api return app