mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06:17 +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:
commit
2c41e1af64
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,2 +1,6 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
|
*~
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
MANIFEST
|
||||||
|
1
MANIFEST.in
Normal file
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
include README.md
|
@ -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):
|
@ -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
31
setup.py
Executable 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']}
|
||||||
|
)
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
0
supysonic/__init__.py
Normal 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():
|
@ -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():
|
@ -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():
|
@ -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():
|
@ -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():
|
@ -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):
|
@ -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' ])
|
@ -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():
|
@ -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():
|
@ -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():
|
@ -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)
|
||||||
|
|
@ -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():
|
@ -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():
|
@ -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():
|
@ -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'
|
||||||
|
|
||||||
|
try:
|
||||||
if write:
|
if write:
|
||||||
r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs)
|
r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs)
|
||||||
else:
|
else:
|
||||||
r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs)
|
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
|
||||||
|
|
@ -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
|
@ -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
|
@ -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)
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user