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