fookebox-0.6.1/ 0000755 0001750 0001750 00000000000 11743153607 014123 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/docs/ 0000755 0001750 0001750 00000000000 11743153607 015053 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/docs/examples/ 0000755 0001750 0001750 00000000000 11743153607 016671 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/docs/examples/apache/ 0000755 0001750 0001750 00000000000 11743153607 020112 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/docs/examples/apache/apache-fcgi.conf 0000644 0001750 0001750 00000000473 11743150153 023105 0 ustar stefan stefan 0000000 0000000
Alias /fookebox /etc/fookebox/fookebox.fcgi
RedirectMatch ^/fookebox$ /fookebox/
Options +ExecCGI
Order deny,allow
Allow from all
Order deny,allow
Deny from all
Allow from localhost
fookebox-0.6.1/docs/examples/apache/apache-wsgi.conf 0000644 0001750 0001750 00000000500 11743153532 023141 0 ustar stefan stefan 0000000 0000000
WSGIScriptAlias /fookebox /etc/fookebox/fookebox.wsgi
WSGIDaemonProcess fookebox
WSGIProcessGroup fookebox
Order deny,allow
Allow from all
Order deny,allow
Deny from all
Allow from localhost
fookebox-0.6.1/docs/examples/apache/fookebox.fcgi 0000755 0001750 0001750 00000000516 11743150150 022553 0 ustar stefan stefan 0000000 0000000 #!/usr/bin/python
#
# requires the python flup module
BASE_PATH = '/usr/share/fookebox'
APP_CONFIG = '/etc/fookebox/config.ini'
import sys
if BASE_PATH not in sys.path:
sys.path.append(BASE_PATH)
from paste.deploy import loadapp
app = loadapp('config:' + APP_CONFIG)
from flup.server.fcgi import WSGIServer
WSGIServer(app).run()
fookebox-0.6.1/docs/examples/apache/fookebox.wsgi 0000644 0001750 0001750 00000000341 11743150152 022607 0 ustar stefan stefan 0000000 0000000 BASE_PATH = '/usr/share/fookebox'
APP_CONFIG = '/etc/fookebox/config.ini'
import sys
if BASE_PATH not in sys.path:
sys.path.append(BASE_PATH)
from paste.deploy import loadapp
application = loadapp('config:' + APP_CONFIG)
fookebox-0.6.1/docs/changes.txt 0000644 0001750 0001750 00000010301 11743150145 017211 0 ustar stefan stefan 0000000 0000000 #summary ChangeLog for fookebox
#labels Featured,Phase-Deploy
== 0.6.1 (17 Apr 2012) ==
* Use python's json module instead of simplejson
* Use mpd's "setvol" command instead of the deprecated "volume". Thanks to
Jan Hruban for reporting this and sending a patch.
* Skipping the current song would lead to interruptions in the audio stream
if it was the last entry in the playlist. Fixed this by auto-queuing before
skipping tracks. Thanks to Artem Savkov for noticing this and providing a
patch.
* Pulsate the time display when playback is paused.
* Added an example configuration file for using fookebox through fcgi/flup
== 0.6.0 (30 Apr 2011) ==
* Cleaned up the configuration options regarding cover art
* Show a proper error message when the connection to MPD fails
* Properly report errors during template rendering
* Show artists in search result pages again
* Added an option to hide the credits
* Tracks with artist featuring others no longer trigger compilation mode
* Genre/artist links can now be opened in a new browser tab
* (Hopefully) fixed an encoding issue that would cause errors in search
* Reduced the number of HTTP requests when the current song has no cover art
* Generally reduced the number of requests to mpd
* Fixed an issue that could cause errors when modwsgi reloaded the application
* Rewrote/reorganized most of the javascript code
* Added special CSS for mobile devices
* Fixed compatibility with python-mpd 0.3.0
* Updated the included libraries to scriptaculous 1.9.0 / prototype 1.7
* Updated example config files for apache
* Added a proper favicon
* Some API changes
== 0.5.2 (02 Dec 2010) ==
* Use mpd's "consume" command if available (versions >= 0.15)
* Added support for in-directory cover art
* Cover art can now be cached
* Auto-queuing can now be limited to a specific genre
* Albums are now properly sorted on the search result page
* The current track's time display should be updated much more smoothly now
== 0.5.1 (07 Jul 2010) ==
* Catch some more exceptions and properly close all connections if they occur
* Added classifiers to setup.py
* fookebox' version information is now read from the package meta data
== 0.5.0 (01 Jul 2010) ==
* Re-implemented fookebox in Python (using Pylons)
* Added support for rhythmbox' covert art file naming scheme
* Added German translation
== 0.4.3 (21 Jun 2010) ==
* Fixed a possible race-condition where a song would get played twice in a row
* Synchronized javascript with upcoming 0.5 release
* Artist, genre names and cover paths are now base64 encoded
== 0.4.2 (24 May 2010) ==
* Database rebuild was broken, fixed
* Fixed typo in README
== 0.4.1 (23 Mar 2010) ==
* Thanks to Gavin Cameron for his contributions
* Can now pick the auto-queued songs from a predefined playlist
* Some internal code cleanup
* Changed license to GPLv3
* Set the default time zone to UTC to get PHP to shut up (Gavin)
* Replaced calls to split (deprecated) with explode (Gavin)
* Added support for cover art for compilations (Gavin again, also: see README)
* Added the ability to auto queue songs before the playlist is empty
== 0.4.0 (4 Feb 2010) ==
* Fixed queuing of full albums
* Added support for cover art
* Minor CSS changes
* Some internal code cleanup
* We now need at least PHP 5.1.0
* Updated smarty to version 2.6.26
* Updated prototype to version 1.6.1
* Updated scriptaculous to version 1.8.3
== 0.3.0 (11 May 2009) ==
* Dropped php4 compatibility
* Albums in search results are now automatically expanded
* Added support for forward/back buttons
* Fixed filename/any search
* Updated smarty
* Code cleanup
== 0.2.0 (1 Dec 2008) ==
* Added the ability to auto-queue songs when the playlist gets empty
* Fixed some issues when running fookebox in a sub directory
* Switched to using prototype for JSON data transport
* Updated smarty, scriptaculous and prototype to their latest versions
== 0.1.1 (12 Jun 2008) ==
* Made the right part scrollable without scrolling the whole window
* Made sure all clients are informed on playlist updates
* Updated smarty to version 2.6.19
* Updated the JavaScript JSON library
* Minor documentation updates
== 0.1.0 (11 Jun 2008) ==
* Initial public release
fookebox-0.6.1/docs/config.txt 0000644 0001750 0001750 00000015610 11743150144 017055 0 ustar stefan stefan 0000000 0000000 #summary ReadMe for fookebox
#labels Featured,Phase-Deploy
fookebox is a jukebox-style web-frontend to mpd.
It offers the following jukebox features:
* Browse your music library by artist or genre
* Add songs to the play list (obviously)
* Limit the queue size (see note below)
* Add whole albums to the play list (optional)
* Remove songs from the play list (optional)
* Search for artists/albums/titles/files (optional)
* Control mpd (optional)
In addition to that, fookebox has a second frontend intended for a projector
showing the currently playing song as well as the first queue entry. Also, in
case you are organizing an event where you only want to use the jukebox between
other events (live bands, DJs) fookebox can be told so and display that
information on the secondary frontend (details: see below).
== What you need ==
In order to run fookebox you will need:
* Python (at least version 2.6)
* MPD (at least version 0.15)
* Some web server (eg. apache) with WSGI support
* Pylons (http://pylonshq.com/)
== How to set it up ==
=== web server ===
You can use the provided apache-wsgi.conf and fookebox.wsgi to run fookebox
through apache's mod_wsgi or apache-fcgi.conf and fookebox.fcgi if you prefer
mod_fcgi with python's flup module.
=== fookebox ===
Have a look at README.txt for general information on how to install and
configure fookebox. For details on the config options, see below.
Also, make sure that your webserver can write to the data/templates directory.
== Detailed setup ==
You can tweak the following fookebox-specific settings:
* mpd_host: The host name of the machine running mpd (eg. localhost)
* mpd_port: The port mpd is running on (6600 by default)
* mpd_pass: The password required to access mpd
* site_name: A name for your site (eg. 'my home')
* max_queue_length: The maximum queue length (see below for details)
* auto_queue: Whether to play random songs when the queue gets empty (see below)
* auto_queue_genre: Genre to pick auto-queued songs from
* auto_queue_playlist: Playlist to play when idle (see below)
* auto_queue_random: (see below)
* show_search_tab: Enable/disable 'search' tab (eg. for mouse-only jukeboxes)
* enable_controls: Enable/disable mpd control (disable on public jukeboxes)
* enable_song_removal: Whether to allow the user to remove songs in the queue
* enable_queue_album: Enables/disables the ability to queue whole albums
* find_over_search: Whether to use mpd's 'find' or 'search' (see below)
* music_base_path: Path that contains your music (see below)
* hide_credits: Set to true if you want to hide the credits in the client view
* show_cover_art: Whether to show cover art (enabled by default)
* cache_cover_art: Whether to cache cover art (disabled by default)
* album_cover_path: Path that contains album cover art (see below)
== Secondary frontend ==
If you don't care about projectors and stuff, just ignore this section. It's
purely optional.
If you point your browser to yourfookeboxurl/program you will see fookebox'
secondary front end which is intended to be projected to a wall / shown on
a screen without user interaction. It will not only inform users about the
currently playing and the next song but in case you're using fookebox at some
kind of party, this front end will allow you to inform your users about any
other events that might be coming up (eg. a live band or a DJ).
To enter your event's schedule, point your web browser to
yourfookeboxurl/schedule.
== Additional notes ==
=== Queue length ===
It's important to note that while fookebox does try to enforce your mpd queue
length value, people who click fast enough might be able to bypass that check
so make sure to set your mpd's max_playlist_length accordingly (note that mpd's
count includes the song which is being played so if you want 3 queue positions,
set max_playlist_length to 4).
=== Cover art ===
Fookebox can display cover art for your music. In order to do that, set
show_cover_art to true.
If enabled, the following mechanisms are used to find cover art:
* If set, search album_cover_path for files named
- "Artist-Album.jpg", replace all of '\/:<>?*|' with underscores (_)
- Retry as compilation (see below)
- "Artist - Album.jpg", replace slashes (/) with underscores (_)
- Retry as compilation (see below)
* If music_base_path is set, look for JPEG files in the directory where
the album's first track is located (see below)
In case no cover art is found for an album, fookebox tries to be smart and
assumes this is a compilation. The 'compliations_name' ('Various Artists' by
default) option determines what artist name is used for compilations. If you
want to provide a cover for a compilation called 'My Best Music', the
corresponding file would be called 'Various Artists-My Best Music.jpg'.
If you would like fookebox to look for cover art in the same directory where
you store you music files, set 'music_base_path' accordingly. In order for this
to work, you will need to have the music/cover files on the same machine where
fookebox is running (if that's not the case, consider mounting the music
directory remotely, eg. using NFS). fookebox will use any JPEG file in that
directory, giving priority to files named cover.jpg, album.jpg or front.jpg
(.jpeg is acceptable too). File names are case-insensitive, so whether you
name your cover art front.jpg or ALBum.jpeg doesn't matter.
On systems where disk access is expensive and generally on slower hardware you
might want to enable 'cache_cover_art'. This causes fookebox to remember the
result of its search for an album's cover art for 5 minutes. The only way to
clear the cache (i.e. make fookebox forget the cached information) is to
restart fookebox (as in, restart apache or whatever web server you are using).
Note that your covers need to be JPEG files and they will get scaled to
a width of 100 pixels.
=== Find or search? ===
When an artist/genre/album has been clicked, you can decide whether we tell mpd
to 'find' that item or to 'search' for it. The difference is that 'find' only
returns exact matches while 'search' also matches substrings (eg. if you click
on 'Air', 'find' will only return songs by 'Air' while 'search' will also
include 'Air feat. Joe Random' and 'Fair play').
=== Auto-queuing ===
If you enable 'auto_queue', fookebox will automatically pick a completely
random song from your collection whenever there is nothing else to play (by
setting 'auto_queue_time_left' you can define how many seconds before the
current song ends this happens - useful for large music collections on slow
machines where auto-queuing takes a few seconds). If you would like to have a
predefined set of songs to be played instead, use 'auto_queue_playlist' to
specify an mpd playlist. If you want the song selection to be random but
limited to a set of songs, define a playlist as explained before and set
'auto_queue_random' to true.
You can also 'auto_queue_genre' in order to limit auto-playing to a specific
genre.
fookebox-0.6.1/docs/i18n.txt 0000644 0001750 0001750 00000000675 11556665236 016413 0 ustar stefan stefan 0000000 0000000 This document is mostly for myself, in order to remember the i18n-related
commands.
Anyway.
To update the translations, do:
python setup.py extract_messages (needs python-pybabel to be installed)
python setup.py update_catalog -l de
python setup.py compile_catalog
To add a new translation, do:
python setup.py init_catalog -l es
See http://wiki.pylonshq.com/display/pylonsdocs/Internationalization+and+Localization for more details.
fookebox-0.6.1/fookebox/ 0000755 0001750 0001750 00000000000 11743153607 015737 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/config/ 0000755 0001750 0001750 00000000000 11743153607 017204 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/config/__init__.py 0000644 0001750 0001750 00000000000 11525403055 021274 0 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/config/deployment.ini_tmpl 0000644 0001750 0001750 00000005307 11525403055 023117 0 ustar stefan stefan 0000000 0000000 #
# fookebox - Pylons configuration
#
# The %(here)s variable will be replaced with the parent directory of this file
#
[DEFAULT]
debug = false
email_to = you@yourdomain.com
smtp_server = localhost
error_email_from = paste@localhost
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 5000
[app:main]
use = egg:fookebox
#lang = de
full_stack = true
static_files = true
cache_dir = %(here)s/data
sqlalchemy.url = sqlite:///%(cache_dir)s/fookebox.sqlite
beaker.session.key = fookebox
beaker.session.secret = ${app_instance_secret}
app_instance_uuid = ${app_instance_uuid}
#site_name = fookebox
#mpd_host = localhost
#mpd_port = 6600
#mpd_pass = password
# NOTE: See the README for details on this
#max_queue_length = 4
# automatically queue a random song when the playlist gets empty
#auto_queue = true
# genre to use for auto_queue
#auto_queue_genre = Comedy
# do auto-queuing before the playlist is empty [seconds] (0 to disable)
#auto_queue_time_left = 1
# if you want the 'random' song to come from a pre-defined (mpd) playlist
# you can set the playlist's name here
#auto_queue_playlist = idle
# pick a random song from the idle playlist (see README)
#auto_queue_random = false
# show the full-text search tab
#show_search_tab = true
# enable mpd controls
#enable_controls = true
# allow users to remove songs from the queue
#enable_song_removal = true
# allow users to queue a full album with one click
#enable_queue_album = true
# be anal about artist/album names (see README)
#find_over_search = false
# directory with cover art
#album_cover_path = /home/stefan/.cache/rhythmbox/covers/
# directory where the music can be found (used for in-directory cover art)
#music_base_path = /var/lib/mpd/music/
# what to use as artist name when looking for compilations' cover art?
#compliations_name = Various Artists
# if you would like cover art to be cached, set this to true
#cache_cover_art = false
# If you'd like to fine-tune the individual locations of the cache data dirs
# for the Cache data, or the Session saves, un-comment the desired settings
# here:
#beaker.cache.data_dir = %(here)s/data/cache
#beaker.session.data_dir = %(here)s/data/sessions
# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
# Debug mode will enable the interactive debugging tool, allowing ANYONE to
# execute malicious code after an exception is raised.
set debug = false
# Logging configuration
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s
fookebox-0.6.1/fookebox/config/environment.py 0000644 0001750 0001750 00000005514 11556607055 022132 0 ustar stefan stefan 0000000 0000000 """Pylons environment configuration"""
import os
from mako.lookup import TemplateLookup
from pylons.configuration import PylonsConfig
from pylons.error import handle_mako_error
from paste.deploy.converters import asbool
from sqlalchemy import engine_from_config
from sqlalchemy.pool import NullPool
from fookebox.model import init_model
import fookebox.lib.app_globals as app_globals
import fookebox.lib.helpers
from fookebox.config.routing import make_map
def load_environment(global_conf, app_conf):
"""Configure the Pylons environment via the ``pylons.config``
object
"""
config = PylonsConfig()
# Pylons paths
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
paths = dict(root=root,
controllers=os.path.join(root, 'controllers'),
static_files=os.path.join(root, 'public'),
templates=[os.path.join(root, 'templates')])
# Initialize config with the basic options
config.init_app(global_conf, app_conf, package='fookebox', paths=paths)
config['routes.map'] = make_map(config)
config['pylons.app_globals'] = app_globals.Globals(config)
config['pylons.h'] = fookebox.lib.helpers
# Setup cache object as early as possible
import pylons
pylons.cache._push_object(config['pylons.app_globals'].cache)
# Create the database engine
engine = engine_from_config(config, 'sqlalchemy.', poolclass=NullPool)
init_model(engine)
# Create the Mako TemplateLookup, with the default auto-escaping
config['pylons.app_globals'].mako_lookup = TemplateLookup(
directories=paths['templates'],
error_handler=handle_mako_error,
module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
input_encoding='utf-8', default_filters=['escape'],
imports=['from webhelpers.html import escape'])
import pkg_resources
config['version'] = pkg_resources.get_distribution("fookebox").version
# CONFIGURATION OPTIONS HERE (note: all config options will override
# any Pylons config options)
default_strings = {
'site_name': 'fookebox',
'mpd_host': 'localhost',
'compliations_name': 'Various Artists',
}
default_ints = {
'mpd_port': 6600,
'max_queue_length': 4,
'auto_queue_time_left': 1,
}
default_bools = {
'auto_queue': True,
'auto_queue_random': False,
'show_search_tab': True,
'enable_controls': True,
'enable_song_removal': True,
'enable_queue_album': True,
'find_over_search': False,
'cache_cover_art': False,
'hide_credits': False,
'show_cover_art': True,
}
for key in default_strings:
if key not in config:
config[key] = default_strings[key]
for key in default_ints:
if key in config and config[key].isdigit():
config[key] = int(config[key])
else:
config[key] = default_ints[key]
for key in default_bools:
if key in config:
try:
config[key] = asbool(config[key])
except:
config[key] = default_bools[key]
else:
config[key] = default_bools[key]
return config
fookebox-0.6.1/fookebox/config/middleware.py 0000644 0001750 0001750 00000004562 11525403055 021673 0 ustar stefan stefan 0000000 0000000 """Pylons middleware initialization"""
from beaker.middleware import SessionMiddleware
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
from pylons.middleware import ErrorHandler, StatusCodeRedirect
from pylons.wsgiapp import PylonsApp
from routes.middleware import RoutesMiddleware
from fookebox.config.environment import load_environment
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
"""Create a Pylons WSGI application and return it
``global_conf``
The inherited configuration for this application. Normally from
the [DEFAULT] section of the Paste ini file.
``full_stack``
Whether this application provides a full WSGI stack (by default,
meaning it handles its own exceptions and errors). Disable
full_stack when this application is "managed" by another WSGI
middleware.
``static_files``
Whether this application serves its own static files; disable
when another web server is responsible for serving them.
``app_conf``
The application's local configuration. Normally specified in
the [app:] section of the Paste ini file (where
defaults to main).
"""
# Configure the Pylons environment
config = load_environment(global_conf, app_conf)
# The Pylons WSGI app
app = PylonsApp(config=config)
# Routing/Session Middleware
app = RoutesMiddleware(app, config['routes.map'])
app = SessionMiddleware(app, config)
# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
if asbool(full_stack):
# Handle Python exceptions
app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
# Display error documents for 401, 403, 404 status codes (and
# 500 when debug is disabled)
if asbool(config['debug']):
app = StatusCodeRedirect(app, [400, 404, 409, 500])
else:
app = StatusCodeRedirect(app, [400, 401, 403, 404, 409, 500])
# Establish the Registry for this application
app = RegistryManager(app)
if asbool(static_files):
# Serve static files
static_app = StaticURLParser(config['pylons.paths']['static_files'])
app = Cascade([static_app, app])
app.config = config
return app
fookebox-0.6.1/fookebox/config/routing.py 0000644 0001750 0001750 00000003245 11556607056 021255 0 ustar stefan stefan 0000000 0000000 """Routes configuration
The more specific and detailed routes should be defined first so they
may take precedent over the more generic routes. For more information
refer to the routes manual at http://routes.groovie.org/docs/
"""
from routes import Mapper
def make_map(config):
"""Create, configure and return the routes Mapper"""
map = Mapper(directory=config['pylons.paths']['controllers'],
always_scan=config['debug'])
map.minimization = False
map.explicit = False
# The ErrorController route (handles 404/500 error pages); it should
# likely stay at the top, ensuring it can always be resolved
map.connect('/error/{action}', controller='error')
map.connect('/error/{action}/{id}', controller='error')
# CUSTOM ROUTES HERE
map.connect('/{controller}/{action}')
map.connect('/{controller}/{action}/{id}')
map.connect('/program', controller='program', action='index')
map.connect('/schedule/{action}', controller='program')
map.connect('/schedule', controller='program', action='edit')
map.connect('/mobile', controller='jukebox', action='mobile')
map.connect('/', controller='jukebox', action='index')
map.connect('/{action}', controller='jukebox')
map.connect('/findcover', controller='jukebox', action='findcover')
map.connect('/cover/{artist}/{album}', controller='jukebox', action='cover')
map.connect('/genre/{genreBase64}', controller='jukebox', action='genre')
map.connect('/genre/', controller='jukebox', action='genre')
map.connect('/artist/{artistBase64}', controller='jukebox', action='artist')
map.connect('/artist/', controller='jukebox', action='artist')
return map
fookebox-0.6.1/fookebox/controllers/ 0000755 0001750 0001750 00000000000 11743153607 020305 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/controllers/__init__.py 0000644 0001750 0001750 00000000000 11525403055 022375 0 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/controllers/error.py 0000644 0001750 0001750 00000004721 11525403201 021776 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import cgi
import logging
from paste.urlparser import PkgResourcesParser
from pylons import request
from pylons.controllers.util import forward
from pylons.middleware import error_document_template
from webhelpers.html.builder import literal
from fookebox.lib.base import BaseController
log = logging.getLogger(__name__)
class ErrorController(BaseController):
"""Generates error documents as and when they are required.
The ErrorDocuments middleware forwards to ErrorController when error
related status codes are returned from the application.
This behaviour can be altered by changing the parameters to the
ErrorDocuments middleware in your config/middleware.py file.
"""
def document(self):
"""Render the error document"""
resp = request.environ.get('pylons.original_response')
content = literal(resp.body) or cgi.escape(request.GET.get(
'message', ''))
accept = request.headers.get('accept')
error = request.environ.get('pylons.controller.exception')
if accept == 'application/json' and error:
content = error.detail
elif accept == 'application/json':
content = "ERROR"
page = error_document_template % \
dict(prefix=request.environ.get('SCRIPT_NAME', ''),
code=cgi.escape(request.GET.get('code',
str(resp.status_int))), message=content)
return page
def img(self, id):
"""Serve Pylons' stock images"""
return self._serve_file('/'.join(['media/img', id]))
def style(self, id):
"""Serve Pylons' stock stylesheets"""
return self._serve_file('/'.join(['media/style', id]))
def _serve_file(self, path):
"""Call Paste's FileApp (a WSGI application) to serve the file
at the specified path
"""
request.environ['PATH_INFO'] = '/%s' % path
return forward(PkgResourcesParser('pylons', 'pylons'))
fookebox-0.6.1/fookebox/controllers/jukebox.py 0000644 0001750 0001750 00000020734 11743150161 022324 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
import json
import base64
import socket
import logging
from pylons import config, cache, request, response
from pylons.decorators import jsonify, rest
from pylons.controllers.util import abort
from pylons.i18n.translation import _
from fookebox.lib.base import BaseController, render
from fookebox.model.jukebox import Jukebox, QueueFull
from fookebox.model.mpdconn import Album
from fookebox.model.albumart import AlbumArt
logging.basicConfig(level=logging.WARNING)
log = logging.getLogger(__name__)
class JukeboxController(BaseController):
def __render(self, template, extra_vars):
try:
return render(template, extra_vars = extra_vars)
except IOError:
exctype, value = sys.exc_info()[:2]
abort(500, value)
def __search(self, where, what, forceSearch = False):
log.debug("SEARCH: '%s' in '%s'" % (what, where))
jukebox = Jukebox()
tracks = jukebox.search(where, what, forceSearch)
jukebox.close()
log.debug("SEARCH: found %d track(s)" % len(tracks))
return {'meta': {'what': what }, 'tracks': tracks}
@rest.restrict('GET')
def index(self):
try:
jukebox = Jukebox()
except socket.error:
log.error("Error on /index")
return self.__render('/error.tpl', extra_vars={
'error': 'Connection to MPD failed'})
except:
log.error("Error on /index")
exctype, value = sys.exc_info()[:2]
return self.__render('/error.tpl', extra_vars={
'error': value})
artists = jukebox.getArtists()
genres = jukebox.getGenres()
jukebox.close()
user_agent = request.environ.get('HTTP_USER_AGENT')
return self.__render('/client.tpl', extra_vars={
'genres': genres,
'artists': artists,
'config': config,
'mobile': 'mobile' in user_agent.lower(),
})
@rest.restrict('GET')
@jsonify
def status(self):
jukebox = Jukebox()
try:
queueLength = jukebox.getQueueLength()
enabled = jukebox.isEnabled()
timeLeft = jukebox.timeLeft()
except:
log.error("Could not read status")
jukebox.close()
exctype, value = sys.exc_info()[:2]
abort(500, value)
if (config.get('auto_queue') and queueLength == 0 and enabled
and timeLeft <= config.get('auto_queue_time_left')):
try:
jukebox.autoQueue()
except:
log.error("Auto-queue failed")
jukebox.close()
raise
try:
track = jukebox.getCurrentSong()
except:
log.error('Failed to get the current song')
abort(500, _('Failed to get the current song'))
finally:
jukebox.close()
data = {
'queueLength': queueLength,
'jukebox': enabled
}
if track:
songPos = int(track.timePassed)
songTime = track.time
total = "%02d:%02d" % (songTime / 60, songTime % 60)
position = "%02d:%02d" % (songPos / 60, songPos % 60)
album = Album(track.artist, track.album)
album.add(track)
data['artist'] = track.artist
data['track'] = track.title
data['album'] = track.album
data['has_cover'] = album.hasCover()
data['cover_uri'] = album.getCoverURI()
data['timePassed'] = position
data['timeTotal'] = total
data['playing'] = jukebox.isPlaying()
return data
@rest.restrict('POST')
def enqueue(self):
try:
data = json.load(request.environ['wsgi.input'])
except ValueError:
log.error('ENQUEUE: Could not parse JSON data')
abort(400, _('Malformed JSON data'))
files = data.get('files')
if not (files and len(files) > 0):
log.error('ENQUEUE: No files specified')
abort(400, _('No files specified'))
jukebox = Jukebox()
for file in files:
if not file:
log.error('ENQUEUE: Skipping empty file')
abort(400, _('Missing file name'))
try:
jukebox.queue(file.encode('utf8'))
except QueueFull:
jukebox.close()
log.error('ENQUEUE: Full, aborting')
abort(409, _('The queue is full'))
jukebox.close()
abort(204) # no content
@rest.dispatch_on(POST='enqueue')
@rest.restrict('GET')
@jsonify
def queue(self):
jukebox = Jukebox()
items = jukebox.getPlaylist()
jukebox.close()
return {'queue': items[1:]}
@rest.restrict('GET')
@jsonify
def genre(self, genreBase64=''):
try:
genre = genreBase64.decode('base64')
except:
log.error("GENRE: Failed to decode base64 data: %s" %
genreBase64)
abort(400, _('Malformed request'))
return self.__search('genre', genre)
@rest.restrict('GET')
@jsonify
def artist(self, artistBase64=''):
try:
artist = artistBase64.decode('base64')
except:
log.error("ARTIST: Failed to decode base64 data: %s" %
artistBase64)
abort(400, _('Malformed request'))
return self.__search('artist', artist)
@rest.restrict('POST')
@jsonify
def search(self):
try:
data = json.load(request.environ['wsgi.input'])
except ValueError:
log.error('SEARCH: Could not parse JSON data')
abort(400, _('Malformed JSON data'))
what = data.get('what')
where = data.get('where')
if not where:
log.error("SEARCH: Incomplete JSON data")
abort(400, _('Malformed request'))
forceSearch = 'forceSearch' in data and data['forceSearch']
return self.__search(where, what.encode('utf8'), forceSearch)
@rest.restrict('POST')
def remove(self):
if not config.get('enable_song_removal'):
log.error("REMOVE: Disabled")
abort(403, _('Song removal disabled'))
try:
data = json.load(request.environ['wsgi.input'])
except ValueError:
log.error('REMOVE: Could not parse JSON data')
abort(400, _('Malformed JSON data'))
id = data.get('id')
if not id:
log.error('REMOVE: No id specified in JSON data')
abort(400, _('Malformed request'))
jukebox = Jukebox()
jukebox.remove(id)
jukebox.close()
abort(204) # no content
@rest.restrict('POST')
def control(self):
if not config.get('enable_controls'):
log.error('CONTROL: Disabled')
abort(403, _('Controls disabled'))
try:
data = json.load(request.environ['wsgi.input'])
except ValueError:
log.error('CONTROL: Could not parse JSON data')
abort(400, _('Malformed JSON data'))
action = data.get('action')
if not action:
log.error('CONTROL: No action specified in JSON data')
abort(400, _('Malformed request'))
log.debug('CONTROL: Action=%s' % action)
jukebox = Jukebox()
commands = {
'play': jukebox.play,
'pause': jukebox.pause,
'prev': jukebox.previous,
'next': jukebox.next,
'voldown': jukebox.volumeDown,
'volup': jukebox.volumeUp,
'rebuild': jukebox.refreshDB,
}
if action not in commands:
log.error('CONTROL: Invalid command')
jukebox.close()
abort(400, _('Invalid command'))
try:
commands[action]()
except:
log.error('Command %s failed' % action)
abort(500, _('Command failed'))
finally:
jukebox.close()
abort(204) # no content
@rest.restrict('POST')
@jsonify
def findcover(self):
try:
data = json.load(request.environ['wsgi.input'])
except ValueError:
log.error('FINDCOVER: Could not parse JSON data')
abort(400, _('Malformed JSON data'))
artist = data.get('artist')
album = data.get('album')
album = Album(artist, album)
if album.hasCover():
return {'uri': album.getCoverURI()}
abort(404, 'No cover')
@rest.restrict('GET')
def cover(self, artist, album):
try:
artist = base64.urlsafe_b64decode(artist.encode('utf8'))
album = base64.urlsafe_b64decode(album.encode('utf8'))
except:
log.error("COVER: Failed to decode base64 data")
abort(400, _('Malformed base64 encoding'))
album = Album(artist.decode('utf8'), album.decode('utf8'))
art = AlbumArt(album)
path = art.get()
if not path:
log.error("COVER: missing for %s/%s" % (artist,
album.name))
abort(404, _('No cover found for this album'))
file = open(path.encode('utf8'), 'r')
data = file.read()
file.close()
response.headers['content-type'] = 'image/jpeg'
return data
@rest.restrict('GET')
def disabled(self):
return self.__render('/disabled.tpl', extra_vars={
'config': config,
'base_url': request.url.replace('disabled', ''),
})
fookebox-0.6.1/fookebox/controllers/program.py 0000644 0001750 0001750 00000010357 11743150160 022323 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2012 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import json
from datetime import time, datetime
from pylons import request, response, config, app_globals as g
from pylons.controllers.util import abort
from pylons.decorators import jsonify, rest
from fookebox.lib.base import BaseController, render
from fookebox.model import meta
from fookebox.model.jukebox import Jukebox
from fookebox.model.mpdconn import Track
from fookebox.model.schedule import Event, EVENT_TYPE_JUKEBOX
log = logging.getLogger(__name__)
class ProgramController(BaseController):
@rest.restrict('GET')
def index(self):
jukebox = Jukebox()
artists = jukebox.getArtists()
genres = jukebox.getGenres()
jukebox.close()
return render('/program.tpl')
@rest.restrict('GET')
@jsonify
def status(self):
jukebox = Jukebox()
event = jukebox.getCurrentEvent()
next = jukebox.getNextEvent()
now = datetime.now()
if now.second % 2 > 0:
format = "%H %M"
else:
format = "%H:%M"
event = jukebox.getCurrentEvent()
currentEvent = {
'type': event.type,
'title': event.name
}
if event.type == EVENT_TYPE_JUKEBOX:
track = jukebox.getCurrentSong()
if (track.artist == Track.NO_ARTIST and
track.title == Track.NO_TITLE):
track.artist = ''
track.title = ''
currentEvent['tracks'] = [{
'artist': track.artist,
'title': track.title,
}]
playlist = jukebox.getPlaylist()
if len(playlist) > 1:
track = Track()
track.load(playlist[1])
currentEvent['tracks'].append({
'artist': track.artist,
'title': track.title,
})
events = {'current': currentEvent}
next = jukebox.getNextEvent()
jukebox.close()
if next:
events['next'] = {
'type': next.type,
'title': next.name,
'time': next.time.strftime("%H:%M")
}
return {
'events': events,
'time': now.strftime(format),
}
@rest.restrict('POST')
def _edit_post(self):
name = request.params['name']
type = int(request.params['type'])
hour = request.params['hour']
minute = request.params['minute']
dateTime = datetime.strptime("%s:%s" % (hour, minute),
"%H:%M")
if 'id' in request.params:
id = request.params['id']
Event.update(id, name, type, dateTime.time())
else:
Event.add(name, type, dateTime.time())
return render('/program-edit.tpl',
{
'events': Event.all(),
'current': Event.getCurrent()
})
@rest.dispatch_on(POST='_edit_post')
@rest.restrict('GET')
def edit(self):
#if request.method == 'POST':
# pass
event_q = meta.Session.query(Event)
return render('/program-edit.tpl',
{
'events': Event.all(),
'current': Event.getCurrent(),
'edit': int(request.params.get('edit', 0))
})
@rest.restrict('POST')
def current(self):
try:
post = json.load(request.environ['wsgi.input'])
except ValueError:
log.error("QUEUE: Could not parse JSON data")
abort(400, 'Malformed JSON data')
id = post.get('id')
g.eventID = int(id)
abort(204) # no content
@rest.restrict('POST')
def delete(self):
try:
post = json.load(request.environ['wsgi.input'])
except ValueError:
log.error("QUEUE: Could not parse JSON data")
abort(400, 'Malformed JSON data')
id = post.get('id')
Event.delete(int(id))
abort(204) # no content
@rest.restrict('POST')
def move(self):
try:
post = json.load(request.environ['wsgi.input'])
except ValueError:
log.error("QUEUE: Could not parse JSON data")
abort(400, 'Malformed JSON data')
id = post.get('id')
direction = post.get('direction')
if direction == 'up':
Event.up(id)
else:
Event.down(id)
abort(204) # no content
fookebox-0.6.1/fookebox/i18n/ 0000755 0001750 0001750 00000000000 11743153607 016516 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/i18n/de/ 0000755 0001750 0001750 00000000000 11743153607 017106 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/i18n/de/LC_MESSAGES/ 0000755 0001750 0001750 00000000000 11743153607 020673 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/i18n/de/LC_MESSAGES/fookebox.mo 0000644 0001750 0001750 00000003711 11556665241 023052 0 ustar stefan stefan 0000000 0000000 $ < \ ] d j n u } # 5 S f r x % ! * : D K ^ u
)
4 G M ] e l
(none) Album Any Artist Artists Command failed Controls disabled Error Failed to get the current song Filename Genres Invalid command Malformed JSON data Malformed base64 encoding Malformed request Missing file name No cover found for this album No files specified Now playing Queue Search Song removal disabled The queue is full Title Various Artists back coming up enjoy our live artist next now playing pause play rebuild database volume down volume up Project-Id-Version: fookebox 0.5.0
Report-Msgid-Bugs-To: stefan@ott.net
POT-Creation-Date: 2010-06-30 06:22+0200
PO-Revision-Date: 2011-04-30 03:40+0200
Last-Translator: Stefan Ott
Language-Team: de
Plural-Forms: nplurals=2; plural=(n != 1)
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Generated-By: Babel 0.9.4
(kein) Album Beliebig Künstler Künstler Fehlgeschlagen Steuerelemente wurden deaktiviert Fehler Aktueller Song konnte nicht geladen werden Dateiname Genres Ungültiger Befehl Fehlerhafte JSON-Daten Fehlerhaftes base64-Encoding Fehlerhafte Anfrage Dateiname fehlt Kein Albumcover gefunden Keine Dateien angegeben Aktueller Song Warteschlange Suche Das Entfernen von Songs wurde deaktiviert Die Queue ist voll Titel Various Artists zurück danach enjoy our live artist weiter zur zeit pause wiedergabe datenbank aktualisieren leiser lauter fookebox-0.6.1/fookebox/i18n/de/LC_MESSAGES/fookebox.po 0000644 0001750 0001750 00000007406 11556665242 023063 0 ustar stefan stefan 0000000 0000000 # German translations for fookebox.
# Copyright (C) 2010 Stefan Ott
# This file is distributed under the same license as the fookebox project.
# Stefan Ott , 2010.
#
msgid ""
msgstr ""
"Project-Id-Version: fookebox 0.5.0\n"
"Report-Msgid-Bugs-To: stefan@ott.net\n"
"POT-Creation-Date: 2010-06-30 06:22+0200\n"
"PO-Revision-Date: 2011-04-30 03:36+0200\n"
"Last-Translator: Stefan Ott \n"
"Language-Team: de \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.4\n"
#: fookebox/controllers/jukebox.py:111
msgid "Failed to get the current song"
msgstr "Aktueller Song konnte nicht geladen werden"
#: fookebox/controllers/jukebox.py:146 fookebox/controllers/jukebox.py:213
#: fookebox/controllers/jukebox.py:303
msgid "Malformed JSON data"
msgstr "Fehlerhafte JSON-Daten"
#: fookebox/controllers/jukebox.py:152
msgid "No files specified"
msgstr "Keine Dateien angegeben"
#: fookebox/controllers/jukebox.py:159
msgid "Missing file name"
msgstr "Dateiname fehlt"
#: fookebox/controllers/jukebox.py:166
msgid "The queue is full"
msgstr "Die Queue ist voll"
#: fookebox/controllers/jukebox.py:190 fookebox/controllers/jukebox.py:202
#: fookebox/controllers/jukebox.py:220 fookebox/controllers/jukebox.py:242
#: fookebox/controllers/jukebox.py:266
msgid "Malformed request"
msgstr "Fehlerhafte Anfrage"
#: fookebox/controllers/jukebox.py:230
msgid "Song removal disabled"
msgstr "Das Entfernen von Songs wurde deaktiviert"
#: fookebox/controllers/jukebox.py:254
msgid "Controls disabled"
msgstr "Steuerelemente wurden deaktiviert"
#: fookebox/controllers/jukebox.py:284
msgid "Invalid command"
msgstr "Ungültiger Befehl"
#: fookebox/controllers/jukebox.py:290
msgid "Command failed"
msgstr "Fehlgeschlagen"
#: fookebox/controllers/jukebox.py:323
msgid "Malformed base64 encoding"
msgstr "Fehlerhaftes base64-Encoding"
#: fookebox/controllers/jukebox.py:332
msgid "No cover found for this album"
msgstr "Kein Albumcover gefunden"
#: fookebox/model/mpdconn.py:93
msgid "Various Artists"
msgstr "Various Artists"
#: fookebox/templates/browse-menu.tpl:3
msgid "Artists"
msgstr "Künstler"
#: fookebox/templates/browse-menu.tpl:4
msgid "Genres"
msgstr "Genres"
#: fookebox/templates/browse-menu.tpl:6
msgid "Search"
msgstr "Suche"
#: fookebox/templates/browse-menu.tpl:33 fookebox/templates/browse-menu.tpl:48
msgid "(none)"
msgstr "(kein)"
#: fookebox/templates/browse-menu.tpl:56
msgid "Artist"
msgstr "Künstler"
#: fookebox/templates/browse-menu.tpl:57
msgid "Album"
msgstr "Album"
#: fookebox/templates/browse-menu.tpl:58
msgid "Title"
msgstr "Titel"
#: fookebox/templates/browse-menu.tpl:59
msgid "Filename"
msgstr "Dateiname"
#: fookebox/templates/browse-menu.tpl:60
msgid "Any"
msgstr "Beliebig"
#: fookebox/templates/disabled.tpl:9
msgid "enjoy our live artist"
msgstr ""
#: fookebox/templates/error.tpl:8
msgid "Error"
msgstr "Fehler"
#: fookebox/templates/playing.tpl:2
msgid "Now playing"
msgstr "Aktueller Song"
#: fookebox/templates/playing.tpl:9
msgid "back"
msgstr "zurück"
#: fookebox/templates/playing.tpl:10
msgid "pause"
msgstr "pause"
#: fookebox/templates/playing.tpl:11
msgid "play"
msgstr "wiedergabe"
#: fookebox/templates/playing.tpl:12
msgid "next"
msgstr "weiter"
#: fookebox/templates/playing.tpl:13
msgid "volume down"
msgstr "leiser"
#: fookebox/templates/playing.tpl:14
msgid "volume up"
msgstr "lauter"
#: fookebox/templates/playing.tpl:15
msgid "rebuild database"
msgstr "datenbank aktualisieren"
#: fookebox/templates/playlist.tpl:2
msgid "Queue"
msgstr "Warteschlange"
#: fookebox/templates/program.tpl:8
msgid "now playing"
msgstr "zur zeit"
#: fookebox/templates/program.tpl:17
msgid "coming up"
msgstr "danach"
fookebox-0.6.1/fookebox/i18n/fookebox.pot 0000644 0001750 0001750 00000006400 11556665243 021064 0 ustar stefan stefan 0000000 0000000 # Translations template for fookebox.
# Copyright (C) 2011 ORGANIZATION
# This file is distributed under the same license as the fookebox project.
# FIRST AUTHOR , 2011.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: fookebox 0.6.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2011-04-30 03:36+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.4\n"
#: fookebox/controllers/jukebox.py:111
msgid "Failed to get the current song"
msgstr ""
#: fookebox/controllers/jukebox.py:146 fookebox/controllers/jukebox.py:213
#: fookebox/controllers/jukebox.py:303
msgid "Malformed JSON data"
msgstr ""
#: fookebox/controllers/jukebox.py:152
msgid "No files specified"
msgstr ""
#: fookebox/controllers/jukebox.py:159
msgid "Missing file name"
msgstr ""
#: fookebox/controllers/jukebox.py:166
msgid "The queue is full"
msgstr ""
#: fookebox/controllers/jukebox.py:190 fookebox/controllers/jukebox.py:202
#: fookebox/controllers/jukebox.py:220 fookebox/controllers/jukebox.py:242
#: fookebox/controllers/jukebox.py:266
msgid "Malformed request"
msgstr ""
#: fookebox/controllers/jukebox.py:230
msgid "Song removal disabled"
msgstr ""
#: fookebox/controllers/jukebox.py:254
msgid "Controls disabled"
msgstr ""
#: fookebox/controllers/jukebox.py:284
msgid "Invalid command"
msgstr ""
#: fookebox/controllers/jukebox.py:290
msgid "Command failed"
msgstr ""
#: fookebox/controllers/jukebox.py:323
msgid "Malformed base64 encoding"
msgstr ""
#: fookebox/controllers/jukebox.py:332
msgid "No cover found for this album"
msgstr ""
#: fookebox/model/mpdconn.py:93
msgid "Various Artists"
msgstr ""
#: fookebox/templates/browse-menu.tpl:3
msgid "Artists"
msgstr ""
#: fookebox/templates/browse-menu.tpl:4
msgid "Genres"
msgstr ""
#: fookebox/templates/browse-menu.tpl:6
msgid "Search"
msgstr ""
#: fookebox/templates/browse-menu.tpl:33 fookebox/templates/browse-menu.tpl:48
msgid "(none)"
msgstr ""
#: fookebox/templates/browse-menu.tpl:56
msgid "Artist"
msgstr ""
#: fookebox/templates/browse-menu.tpl:57
msgid "Album"
msgstr ""
#: fookebox/templates/browse-menu.tpl:58
msgid "Title"
msgstr ""
#: fookebox/templates/browse-menu.tpl:59
msgid "Filename"
msgstr ""
#: fookebox/templates/browse-menu.tpl:60
msgid "Any"
msgstr ""
#: fookebox/templates/disabled.tpl:9
msgid "enjoy our live artist"
msgstr ""
#: fookebox/templates/error.tpl:8
msgid "Error"
msgstr ""
#: fookebox/templates/playing.tpl:2
msgid "Now playing"
msgstr ""
#: fookebox/templates/playing.tpl:9
msgid "back"
msgstr ""
#: fookebox/templates/playing.tpl:10
msgid "pause"
msgstr ""
#: fookebox/templates/playing.tpl:11
msgid "play"
msgstr ""
#: fookebox/templates/playing.tpl:12
msgid "next"
msgstr ""
#: fookebox/templates/playing.tpl:13
msgid "volume down"
msgstr ""
#: fookebox/templates/playing.tpl:14
msgid "volume up"
msgstr ""
#: fookebox/templates/playing.tpl:15
msgid "rebuild database"
msgstr ""
#: fookebox/templates/playlist.tpl:2
msgid "Queue"
msgstr ""
#: fookebox/templates/program.tpl:8
msgid "now playing"
msgstr ""
#: fookebox/templates/program.tpl:17
msgid "coming up"
msgstr ""
fookebox-0.6.1/fookebox/lib/ 0000755 0001750 0001750 00000000000 11743153607 016505 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/lib/__init__.py 0000644 0001750 0001750 00000000000 11525403055 020575 0 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/lib/app_globals.py 0000644 0001750 0001750 00000001031 11525403171 021325 0 ustar stefan stefan 0000000 0000000 """The application's Globals object"""
from beaker.cache import CacheManager
from beaker.util import parse_cache_config_options
class Globals(object):
"""Globals acts as a container for objects available throughout the
life of the application
"""
def __init__(self, config):
"""One instance of Globals is created during application
initialization and is available during requests via the
'app_globals' variable
"""
self.cache = CacheManager(**parse_cache_config_options(config))
self.eventID = None
self.mpd = None
fookebox-0.6.1/fookebox/lib/base.py 0000644 0001750 0001750 00000001136 11525403055 017763 0 ustar stefan stefan 0000000 0000000 """The base Controller API
Provides the BaseController class for subclassing.
"""
from pylons.controllers import WSGIController
from pylons.templating import render_mako as render
from fookebox.model import meta
class BaseController(WSGIController):
def __call__(self, environ, start_response):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
try:
return WSGIController.__call__(self, environ, start_response)
finally:
meta.Session.remove()
fookebox-0.6.1/fookebox/lib/helpers.py 0000644 0001750 0001750 00000000565 11525403055 020520 0 ustar stefan stefan 0000000 0000000 """Helper functions
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to templates as 'h'.
"""
# Import helpers as desired, or define your own, ie:
#from webhelpers.html.tags import checkbox, password
def event_type_name(event_type):
names = ['Jukebox', 'Live band', 'DJ']
return names[event_type]
fookebox-0.6.1/fookebox/lib/util.py 0000644 0001750 0001750 00000001765 11556665251 020052 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# copyright (c) 2007-2011 stefan ott. all rights reserved.
#
# this program is free software: you can redistribute it and/or modify
# it under the terms of the gnu 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 general public license for more details.
#
# you should have received a copy of the gnu general public license
# along with this program. if not, see .
import os
class FileSystem(object):
@staticmethod
def exists(path):
return os.path.exists(path.encode('utf8'))
@staticmethod
def isdir(path):
return os.path.isdir(path.encode('utf8'))
@staticmethod
def listdir(path):
return os.listdir(path.encode('utf8'))
fookebox-0.6.1/fookebox/model/ 0000755 0001750 0001750 00000000000 11743153607 017037 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/model/__init__.py 0000644 0001750 0001750 00000000611 11525403055 021137 0 ustar stefan stefan 0000000 0000000 import sqlalchemy as sa
from sqlalchemy import orm
from fookebox.model import meta, schedule
def init_model(engine):
"""Call me before using any of the tables or classes in the model."""
#sm = orm.sessionmaker(autoflush=True, autocommit=True, bind=engine)
sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)
meta.engine = engine
meta.Session = orm.scoped_session(sm)
fookebox-0.6.1/fookebox/model/albumart.py 0000644 0001750 0001750 00000006307 11556665250 021232 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import re
import os
import base64
import logging
from datetime import datetime
from threading import BoundedSemaphore
from pylons import config, cache
from fookebox.lib.util import FileSystem as fs
log = logging.getLogger(__name__)
class AlbumArt(object):
def __init__(self, album):
self.album = album
def _getRockboxPath(self, compilation=False):
pattern = re.compile('[\/:<>\?*|]')
album = pattern.sub('_', self.album.name)
if compilation:
artist = pattern.sub('_', config.get(
'compliations_name'))
else:
artist = pattern.sub('_', self.album.artist)
return "%s/%s-%s.jpg" % (config.get('album_cover_path'),
artist, album)
def _getRhythmboxPath(self, compilation=False):
album = self.album.name.replace('/', '-')
if compilation:
artist = config.get('compliations_name').replace(
'/', '-')
else:
artist = self.album.artist.replace('/', '-')
return "%s/%s - %s.jpg" % (config.get('album_cover_path'),
artist, album)
def _getInDirCover(self):
path_cache = cache.get_cache('album_path', type='memory')
key = self.album.key()
dirname = path_cache.get_value(key=key,
createfunc=self.album.getPath, expiretime=60)
if dirname == None:
return None
def best_image(x, y):
pattern = '(cover|album|front)'
if re.match(pattern, x, re.I):
return x
else:
return y
if not (fs.exists(dirname) and fs.isdir(dirname)):
return None
dir = fs.listdir(dirname)
dir = filter(lambda x: x.endswith(
('jpg', 'JPG', 'jpeg', 'JPEG')), dir)
if len(dir) < 1:
return None
bestmatch = reduce(best_image, dir)
return os.path.join(dirname, bestmatch)
def get(self):
if config.get('cache_cover_art'):
cover_path_cache = cache.get_cache('cover_path')
song = "%s - %s" % (str(self.album.artist),
str(self.album.name))
path = cover_path_cache.get_value(key=song,
createfunc=self._getCover, expiretime=300)
if path == None:
cover_path_cache.remove_value(song)
else:
path = self._getCover()
return path
def _getCover(self):
if not config.get('show_cover_art'):
return None
if config.get('album_cover_path'):
path = self._getRockboxPath()
if fs.exists(path):
return path
path = self._getRockboxPath(True)
if fs.exists(path):
return path
path = self._getRhythmboxPath()
if fs.exists(path):
return path
path = self._getRhythmboxPath(True)
if fs.exists(path):
return path
if config.get('music_base_path'):
return self._getInDirCover()
return None
fookebox-0.6.1/fookebox/model/jukebox.py 0000644 0001750 0001750 00000012067 11743150156 021062 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2012 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import re
import sys
import random
import logging
from pylons import config, app_globals as g
from mpdconn import *
from schedule import Event, EVENT_TYPE_JUKEBOX
log = logging.getLogger(__name__)
class QueueFull(Exception):
pass
class Jukebox(object):
client = None
lastAutoQueued = -1
def __init__(self, mpd=None):
self._connect(mpd)
def _connect(self, to=None):
if not to == None:
self.client = to
return
mpd = MPD.get()
self.client = mpd.getWorker()
def close(self):
self.client.release()
def timeLeft(self):
status = self.client.status()
if 'time' not in status:
return 0
(timePlayed, timeTotal) = status['time'].split(':')
timeLeft = int(timeTotal) - int(timePlayed)
return timeLeft
def queue(self, file):
log.info("Queued %s" % file)
if self.getQueueLength() >= config.get('max_queue_length'):
raise QueueFull()
self.client.add(file)
# Prevent (or reduce the probability of) a race-condition where
# the auto-queue functionality adds a new song *after* the last
# one stopped playing (which would re-play the previous song)
if not self.isPlaying() and len(self.getPlaylist()) > 1:
self.client.delete(0)
self.client.play()
def _autoQueueRandom(self):
genre = config.get('auto_queue_genre')
if genre:
songs = self.client.search('Genre', str(genre))
else:
songs = self.client.listall()
if len(songs) < 1:
return
file = []
while 'file' not in file:
# we might have to try several times in case we get
# a directory instead of a file
index = random.randrange(len(songs))
file = songs[index]
self.queue(file['file'])
def _autoQueuePlaylist(self, playlist):
self.client.load(playlist)
if len(playlist) < 1:
return
if config.get('auto_queue_random'):
self.client.shuffle()
playlist = self.client.playlist()
song = playlist[0]
else:
playlist = self.client.playlist()
index = (Jukebox.lastAutoQueued + 1) % len(playlist)
song = playlist[index]
Jukebox.lastAutoQueued += 1
self.client.clear()
self.queue(song)
log.debug(Jukebox.lastAutoQueued)
def autoQueue(self):
log.info("Auto-queuing")
lock = Lock()
if not lock.acquire():
return
playlist = config.get('auto_queue_playlist')
if playlist == None:
self._autoQueueRandom()
else:
self._autoQueuePlaylist(playlist)
lock.release()
def search(self, where, what, forceSearch = False):
if config.get('find_over_search') and not forceSearch:
return self.client.find(where, what)
return self.client.search(where, what)
def getPlaylist(self):
playlist = self.client.playlistinfo()
return playlist
def getGenres(self):
genres = sorted(self.client.list('genre'))
return [Genre(genre.decode('utf8')) for genre in genres]
def getArtists(self):
artists = sorted(self.client.list('artist'))
return [Artist(artist.decode('utf8')) for artist in artists]
def isPlaying(self):
status = self.client.status()
return status['state'] == 'play'
def getCurrentSong(self):
current = self.client.currentsong()
if current == None:
return None
status = self.client.status()
if 'time' in status:
time = status['time'].split(':')[0]
else:
time = 0
track = Track()
track.load(current)
track.timePassed = time
return track
def getQueueLength(self):
playlist = self.client.playlist()
return max(len(playlist) - 1, 0)
def getCurrentEvent(self):
event = Event.getCurrent()
event.jukebox = self
return event
def getNextEvent(self):
event = Event.getNext()
return event
def isEnabled(self):
event = self.getCurrentEvent()
return event.type == EVENT_TYPE_JUKEBOX
def remove(self, id):
log.info("Removing playlist item #%d" % id)
self.client.delete(id)
def play(self):
self.client.play()
def pause(self):
self.client.pause()
def previous(self):
self.client.previous()
def next(self):
# This is to prevent interruptions in the audio stream
# See http://code.google.com/p/fookebox/issues/detail?id=6
if self.getQueueLength() < 1 and config.get('auto_queue'):
self.autoQueue()
self.client.next()
def volumeDown(self):
status = self.client.status()
volume = int(status.get('volume', 0))
self.client.setvol(max(volume - 5, 0))
def volumeUp(self):
status = self.client.status()
volume = int(status.get('volume', 0))
self.client.setvol(min(volume + 5, 100))
def refreshDB(self):
self.client.update()
fookebox-0.6.1/fookebox/model/meta.py 0000644 0001750 0001750 00000002216 11525403162 020330 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""SQLAlchemy Metadata and Session object"""
from sqlalchemy import MetaData
__all__ = ['engine', 'metadata', 'Session']
# SQLAlchemy database engine. Updated by model.init_model().
engine = None
# SQLAlchemy session manager. Updated by model.init_model().
Session = None
# Global metadata. If you have multiple databases with overlapping table
# names, you'll need a metadata for each database.
metadata = MetaData()
fookebox-0.6.1/fookebox/model/mpdconn.py 0000644 0001750 0001750 00000015225 11556665245 021064 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import re
import os
import base64
import logging
from mpd import MPDClient
from datetime import datetime
from threading import BoundedSemaphore
from pkg_resources import get_distribution, DistributionNotFound
from pylons.i18n.translation import _, ungettext
from pylons import config, app_globals as g
from fookebox.model.albumart import AlbumArt
log = logging.getLogger(__name__)
class Lock(object):
class __impl:
def __init__(self):
self.semaphore = BoundedSemaphore(value=1)
def acquire(self):
return self.semaphore.acquire(False)
def release(self):
return self.semaphore.release()
__instance = None
def __init__(self):
if Lock.__instance is None:
Lock.__instance = Lock.__impl()
self.__dict__['_Lock__instance'] = Lock.__instance
def __getattr__(self, attr):
return getattr(self.__instance, attr)
class Genre(object):
def __init__(self, name):
self.name = name
self.base64 = base64.urlsafe_b64encode(name.encode('utf8'))
class Artist(object):
def __init__(self, name):
self.name = name
self.base64 = base64.urlsafe_b64encode(name.encode('utf8'))
class Album(object):
def __init__(self, artist, albumName, disc=None):
self.isCompilation = False
if albumName == None:
self.name = ''
else:
self.name = albumName
if artist == None:
self.artist = ''
else:
self.artist = artist
self.disc = disc
self.tracks = []
def add(self, track):
if track.artist != self.artist and not (
track.artist.startswith(self.artist) or
self.artist.startswith(track.artist)):
self.isCompilation = True
self.artist = _('Various Artists').decode('utf8')
self.tracks.append(track)
def load(self):
try:
mpd = MPD.get()
client = mpd.getWorker()
data = client.find(
'Artist', self.artist.encode('utf8'),
'Album', self.name.encode('utf8'))
client.release()
except:
client.release()
raise
for file in data:
track = Track()
track.load(file)
self.add(track)
def hasCover(self):
art = AlbumArt(self)
return art.get() != None
def getCoverURI(self):
artist = base64.urlsafe_b64encode(self.artist.encode('utf8'))
name = base64.urlsafe_b64encode(self.name.encode('utf8'))
return "%s/%s" % (artist, name)
def getPath(self):
basepath = config.get('music_base_path')
if basepath == None:
return None
if len(self.tracks) > 0:
track = self.tracks[0]
else:
self.load()
if len(self.tracks) < 1:
return None
track = self.tracks[0]
fullpath = os.path.join(basepath, track.file)
return os.path.dirname(fullpath)
def key(self):
return "%s-%s" % (self.artist, self.name)
class Track(object):
NO_ARTIST = 'Unknown artist'
NO_TITLE = 'Unnamed track'
track = 0
def load(self, song):
def __set(key, default):
val = song.get(key, default)
if val is None:
return val
if isinstance(val, list):
val = val[0]
if isinstance(val, int):
return val
else:
return val.decode('utf8')
self.artist = __set('artist', Track.NO_ARTIST)
self.title = __set('title', Track.NO_TITLE)
self.file = __set('file', '')
self.disc = __set('disc', 0)
self.album = __set('album', None)
self.queuePosition = int(__set('pos', 0))
self.time = int(__set('time', 0))
if 'track' in song:
# possible formats:
# - '12'
# - '12/21'
# - ['12', '21']
t = song['track']
if '/' in t:
tnum = t.split('/')[0]
self.track = int(tnum)
elif isinstance(t, list):
self.track = int(t[0])
else:
self.track = int(t)
def __str__(self):
return "%s - %s" % (self.artist, self.title)
class FookeboxMPDClient(MPDClient):
def consume(self, do):
self._docommand('consume', [do], self._getnone)
class MPDWorker(object):
def __init__(self, num):
self.num = num
host = config.get('mpd_host')
port = config.get('mpd_port')
password = config.get('mpd_pass')
try:
pkg = get_distribution('python-mpd')
if pkg.version < '0.3.0':
self.mpd = FookeboxMPDClient()
else:
self.mpd = MPDClient()
except DistributionNotFound:
self.mpd = MPDClient()
self.mpd.connect(host, port)
if password:
self.mpd.password(password)
# enable consume on mpd in the first worker
if num == 0:
self.mpd.consume(1)
self.atime = datetime.now()
self.free = True
def __del__(self):
self.mpd.close()
self.mpd.disconnect()
def __str__(self):
return "MPDWorker %d (last used: %s)" % (self.num, self.atime)
def grab(self):
self.free = False
self.atime = datetime.now()
def release(self):
self.atime = datetime.now()
self.free = True
#log.debug("Worker %s released" % self)
def __getattr__(self, attr):
self.atime = datetime.now()
return getattr(self.mpd, attr)
class MPDPool(object):
lock = None
def __init__(self):
self.lock = BoundedSemaphore(value=1)
self._workers = []
def getWorker(self):
self.lock.acquire()
log.debug("Pool contains %d workers" % len(self._workers))
for worker in self._workers:
if worker.free:
log.debug("Re-using worker %s" % worker)
worker.grab()
self._cleanup()
self.lock.release()
return worker
else:
log.debug("Worker %s is busy" % worker)
now = datetime.now()
diff = (now - worker.atime).seconds
# TODO: here we manipulate the collection that
# we are iterating over - probably a bad idea
if diff > 30:
log.warn("Terminating stale worker")
worker.release()
self._workers.remove(worker)
try:
worker = MPDWorker(len(self._workers))
log.debug("Created new worker %s" % worker)
worker.grab()
self._workers.append(worker)
self.lock.release()
return worker
except Exception:
log.fatal('Could not connect to MPD')
self.lock.release()
raise
def _cleanup(self):
now = datetime.now()
for worker in self._workers:
if worker.free:
if (now - worker.atime).seconds > 10:
log.debug("Removing idle worker %s" %
worker)
self._workers.remove(worker)
class MPD(object):
@staticmethod
def get():
if g.mpd == None:
g.mpd = MPDPool()
return g.mpd
fookebox-0.6.1/fookebox/model/schedule.py 0000644 0001750 0001750 00000006773 11525403160 021210 0 ustar stefan stefan 0000000 0000000 # fookebox, http://fookebox.googlecode.com/
#
# Copyright (C) 2007-2011 Stefan Ott. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import sqlalchemy as sa
from sqlalchemy import orm
from fookebox.model import meta
from pylons import app_globals as g
log = logging.getLogger(__name__)
t_event = sa.Table("Events", meta.metadata,
sa.Column("id", sa.types.Integer, primary_key=True),
sa.Column("index", sa.types.Integer, primary_key=False),
sa.Column("type", sa.types.Integer, nullable=False),
sa.Column("name", sa.types.String(100), nullable=False),
sa.Column("time", sa.types.Time, nullable=False),
)
EVENT_TYPE_JUKEBOX = 0
EVENT_TYPE_BAND = 1
EVENT_TYPE_DJ = 2
class Event(object):
def __str__(self):
return "[type %s] %s @ %s (index=%d)" % (
self.type, self.name, self.time, self.index)
@staticmethod
def currentID():
id = g.eventID;
if id == None:
events = Event.all()
if len(events) > 0:
g.eventID = events[0].id
else:
return -1
return g.eventID
@staticmethod
def all():
event_q = meta.Session.query(Event)
return event_q.order_by(Event.index.asc()).all()
@staticmethod
def get(id):
event_q = meta.Session.query(Event)
return event_q.get(id)
@staticmethod
def getCurrent():
event_q = meta.Session.query(Event)
event = event_q.get(Event.currentID())
if event == None:
event = Event()
event.name = 'fookebox jukebox'
event.type = EVENT_TYPE_JUKEBOX
event.index = 0
return event
@staticmethod
def getNext():
event_q = meta.Session.query(Event)
current = Event.getCurrent()
events = event_q.filter(Event.index > current.index)
return events.order_by(Event.index.asc()).first()
@staticmethod
def delete(id):
event_q = meta.Session.query(Event)
event = event_q.get(id)
log.info("Deleting event %s" % event)
meta.Session.delete(event)
meta.Session.commit()
@staticmethod
def add(name, type, time):
event = Event()
event.name = name
event.type = type
event.time = time
meta.Session.add(event)
meta.Session.commit()
event.index = event.id
meta.Session.commit()
log.info("Created event %s" % event)
@staticmethod
def update(id, name, type, time):
event_q = meta.Session.query(Event)
event = event_q.get(id)
event.name = name
event.type = type
event.time = time
meta.Session.commit()
@staticmethod
def up(id):
event_q = meta.Session.query(Event)
event = event_q.get(id)
prev = event_q.filter(Event.index < event.index).order_by(
Event.index.desc()).first()
if prev != None:
tmp = prev.index
prev.index = event.index
event.index = tmp
meta.Session.commit()
@staticmethod
def down(id):
event_q = meta.Session.query(Event)
event = event_q.get(id)
next = event_q.filter(Event.index > event.index).order_by(
Event.index.asc()).first()
if next != None:
tmp = next.index
next.index = event.index
event.index = tmp
meta.Session.commit()
orm.mapper(Event, t_event)
fookebox-0.6.1/fookebox/public/ 0000755 0001750 0001750 00000000000 11743153607 017215 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/public/css/ 0000755 0001750 0001750 00000000000 11743153607 020005 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/public/css/style-mobile.css 0000644 0001750 0001750 00000000600 11540550326 023112 0 ustar stefan stefan 0000000 0000000 #artistList, #genreList {
position: relative;
top: 0;
}
#browse-menu {
position: relative;
top: 0;
}
#letterSelector {
padding: 0 10px 20px 10px;
position: relative;
top: 0;
}
#searchResult {
position: relative;
left: 26px;
top: 8px;
margin-right: 50px;
}
#meta {
display: none;
}
#status {
float: right;
position: relative;
top: 0;
}
h1 {
margin-bottom: 1px;
}
fookebox-0.6.1/fookebox/public/css/style.css 0000644 0001750 0001750 00000013451 11525403215 021652 0 ustar stefan stefan 0000000 0000000 body {
padding: 0;
border: 0;
margin: 0;
color: white;
background: black;
}
body, input {
font-family: Arial, sans-serif;
}
img {
border: 0;
vertical-align: middle;
}
h1 {
color: white;
border-bottom: 1px #789193 solid;
margin: 0;
font-size: 80px;
padding-left: 9px;
text-align: center;
font-weight: bold;
height: 90px;
}
#browse-menu {
border: 1px white solid;
margin: 25px 0 0 2px;
float: left;
width: 300px;
position: fixed;
top: 92px;
bottom: 10px;
}
#browse-menu, #selectType li {
background: #282828;
}
#selectType {
padding: 0 2px 0 0;
margin: 0;
list-style-type: none;
position: relative;
top: -21px;
left: -1px;
}
#selectType li {
border: 1px white solid;
display: inline;
padding: 1px 4px 2px 4px;
}
#selectType li.active {
border-bottom: none;
padding-bottom: 3px;
}
#selectType li.inactive:hover {
background: #999999;
}
#artistList, #genreList {
margin-top: 0;
overflow: auto;
position: absolute;
top: 20px;
bottom: 0px;
right: 0px;
left: 10px;
}
#artistList ul, #genreList {
margin-top: 0;
list-style-type: decimal;
list-style-image: url(../img/li.png);
padding: 0 1em 0 1em;
}
#letterSelector {
font-size: x-small;
position: fixed;
top: 121px;
width: 280px;
}
#letterSelector a {
font-weight: normal;
text-decoration: underline;
}
#status {
position: absolute;
overflow: auto;
top: 92px;
right: 1px;
bottom: 0px;
width: 200px;
}
#nowPlaying, #playlist {
border: 1px white solid;
border-right: 1px #777777 solid;
border-bottom: 1px #777777 solid;
padding-bottom: 1em;
margin: 25px 0 2px 0;
color: white;
}
#nowPlaying ol {
padding-left: 10px;
margin: 0;
}
#nowPlaying #artist, #nowPlaying #track, #nowPlaying #time, #nowPlaying #control {
margin-left: 1em;
padding-bottom: 2px;
}
#nowPlaying #artist {
list-style-image: url(../img/artist.png);
}
#nowPlaying #track {
list-style-image: url(../img/music.png);
}
#nowPlaying #time {
list-style-image: url(../img/time.png);
}
#nowPlaying #control {
list-style-type: none;
}
#nowPlaying, #nowPlaying h2 {
background: #ffae00;
color: black;
}
#playlist, #playlist h2 {
background: #282828;
}
#nowPlaying h2, #playlist h2 {
border: 1px white solid;
border-right: 1px #777777 solid;
color: white;
border-bottom: 0;
position: relative;
top: -21px;
left: -1px;
font-size: medium;
padding: 1px 4px 2px 4px;
margin: 0;
display: inline;
}
#playlist ul {
list-style-type: none;
padding: 0;
margin-top: 0;
}
#playlist ol {
margin-top: 0;
padding-left: 2em;
}
#playlist li {
padding: 0 1em 0 0;
}
h1 a:hover {
color: #ffffff;
}
a {
font-weight: bold;
text-decoration: none;
color: white;
}
a:hover {
color: #cccccc;
}
a:focus {
outline: none;
}
#message {
position: absolute;
top: 38px;
left: 2px;
padding: 2px 2em 2px 2em;
font-size: large;
background-color: #ffcf28;
color: black;
}
#message .corner {
position: absolute;
background-color: black;
height: 4px;
width: 4px;
}
#message .tl {
top: 0;
left: 0;
background-image: url(../img/msg-tl.png);
}
#message .tr {
top: 0;
right: 0;
background-image: url(../img/msg-tr.png);
}
#message .bl {
bottom: 0;
left: 0;
background-image: url(../img/msg-bl.png);
}
#message .br {
bottom: 0;
right: 0;
background-image: url(../img/msg-br.png);
}
.freeSlot {
font-style: italic;
color: #bbbbbb;
font-size: small;
}
#searchResult {
position: fixed;
top: 99px;
left: 330px;
right: 230px;
bottom: 10px;
color: white;
overflow: auto;
}
#searchResultList {
list-style-type: none;
padding-left: 0
}
li.searchResultItem {
overflow: auto;
width: 99%;
background: #222222;
margin: 2px;
}
ul.trackList {
padding-bottom: 10px;
}
#searchResult h3 {
padding-left: 16px;
}
#searchResult li.track {
list-style-image: url(../img/music.png);
}
#searchResult li.track a {
font-weight: normal;
}
#searchResult li.album {
list-style-image: url(../img/folder.png);
}
#searchResult img.coverArt {
float: right;
clear: both;
margin: 10px;
}
#searchResult h2 {
padding-top: 0;
margin: 0;
border-bottom: 1px white solid;
}
#progress {
position: absolute;
right: 30px;
top: 40px;
padding: 5px;
color: black;
}
#progress #icon {
height: 16px;
width: 16px;
background-image: url(../img/progress.gif);
margin: 2px;
}
#progress #text {
display: inline;
}
li.seperator {
border-top: 1px #999999 solid;
}
#now {
margin-top: 120px;
border: 1px #555555 solid;
border-left: none;
border-right: none;
font-size: 40px;
text-align: center;
height: 140px;
background: #000a00;
background-repeat: no-repeat;
}
.state {
text-align: center;
}
.state span {
font-size: 18px;
position: relative;
top: -12px;
background: black;
border: 1px #555555 solid;
color: white;
padding-left: 1em;
padding-right: 1em;
}
#next {
margin-top: 105px;
border: 1px #555555 solid;
border-left: none;
border-right: none;
padding-top: 10px;
font-size: 25px;
text-align: center;
height: 130px;
background: #050500;
background-repeat: no-repeat;
}
#now .label, #next .label {
font-size: small;
position: relative;
background-color: black;
padding: 5px 10px 5px 10px;
}
#now .label {
top: -33px;
border: 1px #555555 solid;
}
#next .label {
top: -30px;
border: 1px #555555 solid;
}
#next .time {
font-size: 18px;
position: relative;
top: 8px;
color: white;
padding-left: 1em;
padding-right: 1em;
}
#clock {
position: fixed;
padding: 10px;
font-size: xx-large;
font-family: monospace;
color: white;
right: 0;
top: 0;
}
#meta {
position: fixed;
top: 0px;
right: 0px;
font-size: x-small;
}
table.schedule {
margin: 0 auto;
border-collapse: collapse;
}
table.schedule td {
padding: 5px 10px;
}
table.schedule tr.drop {
background-color: blue;
}
.currentEvent {
background-color: green;
}
#disabled-main {
text-align: center;
padding-top: 150px;
}
#disabled-title {
font-size: 150pt;
}
#disabled-text {
font-size: 30pt;
}
fookebox-0.6.1/fookebox/public/img/ 0000755 0001750 0001750 00000000000 11743153607 017771 5 ustar stefan stefan 0000000 0000000 fookebox-0.6.1/fookebox/public/img/arrow_down.png 0000644 0001750 0001750 00000000573 11525403056 022657 0 ustar stefan stefan 0000000 0000000 PNG
IHDR a gAMA 7 tEXtSoftware Adobe ImageReadyqe<