commit 0697a8056d4a2c5816d44c4f2e61c9b8445c6587 Author: root Date: Sat Jun 21 18:58:49 2014 +0000 first commit diff --git a/conf/ampache.cfg.php b/conf/ampache.cfg.php new file mode 100644 index 0000000..ff9bd85 --- /dev/null +++ b/conf/ampache.cfg.php @@ -0,0 +1,815 @@ +;### +;################### +; General Config # +;################### + +; This value is used to detect quickly +; if this config file is up to date +; this is compared against a value hard-coded +; into the init script +config_version = 16 + +;################### +; Path Vars # +;################### + +; The http host of your server. +; If not set, retrieved automatically from client request. +; This setting is required for WebSocket server +; DEFAULT: "" +http_host = "DOMAINTOCHANGE" + +; The path to your ampache install +; Do not put a trailing / on this path +; For example if your site is located at http://localhost +; than you do not need to enter anything for the web_path +; if it is located at http://localhost/music you need to +; set web_path to /music +; DEFAULT: "" +web_path = "PATHTOCHANGE" + +;############################## +; Session and Login Variables # +;############################## + +; Hostname of your database +; DEFAULT: localhost +database_hostname = "localhost" + +; Port to use when connecting to your database +; DEFAULT: none +;database_port = 3306 + +; Name of your ampache database +; DEFAULT: ampache +database_name = "yunobase" + +; Username for your ampache database +; DEFAULT: "" +database_username = "yunouser" + +; Password for your ampache database, this can not be blank +; this is a 'forced' security precaution, the default value +; will not work +; DEFAULT: "" +database_password = "yunopass" + +; Length that a session will last expressed in seconds. Default is +; one hour. +; DEFAULT: 3600 +session_length = "3600" + +; Length that the session for a single streaming instance will last +; the default is two hours. With some clients, and long songs this can +; cause playback to stop, increase this value if you experience that +; DEFAULT: 7200 +stream_length = "7200" + +; This length defines how long a 'remember me' session and cookie will +; last, the default is 7200, same as length. It is up to the administrator +; of the box to increase this, for reference 86400 = 1 day +; 604800 = 1 week and 2419200 = 1 month +; DEFAULT: 86400 +remember_length = "86400" + +; Name of the Session/Cookie that will sent to the browser +; default should be fine +; DEFAULT: ampache +session_name = "ampache" + +; Lifetime of the Cookie, 0 == Forever (until browser close) , otherwise in terms of seconds +; If you want cookies to last past a browser close set this to a value in seconds. +; DEFAULT: 0 +session_cookielife = "0" + +; Is the cookie a "secure" cookie? This should only be set to 1 (true) if you are +; running a secure site (HTTPS). +; DEFAULT: 0 +session_cookiesecure = "1" + +; Auth Methods +; This defines which auth methods Auth will attempt to use and in which order. +; If auto_create isn't enabled the user must exist locally. +; DEFAULT: mysql +; VALUES: mysql,ldap,http,pam,external,openid +auth_methods = "http,mysql" + +; External authentication +; This sets the helper used for external authentication. It should conform to +; the interface used by mod_authnz_external +; DEFAULT: none +;external_authenticator = "/usr/sbin/pwauth" + +; Automatic local password updating +; Determines whether successful authentication against an external source +; will result in an update to the password stored in the database. +; A locally stored password is needed for API access. +; DEFAULT: false +;auth_password_save = "false" + +; Logout redirection target +; Defaults to our own login.php, but we can override it here if, for instance, +; we want to redirect to an SSO provider instead. +logout_redirect = "https://DOMAINTOCHANGE/yunohost/sso/?action=logout" + +;##################### +; Program Settings # +;##################### + +; File Pattern +; This defines which file types Ampache will attempt to catalog +; You can specify any file extension you want in here separating them +; with a | +; DEFAULT: mp3|mpc|m4p|m4a|mp4|aac|ogg|rm|wma|asf|flac|spx|ra|ape|shn|wv +catalog_file_pattern = "mp3|mpc|m4p|m4a|mp4|aac|ogg|rm|wma|asf|flac|spx|ra|ape|shn|wv" + +; Video Pattern +; This defines which video file types Ampache will attempt to catalog +; You can specify any file extension you want in here seperating them with +; a | but ampache may not be able to parse them +; DEAFULT: avi|mpg|flv|m4v|webm +catalog_video_pattern = "avi|mpg|flv|m4v|webm" + +; Playlist Pattern +; This defines which playlist types Ampache will attempt to catalog +; You can specify any file extension you want in here seperating them with +; a | but ampache may not be able to parse them +; DEFAULT: m3u|pls|asx|xspf +catalog_playlist_pattern = "m3u|pls|asx|xspf" + +; Prefix Pattern +; This defines which prefix Ampache will ignore when importing tags from +; your music. You may add any prefix you want seperating them with a | +; DEFAULT: The|An|A|Die|Das|Ein|Eine|Les|Le|La +catalog_prefix_pattern = "The|An|A|Die|Das|Ein|Eine|Les|Le|La" + +; Catalog disable +; This defines if catalog can be disabled without removing database entries +; WARNING: this increase sensibly sql requests and slow down Ampache a lot +; DEFAULT: false +;catalog_disable = "false" + +; Use Access List +; Toggle this on if you want ampache to pay attention to the access list +; and only allow streaming/downloading/api-rpc from known hosts api-rpc +; will not work without this on. +; NOTE: Default Behavior is DENY FROM ALL +; DEFAULT: true +access_control = "true" + +; Require Session +; If this is set to true ampache will make sure that the URL passed when +; attempting to retrieve a song contains a valid Session ID This prevents +; others from guessing URL's. This setting is ignored if you have use_auth +; disabled. +; DEFAULT: true +require_session = "true" + +; Require LocalNet Session +; If this is set to true then ampache will require that a valid session +; is passed even on hosts defined in the Local Network ACL. This setting +; has no effect if access_control is not enabled +; DEFAULT: true +require_localnet_session = "true" + +; Multiple Logins +; Added by Vlet 07/25/07 +; When this setting is enabled a user may only be logged in from a single +; IP address at any one time, this is to prevent sharing of accounts +; DEFAULT: false +;prevent_multiple_logins = "false" + +; Downsample Remote +; If this is set to true and access control is on any users who are not +; coming from a defined 'network' ACL will be automatically downsampled +; regardless of their preferences. Requires access_control to be enabled +; DEFAULT: false +;downsample_remote = "false" + +; Track User IPs +; If this is enabled Ampache will log the IP of every completed login +; it will store user,ip,time at one row per login. The results are +; displayed in Admin --> Users +; DEFAULT: false +;track_user_ip = "false" + +; User IP Cardinality +; This defines how many days worth of IP history Ampache will track +; As it is one row per login on high volume sites you will want to +; clear it every now and then. +; DEFAULT: 42 days +;user_ip_cardinality = "42" + +; Allow Zip Download +; This setting allows/disallows using zlib to zip up an entire +; playlist/album for download. Even if this is turned on you will +; still need to enabled downloading for the specific user you +; want to be able to use this function +; DEFAULT: false +;allow_zip_download = "false" + +; File Zip Download +; This settings tells Ampache to attempt to save the zip file +; to the filesystem instead of creating it in memory, you must +; also set tmp_dir_path in order for this to work +; DEFAULT: false +;file_zip_download = "false" + +; File Zip Comment +; This is an optional configuration option that adds a comment +; to your zip files, this only applies if you've got allow_zip_downloads +; DEFAULT: Ampache - Zip Batch Download +;file_zip_comment = "Ampache - Zip Batch Download" + +; Waveform +; This settings tells Ampache to attempt to generate a waveform +; for each song. It requires transcode and encode_args_wav settings. +; You must also set tmp_dir_path in order for this to work +; DEFAULT: false +;waveform = "false" + +; Waveform color +; The waveform color. +; DEFAULT: #FF0000 +;waveform_color = "#FF0000" + +; Temporary Directory Path +; If File Zip Download or Waveform is enabled this must be set to tell +; Ampache which directory to save the temporary file to. Do not put a +; trailing slash or this will not work. +; DEFAULT: false +;tmp_dir_path = "false" + +; This setting throttles a persons downloading to the specified +; bytes per second. This is not a 100% guaranteed function, and +; you should really use a server based rate limiter if you want +; to do this correctly. +; DEFAULT: off +; VALUES: any whole number (in bytes per second) +;throttle_download = 10 + +; This determines the tag order for all cataloged +; music. If none of the listed tags are found then +; ampache will randomly use whatever was found. +; POSSIBLE VALUES: ape asf avi id3v1 id3v2 lyrics3 matroska mpeg quicktime riff +; vorbiscomment +; DEFAULT: id3v2 id3v1 vorbiscomment quicktime matroska ape asf avi mpeg riff +getid3_tag_order = "id3v2,id3v1,vorbiscomment,quicktime,matroska,ape,asf,avi,mpeg,riff" + +; Determines whether we try to autodetect the encoding for id3v2 tags. +; May break valid tags. +; DEFAULT: false +;getid3_detect_id3v2_encoding = "false" + +; This determines the order in which metadata sources are used (and in the +; case of plugins, checked) +; POSSIBLE VALUES (builtins): filename and getID3 +; POSSIBLE VALUES (plugins): MusicBrainz, plus any others you've installed. +; DEFAULT: getID3 filename +metadata_order = "getID3,filename" + +; Un comment if don't want ampache to follow symlinks +; DEFAULT: false +;no_symlinks = "false" + +; Use auth? +; If this is set to "Yes" ampache will require a valid +; Username and password. If this is set to false then ampache +; will not ask you for a username and password. false is only +; recommended for internal only instances +; DEFAULT true +use_auth = "true" + +; Default Auth Level +; If use_auth is set to false then this option is used +; to determine the permission level of the 'default' users +; default is administrator. This setting only takes affect +; if use_auth if false +; POSSIBLE VALUES: user, admin, manager, guest +; DEFAULT: admin +default_auth_level = "admin" + +; 5 Star Ratings +; This allows ratings for almost any object in ampache +; POSSIBLE VALUES: false true +; DEFAULT: true +ratings = "true" + +; User flags +; This allows user flags for almost any object in ampache +; POSSIBLE VALUES: false true +; DEFAULT: true +userflags = "true" + +; Direct play +; This allows user to play directly a song or album +; POSSIBLE VALUES: false true +; DEFAULT: true +directplay = "true" + +; Sociable +; This turns on / off all of the "social" features of ampache +; default is on, but if you don't care and just want music +; turn this off to disable all social features. +; DEFAULT: true +sociable = "true" + +; Notify +; This turns on / off all Ampache notifications +; DEFAULT: true +notify = "true" + +; This options will turn on/off Demo Mode +; If Demo mode is on you can not play songs or update your catalog +; in other words.. leave this commented out +; DEFAULT: false +;demo_mode = "false" + +; Caching +; This turns the caching mechanisms on or off, due to a large number of +; problems with people with very large catalogs and low memory settings +; this is off by default as it does significantly increase the memory +; requirments on larger catalogs. If you have the memory this can create +; a 2-3x speed improvement. +; DEFAULT: false +memory_cache = "true" + +; Memory Limit +; This defines the "Min" memory limit for PHP if your php.ini +; has a lower value set Ampache will set it up to this. If you +; set it below 16MB getid3() will not work! +; DEFAULT: 32 +;memory_limit = 32 + +; Album Art Preferred Filename +; Specify a filename to look for if you always give the same filename +; i.e. "folder.jpg" Ampache currently only supports jpg/gif and png +; Especially useful if you have a front and a back image in a folder +; comment out if ampache should search for any jpg,gif or png +; DEFAULT: folder.jpg +;album_art_preferred_filename = "folder.jpg" + +; Resize Images * Requires PHP-GD * +; Set this to true if you want Ampache to resize the Album +; art on the fly, this increases load time and CPU usage +; and also requires the PHP-GD library. This is very useful +; If you have high-quality album art and a small upload cap +; DEFAULT: false +;resize_images = "false" + +; Art Gather Order +; Simply arrange the following in the order you would like +; ampache to search. If you want to disable one of the search +; methods simply leave it out. DB should be left as the first +; method unless you want it to overwrite what's already in the +; database +; POSSIBLE VALUES: db tags folder amazon lastfm musicbrainz google +; DEFAULT: db,tags,folder,musicbrainz,lastfm,google +art_order = "db,tags,folder,musicbrainz,lastfm,google" + +; Amazon Developer Key +; These are needed in order to actually use the amazon album art +; Your public key is your 'Access Key ID' +; Your private key is your 'Secret Access Key' +; DEFAULT: false +;amazon_developer_public_key = "" +;amazon_developer_private_key = "" + +; Recommendations +; Set this to true to enable display of similar artists or albums +; while browsing. Requires Last.FM. +; DEFAULT: false +;show_similar = "false" + +; Concerts +; Set this to true to enable display of artist concerts +; Requires Last.FM. +; DEFAULT: false +;show_concerts = "false" + +; Last.FM API Key +; Set this to your Last.FM api key to actually use Last.FM for +; recommendations. +;lastfm_api_key = "" + +; Wanted +; Set this to true to enable display missing albums and the +; possibility for users to mark it as wanted. +; DEFAULT: false +;wanted = "false" + +; Wanted types +; Set the allowed types of wanted releases (album,compilation,single,ep,live,remix,promotion,official) +; DEFAULT: album,official +wanted_types = "album,official" + +; Wanted Auto Accept +; Mark wanted requests as accepted by default (no content manager agreement required) +; DEFAULT: false +;wanted_auto_accept = "false" + +; EchoNest API key +; EchoNest provides several music services. Currently used for missing song 30 seconds preview. +;echonest_api_key = "" + +; Broadcasts +; Allow users to broadcast music. +; This feature requires advanced server configuration, please take a look on the wiki for more information. +; DEFAULT: false +;broadcast = "false" + +; Web Socket address +; Declare the web socket server address +; DEFAULT: determined automatically +;websocket_address = "ws://localhost:8100" + +; Amazon base urls +; An array of Amazon sites to search. +; NOTE: This will search each of these sites in turn so don't expect it +; to be lightning fast! +; It is strongly recommended that only one of these is selected at any +; one time +; POSSIBLE VALUES: +; http://webservices.amazon.com +; http://webservices.amazon.co.uk +; http://webservices.amazon.de +; http://webservices.amazon.co.jp +; http://webservices.amazon.fr +; http://webservices.amazon.ca +; Default: http://webservices.amazon.com +;amazon_base_urls = "http://webservices.amazon.com" + +; max_amazon_results_pages +; The maximum number of results pages to pull from EACH amazon site +; NOTE: The art search pages through the results returned by your search +; up to this number of pages. As with the base_urls above, this is going +; to take more time, the more pages you ask it to process. +; Of course a good search will return only a few matches anyway. +; It is strongly recommended that you do _not_ change this value +; DEFAULT: 1 page (10 items) +max_amazon_results_pages = "1" + +; Debug +; If this is enabled Ampache will write debugging information to the log file +; DEFAULT: false +debug = "true" + +; Debug Level +; This should always be set in conjunction with the +; debug option, it defines how prolific you want the +; debugging in ampache to be. values are 1-5. +; 1 == Errors only +; 2 == Error + Failures (login attempts etc.) +; 3 == ?? +; 4 == ?? (Profit!) +; 5 == Information (cataloging progress etc.) +; DEFAULT: 5 +debug_level = "5" + +; Path to Log File +; This defines where you want ampache to log events to +; this will only happen if debug is turned on. Do not +; include trailing slash. You will need to make sure that +; the specified directory exists and your HTTP server has +; write access. +; DEFAULT: NULL +log_path = "/var/www/ampache/log" + +; Log filename pattern +; This defines where the log file name pattern. +; %name.%Y%m%d.log will create a different log file every day. +; DEFAULT: %name.%Y%m%d.log +log_filename = "%name.%Y%m%d.log" + +; Charset of generated HTML pages +; Default of UTF-8 should work for most people +; DEFAULT: UTF-8 +site_charset = "UTF-8" + +; Locale Charset +; In some cases this has to be different +; in order for XHTML and other things to work +; This is disabled by default, enabled only +; if needed. It's specifically needed for Russian +; so that is the default +; DEFAULT: cp1251 +;lc_charset = cp1251 + +; Refresh Limit +; This defines the default refresh limit in seconds for +; pages with dynamic content, such as now playing +; DEFAULT: 60 +; Possible Values: Int > 5 +refresh_limit = "60" + +;######################################################### +; Custom actions (optional) # +;######################################################### + +; Your custom play action title +;custom_play_action_title_0 = "" +; Your custom play action icon name (stored as /images/icon_[your_image].png) +;custom_play_action_icon_0 = "" +; Your custom action script, where: +; - %f: the media file path +; - %c: the excepted codec target (mp3, ogg, ...) +; - %a: the artist name +; - %A: the album name +; - %t: the song title +;custom_play_action_run_0 = "" + +; Example for Karaoke playing +;custom_play_action_title_0 = "Karaoke" +;custom_play_action_icon_0 = "microphone" +;custom_play_action_run_0 = "sox \"%f\" -p oops | ffmpeg -i pipe:0 -f %c pipe:1" + +;######################################################### +; LDAP login info (optional) # +;######################################################### + +; LDAP filter string to use (required) +; For OpenLDAP use "uid" +; For Microsoft Active Directory (MAD) use "sAMAccountName" +; DEFAULT: null +; ldap_filter = "sAMAccountName" + +; LDAP objectclass (required) +; OpanLDAP objectclass = "*" +; MAD objectclass = "organizationalPerson" +; DEFAULT null +ldap_objectclass = "posixAccount" + +; Initial credentials to bind with for searching (optional) +; DEFAULT: null +;ldap_username = "" +;ldap_password = "" + +; Require that the user is in a specific group (optional) +; DEFAULT: null +;ldap_require_group = "cn=yourgroup,ou=yourorg,dc=yoursubdomain,dc=yourdomain,dc=yourtld" + +; This is the search dn used to find users (required) +; DEFAULT: null +ldap_search_dn = "dc=yunohost,dc=org" + +; This is the address of your ldap server (required) +; DEFAULT: null +ldap_url = "192.168.1.88" + +; Attributes where additional user information is stored (optional) +; OpenLDAP ldap_name_field = "cn" +; MAD ldap_name_field = "displayname" +; DEFAULT: null +;ldap_email_field = "mail" +ldap_name_field = "cn" + +;######################################################### +; OpenID login info (optional) # +;######################################################### + +; Requires specific OpenID Provider Authentication Policy +; DEFAULT: null +; VALUES: PAPE_AUTH_MULTI_FACTOR_PHYSICAL,PAPE_AUTH_MULTI_FACTOR,PAPE_AUTH_PHISHING_RESISTANT +;openid_required_pape = "" + +;######################################################### +; Public Registration settings, defaults to disabled # +;######################################################### + +; This setting will silently create an ampache account +; for anyone who can login using ldap (or any other login +; extension). The default is to create new users as guests +; see auto_user config option if you would like to change this +; DEFAULT: false +auto_create = "true" + +; This setting turns on/off public registration. It is +; recommended you leave this off, as it will allow anyone to +; sign up for an account on your server. +; REMEMBER: don't forget to set the mail from address further down in the config. +; DEFAULT: false +;allow_public_registration = "false" + +; Require Captcha Text on Image confirmation +; Turning this on requires the user to correctly +; type in the letters in the image created by Captcha +; Default is off because its very hard to detect if it failed +; to draw, or they failed to enter it. +; DEFAULT: false +;captcha_public_reg = "false" + +; This setting turns on/off admin notification of registration. +; DEFAULT: false +;admin_notify_reg = "false" + +; This setting determines whether the user will be created as a disabled user. +; If this is on, an administrator will need to manually enable the account +; before it's usable. +; DEFAULT: false +;admin_enable_required = "false" + +; This setting will allow all registrants/ldap/http users +; to be auto-approved as a user. By default, they will be +; added as a guest and must be promoted by the admin. +; POSSIBLE VALUES: guest, user, admin +; DEFAULT: guest +auto_user = "admin" + +; This will display the user agreement when registering +; For agreement text, edit templates/user_agreement.php +; User will need to accept the agreement before they can register +; DEFAULT: false +;user_agreement = "false" + +;######################################################## +; These options control the dynamic downsampling based # +; on current usage # +; *Note* Transcoding must be enabled and working # +;######################################################## + +; Attempt to optimize bandwidth by dynamically lowering the bit rate of new +; streams. Since the bit rate is only adjusted at the beginning of a song, the +; actual cumulative bitrate for concurrent streams can be up to around +; double the configured value. It also only applies to streams that are +; transcoded. +; DEFAULT: none +max_bit_rate = 576 + +; New dynamically downsampled streams will be denied if they are forced below +; this value. +; DEFAULT: 8 +min_bit_rate = 48 + +;###################################################### +; These are commands used to transcode non-streaming +; formats to the target file type for streaming. +; This can be useful in re-encoding file types that don't stream +; very well, or if your player doesn't support some file types. +; +; 'Downsampling' will also use these commands. +; +; To state the bleeding obvious, any programs referenced in the transcode +; commands must be installed, in the web server's search path (or referenced +; by their full path), and executable by the web server. + +; Input type selection +; TYPE is the extension. 'allowed' certifies that transcoding works properly for +; this input format. 'required' further forbids the direct streaming of a format +; (e.g. if you store everything in FLAC, but don't want to ever stream that.) +; transcode_TYPE = {allowed|required|false} +; DEFAULT: false +;transcode_m4a = allowed +transcode_flac = required +;transcode_mpc = required +transcode_mp3 = allowed + +; Default output format +; DEFAULT: none +encode_target = mp3 + +; Override the default output format on a per-type basis +; encode_target_TYPE = TYPE +; DEFAULT: none +; encode_target_flac = ogg + +; Allow clients to override transcode settings (output type, bitrate, codec ...) +; DEFAULT: true +transcode_player_customize = "1" + +; Command configuration. Substitutions will be made as follows: +; %FILE% => filename +; %SAMPLE% => target sample rate +; You can do fancy things like VBR, but consider whether the consequences are +; acceptable in your environment. + +; Master transcode command +; transcode_cmd should be a single command that supports multiple file types, +; such as ffmpeg or avconv. It's still possible to make a configuration that's +; equivalent to the old default, but if you find that necessary you should be +; clever enough to figure out how on your own. +; DEFAULT: none +;transcode_cmd = "ffmpeg -i %FILE%" +transcode_cmd = "ffmpeg -i %FILE%" +;transcode_cmd = "/usr/bin/neatokeen %FILE%" + +; Specific transcode commands +; It shouldn't be necessary in most cases, but you can override the transcode +; command for specific source formats. It still needs to accept the +; encoding arguments, so the easiest approach is to use your normal command as +; a clearing-house. +; transcode_cmd_TYPE = TRANSCODE_CMD +;transcode_cmd_mid = "timidity -Or -o – %FILE% | ffmpeg -f s16le -i pipe:0" + +; Encoding arguments +; For each output format, you should provide the necessary arguments for +; your transcode_cmd. +; encode_args_TYPE = TRANSCODE_CMD_ARGS +;encode_args_mp3 = "-vn -b:a %SAMPLE%K -c:a libmp3lame -f mp3 pipe:1" +;encode_args_ogg = "-vn -b:a %SAMPLE%K -c:a libvorbis -f ogg pipe:1" +;encode_args_m4a = "-vn -b:a %SAMPLE%K -c:a libfdk_aac -f adts pipe:1" +;encode_args_wav = "-vn -b:a %SAMPLE%K -c:a pcm_s16le -f wav pipe:1" +encode_args_ogg = "-vn -b:a max\(%SAMPLE%K\,49K\) -acodec libvorbis -vcodec libtheora -f ogg pipe:1" +encode_args_mp3 = "-vn -b:a %SAMPLE%K -acodec libmp3lame -f mp3 pipe:1" +encode_args_ogv = "-vcodec libtheora -acodec libvorbis -ar 44100 -f ogv pipe:1" +encode_args_mp4 = "-profile:0 baseline -frag_duration 2 -ar 44100 -f mp4 pipe:1" + +;###################################################### +; these options allow you to configure your rss-feed +; layout. rss exists of two parts, main and song main is the information about the feed +; song is the information in the feed. can be multiple items. +; use_rss = false (values true | false) +;DEFAULT: use_rss = false +;use_rss = false +;##################################################### + +;############################# +; Proxy Settings (optional) # +;############################# +; If Ampache is behind an http proxy, specifiy the hostname or IP address +; port, proxyusername, and proxypassword here. +;DEFAULT: not in use +;proxy_host = "192.168.0.1" +;proxy_port = "8080" +;proxy_user = "" +;proxy_pass = "" + +; If Ampache is behind an https reverse proxy, force use HTTPS protocol. +;Default: false +force_ssl = true + +;############################# +; Mail Settings # +;############################# + +;Method used to send mail +;POSSIBLE VALUES: smtp sendmail php +;DEFAULT: php +;mail_type = "php" + +;Mail domain. +;DEFAULT: example.com +;mail_domain = "example.com" + +;This will be combined with mail_domain and used as the source address for +;emails generated by Ampache. For example, setting this to 'me' will set the +;sender to 'me@example.com'. +;DEFAULT: info +;mail_user = "info" + +;A name to go with the email address. +;DEFAULT: Ampache +;mail_name = "Ampache" + +;How strictly email addresses should be checked. +;easy does a regex match, strict actually performs some SMTP transactions +;to see if we can send to this address. +;POSSIBLE VALUES: strict easy none +; DEFAULT: strict +;mail_check = "strict" + + +;############################ +; sendmail Settings # +;############################ + +;DEFAULT: /usr/sbin/sendmail +;sendmail_path = "/usr/sbin/sendmail" + +;############################# +; SMTP Settings # +;############################# + +;Mail server (hostname or IP address) +;DEFAULT: localhost +;mail_host = "localhost" + +; SMTP port +;DEFAULT: 25 +;mail_port = 25 + +;Secure SMTP +;POSSIBLE VALUES: ssl tls +;DEFAULT: none +;mail_secure_smtp = tls + +;Enable SMTP authentication +;DEFAULT: false +;mail_auth = true + +;SMTP Username +;your mail auth username. +;mail_auth_user = "" + +; SMTP Password +; your mail auth password. +;mail_auth_pass = "" + +;############################# +; Multibyte Settings # +;############################# +; See http://php.net/manual/mbstring.supported-encodings.php +; If you want ID3v1 encoding detection to work, you should uncomment this line +; so that the ordering is sane. +; DEFAULT: auto +;mb_detect_order = "ASCII,UTF-8,EUC-JP,ISO-2022-JP,SJIS,JIS" + diff --git a/conf/nginx.conf b/conf/nginx.conf new file mode 100644 index 0000000..749464b --- /dev/null +++ b/conf/nginx.conf @@ -0,0 +1,23 @@ +location PATHTOCHANGE { + + alias ALIASTOCHANGE; + + if ($scheme = http) { + rewrite ^ https://$server_name$request_uri? permanent; + } + index index.php; + try_files $uri $uri/ index.php; + location ~ [^/]\.php(/|$) { + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param REMOTE_USER $remote_user; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + +rewrite ^PATHTOCHANGE/play/ssid/(\w+)/type/(\w+)/oid/([0-9]+)/uid/([0-9]+)/name/(.*)$ PATHTOCHANGE/play/index.php?ssid=$1&type=$2&oid=$3&uid=$4&name=$5 last; + if ( !-d $request_filename ) { + rewrite ^PATHTOCHANGE/rest/(.*)\.view$ PATHTOCHANGE/rest/index.php?action=$1 last; + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ad32394 --- /dev/null +++ b/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "Ampache", + "id": "ampache", + "description": { + "en": "A web based audio/video streaming application", + "fr": "Une application de streaming audio et vidéo" + }, + "developer": { + "name": "beudbeud", + "email": "beudbeud@beudibox.fr", + "url": "http://ampache.org" + }, + "multi_instance": "true", + "arguments": { + "install" : [ + { + "name": "domain", + "ask": { + "en": "Choose a domain for Ampache", + "fr": "Choisissez un domaine pour Ampache" + }, + "example": "domain.org" + }, + { + "name": "path", + "ask": { + "en": "Choose a path for Ampache", + "fr": "Choisissez un chemin pour Ampache" + }, + "example": "/ampache", + "default": "/ampache" + } + ] + } +} diff --git a/scripts/install b/scripts/install new file mode 100644 index 0000000..84be6da --- /dev/null +++ b/scripts/install @@ -0,0 +1,47 @@ +#!/bin/bash + +# Retrieve arguments +domain=$1 +path=$2 + +# Check domain/path availability +sudo yunohost app checkurl $domain$path -a ampache +if [[ ! $? -eq 0 ]]; then + exit 1 +fi + +# Generate random password +db_pwd=$(dd if=/dev/urandom bs=1 count=200 2> /dev/null | tr -c -d '[A-Za-z0-9]' | sed -n 's/\(.\{24\}\).*/\1/p') + +# Use 'ampache' as database name and user +db_user=ampache + +# Initialize database and store mysql password for upgrade +sudo yunohost app initdb $db_user -p $db_pwd -s $(readlink -e ../sources/sql/ampache.sql) +sudo yunohost app setting ampache mysqlpwd -v $db_pwd + +# Copy files to the right place +final_path=/var/www/ampache +sudo mkdir -p $final_path/log +sudo cp -a ../sources/* $final_path +sudo cp ../conf/ampache.cfg.php $final_path/config/ampache.cfg.php + +# Change variables in Ampache configuration +sudo sed -i "s/yunouser/$db_user/g" $final_path/config/ampache.cfg.php +sudo sed -i "s/yunopass/$db_pwd/g" $final_path/config/ampache.cfg.php +sudo sed -i "s/yunobase/$db_user/g" $final_path/config/ampache.cfg.php +sed -i "s@PATHTOCHANGE@$path@g" $final_path/config/ampache.cfg.php +sed -i "s@DOMAINTOCHANGE@$domain@g" $final_path/config/ampache.cfg.php + +# Set permissions to roundcube directory +sudo chown -R www-data: $final_path + +# Modify Nginx configuration file and copy it to Nginx conf directory +sed -i "s@PATHTOCHANGE@$path@g" ../conf/nginx.conf* +sed -i "s@ALIASTOCHANGE@$final_path/@g" ../conf/nginx.conf* +sudo cp ../conf/nginx.conf /etc/nginx/conf.d/$domain.d/ampache.conf + +# Reload Nginx and regenerate SSOwat conf +sudo service nginx reload +#sudo yunohost app setting ampache skipped_uris -v "/" +sudo yunohost app ssowatconf diff --git a/scripts/remove b/scripts/remove new file mode 100644 index 0000000..fae0af8 --- /dev/null +++ b/scripts/remove @@ -0,0 +1,10 @@ +#!/bin/bash + +db_user=ampache +db_name=ampache +root_pwd=$(sudo cat /etc/yunohost/mysql) +domain=$(sudo yunohost app setting ampache domain) + +mysql -u root -p$root_pwd -e "DROP DATABASE $db_name ; DROP USER $db_user@localhost ;" +sudo rm -rf /var/www/ampache +sudo rm -f /etc/nginx/conf.d/$domain.d/ampache.conf diff --git a/sources/README.md b/sources/README.md new file mode 100755 index 0000000..9c68a48 --- /dev/null +++ b/sources/README.md @@ -0,0 +1,142 @@ +Ampache +======= +[www.ampache.org](http://www.ampache.org) | +[ampache.github.io](http://ampache.github.io) + +Basics +------ + +Ampache is a web based audio/video streaming application and file +manager allowing you to access your music & videos from anywhere, +using almost any internet enabled device. + +Ampache's usefulness is heavily dependent on being able to extract +correct metadata from embedded tags in your files and/or the filename. +Ampache is not a media organiser; it is meant to be a tool which +presents an already organised collection in a useful way. It assumes +that you know best how to manage your files and are capable of +choosing a suitable method for doing so. + +Recommended Version +------------------- + +Currently, the recommended version is [git HEAD](https://github.com/ampache/ampache/archive/master.tar.gz). +[![Build Status](https://api.travis-ci.org/ampache/ampache.png?branch=master)](https://travis-ci.org/ampache/ampache) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ampache/ampache/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ampache/ampache/?branch=master) + +Latest changes but unstable is [develop branch](https://github.com/ampache/ampache/archive/develop.tar.gz). +[![Build Status](https://api.travis-ci.org/ampache/ampache.png?branch=develop)](https://travis-ci.org/ampache/ampache) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ampache/ampache/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/ampache/ampache/?branch=develop) + +Requirements +------------ + +* A web server. All of the following have been used, though Apache +receives the most testing: + * Apache + * lighttpd + * nginx + * IIS + +* PHP 5.3 or greater. + +* PHP modules: + * PDO + * PDO_MYSQL + * hash + * session + * json + +* MySQL 5.x + +Installation +------------ + +Please see [the wiki](https://github.com/ampache/ampache/wiki/Installation) + +Upgrading +--------- + +If you are upgrading from an older version of Ampache we recommend +moving the old directory out of the way, extracting the new copy in +its place and then copying the old config file into config/. All +database updates will be handled by Ampache. + +License +------- + +Ampache is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License v2 +as published by the Free Software Foundation. + +Ampache includes some external modules that carry their own licensing. + +* [getID3](http://getid3.sourceforge.net): GPL v2 +* [Horde_Browser](http://www.horde.org): LGPL v2.1 +* [PHP-gettext](https://launchpad.net/php-gettext): GPL v2 +* [MusicBrainz](https://github.com/mikealmond/MusicBrainz): MIT +* PHP MPD interface: GPL v2 +* [PHPMailer](https://github.com/PHPMailer/PHPMailer): LGPL v2.1 +* [jQuery](http://jquery.org): MIT +* [Requests](http://requests.ryanmccue.info): ISC Licensed +* [Whatever:hover](http://www.xs4all.nl/~peterned): LGPL v2.1 +* [xbmc-php-rpc](https://github.com/karlrixon/xbmc-php-rpc): GPL v3 +* [Dropbox SDK](https://github.com/dropbox/dropbox-sdk-php): MIT +* [jPlayer](http://jplayer.org): MIT +* [prettyPhoto](http://www.no-margin-for-errors.com/projects/prettyphoto-jquery-lightbox-clone): GPL v2 +* [Tag-it!] (http://aehlke.github.io/tag-it): MIT +* [PHP Echo Nest API] (https://github.com/bshaffer/php-echonest-api): MIT +* [Noty] (http://ned.im/noty): MIT +* [jScroll] (https://github.com/pklauzinski/jscroll): MIT +* [jquery.qrcode] (http://jeromeetienne.github.io/jquery-qrcode): MIT +* [PHP OpenID] (https://github.com/openid/php-openid): Apache License +* [Ratchet] (http://socketo.me): MIT +* [ReactPHP] (https://github.com/reactphp/react): MIT +* [Guzzle] (https://github.com/guzzle/guzzle): MIT +* [Symfony Components] (https://github.com/symfony): MIT +* [Evenement] (https://github.com/igorw/evenement): MIT +* [RhinoSlider] (http://rhinoslider.com): MIT +* [MediaTable] (https://github.com/edenspiekermann/MediaTable): MIT +* [Responsive Elements] (https://github.com/kumailht/responsive-elements): MIT +* [Bootstrap] (http://getbootstrap.com): MIT + + +Translations +------------ + +Ampache is currently translated (at least partially) into the +following languages. If you are interested in updating an existing +translation or adding a new one please see /locale/base/TRANSLATIONS +for more instructions. + +* English (en_US) +* German (de_DE) +* Spanish (es_ES) +* Dutch (nl_NL) +* Norwegian (nb_NO) +* UK English (en_GB) +* Italian (it_IT) +* French (fr_FR) +* Swedish (sv_SE) +* Japanese (ja_JP) +* Catalan (ca_ES) +* Russian (ru_RU) +* Czech (cs_CZ) + +Credits +------- + +Thanks to all those who have helped make Ampache awesome: [Credits](docs/ACKNOWLEDGEMENTS) + + +Contact Us +---------- + +Hate it? Love it? Let us know. Also let us know if you think of any +more features, encounter bugs, etc. + +* [Public Repository](http://github.com/ampache) +* IRC: chat.freenode.net #ampache +* [Issue Tracker](https://github.com/ampache/ampache/issues) +* [Documentation](https://github.com/ampache/ampache/wiki) + diff --git a/sources/admin/access.php b/sources/admin/access.php new file mode 100644 index 0000000..6146992 --- /dev/null +++ b/sources/admin/access.php @@ -0,0 +1,110 @@ +name, + 'admin/access.php?action=delete_record&access_id=' . $access->id,1,'delete_access'); + break; + case 'add_host': + + // Make sure we've got a valid form submission + if (!Core::form_verify('add_acl','post')) { + UI::access_denied(); + exit; + } + + Access::create($_POST); + + // Create Additional stuff based on the type + if ($_POST['addtype'] == 'stream' || + $_POST['addtype'] == 'all' + ) { + $_POST['type'] = 'stream'; + Access::create($_POST); + } + if ($_POST['addtype'] == 'all') { + $_POST['type'] = 'interface'; + Access::create($_POST); + } + + if (!Error::occurred()) { + $url = AmpConfig::get('web_path') . '/admin/access.php'; + show_confirmation(T_('Added'), T_('Your new Access Control List(s) have been created'),$url); + } else { + $action = 'show_add_' . $_POST['type']; + require_once AmpConfig::get('prefix') . '/templates/show_add_access.inc.php'; + } + break; + case 'update_record': + if (!Core::form_verify('edit_acl')) { + UI::access_denied(); + exit; + } + $access = new Access($_REQUEST['access_id']); + $access->update($_POST); + if (!Error::occurred()) { + show_confirmation(T_('Updated'), T_('Access List Entry updated'), AmpConfig::get('web_path').'/admin/access.php'); + } else { + $access->format(); + require_once AmpConfig::get('prefix') . '/templates/show_edit_access.inc.php'; + } + break; + case 'show_add_current': + case 'show_add_rpc': + case 'show_add_local': + case 'show_add_advanced': + $action = $_REQUEST['action']; + require_once AmpConfig::get('prefix') . '/templates/show_add_access.inc.php'; + break; + case 'show_edit_record': + $access = new Access($_REQUEST['access_id']); + $access->format(); + require_once AmpConfig::get('prefix') . '/templates/show_edit_access.inc.php'; + break; + default: + $list = array(); + $list = Access::get_access_lists(); + require_once AmpConfig::get('prefix') .'/templates/show_access_list.inc.php'; + break; +} // end switch on action +UI::show_footer(); diff --git a/sources/admin/catalog.php b/sources/admin/catalog.php new file mode 100644 index 0000000..0bf5994 --- /dev/null +++ b/sources/admin/catalog.php @@ -0,0 +1,303 @@ +add_to_catalog($_POST); + } + } + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog Updated'); + $body = ''; + show_confirmation($title, $body, $url); + toggle_visible('ajax-loading'); + break; + case 'update_all_catalogs': + $_REQUEST['catalogs'] = Catalog::get_catalogs(); + case 'update_catalog': + toggle_visible('ajax-loading'); + ob_end_flush(); + /* If they are in demo mode stop here */ + if (AmpConfig::get('demo_mode')) { break; } + + if (isset($_REQUEST['catalogs'])) { + foreach ($_REQUEST['catalogs'] as $catalog_id) { + $catalog = Catalog::create_from_id($catalog_id); + $catalog->verify_catalog(); + } + } + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog Updated'); + $body = ''; + show_confirmation($title,$body,$url); + toggle_visible('ajax-loading'); + break; + case 'full_service': + toggle_visible('ajax-loading'); + ob_end_flush(); + /* Make sure they aren't in demo mode */ + if (AmpConfig::get('demo_mode')) { UI::access_denied(); break; } + + if (!$_REQUEST['catalogs']) { + $_REQUEST['catalogs'] = Catalog::get_catalogs(); + } + + /* This runs the clean/verify/add in that order */ + foreach ($_REQUEST['catalogs'] as $catalog_id) { + $catalog = Catalog::create_from_id($catalog_id); + $catalog->clean_catalog(); + $catalog->count = 0; + $catalog->verify_catalog(); + $catalog->count = 0; + $catalog->add_to_catalog(); + } + Dba::optimize_tables(); + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog Updated'); + $body = ''; + show_confirmation($title,$body,$url); + toggle_visible('ajax-loading'); + break; + case 'delete_catalog': + /* Make sure they aren't in demo mode */ + if (AmpConfig::get('demo_mode')) { break; } + + if (!Core::form_verify('delete_catalog')) { + UI::access_denied(); + exit; + } + + /* Delete the sucker, we don't need to check perms as thats done above */ + Catalog::delete($_GET['catalog_id']); + $next_url = AmpConfig::get('web_path') . '/admin/catalog.php'; + show_confirmation(T_('Catalog Deleted'), T_('The Catalog and all associated records have been deleted'),$next_url); + break; + case 'show_delete_catalog': + $catalog_id = scrub_in($_GET['catalog_id']); + + $next_url = AmpConfig::get('web_path') . '/admin/catalog.php?action=delete_catalog&catalog_id=' . scrub_out($catalog_id); + show_confirmation(T_('Catalog Delete'), T_('Confirm Deletion Request'),$next_url,1,'delete_catalog'); + break; + case 'enable_disabled': + if (AmpConfig::get('demo_mode')) { break; } + + $songs = $_REQUEST['song']; + + if (count($songs)) { + foreach ($songs as $song_id) { + Song::update_enabled(true, $song_id); + } + $body = count($songs) . ngettext(' Song Enabled', ' Songs Enabled', count($songs)); + } else { + $body = T_('No Disabled Songs selected'); + } + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = count($songs) . ngettext(' Disabled Song Processed', ' Disabled Songs Processed', count($songs)); + show_confirmation($title,$body,$url); + break; + case 'clean_all_catalogs': + $_REQUEST['catalogs'] = Catalog::get_catalogs(); + case 'clean_catalog': + toggle_visible('ajax-loading'); + ob_end_flush(); + /* If they are in demo mode stop them here */ + if (AmpConfig::get('demo_mode')) { break; } + + // Make sure they checked something + if (isset($_REQUEST['catalogs'])) { + foreach ($_REQUEST['catalogs'] as $catalog_id) { + $catalog = Catalog::create_from_id($catalog_id); + $catalog->clean_catalog(); + } // end foreach catalogs + Dba::optimize_tables(); + } + + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog Cleaned'); + $body = ''; + show_confirmation($title,$body,$url); + toggle_visible('ajax-loading'); + break; + case 'update_catalog_settings': + /* No Demo Here! */ + if (AmpConfig::get('demo_mode')) { break; } + + /* Update the catalog */ + Catalog::update_settings($_POST); + + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog Updated'); + $body = ''; + show_confirmation($title,$body,$url); + break; + case 'update_from': + if (AmpConfig::get('demo_mode')) { break; } + + // First see if we need to do an add + if ($_POST['add_path'] != '/' AND strlen($_POST['add_path'])) { + if ($catalog_id = Catalog_local::get_from_path($_POST['add_path'])) { + $catalog = Catalog::create_from_id($catalog_id); + $catalog->add_to_catalog(array('subdirectory'=>$_POST['add_path'])); + } + } // end if add + + // Now check for an update + if ($_POST['update_path'] != '/' AND strlen($_POST['update_path'])) { + if ($catalog_id = Catalog_local::get_from_path($_POST['update_path'])) { + $songs = Song::get_from_path($_POST['update_path']); + foreach ($songs as $song_id) { Catalog::update_single_item('song',$song_id); } + } + } // end if update + + echo T_("Done."); + break; + case 'add_catalog': + /* Wah Demo! */ + if (AmpConfig::get('demo_mode')) { break; } + + ob_end_flush(); + + if (!strlen($_POST['type']) || $_POST['type'] == 'none') { + Error::add('general', T_('Error: Please select a catalog type')); + } + + if (!strlen($_POST['name'])) { + Error::add('general', T_('Error: Name not specified')); + } + + if (!Core::form_verify('add_catalog','post')) { + UI::access_denied(); + exit; + } + + // If an error hasn't occured + if (!Error::occurred()) { + + $catalog_id = Catalog::create($_POST); + + if (!$catalog_id) { + require AmpConfig::get('prefix') . '/templates/show_add_catalog.inc.php'; + break; + } + + $catalog = Catalog::create_from_id($catalog_id); + + // Run our initial add + $catalog->add_to_catalog($_POST); + + UI::show_box_top(T_('Catalog Created'), 'box box_catalog_created'); + echo "

" . T_('Catalog Created') . "

"; + Error::display('general'); + Error::display('catalog_add'); + UI::show_box_bottom(); + + show_confirmation('','', AmpConfig::get('web_path').'/admin/catalog.php'); + + } else { + require AmpConfig::get('prefix') . '/templates/show_add_catalog.inc.php'; + } + break; + case 'clear_stats': + if (AmpConfig::get('demo_mode')) { UI::access_denied(); break; } + Stats::clear(); + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Catalog statistics cleared'); + $body = ''; + show_confirmation($title, $body, $url); + break; + case 'show_add_catalog': + require AmpConfig::get('prefix') . '/templates/show_add_catalog.inc.php'; + break; + case 'clear_now_playing': + if (AmpConfig::get('demo_mode')) { UI::access_denied(); break; } + Stream::clear_now_playing(); + show_confirmation(T_('Now Playing Cleared'), T_('All now playing data has been cleared'),AmpConfig::get('web_path') . '/admin/catalog.php'); + break; + case 'show_disabled': + /* Stop the demo hippies */ + if (AmpConfig::get('demo_mode')) { break; } + + $songs = Song::get_disabled(); + if (count($songs)) { + require AmpConfig::get('prefix') . '/templates/show_disabled_songs.inc.php'; + } else { + echo "
" . T_('No Disabled songs found') . "
"; + } + break; + case 'show_delete_catalog': + /* Stop the demo hippies */ + if (AmpConfig::get('demo_mode')) { UI::access_denied(); break; } + + $catalog = Catalog::create_from_id($_REQUEST['catalog_id']); + $nexturl = AmpConfig::get('web_path') . '/admin/catalog.php?action=delete_catalog&catalog_id=' . scrub_out($_REQUEST['catalog_id']); + show_confirmation(T_('Delete Catalog'), T_('Do you really want to delete this catalog?') . " -- $catalog->name ($catalog->path)",$nexturl,1); + break; + case 'show_customize_catalog': + $catalog = Catalog::create_from_id($_REQUEST['catalog_id']); + $catalog->format(); + require_once AmpConfig::get('prefix') . '/templates/show_edit_catalog.inc.php'; + break; + case 'gather_album_art': + toggle_visible('ajax-loading'); + ob_end_flush(); + + $catalogs = $_REQUEST['catalogs'] ? $_REQUEST['catalogs'] : Catalog::get_catalogs(); + + // Iterate throught the catalogs and gather as needed + foreach ($catalogs as $catalog_id) { + $catalog = Catalog::create_from_id($catalog_id); + require AmpConfig::get('prefix') . '/templates/show_gather_art.inc.php'; + flush(); + $catalog->gather_art(); + } + $url = AmpConfig::get('web_path') . '/admin/catalog.php'; + $title = T_('Album Art Search Finished'); + $body = ''; + show_confirmation($title,$body,$url); + break; + case 'show_catalogs': + default: + require_once AmpConfig::get('prefix') . '/templates/show_manage_catalogs.inc.php'; + break; +} // end switch + +/* Show the Footer */ +UI::show_footer(); diff --git a/sources/admin/duplicates.php b/sources/admin/duplicates.php new file mode 100644 index 0000000..ab03ed6 --- /dev/null +++ b/sources/admin/duplicates.php @@ -0,0 +1,45 @@ +set_type('catalog'); + $browse->set_static_content(true); + $browse->save_objects($catalog_ids); + $browse->show_objects($catalog_ids); + $browse->store(); + break; +} + +UI::show_footer(); diff --git a/sources/admin/mail.php b/sources/admin/mail.php new file mode 100644 index 0000000..46df45c --- /dev/null +++ b/sources/admin/mail.php @@ -0,0 +1,75 @@ +subject = $_REQUEST['subject']; + $mailer->message = $_REQUEST['message']; + + if ($_REQUEST['from'] == 'system') { + $mailer->set_default_sender(); + } else { + $mailer->sender = $GLOBALS['user']->email; + $mailer->sender_name = $GLOBALS['user']->fullname; + } + + if ($mailer->send_to_group($_REQUEST['to'])) { + $title = T_('E-mail Sent'); + $body = T_('Your E-mail was successfully sent.'); + } else { + $title = T_('E-mail Not Sent'); + $body = T_('Your E-mail was not sent.'); + } + $url = AmpConfig::get('web_path') . '/admin/mail.php'; + show_confirmation($title,$body,$url); + + break; + default: + require_once AmpConfig::get('prefix') . '/templates/show_mail_users.inc.php'; + break; +} // end switch + +UI::show_footer(); diff --git a/sources/admin/modules.php b/sources/admin/modules.php new file mode 100644 index 0000000..f5657f8 --- /dev/null +++ b/sources/admin/modules.php @@ -0,0 +1,203 @@ +has_access(100)) { + UI::access_denied(); + exit(); +} + + +/* Always show the header */ +UI::show_header(); + +switch ($_REQUEST['action']) { + case 'install_localplay': + $localplay = new Localplay($_REQUEST['type']); + if (!$localplay->player_loaded()) { + Error::add('general', T_('Install Failed, Controller Error')); + Error::display('general'); + break; + } + // Install it! + $localplay->install(); + + // Go ahead and enable Localplay (Admin->System) as we assume they want to do that + // if they are enabling this + Preference::update('allow_localplay_playback','-1','1'); + Preference::update('localplay_level',$GLOBALS['user']->id,'100'); + Preference::update('localplay_controller',$GLOBALS['user']->id,$localplay->type); + + header("Location:" . AmpConfig::get('web_path') . '/admin/modules.php?action=show_localplay'); + break; + case 'install_catalog_type': + $type = scrub_in($_REQUEST['type']); + $catalog = Catalog::create_catalog_type($type); + if ($catalog == null) { + Error::add('general', T_('Install Failed, Catalog Error')); + Error::display('general'); + break; + } + + $catalog->install(); + + /* Show Confirmation */ + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_catalog_types'; + $title = T_('Plugin Installed'); + $body = ''; + show_confirmation($title ,$body, $url); + break; + case 'confirm_uninstall_localplay': + $type = scrub_in($_REQUEST['type']); + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=uninstall_localplay&type=' . $type; + $title = T_('Are you sure you want to remove this plugin?'); + $body = ''; + show_confirmation($title,$body,$url,1); + break; + case 'confirm_uninstall_catalog_type': + $type = scrub_in($_REQUEST['type']); + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=uninstall_catalog_type&type=' . $type; + $title = T_('Are you sure you want to remove this plugin?'); + $body = ''; + show_confirmation($title,$body,$url,1); + break; + case 'uninstall_localplay': + $type = scrub_in($_REQUEST['type']); + + $localplay = new Localplay($type); + $localplay->uninstall(); + + /* Show Confirmation */ + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_localplay'; + $title = T_('Plugin Deactivated'); + $body = ''; + show_confirmation($title,$body,$url); + break; + case 'uninstall_catalog_type': + $type = scrub_in($_REQUEST['type']); + + $catalog = Catalog::create_catalog_type($type); + if ($catalog == null) { + Error::add('general', T_('Uninstall Failed, Catalog Error')); + Error::display('general'); + break; + } + $catalog->uninstall(); + + /* Show Confirmation */ + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_catalog_types'; + $title = T_('Plugin Deactivated'); + $body = ''; + show_confirmation($title, $body, $url); + break; + case 'install_plugin': + /* Verify that this plugin exists */ + $plugins = Plugin::get_plugins(); + if (!array_key_exists($_REQUEST['plugin'],$plugins)) { + debug_event('plugins','Error: Invalid Plugin: ' . $_REQUEST['plugin'] . ' selected','1'); + break; + } + $plugin = new Plugin($_REQUEST['plugin']); + if (!$plugin->install()) { + debug_event('plugins','Error: Plugin Install Failed, ' . $_REQUEST['plugin'],'1'); + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_plugins'; + $title = T_('Unable to Install Plugin'); + $body = ''; + show_confirmation($title,$body,$url); + break; + } + + // Don't trust the plugin to this stuff + User::rebuild_all_preferences(); + + /* Show Confirmation */ + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_plugins'; + $title = T_('Plugin Activated'); + $body = ''; + show_confirmation($title,$body,$url); + break; + case 'confirm_uninstall_plugin': + $plugin = scrub_in($_REQUEST['plugin']); + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=uninstall_plugin&plugin=' . $plugin; + $title = T_('Are you sure you want to remove this plugin?'); + $body = ''; + show_confirmation($title,$body,$url,1); + break; + case 'uninstall_plugin': + /* Verify that this plugin exists */ + $plugins = Plugin::get_plugins(); + if (!array_key_exists($_REQUEST['plugin'],$plugins)) { + debug_event('plugins','Error: Invalid Plugin: ' . $_REQUEST['plugin'] . ' selected','1'); + break; + } + $plugin = new Plugin($_REQUEST['plugin']); + $plugin->uninstall(); + + // Don't trust the plugin to do it + User::rebuild_all_preferences(); + + /* Show Confirmation */ + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_plugins'; + $title = T_('Plugin Deactivated'); + $body = ''; + show_confirmation($title,$body,$url); + break; + case 'upgrade_plugin': + /* Verify that this plugin exists */ + $plugins = Plugin::get_plugins(); + if (!array_key_exists($_REQUEST['plugin'],$plugins)) { + debug_event('plugins','Error: Invalid Plugin: ' . $_REQUEST['plugin'] . ' selected','1'); + break; + } + $plugin = new Plugin($_REQUEST['plugin']); + $plugin->upgrade(); + User::rebuild_all_preferences(); + $url = AmpConfig::get('web_path') . '/admin/modules.php?action=show_plugins'; + $title = T_('Plugin Upgraded'); + $body = ''; + show_confirmation($title, $body, $url); + break; + case 'show_plugins': + $plugins = Plugin::get_plugins(); + UI::show_box_top(T_('Plugins'), 'box box_localplay_plugins'); + require_once AmpConfig::get('prefix') . '/templates/show_plugins.inc.php'; + UI::show_box_bottom(); + break; + case 'show_localplay': + $controllers = Localplay::get_controllers(); + UI::show_box_top(T_('Localplay Controllers'), 'box box_localplay_controllers'); + require_once AmpConfig::get('prefix') . '/templates/show_localplay_controllers.inc.php'; + UI::show_box_bottom(); + break; + case 'show_catalog_types': + $catalogs = Catalog::get_catalog_types(); + UI::show_box_top(T_('Catalog Types'), 'box box_catalog_types'); + require_once AmpConfig::get('prefix') . '/templates/show_catalog_types.inc.php'; + UI::show_box_bottom(); + break; + default: + // Rien a faire + break; +} // end switch + +UI::show_footer(); diff --git a/sources/admin/shout.php b/sources/admin/shout.php new file mode 100644 index 0000000..04a1061 --- /dev/null +++ b/sources/admin/shout.php @@ -0,0 +1,61 @@ +object_type,$shout->object_id); + $object->format(); + $client = new User($shout->user); + $client->format(); + require_once AmpConfig::get('prefix') . '/templates/show_edit_shout.inc.php'; + break; + case 'delete': + Shoutbox::delete($_REQUEST['shout_id']); + show_confirmation(T_('Shoutbox Post Deleted'),'',AmpConfig::get('web_path').'/admin/shout.php'); + break; + default: + $browse = new Browse(); + $browse->set_type('shoutbox'); + $browse->set_simple_browse(true); + $shoutbox_ids = $browse->get_objects(); + $browse->show_objects($shoutbox_ids); + $browse->store(); + break; +} // end switch on action + +UI::show_footer(); diff --git a/sources/admin/system.php b/sources/admin/system.php new file mode 100644 index 0000000..d55f108 --- /dev/null +++ b/sources/admin/system.php @@ -0,0 +1,61 @@ +downloadHeaders('ampache.cfg.php','text/plain',false,filesize(AmpConfig::get('prefix') . '/config/ampache.cfg.php.dist')); + echo $final; + exit; + case 'reset_db_charset': + Dba::reset_db_charset(); + show_confirmation(T_('Database Charset Updated'), T_('Your Database and associated tables have been updated to match your currently configured charset'), AmpConfig::get('web_path').'/admin/system.php?action=show_debug'); + break; + case 'show_debug': + $configuration = AmpConfig::get_all(); + if ($_REQUEST['autoupdate'] == 'force') { + $version = AutoUpdate::get_latest_version(true); + } + require_once AmpConfig::get('prefix') . '/templates/show_debug.inc.php'; + break; + default: + // Rien a faire + break; +} // end switch + +UI::show_footer(); diff --git a/sources/admin/users.php b/sources/admin/users.php new file mode 100644 index 0000000..7068d4b --- /dev/null +++ b/sources/admin/users.php @@ -0,0 +1,251 @@ +access) { + $client->update_access($access); + } + if ($email != $client->email) { + $client->update_email($email); + } + if ($website != $client->website) { + $client->update_website($website); + } + if ($username != $client->username) { + $client->update_username($username); + } + if ($fullname != $client->fullname) { + $client->update_fullname($fullname); + } + if ($pass1 == $pass2 && strlen($pass1)) { + $client->update_password($pass1); + } + $client->upload_avatar(); + + show_confirmation(T_('User Updated'), $client->fullname . "(" . $client->username . ")" . T_('updated'), AmpConfig::get('web_path'). '/admin/users.php'); + break; + case 'add_user': + if (AmpConfig::get('demo_mode')) { break; } + + if (!Core::form_verify('add_user','post')) { + UI::access_denied(); + exit; + } + + $username = scrub_in($_POST['username']); + $fullname = scrub_in($_POST['fullname']); + $email = scrub_in($_POST['email']); + $website = scrub_in($_POST['website']); + $access = scrub_in($_POST['access']); + $pass1 = $_POST['password_1']; + $pass2 = $_POST['password_2']; + + if ($pass1 !== $pass2 || !strlen($pass1)) { + Error::add('password', T_("Error Passwords don't match")); + } + + if (empty($username)) { + Error::add('username', T_('Error Username Required')); + } + + /* make sure the username doesn't already exist */ + if (!User::check_username($username)) { + Error::add('username', T_('Error Username already exists')); + } + + if (!Error::occurred()) { + /* Attempt to create the user */ + $user_id = User::create($username, $fullname, $email, $website, $pass1, $access); + if (!$user_id) { + Error::add('general', T_("Error: Insert Failed")); + } + + $user = new User($user_id); + $user->upload_avatar(); + } // if no errors + else { + $_REQUEST['action'] = 'show_add_user'; + break; + } + if ($access == 5) { $access = T_('Guest');} elseif ($access == 25) { $access = T_('User');} elseif ($access == 100) { $access = T_('Admin');} + + /* HINT: %1 Username, %2 Access num */ + show_confirmation(T_('New User Added'),sprintf(T_('%1$s has been created with an access level of %2$s'), $username, $access), AmpConfig::get('web_path').'/admin/users.php'); + break; + case 'enable': + $client = new User($_REQUEST['user_id']); + $client->enable(); + show_confirmation(T_('User Enabled'),$client->fullname . ' (' . $client->username . ')', AmpConfig::get('web_path'). '/admin/users.php'); + break; + case 'disable': + $client = new User($_REQUEST['user_id']); + if ($client->disable()) { + show_confirmation(T_('User Disabled'),$client->fullname . ' (' . $client->username . ')', AmpConfig::get('web_path'). '/admin/users.php'); + } else { + show_confirmation(T_('Error'), T_('Unable to Disabled last Administrator'), AmpConfig::get('web_path').'/admin/users.php'); + } + break; + case 'show_edit': + if (AmpConfig::get('demo_mode')) { break; } + $client = new User($_REQUEST['user_id']); + require_once AmpConfig::get('prefix') . '/templates/show_edit_user.inc.php'; + break; + case 'confirm_delete': + if (AmpConfig::get('demo_mode')) { break; } + if (!Core::form_verify('delete_user')) { + UI::access_denied(); + exit; + } + $client = new User($_REQUEST['user_id']); + if ($client->delete()) { + show_confirmation(T_('User Deleted'), sprintf(T_('%s has been Deleted'), $client->username), AmpConfig::get('web_path'). "/admin/users.php"); + } else { + show_confirmation(T_('Delete Error'), T_("Unable to delete last Admin User"), AmpConfig::get('web_path')."/admin/users.php"); + } + break; + case 'delete': + if (AmpConfig::get('demo_mode')) { break; } + $client = new User($_REQUEST['user_id']); + show_confirmation(T_('Deletion Request'), + sprintf(T_('Are you sure you want to permanently delete %s?'), $client->fullname), + AmpConfig::get('web_path')."/admin/users.php?action=confirm_delete&user_id=" . $_REQUEST['user_id'],1,'delete_user'); + break; + case 'show_delete_avatar': + $user_id = $_REQUEST['user_id']; + + $next_url = AmpConfig::get('web_path') . '/admin/users.php?action=delete_avatar&user_id=' . scrub_out($user_id); + show_confirmation(T_('User Avatar Delete'), T_('Confirm Deletion Request'), $next_url, 1, 'delete_avatar'); + break; + case 'delete_avatar': + if (AmpConfig::get('demo_mode')) { break; } + + if (!Core::form_verify('delete_avatar','post')) { + UI::access_denied(); + exit; + } + + $client = new User($_REQUEST['user_id']); + $client->delete_avatar(); + + $next_url = AmpConfig::get('web_path') . '/admin/users.php'; + show_confirmation(T_('User Avater Deleted'), T_('User Avatar has been deleted'), $next_url); + break; + case 'show_generate_apikey': + $user_id = $_REQUEST['user_id']; + + $next_url = AmpConfig::get('web_path') . '/admin/users.php?action=generate_apikey&user_id=' . scrub_out($user_id); + show_confirmation(T_('Generate new API Key'), T_('Confirm API Key Generation'), $next_url, 1, 'generate_apikey'); + break; + case 'generate_apikey': + if (AmpConfig::get('demo_mode')) { break; } + + if (!Core::form_verify('generate_apikey','post')) { + UI::access_denied(); + exit; + } + + $client = new User($_REQUEST['user_id']); + $client->generate_apikey(); + + $next_url = AmpConfig::get('web_path') . '/admin/users.php'; + show_confirmation(T_('API Key Generated'), T_('New user API Key has been generated.'), $next_url); + break; + /* Show IP History for the Specified User */ + case 'show_ip_history': + /* get the user and their history */ + $working_user = new User($_REQUEST['user_id']); + + if (!isset($_REQUEST['all'])) { + $history = $working_user->get_ip_history(0,1); + } else { + $history = $working_user->get_ip_history(); + } + require AmpConfig::get('prefix') . '/templates/show_ip_history.inc.php'; + break; + case 'show_add_user': + if (AmpConfig::get('demo_mode')) { break; } + require_once AmpConfig::get('prefix') . '/templates/show_add_user.inc.php'; + break; + case 'show_preferences': + $client = new User($_REQUEST['user_id']); + $preferences = Preference::get_all($client->id); + require_once AmpConfig::get('prefix') . '/templates/show_user_preferences.inc.php'; + break; + default: + $browse = new Browse(); + $browse->reset_filters(); + $browse->set_type('user'); + $browse->set_simple_browse(1); + $browse->set_sort('name','ASC'); + $user_ids = $browse->get_objects(); + $browse->show_objects($user_ids); + $browse->store(); + break; +} // end switch on action + +/* Show the footer */ +UI::show_footer(); diff --git a/sources/albums.php b/sources/albums.php new file mode 100644 index 0000000..6949003 --- /dev/null +++ b/sources/albums.php @@ -0,0 +1,239 @@ +has_access('75')) { UI::access_denied(); } + $art = new Art($_GET['album_id'],'album'); + $art->reset(); + show_confirmation(T_('Album Art Cleared'), T_('Album Art information has been removed from the database'),"/albums.php?action=show&album=" . $art->uid); + break; + // Upload album art + case 'upload_art': + + // we didn't find anything + if (empty($_FILES['file']['tmp_name'])) { + show_confirmation(T_('Album Art Not Located'), T_('Album Art could not be located at this time. This may be due to write access error, or the file is not received correctly.'),"/albums.php?action=show&album=" . $_REQUEST['album_id']); + break; + } + + $album = new Album($_REQUEST['album_id']); + // Pull the image information + $data = array('file'=>$_FILES['file']['tmp_name']); + $image_data = Art::get_from_source($data, 'album'); + + // If we got something back insert it + if ($image_data) { + $art = new Art($album->id,'album'); + $art->insert($image_data,$_FILES['file']['type']); + show_confirmation(T_('Album Art Inserted'),'',"/albums.php?action=show&album=" . $album->id); + } + // Else it failed + else { + show_confirmation(T_('Album Art Not Located'), T_('Album Art could not be located at this time. This may be due to write access error, or the file is not received correctly.'),"/albums.php?action=show&album=" . $album->id); + } + + break; + case 'find_art': + // If not a user then kick em out + if (!Access::check('interface','25')) { UI::access_denied(); exit; } + + // Prevent the script from timing out + set_time_limit(0); + + // get the Album information + $album = new Album($_GET['album_id']); + $album->format(); + $art = new Art($album->id,'album'); + $images = array(); + $cover_url = array(); + + // If we've got an upload ignore the rest and just insert it + if (!empty($_FILES['file']['tmp_name'])) { + $path_info = pathinfo($_FILES['file']['name']); + $upload['file'] = $_FILES['file']['tmp_name']; + $upload['mime'] = 'image/' . $path_info['extension']; + $image_data = Art::get_from_source($upload, 'album'); + + if ($image_data) { + $art->insert($image_data,$upload['0']['mime']); + show_confirmation(T_('Album Art Inserted'),'',"/albums.php?action=show&album=" . $_REQUEST['album_id']); + break; + + } // if image data + + } // if it's an upload + + // Build the options for our search + if (isset($_REQUEST['artist_name'])) { + $artist = scrub_in($_REQUEST['artist_name']); + } elseif ($album->artist_count == '1') { + $artist = $album->f_artist_name; + } else { + $artist = ""; + } + if (isset($_REQUEST['album_name'])) { + $album_name = scrub_in($_REQUEST['album_name']); + } else { + $album_name = $album->full_name; + } + + $options['artist'] = $artist; + $options['album_name'] = $album_name; + $options['keyword'] = trim($artist . " " . $album_name); + + // Attempt to find the art. + $images = $art->gather($options); + + if (!empty($_REQUEST['cover'])) { + $path_info = pathinfo($_REQUEST['cover']); + $cover_url[0]['url'] = scrub_in($_REQUEST['cover']); + $cover_url[0]['mime'] = 'image/' . $path_info['extension']; + } + $images = array_merge($cover_url,$images); + + // If we've found anything then go for it! + if (count($images)) { + // We don't want to store raw's in here so we need to strip them out into a separate array + foreach ($images as $index=>$image) { + if ($image['raw']) { + unset($images[$index]['raw']); + } + } // end foreach + // Store the results for further use + $_SESSION['form']['images'] = $images; + require_once AmpConfig::get('prefix') . '/templates/show_album_art.inc.php'; + } + // Else nothing + else { + show_confirmation(T_('Album Art Not Located'), T_('Album Art could not be located at this time. This may be due to write access error, or the file is not received correctly.'),"/albums.php?action=show&album=" . $album->id); + } + + $albumname = $album->name; + $artistname = $artist; + + // Remember the last typed entry, if there was one + if (!empty($_REQUEST['album_name'])) { $albumname = scrub_in($_REQUEST['album_name']); } + if (!empty($_REQUEST['artist_name'])) { $artistname = scrub_in($_REQUEST['artist_name']); } + + require_once AmpConfig::get('prefix') . '/templates/show_get_albumart.inc.php'; + + break; + case 'select_art': + + /* Check to see if we have the image url still */ + $image_id = $_REQUEST['image']; + $album_id = $_REQUEST['album_id']; + + // Prevent the script from timing out + set_time_limit(0); + + $album = new Album($album_id); + $album_groups = $album->get_group_disks_ids(); + + $image = Art::get_from_source($_SESSION['form']['images'][$image_id], 'album'); + $mime = $_SESSION['form']['images'][$image_id]['mime']; + + foreach ($album_groups as $a_id) { + $art = new Art($a_id, 'album'); + $art->insert($image, $mime); + } + + header("Location:" . AmpConfig::get('web_path') . "/albums.php?action=show&album=" . $album_id); + break; + case 'update_from_tags': + // Make sure they are a 'power' user at least + if (!Access::check('interface','75')) { + UI::access_denied(); + exit; + } + + $type = 'album'; + $object_id = intval($_REQUEST['album_id']); + $target_url = AmpConfig::get('web_path') . '/albums.php?action=show&album=' . $object_id; + require_once AmpConfig::get('prefix') . '/templates/show_update_items.inc.php'; + break; + case 'set_track_numbers': + debug_event('albums', 'Set track numbers called.', '5'); + + if (!Access::check('interface','75')) { + UI::access_denied(); + exit; + } + + // Retrieving final song order from url + foreach ($_GET as $key => $data) { + $_GET[$key] = unhtmlentities(scrub_in($data)); + debug_event('albums', $key.'='.$_GET[$key], '5'); + } + + if (isset($_GET['order'])) { + $songs = explode(";", $_GET['order']); + $track = 1; + foreach ($songs as $song_id) { + if ($song_id != '') { + Song::update_track($track, $song_id); + ++$track; + } + } + } + break; + case 'show_missing': + set_time_limit(600); + $mbid = $_REQUEST['mbid']; + $walbum = new Wanted(Wanted::get_wanted($mbid)); + + if (!$walbum->id) { + $walbum->mbid = $mbid; + if (isset($_REQUEST['artist'])) { + $artist = new Artist($_REQUEST['artist']); + $walbum->artist = $artist->id; + $walbum->artist_mbid = $artist->mbid; + } elseif (isset($_REQUEST['artist_mbid'])) { + $walbum->artist_mbid = $_REQUEST['artist_mbid']; + } + } + $walbum->load_all(); + $walbum->format(); + require AmpConfig::get('prefix') . '/templates/show_missing_album.inc.php'; + break; + // Browse by Album + case 'show': + default: + $album = new Album($_REQUEST['album']); + $album->format(); + + if (!count($album->album_suite)) { + require AmpConfig::get('prefix') . '/templates/show_album.inc.php'; + } else { + require AmpConfig::get('prefix') . '/templates/show_album_group_disks.inc.php'; + } + + break; +} // switch on view + +UI::show_footer(); diff --git a/sources/artists.php b/sources/artists.php new file mode 100644 index 0000000..c6d86c8 --- /dev/null +++ b/sources/artists.php @@ -0,0 +1,83 @@ +format(); + $object_ids = $artist->get_albums($_REQUEST['catalog']); + $object_type = 'album'; + require_once AmpConfig::get('prefix') . '/templates/show_artist.inc.php'; + break; + case 'show_all_songs': + $artist = new Artist($_REQUEST['artist']); + $artist->format(); + $object_type = 'song'; + $object_ids = $artist->get_songs(); + require_once AmpConfig::get('prefix') . '/templates/show_artist.inc.php'; + break; + case 'update_from_tags': + $type = 'artist'; + $object_id = intval($_REQUEST['artist']); + $target_url = AmpConfig::get('web_path') . "/artists.php?action=show&artist=" . $object_id; + require_once AmpConfig::get('prefix') . '/templates/show_update_items.inc.php'; + break; + case 'match': + case 'Match': + $match = scrub_in($_REQUEST['match']); + if ($match == "Browse" || $match == "Show_all") { $chr = ""; } else { $chr = $match; } + /* Enclose this in the purty box! */ + require AmpConfig::get('prefix') . '/templates/show_box_top.inc.php'; + show_alphabet_list('artists','artists.php',$match); + show_alphabet_form($chr, T_('Show Artists starting with'),"artists.php?action=match"); + require AmpConfig::get('prefix') . '/templates/show_box_bottom.inc.php'; + + if ($match === "Browse") { + show_artists(); + } elseif ($match === "Show_all") { + $offset_limit = 999999; + show_artists(); + } else { + if ($chr == '') { + show_artists('A'); + } else { + show_artists($chr); + } + } + break; + case 'show_missing': + set_time_limit(600); + $mbid = $_REQUEST['mbid']; + $wartist = Wanted::get_missing_artist($mbid); + + require AmpConfig::get('prefix') . '/templates/show_missing_artist.inc.php'; + break; +} // end switch + +UI::show_footer(); diff --git a/sources/batch.php b/sources/batch.php new file mode 100644 index 0000000..d4a9d95 --- /dev/null +++ b/sources/batch.php @@ -0,0 +1,108 @@ +playlist->get_items(); + $name = $GLOBALS['user']->username . ' - Playlist'; + break; + case 'playlist': + $playlist = new Playlist($_REQUEST['id']); + $media_ids = $playlist->get_songs(); + $name = $playlist->name; + break; + case 'smartplaylist': + $search = new Search('song', $_REQUEST['id']); + $sql = $search->to_sql(); + $sql = $sql['base'] . ' ' . $sql['table_sql'] . ' WHERE ' . + $sql['where_sql']; + $db_results = Dba::read($sql); + while ($row = Dba::fetch_assoc($db_results)) { + $media_ids[] = $row['id']; + } + $name = $search->name; + break; + case 'album': + foreach ($_REQUEST['id'] as $a) { + $album = new Album($a); + if (empty($name)) { + $name = $album->name; + } + $asongs = $album->get_songs(); + foreach ($asongs as $song_id) { + $media_ids[] = $song_id; + } + } + break; + case 'artist': + $artist = new Artist($_REQUEST['id']); + $media_ids = $artist->get_songs(); + $name = $artist->name; + break; + case 'browse': + $id = scrub_in($_REQUEST['browse_id']); + $browse = new Browse($id); + $browse_media_ids = $browse->get_saved(); + foreach ($browse_media_ids as $media_id) { + switch ($_REQUEST['type']) { + case 'album': + $album = new Album($media_id); + $media_ids = array_merge($media_ids, $album->get_songs()); + break; + case 'song': + $media_ids[] = $media_id; + break; + case 'video': + $media_ids[] = array('Video', $media_id); + break; + } // switch on type + } // foreach media_id + $name = 'Batch-' . date("dmY",time()); + default: + // Rien a faire + break; +} // action switch + +// Take whatever we've got and send the zip +$song_files = get_song_files($media_ids); +if (is_array($song_files['0'])) { + set_memory_limit($song_files['1']+32); + send_zip($name,$song_files['0']); +} +exit; diff --git a/sources/bin/.htaccess b/sources/bin/.htaccess new file mode 100644 index 0000000..896fbc5 --- /dev/null +++ b/sources/bin/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all \ No newline at end of file diff --git a/sources/bin/catalog_update.inc b/sources/bin/catalog_update.inc new file mode 100644 index 0000000..5807f3f --- /dev/null +++ b/sources/bin/catalog_update.inc @@ -0,0 +1,177 @@ + 1) { + for ($x = 1; $x < count($_SERVER['argv']); $x++) { + + if ($_SERVER['argv'][$x] == "-c") { + $operations_string .= "\n\t" . T_('- Catalog Clean'); + $catclean = 1; + } + elseif ($_SERVER['argv'][$x] == "-v") { + $operations_string .= "\n\t" . T_('- Catalog Verify'); + $catverify = 1; + } + elseif ($_SERVER['argv'][$x] == "-a") { + $operations_string .= "\n\t" . T_('- Catalog Add'); + $catadd = 1; + } + elseif ($_SERVER['argv'][$x] == "-g") { + $operations_string .= "\n\t" . T_('- Catalog Art Gather'); + $artadd = 1; + } + elseif ($_SERVER['argv'][$x] == "-i") { + $operations_string .= "\n\t" . T_('- Playlist Import'); + $plimp = 1; + } + else { + if ($where) $where .= " OR "; + $where .= "name LIKE '%" . Dba::escape(preg_replace("/[^a-z0-9\. -]/i", "", $_SERVER['argv'][$x])) . "%'"; + } + } +} + +if (count($_SERVER['argv']) != 1 AND $artadd != 1 && $catclean != 1 && $catverify != 1 && $catadd != 1) { + usage(); + exit; +} + +if ($artadd == 0 && $catclean == 0 && $catverify == 0 && $catadd == 0) { //didn't pass any clean/verify/add arguments + $catclean = 1; //set them all to on + $catverify = 1; + $catadd = 1; + $artadd = 1; +} + +echo T_("Starting Catalog Operations...") . $operations_string . "\n"; + +if ($where) $where = "($where) AND catalog_type='local'"; +else $where = "catalog_type='local'"; +$sql = "SELECT id FROM catalog"; +if ($where) $sql .= " WHERE $where"; +$db_results = Dba::read($sql); + +ob_start("ob_html_strip",'1024',true); + +while ($row = Dba::fetch_row($db_results)) { + + $catalog = Catalog::create_from_id($row['0']); + printf(T_('Reading: %s'), $catalog->name); + ob_flush(); + echo "\n"; + if ($catclean == 1) { + // Clean out dead files + echo T_("- Starting Clean - "); + echo "\n"; + $catalog->clean_catalog(); + echo "------------------\n\n"; + } + + if ($catverify == 1) { + // Verify Existing + echo T_("- Starting Verify - "); + echo "\n"; + $catalog->verify_catalog($row['0']); + echo "-------------------\n\n"; + } + + if ($catadd == 1) { + // Look for new files + echo T_("- Starting Add - "); + echo "\n"; + $options = array(); + if ($artadd == 1) { + $options['gather_art'] = true; + } + if ($plimp == 1) { + $options['parse_playlist'] = true; + } + $catalog->add_to_catalog($options); + echo "----------------\n\n"; + } elseif ($artadd == 1) { + // Look for album art + echo T_('Starting Album Art Search'); + echo "\n"; + $catalog->gather_art(); + echo "----------------\n\n"; + } +} + +Dba::optimize_tables(); + +ob_end_flush(); +echo "\n"; + +function ob_html_strip($string) { + + //$string = preg_replace("/update_txt\('.+'\);update_txt\('(.+)','.+'\);/","$1",$string); + //$string = preg_replace("/update_.+/","",$string); + $string = str_replace('
', "\n", $string); + $string = strip_tags($string); + $string = html_entity_decode($string); + $string = preg_replace("/[\r\n]+[\s\t]*[\r\n]+/","\n",$string); + $string = trim($string); + return $string; + +} // ob_html_strip + +function usage() { + echo T_("- Catalog Update -"); + echo "\n"; + echo T_("Usage: catalog_update.inc [CATALOG NAME] [-c|-v|-a|-g|-t|-i]"); + echo "\n\t"; + echo T_("Default behavior is to do all except playlist import"); + echo "\n-c\t"; + echo T_('Clean Catalogs'); + echo "\n-v\t"; + echo T_('Verify Catalogs'); + echo "\n-a\t"; + echo T_('Add to Catalogs'); + echo "\n-i\t"; + echo T_('Import Playlists'); + echo "\n-g\t"; + echo T_('Gather Art'); + echo "\n"; + echo "----------------------------------------------------------"; + echo "\n"; +} + +?> diff --git a/sources/bin/channel_run.inc b/sources/bin/channel_run.inc new file mode 100644 index 0000000..8c691ea --- /dev/null +++ b/sources/bin/channel_run.inc @@ -0,0 +1,489 @@ + 1) { + for ($x = 1; $x < $cargv; $x++) { + + if ($_SERVER['argv'][$x] == "-c" && ($x + 1) < $cargv) { + $chanid = intval($_SERVER['argv'][++$x]); + $operations_string .= "\n\t" . T_('- Channel ' . $chanid); + } + elseif ($_SERVER['argv'][$x] == "-v") { + $operations_string .= "\n\t" . T_('- Verbose'); + $verbose = true; + } + elseif ($_SERVER['argv'][$x] == "-p" && ($x + 1) < $cargv) { + $port = intval($_SERVER['argv'][++$x]); + $operations_string .= "\n\t" . T_('- Port ' . $port); + } + } +} + +if ($chanid <= 0) { + usage(); + exit; +} + +// Transcode is mandatory to have consistent stream codec +$transcode_cfg = AmpConfig::get('transcode'); + +if ($transcode_cfg == 'never') { + die('Cannot start channel, transcoding is mandatory to work.'); +} + +echo T_("Starting Channel...") . $operations_string . "\n"; + +$channel = new Channel($chanid); +if (!$channel->id) { + die (T_("Unknown channel.")); +} + +if ($port <= 0) { + if ($channel->fixed_endpoint) { + $address = $channel->interface; + $port = $channel->port; + } else { + $address = "127.0.0.1"; + // Try to find an available port + for ($p = 8200; $p < 8300; ++$p) { + $connection = @fsockopen($address, $p); + if (is_resource($connection)) { + fclose($connection); + } else { + echo T_("Found available port ") . $p . "\n"; + $port = $p; + break; + } + } + } +} + +ob_start(); + +$server_uri = 'tcp://' . $address . ':' . $port; +$server = stream_socket_server($server_uri, $errno, $errorMessage); +if ($server === false) +{ + die("Could not bind to socket: " . $errorMessage); +} +$channel->update_start($start_date, $address, $port, getmypid()); +echo T_("Listening on ") . $address . ':' . $port . "\n"; + +$stream_clients = array(); +$client_socks = array(); +$last_stream = 0; +while(true) +{ + //prepare readable sockets + $read_socks = $client_socks; + if (count($client_socks) < $channel->max_listeners) { + $read_socks[] = $server; + } + + //echo "b\n";ob_flush(); + //start reading and use a large timeout + if(stream_select ( $read_socks, $write, $except, 1)) + { + //new client + if (in_array($server, $read_socks)) + { + $new_client = stream_socket_accept($server); + + if ($new_client) + { + debug_event('channel', 'Connection accepted from ' . stream_socket_get_name($new_client, true) . '.', '5'); + $client_socks[] = $new_client; + $channel->update_listeners(count($client_socks), true); + debug_event('channel', 'Now there are total '. count($client_socks) . ' clients.', '5'); + echo "New client connected.\n"; + ob_flush(); + } + + //delete the server socket from the read sockets + unset($read_socks[array_search($server, $read_socks)]); + } + + // Get new message from existing client + foreach($read_socks as $sock) + { + $data = fread($sock, 1024); + if(!$data) + { + client_disconnect($channel, $client_socks, $stream_clients, $sock); + continue; + } + + $headers = explode("\n", $data); + + if (count($headers) > 0) { + $cmd = explode(" ", $headers[0]); + if ($cmd['0'] == 'GET') { + switch ($cmd['1']) { + case '/stream.' . $channel->stream_type: + $options = array( + 'socket' => $sock, + 'length' => 0 + ); + + for ($i = 1; $i < count($headers); $i++) { + $headerpart = explode(":", $headers[$i], 2); + $header = strtolower(trim($headerpart[0])); + $value = trim($headerpart[1]); + switch ($header) { + case 'icy-metadata': + $options['metadata'] = ($value == '1'); + $options['metadata_lastsent'] = 0; + $options['metadata_lastsong'] = 0; + break; + } + } + + // Stream request + if ($options['metadata']) { + //fwrite($sock, "ICY 200 OK\r\n"); + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + } else { + fwrite($sock, "HTTP/1.1 200 OK\r\n"); + fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); + } + fwrite($sock, "Content-Type: " . Song::type_to_mime($transcode_to) . "\r\n"); + fwrite($sock, "Accept-Ranges: none\r\n"); + + $genre = $channel->get_genre(); + // Send Shoutcast metadata on demand + if ($options['metadata']) { + fwrite($sock, "icy-notice1: " . AmpConfig::get('title') . "\r\n"); + fwrite($sock, "icy-name: " . $channel->name . "\r\n"); + if (!empty($genre)) { + fwrite($sock, "icy-genre: " . $genre . "\r\n"); + } + fwrite($sock, "icy-url: " . $channel->url . "\r\n"); + fwrite($sock, "icy-pub: " . ($channel->is_private) ? '0' : '1' . "\r\n"); + if ($channel->bitrate) { + fwrite($sock, "icy-br: " . strval($channel->bitrate) . "\r\n"); + } + fwrite($sock, "icy-metaint: " . strval($metadata_interval) . "\r\n"); + } + // Send additional Icecast metadata + fwrite($sock, "x-audiocast-server-url: " . $channel->url . "\r\n"); + fwrite($sock, "x-audiocast-name: " . $channel->name . "\r\n"); + fwrite($sock, "x-audiocast-description: " . $channel->description . "\r\n"); + fwrite($sock, "x-audiocast-url: " . $channel->url . "\r\n"); + if (!empty($genre)) { + fwrite($sock, "x-audiocast-genre: " . $genre . "\r\n"); + } + fwrite($sock, "x-audiocast-bitrate: " . strval($channel->bitrate) . "\r\n"); + fwrite($sock, "x-audiocast-public: " . (($channel->is_private) ? "0" : "1") . "\r\n"); + + fwrite($sock, "\r\n"); + + // Add to stream clients list + $key = array_search($sock, $read_socks); + $stream_clients[$key] = $options; + break; + + case '/': + case '/status.xsl': + // Stream request + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Cache-Control: no-store, no-cache, must-revalidate\r\n"); + fwrite($sock, "Content-Type: text/html\r\n"); + fwrite($sock, "\r\n"); + + // Create xsl structure + + // Header + $xsl = ""; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "Icecast Streaming Media Server - Ampache" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "
" . "\n"; + + // Content + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "\"\"" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "

Mount Point /stream." . $channel->stream_type . "

" . "\n"; + $xsl .= "stream_type .".m3u\">M3U" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $genre = $channel->get_genre(); + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $currentsong = ""; + if ($channel->media) { + $currentsong = $channel->media->f_artist . " - " . $channel->media->f_title; + } + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "
Stream Title:" . $channel->name . "
Stream Description:" . $channel->description . "
Content Type:" . Song::type_to_mime($channel->stream_type) . "
Mount Start:" . date("c", $channel->start_date) . "
Bitrate:" . $channel->bitrate . "
Current Listeners:" . $channel->listeners . "
Peak Listeners:" . $channel->peak_listeners . "
Stream Genre:" . $genre . "
Stream URL:url . "\" target=\"_blank\">" . $channel->url . "
Current Song:" . $currentsong . "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "\"\"" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "

" . "\n"; + + // Footer + $xsl .= "
" . "\n"; + $xsl .= "Support Icecast development at www.icecast.org" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "
" . "\n"; + $xsl .= "" . "\n"; + $xsl .= "" . "\n"; + + fwrite($sock, $xsl); + + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + + case '/style.css': + case '/favicon.ico': + case '/images/corner_bottomleft.jpg': + case '/images/corner_bottomright.jpg': + case '/images/corner_topleft.jpg': + case '/images/corner_topright.jpg': + case '/images/icecast.png': + case '/images/key.png': + case '/images/tunein.png': + // Get read file data + $fpath = AmpConfig::get('prefix') . '/channel' . $cmd['1']; + $pinfo = pathinfo($fpath); + + $content_type = 'text/html'; + switch ($pinfo['extension']) { + case 'css': + $content_type = "text/css"; + break; + case 'jpg': + $content_type = "image/jpeg"; + break; + case 'png': + $content_type = "image/png"; + break; + case 'ico': + $content_type = "image/vnd.microsoft.icon"; + break; + } + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Content-Type: " . $content_type . "\r\n"); + $fdata = file_get_contents($fpath); + fwrite($sock, "Content-Length: " . strlen($fdata) . "\r\n"); + fwrite($sock, "\r\n"); + fwrite($sock, $fdata); + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + case '/stream.' . $channel->stream_type . '.m3u': + fwrite($sock, "HTTP/1.0 200 OK\r\n"); + fwrite($sock, "Cache-control: public\r\n"); + fwrite($sock, "Content-Disposition: filename=stream." . $channel->stream_type . ".m3u\r\n"); + fwrite($sock, "Content-Type: audio/x-mpegurl\r\n"); + fwrite($sock, "\r\n"); + + fwrite($sock, $channel->get_stream_url() . "\n"); + + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + default: + debug_event('channel', 'Unknown request. Closing connection.', '3'); + fclose($sock); + unset($client_socks[array_search($sock, $client_socks)]); + break; + } + } + } + // Handle data parse + } + } + + if ($channel->bitrate) { + $time_offset = microtime(true) - $last_stream; + //$mtime = ($last_stream > 0 && $time_offset < 1000000) ? $time_offset : 1; + $mtime = 1; + if ($last_stream > 0 && $time_offset < 1) { + usleep(1000000 - ($time_offset * 1000000)); + } elseif ($last_stream > 0) { + //$mtime = $time_offset; + } + $nb_chunks = ceil(($mtime * $channel->bitrate * 1000) / 4096); + } else { + $nb_chunks = 1; + } + + // Get multiple chunks according to bitrate to return enough data per second (because sleep with socket select) + for ($c = 0; $c < $nb_chunks; $c++) { + $chunk = $channel->get_chunk(); + $chunklen = strlen($chunk); + if ($chunklen > 0) { + foreach($stream_clients as $key => $client) + { + $sock = $client['socket']; + if(!is_resource($sock)) { + client_disconnect($channel, $client_socks, $stream_clients, $sock); + continue; + } + + $clchunk = $chunk; + // Check if we need to insert metadata information + if ($client['metadata']) { + $chkmdlen = ($client['length'] + $chunklen) - $client['metadata_lastsent']; + if ($chkmdlen >= $metadata_interval) { + $subpos = ($client['metadata_lastsent'] + $metadata_interval) - $client['length']; + fwrite($sock, substr($clchunk, 0, $subpos)); + $client['length'] += $subpos; + if ($channel->media->id != $client['metadata_lastsong']) { + $metadata = "StreamTitle='" . str_replace('-', ' ', $channel->media->f_artist) . "-" . $channel->media->f_title . "';"; + $metadata .= chr(0x00); + $metadatalen = ceil(strlen($metadata) / 16); + $metadata = str_pad($metadata, $metadatalen * 16, chr(0x00), STR_PAD_RIGHT); + //debug_event('channel', 'Sending metadata to client...', '5'); + fwrite($sock, chr($metadatalen) . $metadata); + $client['metadata_lastsong'] = $channel->media->id; + } else { + fwrite($sock, chr(0x00)); + } + $client['metadata_lastsent'] = $client['length']; + $clchunk = substr($chunk, $subpos); + } + } + if (strlen($clchunk) > 0) { + fwrite($sock, $clchunk); + $client['length'] += strlen($clchunk); + } + + $stream_clients[$key] = $client; + //debug_event('channel', 'Client stream current length: ' . $client['length'], '5'); + } + } else { + $channel->update_listeners(0); + die('No more data, stream ended.'); + } + + $last_stream = microtime(true); + } +} + +ob_end_flush(); +echo "\n"; + +function client_disconnect($channel, &$client_socks, &$stream_clients, $sock) +{ + $key = array_search($sock, $client_socks); + unset($client_socks[$key]); + unset($stream_clients[$key]); + @fclose($sock); + $channel->update_listeners(count($client_socks)); + debug_event('channel', 'A client disconnected. Now there are total '. count($client_socks) . ' clients.', '5'); + echo "Client disconnected.\n"; + ob_flush(); +} + +function usage() +{ + echo T_("- Channel Listening -"); + echo "\n"; + echo T_("Usage: channel_run.inc [-c {CHANNEL ID}|-p {PORT}|-v]"); + echo "\n\t"; + echo "\n-c {CHANNEL ID}\t"; + echo T_('Channel id to start'); + echo "\n-p {PORT}\t"; + echo T_('Listening port, default get an available port automatically'); + echo "\n-v\t"; + echo T_('Verbose'); + echo "\n"; + echo "----------------------------------------------------------"; + echo "\n"; +} + +?> diff --git a/sources/bin/delete_disabled.inc b/sources/bin/delete_disabled.inc new file mode 100644 index 0000000..38601c2 --- /dev/null +++ b/sources/bin/delete_disabled.inc @@ -0,0 +1,56 @@ + diff --git a/sources/bin/dump_album_art.inc b/sources/bin/dump_album_art.inc new file mode 100644 index 0000000..8238975 --- /dev/null +++ b/sources/bin/dump_album_art.inc @@ -0,0 +1,46 @@ + 1) { + $meta = ($_SERVER['argv']['1'] == 'windows') ? 'windows' : 'linux'; +} + +$catalogs = Catalog::get_catalogs(); + +foreach ($catalogs as $catalog_id) { + $catalog = Catalog::create_from_id($catalog_id); + $catalog->dump_album_art(array('metadata' => $meta)); +} + + +?> diff --git a/sources/bin/fix_filenames.inc b/sources/bin/fix_filenames.inc new file mode 100644 index 0000000..5aef3b3 --- /dev/null +++ b/sources/bin/fix_filenames.inc @@ -0,0 +1,239 @@ + 0) { $source_encoding = trim($input); } +printf (T_('Using %s as source character set'), $source_encoding); +echo "\n"; + +$sql = "SELECT id FROM `catalog` WHERE `catalog_type`='local'"; +$db_results = Dba::read($sql); + +while ($row = Dba::fetch_assoc($db_results)) { + + $catalog = Catalog::create_from_id($row['0']); + printf(T_('Checking %s (%s)'), $catalog->name, $catalog->path); + echo "\n"; + charset_directory_correct($catalog->path); + +} // end of the catalogs + +echo T_('Finished checking filenames for valid chacters'); +echo "\n"; + +/************************************************** + ****************** FUNCTIONS ********************* + **************************************************/ +/** + * charset_directory_correct + * This function calls its self recursivly + * and corrects all of the non-matching filenames + * it looks at the i_am_crazy var and if not set prompts for change + */ +function charset_directory_correct($path) { + + // Correctly detect the slash we need to use here + if (strstr($path,"/")) { + $slash_type = '/'; + } + else { + $slash_type = '\\'; + } + + /* Open up the directory */ + $handle = opendir($path); + + if (!is_resource($handle)) { + printf (T_('ERROR: Unable to open %s'), $path); + echo "\n"; + return false; + } + + if (!chdir($path)) { + printf (T_('ERROR: Unable to chdir to %s'), $path); + echo "\n"; + return false; + } + + while ( false !== ($file = readdir($handle) ) ) { + + if ($file == '.' || $file == '..') { continue; } + + $full_file = $path.$slash_type.$file; + + if (is_dir($full_file)) { + charset_directory_correct($full_file); + continue; + } + + $verify_filename = iconv(AmpConfig::get('site_charset'),AmpConfig::get('site_charset') . '//IGNORE',$full_file); + + if (strcmp($full_file,$verify_filename) != '0') { + $translated_filename = iconv($source_encoding,AmpConfig::get('site_charset') . '//TRANSLIT',$full_file); + + // Make sure the extension stayed the same + if (substr($translated_filename,strlen($translated_filename)-3,3) != substr($full_file,strlen($full_file)-3,3)) { + echo T_("Translation failure, stripping non-valid characters"); + echo "\n"; + $translated_filename = iconv($source_encoding,AmpConfig::get('site_charset') . '//IGNORE',$full_file); + } + + printf (T_('Attempting to Transcode to %s'), AmpConfig::get('site_charset')); + echo "\n"; + echo "--------------------------------------------------------------------------------------------\n"; + printf (T_('OLD: %s has invalid chars'), $full_file); + echo "\n"; + printf (T_('NEW: %s'), $translated_filename); + echo "\n"; + echo "--------------------------------------------------------------------------------------------\n"; + if (!$GLOBALS['i_am_crazy']) { + echo T_("Rename File (Y/N):"); + $input = trim(fgets(STDIN)); + if (strcasecmp($input,'Y') == 0) { charset_rename_file($full_file,$translated_filename); } + else { echo "\n\t"; echo T_('Not Renaming...'); echo "\n\n"; } + } + else { + charset_rename_file($full_file,$translated_filename); + } + } + + } // while reading file + +} // charset_directory_correct + +/** + * charset_rename_file + * This just takes a source / dest and does the renaming + */ +function charset_rename_file($full_file,$translated_filename) { + + // First break out the base directory name and make sure it exists + // in case our crap char is in the directory + $directory = dirname($translated_filename); + $data = preg_split("/[\/\\\]/",$directory); + $path = ''; + + foreach ($data as $dir) { + + $dir = charset_clean_name($dir); + $path .= "/" . $dir; + + if (!is_dir($path)) { + echo "\tMaking $path directory\n"; + $results = mkdir($path); + if (!$results) { + printf (T_('Error: Unable to create %s move failed, stopping'), $path); + echo "\n"; + return false; + } + } // if the dir doesn't exist + + } // end foreach + + // Now to copy the file + $results = copy($full_file,$translated_filename); + + if (!$results) { + echo T_('Error: Copy Failed, not deleteing old file'); + echo "\n"; + return false; + } + + $old_sum = filesize($full_file); + $new_sum = filesize($translated_filename); + + if ($old_sum != $new_sum OR !$new_sum) { + printf (T_('Error: Size Inconsistency, not deleting %s'), $full_file); + echo "\n"; + return false; + } + + $results = unlink($full_file); + + if (!$results) { printf (T_('Error: Unable to delete %s'), $full_file); echo "\n"; return false; } + + echo T_("File Moved..."); + echo "\n\n"; + + return true; + +} // charset_rename_file + +/** + * charset_clean_name + * We have to have some special rules here + * This is run on every individual element of the search + * Before it is put togeather, this removes / and \ and also + * once I figure it out, it'll clean other stuff + */ +function charset_clean_name($string) { + + /* First remove any / or \ chars */ + $string = preg_replace('/[\/\\\]/','-',$string); + + $string = str_replace(':',' ',$string); + + $string = preg_replace('/[\!\:\*]/','_',$string); + + return $string; + +} // charset_clean_name + +?> diff --git a/sources/bin/install/.htaccess b/sources/bin/install/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/sources/bin/install/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/sources/bin/install/add_user.inc b/sources/bin/install/add_user.inc new file mode 100644 index 0000000..570afe3 --- /dev/null +++ b/sources/bin/install/add_user.inc @@ -0,0 +1,60 @@ + [ -l ] [ -p ] [ -w ] [ -n ]\n"; + exit(1); +} + +$username = $options['u']; +$password = isset($options['p']) ? $options['p'] : mt_rand(); +$access = isset($options['l']) ? $options['l'] : AmpConfig::get('auto_user'); +$access = isset($access) ? $access : 'guest'; +$access = is_numeric($access) ? $access : User::access_name_to_level($access); +$email = isset($options['e']) ? $options['e'] : ''; +$website = isset($options['w']) ? $options['w'] : ''; +$name = isset($options['n']) ? $options['n'] : ''; + +if (User::create($username, $name, $email, $website, $password, $access)) { + printf(T_('Created %s user %s with password %s'), T_($access), $username, $password); + echo "\n"; +} +else { + echo T_('User creation failed'), "\n"; + exit(1); +} + +User::fix_preferences('-1'); + +?> diff --git a/sources/bin/install/install_db.inc b/sources/bin/install/install_db.inc new file mode 100644 index 0000000..bd1fb9f --- /dev/null +++ b/sources/bin/install/install_db.inc @@ -0,0 +1,134 @@ + $web_path, + 'database_name' => $db_name, + 'database_username' => $db_user, + 'database_password' => $db_pass, + 'database_hostname' => $db_host, + 'database_port' => $db_port +), true); + +// Install the database +if (!install_insert_db($new_db_user, $new_db_pass, true, $force, true)) { + echo T_('Database creation failed'), "\n"; + echo Error::get('general'), "\n\n"; + exit(1); +} + +AmpConfig::set_by_array(array( + 'database_username' => $new_db_user ?: $db_user, + 'database_password' => $new_db_pass ?: $db_pass +), true); + +// Write the config file +if (!install_create_config()) { + echo T_('Config file creation failed'), "\n"; + echo Error::get('general') . "\n\n"; + exit(1); +} + +/** + * usage + * This just prints out the required params for this script + **/ +function usage() { + echo "Usage: install_db.inc [options]"; + echo "\n\t-U, --database-user\t\t\t"; + echo 'MySQL Admin User'; + echo "\n\t-P, --database-password\t\t\t"; + echo 'MySQL Admin Password'; + echo "\n\t-h, --database-host\t\t\t"; + echo 'MySQL Hostname'; + echo "\n\t--database-port\t\t\t"; + echo 'MySQL Database Port'; + echo "\n\t-d, --database-name\t\t\t"; + echo "MySQL Database Name"; + echo "\n\t-u, --ampache-database-user\t"; + echo 'MySQL Application Username'; + echo "\n\t-p, --ampache-database-password\t"; + echo 'MySQL Application Password'; + echo "\n\t-w, --webpath\t\t\t\t"; + echo 'Web path'; + echo "\n\t-f, --force\t\t\t\t"; + echo 'Force installation', "\n"; + + exit(1); +} + +?> diff --git a/sources/bin/install/update_db.inc b/sources/bin/install/update_db.inc new file mode 100644 index 0000000..997558d --- /dev/null +++ b/sources/bin/install/update_db.inc @@ -0,0 +1,49 @@ + diff --git a/sources/bin/migrate_config.inc b/sources/bin/migrate_config.inc new file mode 100644 index 0000000..55af8b8 --- /dev/null +++ b/sources/bin/migrate_config.inc @@ -0,0 +1,96 @@ +'mysql', + 'tag_order'=>'id3v2,id3v1,vorbiscomment,quicktime,ape,asf', + 'album_art_order'=>'db,id3,folder,lastfm,amazon', + 'amazon_base_urls'=>'http://webservices.amazon.com'); + +$translate = array('local_host'=>'database_hostname', + 'local_db'=>'database_name', + 'local_username'=>'database_username', + 'local_pass'=>'database_password', + 'local_length'=>'session_length', + 'stream_cmd_flac'=>'transcode_cmd_flac', + 'stream_cmd_mp3'=>'transcode_cmd_mp3', + 'stream_cmd_m4a'=>'transcode_cmd_m4a', + 'stream_cmd_ogg'=>'transcode_cmd_ogg', + 'stream_cmd_mpc'=>'transcode_cmd_mpc', + 'sess_name'=>'session_name', + 'sess_cookielife'=>'session_cookielife', + 'sess_cookiesecure'=>'session_cookiesecure'); + +$path = dirname(__FILE__); +$prefix = realpath($path . '/../'); +$old_config = file_get_contents($prefix . '/config/ampache.cfg.php'); + +$data = explode("\n",$old_config); + +echo T_("Parsing old config file..."); +echo "\n"; + +foreach ($data as $line) { + + // Replace a # with ; + if ($line['0'] == '#') { + $line = substr_replace($line,";",0,1); + } + + foreach ($unmigratable as $option=>$default) { + if (strstr($line,$option) AND !$migrated[$option]) { + $line = $option . " = \"$default\""; + $migrated[$option] = true; + } + elseif (strstr($line,$option)) { + $line = ';' . $line; + } + } + + foreach ($translate as $old=>$new) { + if (strstr($line,$old)) { + $line = str_replace($old,$new,$line); + } + } + + $new_config .= $line . "\n"; + +} // end foreach lines + +echo T_("Parse complete, writing"); +echo "\n"; + +$handle = fopen($prefix . '/config/ampache.cfg.php','w'); + +$worked = fwrite($handle,$new_config); + +if ($worked) { + echo T_("Write success, config migrated"); + echo "\n"; +} +else { + echo T_("Access Denied, config migration failed"); + echo "\n"; +} + +?> diff --git a/sources/bin/print_tags.inc b/sources/bin/print_tags.inc new file mode 100644 index 0000000..3890975 --- /dev/null +++ b/sources/bin/print_tags.inc @@ -0,0 +1,89 @@ +sort_pattern; +$file_pattern = $catalog->rename_pattern; + +$info = new vainfo($filename, '', '', '', $dir_pattern, $file_pattern); +if(isset($dir_pattern) || isset($file_pattern)) { + printf(T_('Using: %s AND %s for file pattern matching'), $dir_pattern, $file_pattern); + print "\n"; +} +$info->get_info(); +$results = $info->tags; +$keys = vainfo::get_tag_type($results); +$ampache_results = vainfo::clean_tag_info($results, $keys, $filename); + +echo "\n"; +echo T_('Raw results:'); +echo "\n\n"; +print_r($info); +echo "\n------------------------------------------------------------------\n"; +printf(T_('Final results seen by Ampache using %s:'), implode(' + ', $keys)); +echo "\n\n"; +print_r($ampache_results); + +function usage() { + global $version; + + $text = sprintf(T_('%s Version %s'), 'print_tags.inc', $version); + $text .= "\n\n"; + $text .= T_('Usage:'); + $text .= "\n"; + $text .= T_('php print_tags.inc '); + $text .= "\n\n"; + + return print $text; + +}// usage() + +?> diff --git a/sources/bin/sort_files.inc b/sources/bin/sort_files.inc new file mode 100644 index 0000000..ff449c7 --- /dev/null +++ b/sources/bin/sort_files.inc @@ -0,0 +1,308 @@ +get_songs(); + + printf (T_('Starting Catalog: %s'), stripslashes($catalog->name)); + echo "\n"; + + /* Foreach through each file and find it a home! */ + foreach ($songs as $song) { + /* Find this poor song a home */ + $song->format(); + $song->format_pattern(); + $directory = sort_find_home($song,$catalog->sort_pattern,$catalog->path); + $filename = $song->f_file; + $fullpath = $directory . "/" . $filename; + + /* Check for Demo Mode */ + if ($test_mode) { + /* We're just talking here... no work */ + echo T_("Moving File..."); + echo "\n\t"; + printf (T_('Source: %s'), $song->file); + echo "\n\t"; + printf (T_('Dest: %s'), $fullpath); + echo "\n"; + flush(); + } + /* We need to actually do the moving (fake it if we are testing) + * Don't try to move it, if it's already the same friggin thing! + */ + if ($song->file != $fullpath && strlen($fullpath)) { + sort_move_file($song,$fullpath); + } + + } // end foreach song + +} // end foreach catalogs + +/************** FUNCTIONS *****************/ +/** + * sort_find_home + * Get the directory for this file from the catalog and the song info using the sort_pattern + * takes into account various artists and the alphabet_prefix + */ +function sort_find_home($song,$sort_pattern,$base) { + + $home = rtrim($base,"\/"); + $home = rtrim($home,"\\"); + + /* Create the filename that this file should have */ + $album = sort_clean_name($song->f_album_full); + $artist = sort_clean_name($song->f_artist_full); + $track = sort_clean_name($song->track); + $title = sort_clean_name($song->title); + $year = sort_clean_name($song->year); + $comment = sort_clean_name($song->comment); + + /* Do the various check */ + $album_object = new Album($song->album); + $album_object->format(); + if ($album_object->artist_count != '1') { + $artist = "Various"; + } + + /* IF we want a,b,c,d we need to know the first element */ + if ($GLOBALS['alphabet_prefix']) { + $sort_pattern = preg_replace("/\/?%o\//","",$sort_pattern); + $first_element = substr($sort_pattern,0,2); + $element = sort_element_name($first_element); + if (!$element) { $alphabet = 'ZZ'; } + else { $alphabet = strtoupper(substr(${$element},0,1)); } + $alphabet = preg_replace("/[^A-Za-z0-9]/","ZZ",$alphabet); + + $home .= "/$alphabet"; + } + + /* Replace everything we can find */ + $replace_array = array('%a','%A','%t','%T','%y','%c'); + $content_array = array($artist,$album,$title,$track,$year,$comment); + $sort_pattern = str_replace($replace_array,$content_array,$sort_pattern); + + /* Remove non A-Z0-9 chars */ + $sort_pattern = preg_replace("[^\\\/A-Za-z0-9\-\_\ \'\,\(\)]","_",$sort_pattern); + + $home .= "/$sort_pattern"; + + return $home; + +} // sort_find_home + +/** + * sort_element_name + * gets the name of the %? in a yea.. too much beer + */ +function sort_element_name($key) { + + switch ($key) { + case '%t': + return 'title'; + break; + case '%T': + return 'track'; + break; + case '%a': + return 'artist'; + break; + case '%A': + return 'album'; + break; + case '%y': + return 'year'; + break; + default: + break; + } // switch on key + + return false; + +} // sort_element_name + +/** + * sort_clean_name + * We have to have some special rules here + * This is run on every individual element of the search + * Before it is put togeather, this removes / and \ and also + * once I figure it out, it'll clean other stuff + */ +function sort_clean_name($string) { + + /* First remove any / or \ chars */ + $string = preg_replace('/[\/\\\]/','-',$string); + + $string = str_replace(':',' ',$string); + + $string = preg_replace('/[\!\:\*]/','_',$string); + + return $string; + +} // sort_clean_name + +/** + * sort_move_file + * All this function does is, move the friggin file and then update the database + * We can't use the rename() function of PHP because it's functionality depends on the + * current phase of the moon, the alignment of the planets and my current BAL + * Instead we cheeseball it and walk through the new dir structure and make + * sure that the directories exist, once the dirs exist then we do a copy + * and unlink.. This is a little unsafe, and as such it verifys the copy + * worked by doing a filesize() before unlinking. + */ +function sort_move_file($song,$fullname) { + + $old_dir = dirname($song->file); + + $info = pathinfo($fullname); + + $directory = $info['dirname']; + $file = $info['basename']; + $data = preg_split("/[\/\\\]/",$directory); + $path = ''; + + /* We not need the leading / */ + unset($data[0]); + + foreach ($data as $dir) { + + $dir = sort_clean_name($dir); + $path .= "/" . $dir; + + /* We need to check for the existance of this directory */ + if (!is_dir($path)) { + if ($GLOBALS['test_mode']) { + echo "\t"; + printf (T_('Making %s Directory'), $path); + echo "\n"; + } + else { + debug_event('mkdir',"Creating $path directory",'5'); + $results = mkdir($path); + if (!$results) { + printf (T_('Error: Unable to create %s move failed'), $path); + echo "\n"; + return false; + } + } // else we aren't in test mode + } // if it's not a dir + + } // foreach dir + + /* Now that we've got the correct directory structure let's try to copy it */ + if ($GLOBALS['test_mode']) { + echo "\t"; + // HINT: %1$s: file, %2$s: directory + printf (T_('Copying %1$s to %2$s'), $file, $directory); + echo "\n"; + $sql = "UPDATE song SET file='" . Dba::escape($fullname) . "' WHERE id='" . Dba::escape($song->id) . "'"; + echo "\tSQL: $sql\n"; + flush(); + } + else { + + /* Check for file existance */ + if (file_exists($fullname)) { + debug_event('file exists','Error: $fullname already exists','1'); + printf (T_('Error: %s already exists'), $filename); + echo "\n"; + return false; + } + + $results = copy($song->file,$fullname); + debug_event('copy','Copied ' . $song->file . ' to ' . $fullname,'5'); + + /* Look for the folder art and copy that as well */ + if (!AmpConfig::get('album_art_preferred_filename') OR strstr(AmpConfig::get('album_art_preferred_filename'),"%")) { + $folder_art = $directory . '/folder.jpg'; + $old_art = $old_dir . '/folder.jpg'; + } + else { + $folder_art = $directory . "/" . sort_clean_name(AmpConfig::get('album_art_preferred_filename')); + $old_art = $old_dir . "/" . sort_clean_name(AmpConfig::get('album_art_preferred_filename')); + } + + debug_event('copy_art','Copied ' . $old_art . ' to ' . $folder_art,'5'); + @copy($old_art,$folder_art); + + if (!$results) { printf (T_('Error: Unable to copy file to %s'), $fullname); echo "\n"; return false; } + + /* Check the filesize */ + $new_sum = filesize($fullname); + $old_sum = filesize($song->file); + + if ($new_sum != $old_sum OR !$new_sum) { + printf (T_('Error: Size Inconsistency, not deleting %s'), $song->file); + echo "\n"; + return false; + } // end if sum's don't match + + /* If we've made it this far it should be safe */ + $results = unlink($song->file); + if (!$results) { printf (T_('Error: Unable to delete %s'), $song->file); echo "\n"; } + + /* Update the catalog */ + $sql = "UPDATE song SET file='" . Dba::escape($fullname) . "' WHERE id='" . Dba::escape($song->id) . "'"; + $db_results = Dba::write($sql); + + } // end else + + return true; + +} // sort_move_file + +?> diff --git a/sources/bin/websocket_run.inc b/sources/bin/websocket_run.inc new file mode 100644 index 0000000..4bf2d7b --- /dev/null +++ b/sources/bin/websocket_run.inc @@ -0,0 +1,80 @@ + 1) { + for ($x = 1; $x < $cargv; $x++) { + + if ($_SERVER['argv'][$x] == "-v") { + $operations_string .= "\n\t" . T_('- Verbose'); + $verbose = true; + } + elseif ($_SERVER['argv'][$x] == "-p" && ($x + 1) < $cargv) { + $port = intval($_SERVER['argv'][++$x]); + $operations_string .= "\n\t" . T_('- Port ' . $port); + } + } +} + +$urlinfo = parse_url(AmpConfig::get('websocket_address')); +$host = $urlinfo['host']; +if (empty($host)) { + $host = "localhost"; +} + +$app = new Ratchet\App($host, $port, '0.0.0.0'); +$brserver = new Broadcast_Server(); +$brserver->verbose = $verbose; +$app->route('/broadcast', $brserver); +$app->route('/echo', new Ratchet\Server\EchoServer, array('*')); +$app->run(); + +function usage() +{ + echo T_("- WebSocket server -"); + echo "\n"; + echo T_("Usage: websocket_run.inc [-p {PORT}|-v]"); + echo "\n\t"; + echo "\n-p {PORT}\t"; + echo T_('Listening port, default 8100'); + echo "\n-v\t"; + echo T_('Verbose'); + echo "\n"; + echo "----------------------------------------------------------"; + echo "\n"; +} + +?> diff --git a/sources/bin/write_playlists.inc b/sources/bin/write_playlists.inc new file mode 100644 index 0000000..b952ee6 --- /dev/null +++ b/sources/bin/write_playlists.inc @@ -0,0 +1,76 @@ + + + $desc1 + $desc2 + default $desc3 + playlists $desc4 + artist $desc5\n\n"; + + exit($string); + +} // useage +?> diff --git a/sources/broadcast.php b/sources/broadcast.php new file mode 100644 index 0000000..2328216 --- /dev/null +++ b/sources/broadcast.php @@ -0,0 +1,54 @@ +delete()) { + $next_url = AmpConfig::get('web_path') . '/browse.php?action=broadcast'; + show_confirmation(T_('Broadcast Deleted'), T_('The Broadcast has been deleted'), $next_url); + } + UI::show_footer(); + exit; +} // switch on the action + +UI::show_footer(); diff --git a/sources/browse.php b/sources/browse.php new file mode 100644 index 0000000..01a8c1b --- /dev/null +++ b/sources/browse.php @@ -0,0 +1,147 @@ +set_type($_REQUEST['action']); + $browse->set_simple_browse(true); + break; +} // end switch + +UI::show_header(); + +switch ($_REQUEST['action']) { + case 'file': + break; + case 'album': + $browse->set_filter('catalog',$_SESSION['catalog']); + if (AmpConfig::get('catalog_disable')) { + $browse->set_filter('catalog_enabled', '1'); + } + $browse->set_sort('name','ASC'); + $browse->show_objects(); + break; + case 'tag': + //FIXME: This whole thing is ugly, even though it works. + $browse->set_sort('count','ASC'); + // This one's a doozy + $browse->set_simple_browse(false); + $browse->save_objects(Tag::get_tags(/*AmpConfig::get('offset_limit')*/)); // Should add a pager? + $object_ids = $browse->get_saved(); + $keys = array_keys($object_ids); + Tag::build_cache($keys); + UI::show_box_top(T_('Tag Cloud'), 'box box_tag_cloud'); + $browse2 = new Browse(); + $browse2->set_type('song'); + $browse2->store(); + require_once AmpConfig::get('prefix') . '/templates/show_tagcloud.inc.php'; + UI::show_box_bottom(); + $type = $browse2->get_type(); + require_once AmpConfig::get('prefix') . '/templates/browse_content.inc.php'; + break; + case 'artist': + $browse->set_filter('catalog',$_SESSION['catalog']); + if (AmpConfig::get('catalog_disable')) { + $browse->set_filter('catalog_enabled', '1'); + } + $browse->set_sort('name','ASC'); + $browse->show_objects(); + break; + case 'song': + $browse->set_filter('catalog',$_SESSION['catalog']); + if (AmpConfig::get('catalog_disable')) { + $browse->set_filter('catalog_enabled', '1'); + } + $browse->set_sort('title','ASC'); + $browse->show_objects(); + break; + case 'live_stream': + if (AmpConfig::get('catalog_disable')) { + $browse->set_filter('catalog_enabled', '1'); + } + $browse->set_sort('name','ASC'); + $browse->show_objects(); + break; + case 'catalog': + + break; + case 'playlist': + $browse->set_sort('type','ASC'); + $browse->set_filter('playlist_type','1'); + $browse->show_objects(); + break; + case 'smartplaylist': + $browse->set_sort('type', 'ASC'); + $browse->set_filter('playlist_type','1'); + $browse->show_objects(); + break; + case 'channel': + $browse->set_sort('id', 'ASC'); + $browse->show_objects(); + break; + case 'broadcast': + $browse->set_sort('id', 'ASC'); + $browse->show_objects(); + break; + case 'video': + if (AmpConfig::get('catalog_disable')) { + $browse->set_filter('catalog_enabled', '1'); + } + $browse->set_sort('title','ASC'); + $browse->show_objects(); + break; + default: + + break; +} // end Switch $action + +$browse->store(); + +/* Show the Footer */ +UI::show_footer(); diff --git a/sources/channel.php b/sources/channel.php new file mode 100644 index 0000000..c58854d --- /dev/null +++ b/sources/channel.php @@ -0,0 +1,89 @@ +id) { + $object->format(); + require_once AmpConfig::get('prefix') . '/templates/show_add_channel.inc.php'; + } + } + UI::show_footer(); + exit; + case 'create': + if (AmpConfig::get('demo_mode')) { + UI::access_denied(); + exit; + } + + if (!Core::form_verify('add_channel','post')) { + UI::access_denied(); + exit; + } + + UI::show_header(); + $created = Channel::create($_REQUEST['name'], $_REQUEST['description'], $_REQUEST['url'], $_REQUEST['type'], $_REQUEST['id'], $_REQUEST['interface'], $_REQUEST['port'], $_REQUEST['admin_password'], $_REQUEST['private'] ?: 0, $_REQUEST['max_listeners'], $_REQUEST['random'] ?: 0, $_REQUEST['loop'] ?: 0, $_REQUEST['stream_type'], $_REQUEST['bitrate']); + + if (!$created) { + require_once AmpConfig::get('prefix') . '/templates/show_add_channel.inc.php'; + } else { + $title = T_('Channel Created'); + show_confirmation($title, $body, AmpConfig::get('web_path') . '/browse.php?action=channel'); + } + UI::show_footer(); + exit; + case 'show_delete': + UI::show_header(); + $id = $_REQUEST['id']; + + $next_url = AmpConfig::get('web_path') . '/channel.php?action=delete&id=' . scrub_out($id); + show_confirmation(T_('Channel Delete'), T_('Confirm Deletion Request'), $next_url, 1, 'delete_channel'); + UI::show_footer(); + exit; + case 'delete': + if (AmpConfig::get('demo_mode')) { + UI::access_denied(); + exit; + } + + UI::show_header(); + $id = $_REQUEST['id']; + $channel = new Channel($id); + if ($channel->delete()) { + $next_url = AmpConfig::get('web_path') . '/browse.php?action=channel'; + show_confirmation(T_('Channel Deleted'), T_('The Channel has been deleted'), $next_url); + } + UI::show_footer(); + exit; +} // switch on the action + +UI::show_footer(); diff --git a/sources/channel/.htaccess b/sources/channel/.htaccess new file mode 100644 index 0000000..0eb7988 --- /dev/null +++ b/sources/channel/.htaccess @@ -0,0 +1,6 @@ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-s + RewriteRule ^([0-9]+)/(.*)$ index.php?channel=$1&target=$2 [PT,L,QSA] + \ No newline at end of file diff --git a/sources/channel/favicon.ico b/sources/channel/favicon.ico new file mode 100644 index 0000000..6ce52c9 Binary files /dev/null and b/sources/channel/favicon.ico differ diff --git a/sources/channel/images/corner_bottomleft.jpg b/sources/channel/images/corner_bottomleft.jpg new file mode 100644 index 0000000..ca66106 Binary files /dev/null and b/sources/channel/images/corner_bottomleft.jpg differ diff --git a/sources/channel/images/corner_bottomright.jpg b/sources/channel/images/corner_bottomright.jpg new file mode 100644 index 0000000..7717442 Binary files /dev/null and b/sources/channel/images/corner_bottomright.jpg differ diff --git a/sources/channel/images/corner_topleft.jpg b/sources/channel/images/corner_topleft.jpg new file mode 100644 index 0000000..7bac781 Binary files /dev/null and b/sources/channel/images/corner_topleft.jpg differ diff --git a/sources/channel/images/corner_topright.jpg b/sources/channel/images/corner_topright.jpg new file mode 100644 index 0000000..a33a032 Binary files /dev/null and b/sources/channel/images/corner_topright.jpg differ diff --git a/sources/channel/images/icecast.png b/sources/channel/images/icecast.png new file mode 100644 index 0000000..bff2478 Binary files /dev/null and b/sources/channel/images/icecast.png differ diff --git a/sources/channel/images/key.png b/sources/channel/images/key.png new file mode 100644 index 0000000..8c610e7 Binary files /dev/null and b/sources/channel/images/key.png differ diff --git a/sources/channel/images/tunein.png b/sources/channel/images/tunein.png new file mode 100644 index 0000000..44d9347 Binary files /dev/null and b/sources/channel/images/tunein.png differ diff --git a/sources/channel/index.php b/sources/channel/index.php new file mode 100644 index 0000000..4f061a1 --- /dev/null +++ b/sources/channel/index.php @@ -0,0 +1,122 @@ +id) { + debug_event('channel', 'Unknown channel.', '1'); + exit; +} + +if (!function_exists('curl_version')) { + debug_event('channel', 'Error: Curl is required for this feature.', '1'); + exit; +} + +// Authenticate the user here +if ($channel->is_private) { + $is_auth = false; + if (isset($_SERVER['PHP_AUTH_USER'])) { + $htusername = $_SERVER['PHP_AUTH_USER']; + $htpassword = $_SERVER['PHP_AUTH_PW']; + + $auth = Auth::login($htusername, $htpassword); + if ($auth['success']) { + $username = $auth['username']; + $GLOBALS['user'] = new User($username); + $is_auth = true; + Preference::init(); + + if (AmpConfig::get('access_control')) { + if (!Access::check_network('stream',$GLOBALS['user']->id,'25') AND + !Access::check_network('network',$GLOBALS['user']->id,'25')) { + debug_event('UI::access_denied', "Streaming Access Denied: " . $_SERVER['REMOTE_ADDR'] . " does not have stream level access",'3'); + UI::access_denied(); + exit; + } + } + } + } + + if (!$is_auth) { + header('WWW-Authenticate: Basic realm="Ampache Channel Authentication"'); + header('HTTP/1.0 401 Unauthorized'); + echo T_('Unauthorized.'); + exit; + } +} + +$url = 'http://' . $channel->interface . ':' . $channel->port . '/' . $_REQUEST['target']; +// Redirect request to the real channel server +$headers = getallheaders(); +$headers['Host'] = $channel->interface; +$reqheaders = array(); +foreach ($headers as $key => $value) { + $reqheaders[] = $key . ': ' . $value; +} + +$ch = curl_init($url); +curl_setopt_array($ch, array( + CURLOPT_HTTPHEADER => $reqheaders, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HEADERFUNCTION => 'output_header', + CURLOPT_NOPROGRESS => false, + CURLOPT_PROGRESSFUNCTION => 'progress', +)); +curl_exec($ch); +curl_close($ch); + +/** + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function progress($totaldownload, $downloaded, $us, $ud) +{ + ob_flush(); +} + +/** + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function output_header($ch, $header) +{ + $th = trim($header); + if (!empty($th)) { + header($th); + } + return strlen($header); +} diff --git a/sources/channel/style.css b/sources/channel/style.css new file mode 100644 index 0000000..7a10708 --- /dev/null +++ b/sources/channel/style.css @@ -0,0 +1,305 @@ +/****************************************************************************** + + This file styles the bar that goes across the top of all Xiph.Org + pages. + + The style that comes from this was first (to my knowledge) at + http://alistapart.com/stories/practicalcss/ in the + "Splitting the Difference" section. + +******************************************************************************/ + +/* This effect doesn't work at all if all content is pinched in a bit. */ +html, body { + margin: 0; + padding: 0; + color: white !important; + background: black; +} + +.xiphnav { + font-family: Verdana, sans-serif; + font-weight: normal; + padding: .25em; + margin-bottom: .5em; + border-bottom: 1px solid #000; + color: #000; + background: #aaa; +} +.nav h1 { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + font-size: 300%; + color: #fff; + margin-top: 0px; + margin-bottom: 10px; + padding-top: 10px; + padding-bottom: 0px; + padding-left: 90px; + height: 70px; + background: url(images/icecast.png) no-repeat left center; +} +.nav { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + font-size: 90%; +} +.nav a { + color: white; + text-decoration: none; +} +.nav a:hover { + color: #f8ef64; +} +.xiphnav_a { + text-decoration: none; + font-weight: normal; + color: #000; +} +.main { + font-family: Verdana, sans-serif; + background-color: #000; + margin-left: 50px; + width: 90%; +} +.main h1 { + text-decoration: none; + font-weight: bold; + font-size: 300%; + color: #fff; + padding-top: 30px; + padding-bottom: 30px; + padding-left: 90px; + margin-top: 0px; + margin-bottom: 10px; + background: url(images/icecast.png) no-repeat left center; +} +.main iframe { + width: 100%; + border: 0; +} +.news { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + color: #fff; +} +.newsheader { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 110%; + color: #f8ef64; + background: #444; +} +.streamtd { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 85%; + color: #fff; + padding:15px; +} +.streamtd_alt { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 85%; + color: #fff; +} + +.streamtd_alt_2 { + font-family: Verdana, sans-serif; + text-decoration: underline; + font-weight: normal; + font-size: 85%; + color: #fff; +} + +td { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + color: #fff; +} +.roundcont { + background-color: #656565; + color: #fff; + padding: 0px; + margin: 0px; + border-collapse: collapse; +} +.roundcont table{ + border: none; + border-collapse: collapse; +} + +.newscontent { + margin: 0 20px; +} +h3 { + margin: 0px; + padding: 0px; + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + font-size: 110%; + color: #f8ef64; +} +.newscontent h3 { + margin-bottom: 10px; + border-bottom: 1px groove #ACACAC; +} +.newscontent h4 { + margin: 10px 0px; + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + font-size: 110%; + color: #f8ef64; +} +.newscontent p { + margin: 0 0; + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 90%; +} +.newscontent td { + margin: 0 0; + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 90%; +} +.newscontent td.streamdata { + margin: 0 0; + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: normal; + font-size: 90%; + color: #f8ef64; +} +.streamheader table { + width: 100%; + margin-bottom: 10px; + border-bottom: 1px groove #ACACAC; +} +.streamheader td { + margin: 0px; + padding: 6px 0px; + border: 0px solid cyan; +} +.streamheader h3 { + margin: 0px; + border: 0px solid blue; + vertical-align: lower; +} +.streamheader a { + padding: 8px 5px 3px 30px; + text-decoration: none; + margin: 0px 0px 0px 20px; + background: transparent url("images/tunein.png") no-repeat left center; +} +.streamheader a.auth { + margin: 0px; + padding-top: 14px; + padding-bottom: 14px; + background: transparent url("images/key.png") no-repeat left center; +} +.newscontent a { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + color: #f8ef64; +} +.newscontent a:hover { + font-family: Verdana, sans-serif; + color: #fff; +} +.newscontent a.nav2 { + font-family: Verdana, sans-serif; + text-decoration: none; + font-weight: bold; + padding: 2px 9px; + background: #444; + color: #f8ef64; +} +.newscontent a.nav2:hover { + font-family: Verdana, sans-serif; + text-decoration: none; + background: #777; + font-weight: bold; + color: #fff; +} +.poster { + font-family: Verdana, sans-serif; + margin: 50px 0px 20px 0px; + padding-top: 10px; + padding-bottom: 10px; + display: block; + text-decoration: none; + font-size: 100%; + font-weight: bold; + color: #f8ef64; + border-top: 1px groove #ACACAC; +} +.poster a { + color: white; + text-decoration: none; +} + +.nav body { + color: white; + background-color: #656565; +} +.nav a { + margin: 15px; + padding: 0px; +} +.nav table { + font-size: 110%; + text-align: center; + border: none; +} +.roundtop { + background: url(images/corner_topright.jpg) no-repeat top right; +} + +.roundbottom { + background: url(images/corner_bottomright.jpg) no-repeat top right; +} + +.banner td { + font-size: 150%; + vertical-align: top; +} +td.topleft { + background: url("images/corner_topleft.jpg") no-repeat top left; + height: 15px; + width: 15px; +} +td.topright { + background: url("images/corner_topright.jpg") no-repeat top right; + height: 15px; + width: 15px; +} +td.bottomleft { + background: url("images/corner_bottomleft.jpg") no-repeat bottom left; + height: 15px; + width: 15px; +} +td.bottomright { + background: url("images/corner_bottomright.jpg") no-repeat bottom right; + height: 15px; + width: 15px; +} + +img.corner { + width: 15px; + height: 15px; + border: none; + display: block !important; +} diff --git a/sources/config/.gitignore b/sources/config/.gitignore new file mode 100644 index 0000000..654ab29 --- /dev/null +++ b/sources/config/.gitignore @@ -0,0 +1 @@ +ampache.cfg.php diff --git a/sources/config/.htaccess b/sources/config/.htaccess new file mode 100644 index 0000000..896fbc5 --- /dev/null +++ b/sources/config/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all \ No newline at end of file diff --git a/sources/config/ampache.cfg.php.dist b/sources/config/ampache.cfg.php.dist new file mode 100644 index 0000000..07a0102 --- /dev/null +++ b/sources/config/ampache.cfg.php.dist @@ -0,0 +1,816 @@ +;### +;################### +; General Config # +;################### + +; This value is used to detect quickly +; if this config file is up to date +; this is compared against a value hard-coded +; into the init script +config_version = 16 + +;################### +; Path Vars # +;################### + +; The http host of your server. +; If not set, retrieved automatically from client request. +; This setting is required for WebSocket server +; DEFAULT: "" +;http_host = "localhost" + +; The path to your ampache install +; Do not put a trailing / on this path +; For example if your site is located at http://localhost +; than you do not need to enter anything for the web_path +; if it is located at http://localhost/music you need to +; set web_path to /music +; DEFAULT: "" +;web_path = "" + +;############################## +; Session and Login Variables # +;############################## + +; Hostname of your database +; DEFAULT: localhost +database_hostname = localhost + +; Port to use when connecting to your database +; DEFAULT: none +;database_port = 3306 + +; Name of your ampache database +; DEFAULT: ampache +database_name = ampache + +; Username for your ampache database +; DEFAULT: "" +database_username = username + +; Password for your ampache database, this can not be blank +; this is a 'forced' security precaution, the default value +; will not work +; DEFAULT: "" +database_password = password + +; Length that a session will last expressed in seconds. Default is +; one hour. +; DEFAULT: 3600 +session_length = 3600 + +; Length that the session for a single streaming instance will last +; the default is two hours. With some clients, and long songs this can +; cause playback to stop, increase this value if you experience that +; DEFAULT: 7200 +stream_length = 7200 + +; This length defines how long a 'remember me' session and cookie will +; last, the default is 7200, same as length. It is up to the administrator +; of the box to increase this, for reference 86400 = 1 day +; 604800 = 1 week and 2419200 = 1 month +; DEFAULT: 86400 +remember_length = 86400 + +; Name of the Session/Cookie that will sent to the browser +; default should be fine +; DEFAULT: ampache +session_name = ampache + +; Lifetime of the Cookie, 0 == Forever (until browser close) , otherwise in terms of seconds +; If you want cookies to last past a browser close set this to a value in seconds. +; DEFAULT: 0 +session_cookielife = 0 + +; Is the cookie a "secure" cookie? This should only be set to 1 (true) if you are +; running a secure site (HTTPS). +; DEFAULT: 0 +session_cookiesecure = 0 + +; Auth Methods +; This defines which auth methods Auth will attempt to use and in which order. +; If auto_create isn't enabled the user must exist locally. +; DEFAULT: mysql +; VALUES: mysql,ldap,http,pam,external,openid +auth_methods = "mysql" + +; External authentication +; This sets the helper used for external authentication. It should conform to +; the interface used by mod_authnz_external +; DEFAULT: none +;external_authenticator = "/usr/sbin/pwauth" + +; Automatic local password updating +; Determines whether successful authentication against an external source +; will result in an update to the password stored in the database. +; A locally stored password is needed for API access. +; DEFAULT: false +;auth_password_save = "false" + +; Logout redirection target +; Defaults to our own login.php, but we can override it here if, for instance, +; we want to redirect to an SSO provider instead. +; logout_redirect = "http://sso.example.com/logout" + +;##################### +; Program Settings # +;##################### + +; File Pattern +; This defines which file types Ampache will attempt to catalog +; You can specify any file extension you want in here separating them +; with a | +; DEFAULT: mp3|mpc|m4p|m4a|mp4|aac|ogg|rm|wma|asf|flac|spx|ra|ape|shn|wv +catalog_file_pattern = "mp3|mpc|m4p|m4a|mp4|aac|ogg|rm|wma|asf|flac|spx|ra|ape|shn|wv" + +; Video Pattern +; This defines which video file types Ampache will attempt to catalog +; You can specify any file extension you want in here seperating them with +; a | but ampache may not be able to parse them +; DEAFULT: avi|mpg|flv|m4v|webm +catalog_video_pattern = "avi|mpg|flv|m4v|webm" + +; Playlist Pattern +; This defines which playlist types Ampache will attempt to catalog +; You can specify any file extension you want in here seperating them with +; a | but ampache may not be able to parse them +; DEFAULT: m3u|pls|asx|xspf +catalog_playlist_pattern = "m3u|pls|asx|xspf" + +; Prefix Pattern +; This defines which prefix Ampache will ignore when importing tags from +; your music. You may add any prefix you want seperating them with a | +; DEFAULT: The|An|A|Die|Das|Ein|Eine|Les|Le|La +catalog_prefix_pattern = "The|An|A|Die|Das|Ein|Eine|Les|Le|La" + +; Catalog disable +; This defines if catalog can be disabled without removing database entries +; WARNING: this increase sensibly sql requests and slow down Ampache a lot +; DEFAULT: false +;catalog_disable = "false" + +; Use Access List +; Toggle this on if you want ampache to pay attention to the access list +; and only allow streaming/downloading/api-rpc from known hosts api-rpc +; will not work without this on. +; NOTE: Default Behavior is DENY FROM ALL +; DEFAULT: true +access_control = "true" + +; Require Session +; If this is set to true ampache will make sure that the URL passed when +; attempting to retrieve a song contains a valid Session ID This prevents +; others from guessing URL's. This setting is ignored if you have use_auth +; disabled. +; DEFAULT: true +require_session = "true" + +; Require LocalNet Session +; If this is set to true then ampache will require that a valid session +; is passed even on hosts defined in the Local Network ACL. This setting +; has no effect if access_control is not enabled +; DEFAULT: true +require_localnet_session = "true" + +; Multiple Logins +; Added by Vlet 07/25/07 +; When this setting is enabled a user may only be logged in from a single +; IP address at any one time, this is to prevent sharing of accounts +; DEFAULT: false +;prevent_multiple_logins = "false" + +; Downsample Remote +; If this is set to true and access control is on any users who are not +; coming from a defined 'network' ACL will be automatically downsampled +; regardless of their preferences. Requires access_control to be enabled +; DEFAULT: false +;downsample_remote = "false" + +; Track User IPs +; If this is enabled Ampache will log the IP of every completed login +; it will store user,ip,time at one row per login. The results are +; displayed in Admin --> Users +; DEFAULT: false +;track_user_ip = "false" + +; User IP Cardinality +; This defines how many days worth of IP history Ampache will track +; As it is one row per login on high volume sites you will want to +; clear it every now and then. +; DEFAULT: 42 days +;user_ip_cardinality = "42" + +; Allow Zip Download +; This setting allows/disallows using zlib to zip up an entire +; playlist/album for download. Even if this is turned on you will +; still need to enabled downloading for the specific user you +; want to be able to use this function +; DEFAULT: false +;allow_zip_download = "false" + +; File Zip Download +; This settings tells Ampache to attempt to save the zip file +; to the filesystem instead of creating it in memory, you must +; also set tmp_dir_path in order for this to work +; DEFAULT: false +;file_zip_download = "false" + +; File Zip Comment +; This is an optional configuration option that adds a comment +; to your zip files, this only applies if you've got allow_zip_downloads +; DEFAULT: Ampache - Zip Batch Download +;file_zip_comment = "Ampache - Zip Batch Download" + +; Waveform +; This settings tells Ampache to attempt to generate a waveform +; for each song. It requires transcode and encode_args_wav settings. +; You must also set tmp_dir_path in order for this to work +; DEFAULT: false +;waveform = "false" + +; Waveform color +; The waveform color. +; DEFAULT: #FF0000 +;waveform_color = "#FF0000" + +; Temporary Directory Path +; If File Zip Download or Waveform is enabled this must be set to tell +; Ampache which directory to save the temporary file to. Do not put a +; trailing slash or this will not work. +; DEFAULT: false +;tmp_dir_path = "false" + +; This setting throttles a persons downloading to the specified +; bytes per second. This is not a 100% guaranteed function, and +; you should really use a server based rate limiter if you want +; to do this correctly. +; DEFAULT: off +; VALUES: any whole number (in bytes per second) +;throttle_download = 10 + +; This determines the tag order for all cataloged +; music. If none of the listed tags are found then +; ampache will randomly use whatever was found. +; POSSIBLE VALUES: ape asf avi id3v1 id3v2 lyrics3 matroska mpeg quicktime riff +; vorbiscomment +; DEFAULT: id3v2 id3v1 vorbiscomment quicktime matroska ape asf avi mpeg riff +getid3_tag_order = "id3v2,id3v1,vorbiscomment,quicktime,matroska,ape,asf,avi,mpeg,riff" + +; Determines whether we try to autodetect the encoding for id3v2 tags. +; May break valid tags. +; DEFAULT: false +;getid3_detect_id3v2_encoding = "false" + +; This determines the order in which metadata sources are used (and in the +; case of plugins, checked) +; POSSIBLE VALUES (builtins): filename and getID3 +; POSSIBLE VALUES (plugins): MusicBrainz, plus any others you've installed. +; DEFAULT: getID3 filename +metadata_order = "getID3,filename" + +; Un comment if don't want ampache to follow symlinks +; DEFAULT: false +;no_symlinks = "false" + +; Use auth? +; If this is set to "Yes" ampache will require a valid +; Username and password. If this is set to false then ampache +; will not ask you for a username and password. false is only +; recommended for internal only instances +; DEFAULT true +use_auth = "true" + +; Default Auth Level +; If use_auth is set to false then this option is used +; to determine the permission level of the 'default' users +; default is administrator. This setting only takes affect +; if use_auth if false +; POSSIBLE VALUES: user, admin, manager, guest +; DEFAULT: admin +default_auth_level = "admin" + +; 5 Star Ratings +; This allows ratings for almost any object in ampache +; POSSIBLE VALUES: false true +; DEFAULT: true +ratings = "true" + +; User flags/favorites +; This allows user flags for almost any object in ampache as favorite +; POSSIBLE VALUES: false true +; DEFAULT: true +userflags = "true" + +; Direct play +; This allows user to play directly a song or album +; POSSIBLE VALUES: false true +; DEFAULT: true +directplay = "true" + +; Sociable +; This turns on / off all of the "social" features of ampache +; default is on, but if you don't care and just want music +; turn this off to disable all social features. +; DEFAULT: true +sociable = "true" + +; Notify +; This turns on / off all Ampache notifications +; DEFAULT: true +notify = "true" + +; This options will turn on/off Demo Mode +; If Demo mode is on you can not play songs or update your catalog +; in other words.. leave this commented out +; DEFAULT: false +;demo_mode = "false" + +; Caching +; This turns the caching mechanisms on or off, due to a large number of +; problems with people with very large catalogs and low memory settings +; this is off by default as it does significantly increase the memory +; requirments on larger catalogs. If you have the memory this can create +; a 2-3x speed improvement. +; DEFAULT: false +;memory_cache = false + +; Memory Limit +; This defines the "Min" memory limit for PHP if your php.ini +; has a lower value set Ampache will set it up to this. If you +; set it below 16MB getid3() will not work! +; DEFAULT: 32 +;memory_limit = 32 + +; Album Art Preferred Filename +; Specify a filename to look for if you always give the same filename +; i.e. "folder.jpg" Ampache currently only supports jpg/gif and png +; Especially useful if you have a front and a back image in a folder +; comment out if ampache should search for any jpg,gif or png +; DEFAULT: folder.jpg +;album_art_preferred_filename = "folder.jpg" + +; Resize Images * Requires PHP-GD * +; Set this to true if you want Ampache to resize the Album +; art on the fly, this increases load time and CPU usage +; and also requires the PHP-GD library. This is very useful +; If you have high-quality album art and a small upload cap +; DEFAULT: false +;resize_images = "false" + +; Art Gather Order +; Simply arrange the following in the order you would like +; ampache to search. If you want to disable one of the search +; methods simply leave it out. DB should be left as the first +; method unless you want it to overwrite what's already in the +; database +; POSSIBLE VALUES: db tags folder amazon lastfm musicbrainz google +; DEFAULT: db,tags,folder,musicbrainz,lastfm,google +art_order = "db,tags,folder,musicbrainz,lastfm,google" + +; Amazon Developer Key +; These are needed in order to actually use the amazon album art +; Your public key is your 'Access Key ID' +; Your private key is your 'Secret Access Key' +; DEFAULT: false +;amazon_developer_public_key = "" +;amazon_developer_private_key = "" +;amazon_developer_associate_tag = "" + +; Recommendations +; Set this to true to enable display of similar artists or albums +; while browsing. Requires Last.FM. +; DEFAULT: false +;show_similar = "false" + +; Concerts +; Set this to true to enable display of artist concerts +; Requires Last.FM. +; DEFAULT: false +;show_concerts = "false" + +; Last.FM API Key +; Set this to your Last.FM api key to actually use Last.FM for +; recommendations. +;lastfm_api_key = "" + +; Wanted +; Set this to true to enable display missing albums and the +; possibility for users to mark it as wanted. +; DEFAULT: false +;wanted = "false" + +; Wanted types +; Set the allowed types of wanted releases (album,compilation,single,ep,live,remix,promotion,official) +; DEFAULT: album,official +wanted_types = "album,official" + +; Wanted Auto Accept +; Mark wanted requests as accepted by default (no content manager agreement required) +; DEFAULT: false +;wanted_auto_accept = "false" + +; EchoNest API key +; EchoNest provides several music services. Currently used for missing song 30 seconds preview. +;echonest_api_key = "" + +; Broadcasts +; Allow users to broadcast music. +; This feature requires advanced server configuration, please take a look on the wiki for more information. +; DEFAULT: false +;broadcast = "false" + +; Web Socket address +; Declare the web socket server address +; DEFAULT: determined automatically +;websocket_address = "ws://localhost:8100" + +; Amazon base urls +; An array of Amazon sites to search. +; NOTE: This will search each of these sites in turn so don't expect it +; to be lightning fast! +; It is strongly recommended that only one of these is selected at any +; one time +; POSSIBLE VALUES: +; http://webservices.amazon.com +; http://webservices.amazon.co.uk +; http://webservices.amazon.de +; http://webservices.amazon.co.jp +; http://webservices.amazon.fr +; http://webservices.amazon.ca +; Default: http://webservices.amazon.com +amazon_base_urls = "http://webservices.amazon.com" + +; max_amazon_results_pages +; The maximum number of results pages to pull from EACH amazon site +; NOTE: The art search pages through the results returned by your search +; up to this number of pages. As with the base_urls above, this is going +; to take more time, the more pages you ask it to process. +; Of course a good search will return only a few matches anyway. +; It is strongly recommended that you do _not_ change this value +; DEFAULT: 1 page (10 items) +max_amazon_results_pages = 1 + +; Debug +; If this is enabled Ampache will write debugging information to the log file +; DEFAULT: false +;debug = "false" + +; Debug Level +; This should always be set in conjunction with the +; debug option, it defines how prolific you want the +; debugging in ampache to be. values are 1-5. +; 1 == Errors only +; 2 == Error + Failures (login attempts etc.) +; 3 == ?? +; 4 == ?? (Profit!) +; 5 == Information (cataloging progress etc.) +; DEFAULT: 5 +debug_level = 5 + +; Path to Log File +; This defines where you want ampache to log events to +; this will only happen if debug is turned on. Do not +; include trailing slash. You will need to make sure that +; the specified directory exists and your HTTP server has +; write access. +; DEFAULT: NULL +;log_path = "/var/log/ampache" + +; Log filename pattern +; This defines where the log file name pattern. +; %name.%Y%m%d.log will create a different log file every day. +; DEFAULT: %name.%Y%m%d.log +log_filename = "%name.%Y%m%d.log" + +; Charset of generated HTML pages +; Default of UTF-8 should work for most people +; DEFAULT: UTF-8 +site_charset = UTF-8 + +; Locale Charset +; Local charset (mainly for file operations) if different +; from site_charset. +; This is disabled by default, enable only if needed +; (for Windows please set lc_charset to ISO8859-1) +; DEFAULT: ISO8859-1 +;lc_charset = "ISO8859-1" + +; Refresh Limit +; This defines the default refresh limit in seconds for +; pages with dynamic content, such as now playing +; DEFAULT: 60 +; Possible Values: Int > 5 +refresh_limit = "60" + +;######################################################### +; Custom actions (optional) # +;######################################################### + +; Your custom play action title +;custom_play_action_title_0 = "" +; Your custom play action icon name (stored as /images/icon_[your_image].png) +;custom_play_action_icon_0 = "" +; Your custom action script, where: +; - %f: the media file path +; - %c: the excepted codec target (mp3, ogg, ...) +; - %a: the artist name +; - %A: the album name +; - %t: the song title +;custom_play_action_run_0 = "" + +; Example for Karaoke playing +;custom_play_action_title_0 = "Karaoke" +;custom_play_action_icon_0 = "microphone" +;custom_play_action_run_0 = "sox \"%f\" -p oops | ffmpeg -i pipe:0 -f %c pipe:1" + +;######################################################### +; LDAP login info (optional) # +;######################################################### + +; LDAP filter string to use (required) +; For OpenLDAP use "uid" +; For Microsoft Active Directory (MAD) use "sAMAccountName" +; DEFAULT: null +; ldap_filter = "sAMAccountName" + +; LDAP objectclass (required) +; OpanLDAP objectclass = "*" +; MAD objectclass = "organizationalPerson" +; DEFAULT null +;ldap_objectclass = "organizationalPerson" + +; Initial credentials to bind with for searching (optional) +; DEFAULT: null +;ldap_username = "" +;ldap_password = "" + +; Require that the user is in a specific group (optional) +; DEFAULT: null +;ldap_require_group = "cn=yourgroup,ou=yourorg,dc=yoursubdomain,dc=yourdomain,dc=yourtld" + +; This is the search dn used to find users (required) +; DEFAULT: null +;ldap_search_dn = "ou=People,dc=yoursubdomain,dc=yourdomain,dc=yourtld" + +; This is the address of your ldap server (required) +; DEFAULT: null +;ldap_url = "" + +; Attributes where additional user information is stored (optional) +; OpenLDAP ldap_name_field = "cn" +; MAD ldap_name_field = "displayname" +; DEFAULT: null +;ldap_email_field = "mail" +;ldap_name_field = "cn" + +;######################################################### +; OpenID login info (optional) # +;######################################################### + +; Requires specific OpenID Provider Authentication Policy +; DEFAULT: null +; VALUES: PAPE_AUTH_MULTI_FACTOR_PHYSICAL,PAPE_AUTH_MULTI_FACTOR,PAPE_AUTH_PHISHING_RESISTANT +;openid_required_pape = "" + +;######################################################### +; Public Registration settings, defaults to disabled # +;######################################################### + +; This setting will silently create an ampache account +; for anyone who can login using ldap (or any other login +; extension). The default is to create new users as guests +; see auto_user config option if you would like to change this +; DEFAULT: false +;auto_create = "false" + +; This setting turns on/off public registration. It is +; recommended you leave this off, as it will allow anyone to +; sign up for an account on your server. +; REMEMBER: don't forget to set the mail from address further down in the config. +; DEFAULT: false +;allow_public_registration = "false" + +; Require Captcha Text on Image confirmation +; Turning this on requires the user to correctly +; type in the letters in the image created by Captcha +; Default is off because its very hard to detect if it failed +; to draw, or they failed to enter it. +; DEFAULT: false +;captcha_public_reg = "false" + +; This setting turns on/off admin notification of registration. +; DEFAULT: false +;admin_notify_reg = "false" + +; This setting determines whether the user will be created as a disabled user. +; If this is on, an administrator will need to manually enable the account +; before it's usable. +; DEFAULT: false +;admin_enable_required = "false" + +; This setting will allow all registrants/ldap/http users +; to be auto-approved as a user. By default, they will be +; added as a guest and must be promoted by the admin. +; POSSIBLE VALUES: guest, user, admin +; DEFAULT: guest +;auto_user = "guest" + +; This will display the user agreement when registering +; For agreement text, edit templates/user_agreement.php +; User will need to accept the agreement before they can register +; DEFAULT: false +;user_agreement = "false" + +; This disable email confirmation when registering. +; DEFAULT: false +;user_no_email_confirm = "false" + +;######################################################## +; These options control the dynamic downsampling based # +; on current usage # +; *Note* Transcoding must be enabled and working # +;######################################################## + +; Attempt to optimize bandwidth by dynamically lowering the bit rate of new +; streams. Since the bit rate is only adjusted at the beginning of a song, the +; actual cumulative bitrate for concurrent streams can be up to around +; double the configured value. It also only applies to streams that are +; transcoded. +; DEFAULT: none +;max_bit_rate = 576 + +; New dynamically downsampled streams will be denied if they are forced below +; this value. +; DEFAULT: 8 +;min_bit_rate = 48 + +;###################################################### +; These are commands used to transcode non-streaming +; formats to the target file type for streaming. +; This can be useful in re-encoding file types that don't stream +; very well, or if your player doesn't support some file types. +; +; 'Downsampling' will also use these commands. +; +; To state the bleeding obvious, any programs referenced in the transcode +; commands must be installed, in the web server's search path (or referenced +; by their full path), and executable by the web server. + +; Input type selection +; TYPE is the extension. 'allowed' certifies that transcoding works properly for +; this input format. 'required' further forbids the direct streaming of a format +; (e.g. if you store everything in FLAC, but don't want to ever stream that.) +; transcode_TYPE = {allowed|required|false} +; DEFAULT: false +;transcode_m4a = allowed +;transcode_flac = required +;transcode_mpc = required +;transcode_ogg = required +;transcode_wav = required +;transcode_mp3 = allowed + +; Default output format +; DEFAULT: none +;encode_target = mp3 + +; Override the default output format on a per-type basis +; encode_target_TYPE = TYPE +; DEFAULT: none +;encode_target_flac = ogg + +; Allow clients to override transcode settings (output type, bitrate, codec ...) +; DEFAULT: true +transcode_player_customize = true + +; Command configuration. Substitutions will be made as follows: +; %FILE% => filename +; %SAMPLE% => target sample rate +; You can do fancy things like VBR, but consider whether the consequences are +; acceptable in your environment. + +; Master transcode command +; transcode_cmd should be a single command that supports multiple file types, +; such as ffmpeg or avconv. It's still possible to make a configuration that's +; equivalent to the old default, but if you find that necessary you should be +; clever enough to figure out how on your own. +; DEFAULT: none +;transcode_cmd = "ffmpeg -i %FILE%" +;transcode_cmd = "avconv -i %FILE%" +;transcode_cmd = "/usr/bin/neatokeen %FILE%" + +; Specific transcode commands +; It shouldn't be necessary in most cases, but you can override the transcode +; command for specific source formats. It still needs to accept the +; encoding arguments, so the easiest approach is to use your normal command as +; a clearing-house. +; transcode_cmd_TYPE = TRANSCODE_CMD +;transcode_cmd_mid = "timidity -Or -o – %FILE% | ffmpeg -f s16le -i pipe:0" + +; Encoding arguments +; For each output format, you should provide the necessary arguments for +; your transcode_cmd. +; encode_args_TYPE = TRANSCODE_CMD_ARGS +;encode_args_mp3 = "-vn -b:a %SAMPLE%K -c:a libmp3lame -f mp3 pipe:1" +;encode_args_ogg = "-vn -b:a %SAMPLE%K -c:a libvorbis -f ogg pipe:1" +;encode_args_m4a = "-vn -b:a %SAMPLE%K -c:a libfdk_aac -f adts pipe:1" +;encode_args_wav = "-vn -b:a %SAMPLE%K -c:a pcm_s16le -f wav pipe:1" + +;###################################################### +; these options allow you to configure your rss-feed +; layout. rss exists of two parts, main and song main is the information about the feed +; song is the information in the feed. can be multiple items. +; use_rss = false (values true | false) +;DEFAULT: use_rss = false +;use_rss = false +;##################################################### + +;############################# +; Proxy Settings (optional) # +;############################# +; If Ampache is behind an http proxy, specifiy the hostname or IP address +; port, proxyusername, and proxypassword here. +;DEFAULT: not in use +;proxy_host = "192.168.0.1" +;proxy_port = "8080" +;proxy_user = "" +;proxy_pass = "" + +; If Ampache is behind an https reverse proxy, force use HTTPS protocol. +;Default: false +;force_ssl = true + +;############################# +; Mail Settings # +;############################# + +;Method used to send mail +;POSSIBLE VALUES: smtp sendmail php +;DEFAULT: php +;mail_type = "php" + +;Mail domain. +;DEFAULT: example.com +;mail_domain = "example.com" + +;This will be combined with mail_domain and used as the source address for +;emails generated by Ampache. For example, setting this to 'me' will set the +;sender to 'me@example.com'. +;DEFAULT: info +;mail_user = "info" + +;A name to go with the email address. +;DEFAULT: Ampache +;mail_name = "Ampache" + +;How strictly email addresses should be checked. +;easy does a regex match, strict actually performs some SMTP transactions +;to see if we can send to this address. +;POSSIBLE VALUES: strict easy none +; DEFAULT: strict +;mail_check = "strict" + + +;############################ +; sendmail Settings # +;############################ + +;DEFAULT: /usr/sbin/sendmail +;sendmail_path = "/usr/sbin/sendmail" + +;############################# +; SMTP Settings # +;############################# + +;Mail server (hostname or IP address) +;DEFAULT: localhost +;mail_host = "localhost" + +; SMTP port +;DEFAULT: 25 +;mail_port = 25 + +;Secure SMTP +;POSSIBLE VALUES: ssl tls +;DEFAULT: none +;mail_secure_smtp = tls + +;Enable SMTP authentication +;DEFAULT: false +;mail_auth = true + +;SMTP Username +;your mail auth username. +;mail_auth_user = "" + +; SMTP Password +; your mail auth password. +;mail_auth_pass = "" + +;############################# +; Multibyte Settings # +;############################# +; See http://php.net/manual/mbstring.supported-encodings.php +; If you want ID3v1 encoding detection to work, you should uncomment this line +; so that the ordering is sane. +; DEFAULT: auto +;mb_detect_order = "ASCII,UTF-8,EUC-JP,ISO-2022-JP,SJIS,JIS" diff --git a/sources/config/motd.php.dist b/sources/config/motd.php.dist new file mode 100644 index 0000000..9c6cbb3 --- /dev/null +++ b/sources/config/motd.php.dist @@ -0,0 +1,2 @@ + + diff --git a/sources/config/registration_agreement.php.dist b/sources/config/registration_agreement.php.dist new file mode 100644 index 0000000..423460c --- /dev/null +++ b/sources/config/registration_agreement.php.dist @@ -0,0 +1 @@ +**This is the plain TXT or HTML document that is put at the top of the User Registration page** diff --git a/sources/crossdomain.xml b/sources/crossdomain.xml new file mode 100644 index 0000000..f25287b --- /dev/null +++ b/sources/crossdomain.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sources/democratic.php b/sources/democratic.php new file mode 100644 index 0000000..a86d596 --- /dev/null +++ b/sources/democratic.php @@ -0,0 +1,125 @@ +set_parent(); + $democratic->format(); + case 'show_create': + if (!Access::check('interface','75')) { + UI::access_denied(); + break; + } + + // Show the create page + require_once AmpConfig::get('prefix') . '/templates/show_create_democratic.inc.php'; + break; + case 'delete': + if (!Access::check('interface','75')) { + UI::access_denied(); + break; + } + + Democratic::delete($_REQUEST['democratic_id']); + + $title = ''; + $text = T_('The Requested Playlist has been deleted.'); + $url = AmpConfig::get('web_path') . '/democratic.php?action=manage_playlists'; + show_confirmation($title,$text,$url); + break; + case 'create': + // Only power users here + if (!Access::check('interface','75')) { + UI::access_denied(); + break; + } + + if (!Core::form_verify('create_democratic')) { + UI::access_denied(); + exit; + } + + $democratic = Democratic::get_current_playlist(); + + // If we don't have anything currently create something + if (!$democratic->id) { + // Create the playlist + Democratic::create($_POST); + $democratic = Democratic::get_current_playlist(); + } else { + $democratic->update($_POST); + } + + // Now check for additional things we might have to do + if ($_POST['force_democratic']) { + Democratic::set_user_preferences(); + } + + header("Location: " . AmpConfig::get('web_path') . "/democratic.php?action=show"); + break; + case 'manage_playlists': + if (!Access::check('interface','75')) { + UI::access_denied(); + break; + } + // Get all of the non-user playlists + $playlists = Democratic::get_playlists(); + + require_once AmpConfig::get('prefix') . '/templates/show_manage_democratic.inc.php'; + + break; + case 'show_playlist': + default: + $democratic = Democratic::get_current_playlist(); + if (!$democratic->id) { + require_once AmpConfig::get('prefix') . '/templates/show_democratic.inc.php'; + break; + } + + $democratic->set_parent(); + $democratic->format(); + require_once AmpConfig::get('prefix') . '/templates/show_democratic.inc.php'; + $objects = $democratic->get_items(); + Song::build_cache($democratic->object_ids); + Democratic::build_vote_cache($democratic->vote_ids); + $browse = new Browse(); + $browse->set_type('democratic'); + $browse->set_static_content(false); + $browse->save_objects($objects); + $browse->show_objects(); + $browse->store(); + break; +} // end switch on action + +UI::show_footer(); diff --git a/sources/docs/ACKNOWLEDGEMENTS b/sources/docs/ACKNOWLEDGEMENTS new file mode 100644 index 0000000..f2d4f0b --- /dev/null +++ b/sources/docs/ACKNOWLEDGEMENTS @@ -0,0 +1,25 @@ +Acknowledgements +---------- + +* Scott Kveton: Original creator of Ampache +* Robert Hopson +* Andy Morgan +* RosenSama +* latka +* Lamar Hansford +* Lacy Morrow +* Karl Vollmer (vollmerk) +* Paul Arthur MacIain (flowerysong) +* Chris Slamar (cslamar) +* Holger Brunn +* Kevin Purdy (purdyk) +* Charlie Smotherman (porthose) +* XGizzmo +* Spock +* Terence Theijn (pb1dft) +* Mark Kasson +* SoundOfEmotion +* Randy Perkins +* Ben Shields +* Afterster +* SUTJael \ No newline at end of file diff --git a/sources/docs/CHANGELOG.md b/sources/docs/CHANGELOG.md new file mode 100755 index 0000000..e06a605 --- /dev/null +++ b/sources/docs/CHANGELOG.md @@ -0,0 +1,630 @@ +CHANGELOG +========= + +3.7 +---------- +- Added Scrutinizer analyze +- Fixed playlist play with disabled songs (reported by stebe) +- Improved user auto-registration to optionally avoid email validation +- Fixed date.timezone php warnings breaking Ampache API (reported by redcap1) +- Fixed playlist browse with items > 1000 (reported by Tetram67) +- Fixed Amazon API Image support (thanks jbrain) +- Fixed id3v2 multiples genres (reported by Rouzax) +- Improved democratic playlist view to select the first one by default +- Improved German translation (thanks Psy-Virus) +- Fixed playlist view of all users for administrator accounts (reported by stonie08) +- Added option to regroup album disks to one album view +- Changed Ampache logo +- Fixed email validation on user registration (reported by redcap1) +- Added local charset setting +- Improved installation steps and design (thanks changi67) +- Improved Recently Played to not filter songs to one display only +- Fixed Subsonic transcoding support +- Fixed Subsonic offline storage file path (reported by Tetram76) +- Added optional top dock menu +- Added html5 web audio api visualizer and equalizer +- Added `Play List` to localplay mode +- Fixed encoding issue in batch download +- Added pagination to democratic playlists +- Added an option to group albums discs to an unique album +- Added alphabeticalByName and alphabeticalByArtist browse view in Subsonic API +- Fixed album art on xspf generated playlist +- Added stats, playlist and new authentication method to Ampache XML API +- Added responsive tables to automatically hide optional information on small screen +- Added song action buttons (user favorite, rating, ...) to the web player +- Added sortable capability to the web player playlist +- Added Growl notification/scrobbler plugin +- Added artist slideshow photos plugin from Flickr +- Added setting to change Ampache log file name +- Added playlists to Quick and Advanced search +- Added pls, asx and xspf playlist file format import +- Fixed playlist import with song file absolute path (reported by ricksorensen) +- Fixed playlist import with same song file names (reported by captainark) +- Added shoutcast notification at specific time when playing a song with a waveform +- Added Tag edit/delete capability +- Added several search engine links +- Added myPlex support on Plex API +- Added cache on LastFM data +- Added custom buttons play actions +- Added artist pictures slideshow for current playing artist +- Added Broadcast feature +- Added Channel feature with Icecast compatibility +- Replaced Muses Radio Player by jPlayer to keep one web player for all +- Added missing artists in similar artists for Wanted feature +- Added concerts information from LastFM +- Added tabs on artist information +- Added 'add to playlist' direct button on browse items +- Added avatar on users and Gravatar/Libravatar plugins +- Fixed playlist visibility (reported by stonie08) +- Added OpenID authentication +- Fixed m3u import to playlist on catalog creation (reported by jaydoes) +- Improved missing/wanted albums with the capability to browse missing artists +- Added share feature +- Updated French translation +- Added options per browse view (alphabetic, infinite scroll, number of items per page...) +- Fixed several Subsonic players (SubHub, Jamstash...) +- Added option to get beautiful stream url with url rewriting +- Added check to use a new thread for scrobbling if available +- Added confirmation option when closing the currently playing web player +- Added auto-pause web player option between several browse tabs +- Fixed similar artists list with disabled catalogs (reported by stebe) +- Improved Shoutbox (css fix, real time notifications...) +- Fixed iframe basket play action reload +- Fixed wanted album auto-remove +- Fixed MusicBrainz get album art from releases +- Added Waveform feature on songs +- Added AutoUpdate Ampache version check +- Added auto-completion in global Ampache search +- Added option to 'lock' header/sidebars UI +- Fixed catalog export when 'All' selected +- Fixed XBMC Local Play (reported by nakinigit) +- Fixed artist search +- Fixed Random Advanced (reported by stebe) +- Changed song preview directplay icons +- Added Headphones Automatic Music Downloader support as a 'Wanted Process' plugin +- Updated PHPMailer to version 5.2.7 +- Updated getID3 to version 1.9.7 +- Added 'Song Preview' feature on missing albums tracks, with EchoNest api +- Added 'Missing Albums' / 'Wanted List' feature +- Upgraded to MusicBrainz api v2 +- Replaced Snoopy project with Requests project +- Added user-agent on recently played +- Added option to show/hide recently played, time and user-agent per user +- Updated French language +- Added option for iframe or popup web player mode +- Improved Song/Video web player with jPlayer, Radio player with Muse Radio Player +- Added 'add media' to the currently played playlist on web player +- Added dedicated 'Recently Played' page +- Added enable/disable feature on catalogs +- Fixed Config class conflict with PEAR +- Improved recommended artists/songs loading using ajax +- Added a new modern 'Reborn' theme +- Improved Subsonic api backend support (json, ...) +- Added Plex api backend support +- Added artist art/summary when using LastFM api +- Added 'all' link when browsing +- Added option to enable/disable web player technology (flash / html5) +- Fixed artist/song edition +- Improved tag edition +- Added song re-order on album / playlists +- Replaced Prototype with jQuery +- Added 'Favorite' feature on songs/albums/artists +- Added 'Direct Play' feature to play songs without using a playlist +- Added Lyrics plugins (ChartLyrics and LyricWiki) +- Fixed ShoutBox enable/disable (reported by cipriant) +- Added SoundCloud, Dropbox, Subsonic and Google Music catalog plugins +- Improved Catalogs using plug-ins +- Added browse paging to all information pages +- Fixed LDAP authentication with password containing '&' (reported by bruth2) +- Added directories to zip archives +- Improved project code style and added Travis builds +- Added albums default sort preference +- Added number of times an artist/album/song was played +- Fixed installation process without database creation +- Removed administrative flags + +3.6-FUTURE +---------- +- Fixed issue with long session IDs that affected OS X Mavericks and possibly + other newer PHP installations (reported by yebo29) +- Fixed some sort issues (patch by Afterster) +- Fixed Fresh theme display on large screens (patch by Afterster) +- Fixed bug that allowed guests to add radio stations +- Added support for aacp transcoding +- Improved storage efficiency for large browse results +- Fixed unnecessary growth of the tmp_browse table from API usage (reported + by Ondalf) +- Removed external module 'validateEmail' +- Updated PHPMailer to 5.2.6 + +3.6-alpha6 *2013-05-30* +----------------------- +- Fixed date searches using 'before' to use the correct comparison + (patch by thinca) +- Fixed long-standing issue affecting Synology users (patch by NigridsVa) +- Added support for MySQL sockets (based on patches by randomessence) +- Fixed some issues with the logic around memory_limit (reported by CableNinja) +- Fixed issue that sometimes removed ratings after catalog operations (reported + by stebe) +- Fixed catalog song stats (reported by stebe) +- Fixed ACL text field length to allow entry of IPv6 addresses (reported + by Baggypants) +- Fixed regression preventing the use of an existing database during + installation (reported by cjsmo) +- Fixed operating on all catalogs via the web interface + (reported by orbisvicis) +- Added support for nonstandard database ports +- Updated getID3 to 1.9.5 +- Improved the performance of stream playlist creation (reported by AkbarSerad) +- Fixed "Pure Random" / Random URLs (reported by mafe) + +3.6-alpha5 *2013-04-15* +---------------------- +- Fixed persistent XSS vulnerability in user self-editing (reported by + Jean-Lou Hau) +- Fixed persistent XSS vulnerabilities in AJAX object editing (reported by + Jean-Lou Hau) +- Fixed character set detection for ID3v1 tags +- Added matroska to the list of known tag types +- Made the getID3 metadata source work better with tag types that Ampache + doesn't recognise +- Switched from the deprecated mysql extension to PDO +- stderr from the transcode command is now logged for debugging +- Made database updates more robust and verified that a fresh 3.3.3.5 import + will run through the updates without errors +- Added support for external authenticators like pwauth (based on a patch by + sjlu) +- Renamed the local auth method to pam, which is less confusing +- Removed the Flash player +- Added an HTML5 player (patch by Holger Brunn) +- Changed the way themes handle RTL languages +- Fixed a display problem with the Penguin theme by adding a new CSS class + (patch by Fred Thomsen) +- Made transcoding and its configuration more flexible +- Made transcoded streams more standards compliant by not sending a random + value as the Content-Length or claiming that ranged requests are + supported +- Changed rating semantics to distinguish between user ratings and the + global average and add the ability to search for unrated items + (< 1 star) +- Updated Prototype to git HEAD (4ce0b0f) +- Fixed bug that disclosed passwords for plugins to users that didn't + have access to update the password (patch by Fred Thomsen) +- Fixed streaming on Android devices and anything else that expects to + be able to pass a playlist URL to an application and have it work +- Removed the SHOUTcast localplay controller + +3.6-Alpha4 *2012-11-27* +----------------------- +- Removed lyric support, which was broken and ugly +- Removed tight coupling to the PHP mysql extension +- Fixed an issue with adding catalogs on Windows caused by inconsistent + behaviour of is_readable() (reported by Lockzi) + +3.6-Alpha3 *2012-10-15* +----------------------- +- Updated getID3 to 1.9.4b1 +- Removed support for extremely old passwords +- Playlists imported from M3U now retain their ordering + (patch by Florent Fourcot) +- Removed HTML entity encoding of plaintext email (reported by USMC Guy) +- Fixed a search issue which prevented the use of multiple tag rules + (reported by Istarion) +- Fixed ASF tag parsing regression (reported by cygn) + +3.6-Alpha2 *2012-08-15* +----------------------- +- Fixed CLI database load to work regardless of whether it's run from + the top-level directory (reported by porthose) +- Fixed XML cleanup to work with newer versions of libpcre + (patch by Natureshadow) +- Fixed ID3v2 disk number parsing +- Updated getID3 to 1.9.3 +- Added php-gettext for fallback emulation when a locale (or gettext) isn't + supported +- Fixed pluralisation issue in Recently Played +- Added support for extracting MBIDs from M4A files +- Fixed parsing of some tag types (most notably M4A) +- Corrected PLS output to work with more players (reported by bhassel) +- Fixed an issue with compound artists in media with MusicBrainz tags + (reported by greengeek) +- Fixed an issue with filename pattern matching when patterns contained + characters that are part of regex syntax (such as -) +- Fixed display of logic operator in rules (reported by Twister) +- Fixed newsearch issue preventing use of more than 9 rules + (reported by Twister) +- Fixed JSON escaping issue that broke search in some cases + (reported by XeeNiX) +- Overhauled CLI tools for installation and database management +- Fixed admin form issue (reported by the3rdbit) +- Improved efficiency of fetching song lists via the API + (reported by lotan_rm) +- Added admin_enable_required option to user registration +- Fixed session issue preventing some users from streaming + (reported by miir01) +- Quote Content-Disposition header for art, fixes Chrome issue + (patch by Sébastien LIENARD) +- Fixed art URL returned via the API (patch by lotan_rm) +- Fixed video searches (reported by mchugh19) +- Fixed Database Upgrade issue that caused catalog user/pass for + remote catalogs to not be added correctly +- Added the ability to locally cache passwords validated by external + means (e.g. to allow LDAP authenticated users to use the API) +- Fixed session handling to actually use our custom handler + (reported by ss23) +- Fixed Last.FM art method (reported by claudio) +- Updated Captcha PHP to 2.3 +- Updated PHPMailer to 5.2.0 +- Fixed bug in MPD module which affected toggling random or repeat + (patch from jherold) +- Properly escape config values when writing ampache.cfg.php +- Fixed session persistence with auth disabled (reported by Nathanael + Anderson) +- Fixed item count retention for Advanced Random (reported by USAF_Pride) +- Made catalog verify respect memory_cache +- Some catalog operations are now done in chunks, which works better on + large catalogs +- API now returns year and bitrate for songs +- Fixed search_songs API method to use Search::run properly +- Fixed require_session when auth_type is 'local' +- Catalog filtering fix +- Toggle artwork with a button instead of a checkbox (patch from mywindow) +- API handshake code cleanup, including a bugfix from postfuturist +- Improved install process when JavaScript is disabled +- Fixed duplicate searching even more +- Committed minor bugfixes for Penguin theme +- Added Fresh theme +- Fixed spurious API handshake failure output + +3.6-Alpha1 *2011-04-27* +----------------------- +- Fixed forced transcoding +- Fixed display during catalog updates (reported by Demonic) +- Fixed duplicate searching (patch from Demonic) +- Cleaned up transcoding assumptions +- Fixed tag browsing +- Added new search/advanced random/dynamic playlist interface +- byterange handling for ranges starting with 0 (patch from uberbrady) +- Fixed issue with updating ACLs under Windows (reported by Citlali) +- Add function that check ampache and php version from each website. +- Updated each ampache header comment based on phpdocumentor. +- Fixed only admin can browse phpinfo() for security reasons on /info.php +- Added a few translation words. +- Updated version 3.6 on docs/* +- Implemented ldap_require group (patch from eliasp) +- Fix \ in web path under Apache + Windows Bug #135 +- Partial MusicBrainz metadata gathering via plugin +- Metadata code cleanup, support for plugins as metadata sources +- New plugin architecture +- Fixed display charset issue with catalog add/update +- Fixed handling of temporary playlists with >100 items +- Changed Browse from a singleton to multiple instances +- Fixed setting access levels for plugin passwords +- Fixed handling of unusual characters in passwords +- Fixed support for requesting different thumbnail sizes +- Added ability to rate Albums of the Moment +- Added ability to edit/delete playlists while they are displayed +- Fix track numbers not being 0 padded when downloading or renaming. +- Rating search now allows specification of operator (>=, <=, or =) + and uses the same ratings as normal display. +- Add -t to catalog_update.inc for generating thumbnails +- Generate Thumbnails during catalog art operations +- Fixed transcode seeking of Flacs by switching to MM:SS format for + flacs being transcoded +- Change album_art_order to art_order to reflect general nature of + config option +- Fix PHP warning with IP History if no data is found. +- Add -g flag to catalog update to allow for art gathering via cmdline +- Change Update frequency of catalog display to 1 second rather then + %10 reduces cpu load due to javascript excution (Thx Dmole) +- Add bmp to the list of allowed / supported album art types +- Strip extranious whitespace from cmdline catalog update (Thx ascheel) +- Fix catalog size math for catalogs up to 4TB (Thx Joost.t.Hart@planet.nl) +- Fix httpq not correctly skipping to new song +- Fix refreshing of localplay playlist when an item is skipped to +- Fix missing Content-Disposition filename= on non-transcoded songs +- Fix refresh of localplay playlist when you delete a track from it +- Added ability to add Ampache as a search descriptor (Thx Vlet) +- Correct issue with single song downloads +- Removed old useless files +- Added local auth method that uses PHP's PAM module +- Correct potential security issues due to misuse of REQUEST for write + operations rather then POST (Thx Raphael Geissert ) +- Finished switching to Dba::read() Dba::write() for database calls + (Thx dipsol) +- Improved File pattern matching (Thx october.rust) +- Updated Amazon Album art search to current Amazon API specs (Thx Vlet) +- Fix typo that caused song count to not be set on tag xml response +- Fix tag methods so that alpha_match and exact_match work +- Fix limit and offset not working on search_songs API method +- Fix import m3u on catalog build so it does something +- Fix inconsistent view during catalog operations +- Sort malformed files into "Unknown (Broken)" rather then leaving + them in "Unknown (Orphaned)" +- Fix API democratic voting methods (Thx kindachris) +- Add server version to API ping response +- Fix Localplay API methods (Thx thomasa) +- Improve bin/catalog_update.inc to allow only verify, clean or add + (Thx ascheel) +- Fix issue with batch download and UNC paths (Thx greengeek) +- Added config option to turn caching on/off, Default is off +- Fix issue where file tag pattern was ignore if files have no tag structure +- Add TDRC to list of parsed id3v2 tags +- Added the rating to a single song view +- Fix caching issue when updating ratings where they would not + display correctly until a page reload +- Altered the behavior of adding to playlists so that it maintains + playlist order rather then using track order +- Strip excessive \n's from catalog_update (Thx ascheel) +- Fix incorrect default ogg transcode target in base config file +- Fix stream user preferences using cached system preferences + rather then their own +- Fixed prevent_multiple_logins preventing all logins (Thx Hugh) +- Added additional information to installation process +- Fix PHP 5.3 errors (Thx momo-i) +- Fix random methods not working for localplay +- Fixed extra space on prefixed albums (Thx ibizaman) +- Add missing operator on tag and rating searches so they will + work with other methods (Thx kiehnet@netscape.net) +- Add MusicBrainz MBID support to uniqly identify albums and + also get more album art (Thx flowerysong) +- Fix the url to song function +- Add full path to the files needed by the installation just to + make it a little clearer +- Fixed potential endless loop with malformed genre tags in mp3s + (Thx Bernhard Weyrauch) +- Fixed web path always returning false on /test.php +- Updated Man Page to fix litian problems for Debian packaging +- Fixed bug where video was registering as songs for now playing + and stats +- Add phpmailer and change ampache.cfg.php.dist +- Fixed manpage (Thx Porthose) + +3.5 *2009-05-05* +---------------- +- Added complete Czech translation (Thx martin hason) +- Add the AlmightyOatmeal-Sanity check to prevent a clean from + removing all songs if your mount failed, but is still + readable by ampache +- Make the Lang Install page prettier +- Added Check for hash,inet_pton,windows PHP Version to init so + that upgrades without pre-reqs are handled correctly +- Allow mms,mmsh,mmsu,mmst,rstp in Radio Stream URLs +- Fixed a problem where after adding a track to a saved playlist + there was no UI response upon deleting the track without + a page refresh +- Fix an issue where the full version of the album art was never + used even when requested +- Fix maxlength on acl fields being to small for all IPv6 addresses +- Add error message when file exists but is unreadable do not + remove unreadable songs from catalog +- Fixed missing title tag on song browse for the title + (Thx flowerysong) +- Fix htmlchar'd rss feed url +- Fix Port not correctly being added to URL in most cases + even when defined in config + + v.3.5-Beta2 04/07/2009 +- Fix ASX playlists so more data shows up in WMP (Thx Jon611) +- Fix dynamic playlist items so they work in stream methods again +- Fixed Recently played so that it correctly shows unique songs + with the correct data +- Fix some issues with filenames with Multi-byte characters + (Thx Momo-i) +- Add WMV/MPG specific parsing functions (Thx Momo-i) +- Add text to /test.php for hash() and SHA256() support under PHP + section +- Fix SHA256 Support so that it references something that exists +- Fix incorrect debug_event() on login due to typo +- Remove manage democratic playlist as it has no meaning in the + current version +- Run Dba::reset_db_charset() after upgrade in case people are playing + hot potato with their charsets. +- Move Server Preferences to Admin menu (Thx geekdawg) +- Fixed missing web_path reference on radio creation link +- Fixed remote catalog_clean not working +- Fixed xmlrpc get image. getEncoding wasn't static + +3.5-Beta1 *2009-03-15* +---------------------- +- Add democratic methods to api, can now vote, devote, get url + and the current democratic playlist through the api +- Revert to old Random Play method +- Added proxy use for xmlrpcclient +- Added Configuration 'Wizard' for democratic play +- Fixed interface feedback issues with democratic play actions +- Add extension to image urls for the API will add to others as + needed due to additional query requirement. Needed to fix + some DLNA devices +- Fixed typo that caused the height of album art not to display +- Modified database and added GC for tmp_browse table +- Added get lyrics and album art using http proxy server #313 + username, + password patch +- Added lyricswiki link Ticket #70 +- Updated README language +- Updated getid3 library 2.0.0b4 to 2.0.0b5 +- Make the Democratic playlist be associated with the user + who sends it to a 'player' +- Fixed missing page headers on democratic playlist +- Show who voted for the sogns on democratic playlist +- Increase default stream length to account for the fact that movies + are a good bit longer then songs +- Correct Issues with multi-byte characters in Lyrics (Thx Momo-i) +- Added caching to Video +- Added Video calls to the API +- Remove redundent code from Browse class by making it extend + nwe Query class +- Update Prototype to 1.6.0.3 +- Add Time range to advanced search +- Add sorting to Video Browse +- Changed to new Query backend for Browsing and Dynamic Playlists + +3.5-Alpha2 *2009-03-08* +----------------------- +- Fixed caching of objects with no return value +- Fixed updating of songs that should not be updated during catalog + verify +- Added default_user_level config option that allows you to define + the user level when use_auth is false. Also allows manual + login of admin users when use_auth is false. +- Fix Version checking and Version Error Message on install (Thx Paleo) +- Moved Statistics to main menu, split out newest/popular/stats +- Fixed bug where saved Thumbnails were almost never used +- Fixed Localplay HTTPQ and MPD controls to reconize Live Stream + urls. +- Added Localplay controls to API +- Added Added/Updated filters to API include the ability to specify + a date range using ISO 8601 format with [START]/[END] +- Changed API Date format to ISO 8601 +- Fixed Incorrect Caching of Album records that caused the + Name + Year + Disk to not be respected +- Added Lyrics Patch (Thx alister55 & momo-i) +- Fixed password not updating when editing an HTTPQ localplay + instance +- Added Video support +- Fixed normalize tracks not re-displaying playlist correctly +- Fixed now playing now showing currently playing song +- Fixed now playing clear all not correctly refreshing screen +- Fixed adding object to playlist so that it correctly shows the + songs rather then an empty playlist +- Added User Agent to IP History information gathering +- Added Access Control List Wizards to make API interface + setup easier +- Added IPv6 support for Access Control, Sessions, IP History +- Fixed sorting issue on artist when using search method +- Updated flash player to 5.9.5 +- Fixed bug where you admins couldn't edit preferences of + users due to missing 'key' on form +- Added Mime type to Song XML + +3.5-Alpha1 *2008-12-31* +----------------------- +- Fixed sort_files script so that it properly handles variable + album art file names in the directories +- Fixed issue where small thumbnails were used for larger images + if gd based resizing was enabled in the config +- Fixed catalog_update.inc so it doesn't produce errors +- Made democratic play respect force http play +- Make installation error messages more helpful +- Added Swedish (sv_SE) translation (Thanks yeager) +- Allow Add / Verify of sub directories of existing catalogs +- Prevent an fread of 0 bytes if you seek to the end of a file +- Added require_localnet_session config that allows you to exclude + IP(s) from session checks, see config.dist +- Added Nusoap (http://sourceforge.net/projects/nusoap/) library + for use with future lyrics feature +- Fixed problem with flash player where random urls were not being + added correctly +- Fixed problem with user creation using old method (Thx Purdyk) +- Switched to SHA256() for API and Passwords +- Added check for BADTIME error code from Last.FM and correctly + return the error rather then a generic one +- Fix http auth session issues, where every request blew away the + old session information +- Many other minor improvements (Thx Dipsol) +- Fixed warnings in caching code (Thx Dipsol) +- Massive text cleanup (Thx Dipsol) +- Fixed tag searching and improved some other search methods to + prevent SQL warnings on no results +- Improved Test page checks to more accuratly verify putENV support +- Make network downsampling a little more sane, don't require + access level +- Added caching to Playlist dropdown +- Fixed double caching on some objects +- Added base.css and 4 tag 'font' sizes depending on weight/count +- Fixed inline song edit +- Updated registration multi-byte mail. +- Fixed vainfo.class.php didn't catch exception for first analyze. +- Fixed iconv() returns an empty strings (Thx abs0) +- Updated getid3 for multi-byte characters, but some wrong id3tags + have occurred exception error. +- Fixed use_auth = false not correctly re-creating the session if + you had just switched from use_auth = true +- Add links to RSS feeds and set default to TRUE in config.dist +- Fixed Dynamic Random/Related URLs with players that always send + a byte offset (MPD) +- Added Checkbox to use existing Database +- Updated language code and Fixed catalan language code +- Added Emulate gettext() from upgradephp-15 + (http://freshmeat.net/p/upgradephp) +- Fixed Test.php parse error. +- Updated multibyte character strings mail. +- Fixed To send mail don't remove the last comma from recipient. +- Updated More translatable templates. +- Removed merge-messages.sh and Add LANGLIST (each languages + translation statistics). +- Fixed If database name don't named ampache, can't renamed tags + to tag. +- Fixed count issue on browse Artists (Thx Sylvander) +- Fixed prevent_multiple_logins, preventing all logins (Thx hugh) +- Fixed Export catalog headers so it corretly prompts you to download + the file +- Add ability to sort by artist name, album name on song browse +- Implemented caching on artist and album browse, added total + artist time to the many artist view +- Fixed test config page so it bounces you back to the test page + if the config starts parsing correctly +- Fixed browsing so that you can browse two different types in two + windows at the same time +- Improved gather script for translations (Thx momo-i) +- Added paging to the localplay playlist +- Updated German Translation (Thx Laurent) +- Fixed issue where Remote songs would never be removed from + the democratic playlist +- Fixed issue where user preferences weren't set correctly + on stream (Thx lorijho) +- Added caching of user preferences to avoid a SQL query on load + (Thx Protagonist) +- Fixed home menu not always displaying the entire contents +- Fixed logic error with duplicate login setting which caused it + to only work if mysql auth was used +- Changed Passwords to SHA1 will prompt to reset password +- Corrected some translation strings and added jp_JP (Thx momo-i) +- Ignore filenames that start with . (hidden) solves an issue + with mac filesystems +- Fix tracking of stats for downloaded songs +- Fix divide by 0 error during transcode in some configurations +- Remove root mysql pw requirement from installer +- Added Image Dimensions on Find Album Art page +- Added Confirmation Screen to Catalog Deletion +- Reorganized Menu System and Added Modules section +- Fix an error if you try to add a shoutbox for an invalid object + (Thx atrophic) +- Fixed issue with art dump on jpeg files (Thx atrophic) +- Fixed issue with force http play and port not correctly specifying + non-standard http port (Thx Deathcrow) +- Remember Starts With value even if you switch tabs +- Fixed rating caching so it actually completely works now +- Removed redundent UPDATE on session table due to /util.php +- Added Batch Download to single Artist view +- Added back in the direct links on songs, requires download set + to enabled as it's essentially the same thing except with + now playing information tied to it +- Bumped API Version to 350001 and require that a version is sent + with handshake to indicate the application will work +- Removed the MyStrands plugin as did not provide good data, and does + not appear to have been used +- Added Catalog Prefix config option used to determine which prefixes + should not be used for sorting +- Merged Browse Menu with Home +- Added checkbox to single artist view allowing you to enable/disable + album art thumbnails on albums of said artist +- Added timeout override on update_single_item because the function + is a lie +- Fix translations so it's not all german +- Genre Tag is now used as a 'Tag', Browse Genre removed +- Ignore getid3() iconv stuff doesn't seem to work +- Improved fix_filenames.inc, tries a translation first then strips + invalid characters +- Fixed album art not clearing thumbnail correctly on gather +- Fixed localplay instance not displaying correctly after change + until a page refresh +- Fixed endless loop on index if you haven't played a song in + over two years +- Fixed gather art and parse m3u not working on catalog create + also added URL read support to m3u import +- Upped Minimum requirements to Mysql 5.x +- Add codeunde1load's Web 2.0 style tag patch +- Fixed typo in e-mail From: name (Thx Xgizzmo) +- Fixed typo in browse auto_init() which could cause ampache to not + remember your start point in some situations. (Thx Xgizzmo) diff --git a/sources/docs/GPL-LICENSE b/sources/docs/GPL-LICENSE new file mode 100755 index 0000000..d65c21b --- /dev/null +++ b/sources/docs/GPL-LICENSE @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) +^L +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + diff --git a/sources/docs/PLUGINS b/sources/docs/PLUGINS new file mode 100644 index 0000000..4c58f39 --- /dev/null +++ b/sources/docs/PLUGINS @@ -0,0 +1,35 @@ +------------------------------------------------------------------------------- +----------------- PLUGINS - Ampache v.3.6 ---------------------- +------------------------------------------------------------------------------- + +Plugins are placed in modules/plugins; the name of the file must be +.plugin.php, e.g. Dummy.plugin.php. The file must declare a +corresponding class and the name of the class must be prefixed with +Ampache, e.g. AmpacheDummy. + +The following public variables must be declared: + name (string) + description (string) + version (int) - This plugin's version + min_ampache (int) - Minimum Ampache DB version required + max_ampache (int) - Maximum Ampache DB version supported + +The following public methods must be implemented: + install + uninstall + load + +The following public methods may be implemented: + upgrade + +Finally, for the plugin to actually be useful one or more of the following hooks +should be implemented as a public method: + get_metadata(Array $metadata) + The passed array contains the best metadata we've got. + save_rating(Rating $rating, int $new_value) + save_songplay(Song $song) + get_lyrics(Song $song) + process_wanted(Wanted $wanted) + shortener(string $url) + get_photos(string $search) + diff --git a/sources/docs/man/man1/ampache.1 b/sources/docs/man/man1/ampache.1 new file mode 100644 index 0000000..ea5ea52 --- /dev/null +++ b/sources/docs/man/man1/ampache.1 @@ -0,0 +1,119 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.TH "AMPACHE" "1" "December 27, 2008" "Karl Vollmer" "Sound" +.SH "NAME" +Ampache \- is a Web\-based Audio file manager. +.SH "DESCRIPTION" + +Ampache is implemented with MySQL, and PHP. It allows you to view, edit, and +play your audio files via the web. It has support for playlists, artist and album +views, album art, random play, playback via Http, on the Fly Transcoding and +Downsampling, Vote based playback, Mpd and Icecast, Integrated Flash Player, as +well as per user themes and song play tracking. You can also Link multiple Ampache +servers together using XML\-RPC. Ampache supports GETTEXT translations and has a +full translation of many languages. + +.SH "OPTIONS" + +The following scripts are included in Ampache and can be used to automate some +aspects of Ampache. + +catalog_update.inc + +You can automate the update of all, or some of your local catalogs by using the +bin/catalog_update.inc file. By default this will run a clean, verify and update +on all of your local catalogs. You can optionally specify the name of the catalog +and it will only update the specified catalog. Very useful when used with cron or +crontab. + +dump_art.inc + +You can output all of the album art stored in ampache to the filesystem. It will +output the image stored in the database to the directory of the first file in the +album. This can cause problems f your files are not in their own directories. You +can specify the filename to be written by modifying the config file options +relating to album_art_preferred_filename. Ampache will by default output meta +data directory files for linux. + +delete_disabled.inc + +If you would like to delete all of the disabled songs in Ampache you can run the +bin/delete_disabled.inc to delete all of the songs that are disabled in Ampache. +Naturally this requires the user you run the script as to have permissions to +delete the files in question. By default this script will not work if you run +it. You must open it and delete or comment out the line that says $debug=true +for it to actually work. + +sort_files.inc + +You can have Ampache automatically rename and move all of your files based +on the meta data in Ampache using the bin/sort_files.inc script. This requires +write access to the files in question. The files will be renamed and moved based +on their respective File and Folder patterns as defined by the catalog. Ampache +will also by default sort your files into A\-Z sub\-folders to reduce the number +of folders at the top level. You can turn this off by editing the file and setting the +$alphabet_prefix to false, or commenting it out. + +fix_filenames.inc + +Ampache provides some additional tools to help you cope with Character set +mis\-matches. These scripts must be run from the command line. After making +any changes to your tags or the filenames please make sure you update your +catalog and that your local client has the character set and fonts required to +display the tag information. This script looks through the path of your local catalogs +for filenames who contain characters that do not exist in the currently configured +site_charset. It will prompt you for a source character charset, if none is +specified it will use the current output_encoding value from iconv(). + +print_tags.inc + +You can also look at the raw tags inside a file by running the included +print_tags.inc file. This example takes advantage of the hexcat application to get +the hex values of the output. You will need to compare the reported HEX value to +the HEX value according to the defined charset. +.PP +php /bin/print_tags.inc [FILE] +.br +.PP +migrate_config.inc + +This is used to migrate your ampache.cfg.php config file from php4 formating to php5 +formating. This is done to decrease the parsing time of ampache.cfg.php. This is +generally not needed unless you are upgrading from 3.3.3.5 to 3.4.x. +debian/ampache.postinst runs this script automatically if needed. + +.SH "EXAMPLES" +php /usr/share/ampache/www/bin/update_catalog.inc\c +.br +php /usr/share/ampache/www/bin/print_tag.inc \ [path to music file]\c +.br +.SH "FILES" +/usr/share/ampache/www/bin + +.SH "SEE ALSO" +/usr/share/doc/ampache/README.Debian.gz\c +.br +/usr/share/doc/ampache/README.gz\c +.br +http://ampache.org/wiki\c +.br +.SH "AUTHOR" +Ampache was written by Karl Vollmer \c +.PP +This manual page was written by Charlie Smotherman\c +.br +, for the Debian project and Karl Vollmer. diff --git a/sources/favicon.ico b/sources/favicon.ico new file mode 100644 index 0000000..dbec521 Binary files /dev/null and b/sources/favicon.ico differ diff --git a/sources/image.php b/sources/image.php new file mode 100644 index 0000000..0591995 --- /dev/null +++ b/sources/image.php @@ -0,0 +1,135 @@ +name; + + $art = new Art($media->id,$type); + $art->get_db(); + + if (!$art->raw_mime) { + $mime = 'image/jpeg'; + $image = file_get_contents(AmpConfig::get('prefix') . + AmpConfig::get('theme_path') . + '/images/blankalbum.jpg'); + } else { + if ($_GET['thumb']) { + $thumb_data = $art->get_thumb($size); + } + + $mime = isset($thumb_data['thumb_mime']) ? $thumb_data['thumb_mime'] : $art->raw_mime; + $image = isset($thumb_data['thumb']) ? $thumb_data['thumb'] : $art->raw; + } +} + +if (!empty($image)) { + $extension = Art::extension($mime); + $filename = scrub_out($filename . '.' . $extension); + + // Send the headers and output the image + $browser = new Horde_Browser(); + header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + 604800)); + $browser->downloadHeaders($filename, $mime, true); + echo $image; +} diff --git a/sources/images/ampache.png b/sources/images/ampache.png new file mode 100644 index 0000000..a106239 Binary files /dev/null and b/sources/images/ampache.png differ diff --git a/sources/images/blank-pixel.gif b/sources/images/blank-pixel.gif new file mode 100644 index 0000000..17d4390 Binary files /dev/null and b/sources/images/blank-pixel.gif differ diff --git a/sources/images/blankalbum.gif b/sources/images/blankalbum.gif new file mode 100644 index 0000000..a1d25b4 Binary files /dev/null and b/sources/images/blankalbum.gif differ diff --git a/sources/images/blankalbum.jpg b/sources/images/blankalbum.jpg new file mode 100644 index 0000000..33e89a0 Binary files /dev/null and b/sources/images/blankalbum.jpg differ diff --git a/sources/images/close.png b/sources/images/close.png new file mode 100644 index 0000000..5446d5f Binary files /dev/null and b/sources/images/close.png differ diff --git a/sources/images/icon_add.png b/sources/images/icon_add.png new file mode 100644 index 0000000..6332fef Binary files /dev/null and b/sources/images/icon_add.png differ diff --git a/sources/images/icon_add_key.png b/sources/images/icon_add_key.png new file mode 100644 index 0000000..d407403 Binary files /dev/null and b/sources/images/icon_add_key.png differ diff --git a/sources/images/icon_add_tag.png b/sources/images/icon_add_tag.png new file mode 100644 index 0000000..83ec984 Binary files /dev/null and b/sources/images/icon_add_tag.png differ diff --git a/sources/images/icon_add_user.png b/sources/images/icon_add_user.png new file mode 100644 index 0000000..deae99b Binary files /dev/null and b/sources/images/icon_add_user.png differ diff --git a/sources/images/icon_add_wanted.png b/sources/images/icon_add_wanted.png new file mode 100644 index 0000000..908612e Binary files /dev/null and b/sources/images/icon_add_wanted.png differ diff --git a/sources/images/icon_admin.png b/sources/images/icon_admin.png new file mode 100644 index 0000000..720a237 Binary files /dev/null and b/sources/images/icon_admin.png differ diff --git a/sources/images/icon_all.png b/sources/images/icon_all.png new file mode 100644 index 0000000..f54bf73 Binary files /dev/null and b/sources/images/icon_all.png differ diff --git a/sources/images/icon_ampache.png b/sources/images/icon_ampache.png new file mode 100644 index 0000000..af65536 Binary files /dev/null and b/sources/images/icon_ampache.png differ diff --git a/sources/images/icon_batch_download.png b/sources/images/icon_batch_download.png new file mode 100644 index 0000000..67a3d9a Binary files /dev/null and b/sources/images/icon_batch_download.png differ diff --git a/sources/images/icon_broadcast.png b/sources/images/icon_broadcast.png new file mode 100644 index 0000000..63f435f Binary files /dev/null and b/sources/images/icon_broadcast.png differ diff --git a/sources/images/icon_cancel.png b/sources/images/icon_cancel.png new file mode 100644 index 0000000..fb66da7 Binary files /dev/null and b/sources/images/icon_cancel.png differ diff --git a/sources/images/icon_cog.png b/sources/images/icon_cog.png new file mode 100644 index 0000000..04f22ba Binary files /dev/null and b/sources/images/icon_cog.png differ diff --git a/sources/images/icon_comment.png b/sources/images/icon_comment.png new file mode 100644 index 0000000..7bc9233 Binary files /dev/null and b/sources/images/icon_comment.png differ diff --git a/sources/images/icon_delete.png b/sources/images/icon_delete.png new file mode 100644 index 0000000..1514d51 Binary files /dev/null and b/sources/images/icon_delete.png differ diff --git a/sources/images/icon_disable.png b/sources/images/icon_disable.png new file mode 100644 index 0000000..08f2493 Binary files /dev/null and b/sources/images/icon_disable.png differ diff --git a/sources/images/icon_download.png b/sources/images/icon_download.png new file mode 100644 index 0000000..6a12420 Binary files /dev/null and b/sources/images/icon_download.png differ diff --git a/sources/images/icon_drag.png b/sources/images/icon_drag.png new file mode 100644 index 0000000..a763b80 Binary files /dev/null and b/sources/images/icon_drag.png differ diff --git a/sources/images/icon_edit.png b/sources/images/icon_edit.png new file mode 100644 index 0000000..af486c9 Binary files /dev/null and b/sources/images/icon_edit.png differ diff --git a/sources/images/icon_enable.png b/sources/images/icon_enable.png new file mode 100644 index 0000000..89c8129 Binary files /dev/null and b/sources/images/icon_enable.png differ diff --git a/sources/images/icon_equalizer.png b/sources/images/icon_equalizer.png new file mode 100644 index 0000000..1a40ce8 Binary files /dev/null and b/sources/images/icon_equalizer.png differ diff --git a/sources/images/icon_feed.png b/sources/images/icon_feed.png new file mode 100644 index 0000000..315c4f4 Binary files /dev/null and b/sources/images/icon_feed.png differ diff --git a/sources/images/icon_flag.png b/sources/images/icon_flag.png new file mode 100644 index 0000000..5898841 Binary files /dev/null and b/sources/images/icon_flag.png differ diff --git a/sources/images/icon_flag_off.png b/sources/images/icon_flag_off.png new file mode 100644 index 0000000..522827b Binary files /dev/null and b/sources/images/icon_flag_off.png differ diff --git a/sources/images/icon_flow.png b/sources/images/icon_flow.png new file mode 100644 index 0000000..43aa9f2 Binary files /dev/null and b/sources/images/icon_flow.png differ diff --git a/sources/images/icon_fullscreen.png b/sources/images/icon_fullscreen.png new file mode 100644 index 0000000..25050d8 Binary files /dev/null and b/sources/images/icon_fullscreen.png differ diff --git a/sources/images/icon_google.png b/sources/images/icon_google.png new file mode 100644 index 0000000..aeaca23 Binary files /dev/null and b/sources/images/icon_google.png differ diff --git a/sources/images/icon_home.png b/sources/images/icon_home.png new file mode 100644 index 0000000..fed6221 Binary files /dev/null and b/sources/images/icon_home.png differ diff --git a/sources/images/icon_image.png b/sources/images/icon_image.png new file mode 100644 index 0000000..53576df Binary files /dev/null and b/sources/images/icon_image.png differ diff --git a/sources/images/icon_info.png b/sources/images/icon_info.png new file mode 100644 index 0000000..94e8a17 Binary files /dev/null and b/sources/images/icon_info.png differ diff --git a/sources/images/icon_lastfm.png b/sources/images/icon_lastfm.png new file mode 100644 index 0000000..0f718da Binary files /dev/null and b/sources/images/icon_lastfm.png differ diff --git a/sources/images/icon_link.png b/sources/images/icon_link.png new file mode 100644 index 0000000..25eacb7 Binary files /dev/null and b/sources/images/icon_link.png differ diff --git a/sources/images/icon_lock.png b/sources/images/icon_lock.png new file mode 100644 index 0000000..2ebc4f6 Binary files /dev/null and b/sources/images/icon_lock.png differ diff --git a/sources/images/icon_logout.png b/sources/images/icon_logout.png new file mode 100644 index 0000000..64bab57 Binary files /dev/null and b/sources/images/icon_logout.png differ diff --git a/sources/images/icon_microphone.png b/sources/images/icon_microphone.png new file mode 100644 index 0000000..12db59e Binary files /dev/null and b/sources/images/icon_microphone.png differ diff --git a/sources/images/icon_money.png b/sources/images/icon_money.png new file mode 100644 index 0000000..42c52d0 Binary files /dev/null and b/sources/images/icon_money.png differ diff --git a/sources/images/icon_next.png b/sources/images/icon_next.png new file mode 100644 index 0000000..31f7fd3 Binary files /dev/null and b/sources/images/icon_next.png differ diff --git a/sources/images/icon_next_hover.png b/sources/images/icon_next_hover.png new file mode 100644 index 0000000..4a2f9d4 Binary files /dev/null and b/sources/images/icon_next_hover.png differ diff --git a/sources/images/icon_pause.png b/sources/images/icon_pause.png new file mode 100644 index 0000000..2d9ce9c Binary files /dev/null and b/sources/images/icon_pause.png differ diff --git a/sources/images/icon_pause_hover.png b/sources/images/icon_pause_hover.png new file mode 100644 index 0000000..ec61099 Binary files /dev/null and b/sources/images/icon_pause_hover.png differ diff --git a/sources/images/icon_play.png b/sources/images/icon_play.png new file mode 100644 index 0000000..0846555 Binary files /dev/null and b/sources/images/icon_play.png differ diff --git a/sources/images/icon_play_add.png b/sources/images/icon_play_add.png new file mode 100644 index 0000000..5a30a7c Binary files /dev/null and b/sources/images/icon_play_add.png differ diff --git a/sources/images/icon_play_add_preview.png b/sources/images/icon_play_add_preview.png new file mode 100644 index 0000000..c13e18e Binary files /dev/null and b/sources/images/icon_play_add_preview.png differ diff --git a/sources/images/icon_play_hover.png b/sources/images/icon_play_hover.png new file mode 100644 index 0000000..f8c8ec6 Binary files /dev/null and b/sources/images/icon_play_hover.png differ diff --git a/sources/images/icon_play_preview.png b/sources/images/icon_play_preview.png new file mode 100644 index 0000000..455c1ea Binary files /dev/null and b/sources/images/icon_play_preview.png differ diff --git a/sources/images/icon_playlist_add.png b/sources/images/icon_playlist_add.png new file mode 100644 index 0000000..d650552 Binary files /dev/null and b/sources/images/icon_playlist_add.png differ diff --git a/sources/images/icon_plugin.png b/sources/images/icon_plugin.png new file mode 100644 index 0000000..6187b15 Binary files /dev/null and b/sources/images/icon_plugin.png differ diff --git a/sources/images/icon_preferences.png b/sources/images/icon_preferences.png new file mode 100644 index 0000000..c02f315 Binary files /dev/null and b/sources/images/icon_preferences.png differ diff --git a/sources/images/icon_prev.png b/sources/images/icon_prev.png new file mode 100644 index 0000000..c029447 Binary files /dev/null and b/sources/images/icon_prev.png differ diff --git a/sources/images/icon_prev_hover.png b/sources/images/icon_prev_hover.png new file mode 100644 index 0000000..15d1584 Binary files /dev/null and b/sources/images/icon_prev_hover.png differ diff --git a/sources/images/icon_random.png b/sources/images/icon_random.png new file mode 100644 index 0000000..d3087df Binary files /dev/null and b/sources/images/icon_random.png differ diff --git a/sources/images/icon_run.png b/sources/images/icon_run.png new file mode 100644 index 0000000..9f24600 Binary files /dev/null and b/sources/images/icon_run.png differ diff --git a/sources/images/icon_save.png b/sources/images/icon_save.png new file mode 100644 index 0000000..99d532e Binary files /dev/null and b/sources/images/icon_save.png differ diff --git a/sources/images/icon_server_lightning.png b/sources/images/icon_server_lightning.png new file mode 100644 index 0000000..b0f4e46 Binary files /dev/null and b/sources/images/icon_server_lightning.png differ diff --git a/sources/images/icon_share.png b/sources/images/icon_share.png new file mode 100644 index 0000000..5c1995e Binary files /dev/null and b/sources/images/icon_share.png differ diff --git a/sources/images/icon_statistics.png b/sources/images/icon_statistics.png new file mode 100644 index 0000000..9051fbc Binary files /dev/null and b/sources/images/icon_statistics.png differ diff --git a/sources/images/icon_stop.png b/sources/images/icon_stop.png new file mode 100644 index 0000000..893bb60 Binary files /dev/null and b/sources/images/icon_stop.png differ diff --git a/sources/images/icon_stop_hover.png b/sources/images/icon_stop_hover.png new file mode 100644 index 0000000..e6f75d2 Binary files /dev/null and b/sources/images/icon_stop_hover.png differ diff --git a/sources/images/icon_tick.png b/sources/images/icon_tick.png new file mode 100644 index 0000000..a9925a0 Binary files /dev/null and b/sources/images/icon_tick.png differ diff --git a/sources/images/icon_view.png b/sources/images/icon_view.png new file mode 100644 index 0000000..908612e Binary files /dev/null and b/sources/images/icon_view.png differ diff --git a/sources/images/icon_visualizer.png b/sources/images/icon_visualizer.png new file mode 100644 index 0000000..60fc284 Binary files /dev/null and b/sources/images/icon_visualizer.png differ diff --git a/sources/images/icon_volumedn.png b/sources/images/icon_volumedn.png new file mode 100644 index 0000000..4d91863 Binary files /dev/null and b/sources/images/icon_volumedn.png differ diff --git a/sources/images/icon_volumemute.png b/sources/images/icon_volumemute.png new file mode 100644 index 0000000..b652d2a Binary files /dev/null and b/sources/images/icon_volumemute.png differ diff --git a/sources/images/icon_volumeup.png b/sources/images/icon_volumeup.png new file mode 100644 index 0000000..6056d23 Binary files /dev/null and b/sources/images/icon_volumeup.png differ diff --git a/sources/images/icon_wand.png b/sources/images/icon_wand.png new file mode 100644 index 0000000..44ccbf8 Binary files /dev/null and b/sources/images/icon_wand.png differ diff --git a/sources/images/icon_wikipedia.png b/sources/images/icon_wikipedia.png new file mode 100644 index 0000000..ed1dfd9 Binary files /dev/null and b/sources/images/icon_wikipedia.png differ diff --git a/sources/images/icon_world_link.png b/sources/images/icon_world_link.png new file mode 100644 index 0000000..b8edc12 Binary files /dev/null and b/sources/images/icon_world_link.png differ diff --git a/sources/images/ratings/star_rating.gif b/sources/images/ratings/star_rating.gif new file mode 100644 index 0000000..6f8d1ed Binary files /dev/null and b/sources/images/ratings/star_rating.gif differ diff --git a/sources/images/ratings/x.gif b/sources/images/ratings/x.gif new file mode 100644 index 0000000..1b46f29 Binary files /dev/null and b/sources/images/ratings/x.gif differ diff --git a/sources/images/ratings/x_off.gif b/sources/images/ratings/x_off.gif new file mode 100644 index 0000000..4edbf8c Binary files /dev/null and b/sources/images/ratings/x_off.gif differ diff --git a/sources/images/top_bg.jpg b/sources/images/top_bg.jpg new file mode 100644 index 0000000..1e9e690 Binary files /dev/null and b/sources/images/top_bg.jpg differ diff --git a/sources/images/topmenu-favorite.png b/sources/images/topmenu-favorite.png new file mode 100644 index 0000000..031b9a7 Binary files /dev/null and b/sources/images/topmenu-favorite.png differ diff --git a/sources/images/topmenu-flag.png b/sources/images/topmenu-flag.png new file mode 100644 index 0000000..5a73315 Binary files /dev/null and b/sources/images/topmenu-flag.png differ diff --git a/sources/images/topmenu-home.png b/sources/images/topmenu-home.png new file mode 100644 index 0000000..8d99675 Binary files /dev/null and b/sources/images/topmenu-home.png differ diff --git a/sources/images/topmenu-music.png b/sources/images/topmenu-music.png new file mode 100644 index 0000000..96132dd Binary files /dev/null and b/sources/images/topmenu-music.png differ diff --git a/sources/images/topmenu-playlist.png b/sources/images/topmenu-playlist.png new file mode 100644 index 0000000..5d8d803 Binary files /dev/null and b/sources/images/topmenu-playlist.png differ diff --git a/sources/index.php b/sources/index.php new file mode 100644 index 0000000..b2eee23 --- /dev/null +++ b/sources/index.php @@ -0,0 +1,53 @@ + 5) { + $refresh_limit = AmpConfig::get('refresh_limit'); + $ajax_url = '?page=index&action=reloadnp'; + require_once AmpConfig::get('prefix') . '/templates/javascript_refresh.inc.php'; +} + +require_once AmpConfig::get('prefix') . '/templates/show_index.inc.php'; + +UI::show_footer(); diff --git a/sources/install.php b/sources/install.php new file mode 100644 index 0000000..c180b33 --- /dev/null +++ b/sources/install.php @@ -0,0 +1,180 @@ + $web_path, + 'database_name' => $database, + 'database_hostname' => $hostname, + 'database_port' => $port +), true); +if (!$skip_admin) { + AmpConfig::set_by_array(array( + 'database_username' => $username, + 'database_password' => $password + ), true); +} + +if (isset($_REQUEST['transcode_template'])) { + $mode = $_REQUEST['transcode_template']; + install_config_transcode_mode($mode); +} + +// Charset and gettext setup +$htmllang = $_REQUEST['htmllang']; +$charset = $_REQUEST['charset']; + +if (!$htmllang) { + if ($_ENV['LANG']) { + $lang = $_ENV['LANG']; + } else { + $lang = 'en_US'; + } + if (strpos($lang, '.')) { + $langtmp = explode('.', $lang); + $htmllang = $langtmp[0]; + $charset = $langtmp[1]; + } else { + $htmllang = $lang; + } +} +AmpConfig::set('lang', $htmllang, true); +AmpConfig::set('site_charset', $charset ?: 'UTF-8', true); +load_gettext(); +header ('Content-Type: text/html; charset=' . AmpConfig::get('site_charset')); + +// Correct potential \ or / in the dirname +$safe_dirname = rtrim(dirname($_SERVER['PHP_SELF']),"/\\"); + +$web_path = $http_type . $_SERVER['HTTP_HOST'] . $safe_dirname; + +unset($safe_dirname); + +switch ($_REQUEST['action']) { + case 'create_db': + $new_user = ''; + $new_pass = ''; + if ($_POST['db_user'] == 'create_db_user') { + $new_user = $_POST['db_username']; + $new_pass = $_POST['db_password']; + + if (!strlen($new_user) || !strlen($new_pass)) { + Error::add('general', T_('Error: Ampache SQL Username or Password missing')); + require_once 'templates/show_install.inc.php'; + break; + } + } + + if (!$skip_admin) { + if (!install_insert_db($new_user, $new_pass, $_REQUEST['create_db'], $_REQUEST['overwrite_db'], $_REQUEST['create_tables'])) { + require_once 'templates/show_install.inc.php'; + break; + } + } + + // Now that it's inserted save the lang preference + Preference::update('lang', '-1', AmpConfig::get('lang')); + + header ('Location: ' . $web_path . "/install.php?action=show_create_config&local_db=$database&local_host=$hostname&local_port=$port&htmllang=$htmllang&charset=$charset"); + break; + case 'create_config': + $download = (!isset($_POST['write'])); + $download_htaccess_rest = (isset($_POST['download_htaccess_rest'])); + $download_htaccess_play = (isset($_POST['download_htaccess_play'])); + $write_htaccess_rest = (isset($_POST['write_htaccess_rest'])); + $write_htaccess_play = (isset($_POST['write_htaccess_play'])); + + if ($write_htaccess_rest || $download_htaccess_rest) { + $created_config = install_rewrite_rules($htaccess_rest_file, $_POST['web_path'], $download_htaccess_rest); + } elseif ($write_htaccess_play || $download_htaccess_play) { + $created_config = install_rewrite_rules($htaccess_play_file, $_POST['web_path'], $download_htaccess_play); + } else { + $created_config = install_create_config($download); + } + + require_once 'templates/show_install_config.inc.php'; + break; + case 'show_create_config': + require_once 'templates/show_install_config.inc.php'; + break; + case 'create_account': + $results = parse_ini_file($configfile); + AmpConfig::set_by_array($results, true); + + $password2 = scrub_in($_REQUEST['local_pass2']); + + if (!install_create_account($username, $password, $password2)) { + require_once AmpConfig::get('prefix') . '/templates/show_install_account.inc.php'; + break; + } + + header ("Location: " . $web_path . '/login.php'); + break; + case 'show_create_account': + $results = parse_ini_file($configfile); + + /* Make sure we've got a valid config file */ + if (!check_config_values($results)) { + Error::add('general', T_('Error: Config file not found or unreadable')); + require_once AmpConfig::get('prefix') . '/templates/show_install_config.inc.php'; + break; + } + + require_once AmpConfig::get('prefix') . '/templates/show_install_account.inc.php'; + break; + case 'init': + require_once 'templates/show_install.inc.php'; + break; + case 'check': + require_once 'templates/show_install_check.inc.php'; + break; + default: + // Show the language options first + require_once 'templates/show_install_lang.inc.php'; + break; +} // end action switch diff --git a/sources/lib/.htaccess b/sources/lib/.htaccess new file mode 100644 index 0000000..896fbc5 --- /dev/null +++ b/sources/lib/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all \ No newline at end of file diff --git a/sources/lib/batch.lib.php b/sources/lib/batch.lib.php new file mode 100644 index 0000000..9d26e8c --- /dev/null +++ b/sources/lib/batch.lib.php @@ -0,0 +1,113 @@ +enabled) { + $total_size += sprintf("%.2f",($media->size/1048576)); + $media->format(); + $dirname = $media->f_album_full; + //debug_event('batch.lib.php', 'Songs file {'.$media->file.'}...', '5'); + if (!array_key_exists($dirname, $media_files)) { + $media_files[$dirname] = array(); + } + array_push($media_files[$dirname], $media->file); + } + } + + return array($media_files, $total_size); +} //get_song_files + +/** + * send_zip + * + * takes array of full paths to songs + * zips them and sends them + * + * @param string $name name of the zip file to be created + * @param array $song_files array of full paths to songs to zip create w/ call to get_song_files + */ +function send_zip($name, $song_files) +{ + // Check if they want to save it to a file, if so then make sure they've + // got a defined path as well and that it's writable. + $basedir = ''; + if (AmpConfig::get('file_zip_download') && AmpConfig::get('tmp_dir_path')) { + // Check writeable + if (!is_writable(AmpConfig::get('tmp_dir_path'))) { + $in_memory = '1'; + debug_event('Error','File Zip Path:' . AmpConfig::get('tmp_dir_path') . ' is not writable','1'); + } else { + $in_memory = '0'; + $basedir = AmpConfig::get('tmp_dir_path'); + } + } else { + $in_memory = '1'; + } // if file downloads + + /* Require needed library */ + require_once AmpConfig::get('prefix') . '/modules/archive/archive.lib.php'; + $arc = new zip_file($name . ".zip" ); + $options = array( + 'inmemory' => $in_memory, // create archive in memory + 'basedir' => $basedir, + 'storepaths' => 0, // only store file name, not full path + 'level' => 0, // no compression + 'comment' => AmpConfig::get('file_zip_comment'), + 'type' => "zip" + ); + + $arc->set_options( $options ); + foreach ($song_files as $dir => $files) { + $arc->add_files($files, $dir); + } + + if (count($arc->error)) { + debug_event('archive',"Error: unable to add songs",'3'); + return false; + } // if failed to add songs + + if (!$arc->create_archive()) { + debug_event('archive',"Error: unable to create archive",'3'); + return false; + } // if failed to create archive + + $arc->download_file(); + +} // send_zip diff --git a/sources/lib/class/access.class.php b/sources/lib/class/access.class.php new file mode 100644 index 0000000..ae09ddb --- /dev/null +++ b/sources/lib/class/access.class.php @@ -0,0 +1,432 @@ +id = intval($access_id); + + $info = $this->_get_info(); + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + } + + /** + * _get_info + * + * Gets the vars for $this out of the database. + */ + private function _get_info() + { + $sql = 'SELECT * FROM `access_list` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($this->id)); + + $results = Dba::fetch_assoc($db_results); + + return $results; + } + + /** + * format + * + * This makes the Access object a nice fuzzy human readable object, spiffy + * ain't it. + */ + public function format() + { + $this->f_start = inet_ntop($this->start); + $this->f_end = inet_ntop($this->end); + + $this->f_user = $this->get_user_name(); + $this->f_level = $this->get_level_name(); + $this->f_type = $this->get_type_name(); + } + + /** + * _verify_range + * + * This outputs an error if the IP range is bad. + */ + private static function _verify_range($startp, $endp) + { + $startn = @inet_pton($startp); + $endn = @inet_pton($endp); + + if (!$startn && $startp != '0.0.0.0' && $startp != '::') { + Error::add('start', T_('Invalid IPv4 / IPv6 Address Entered')); + return false; + } + if (!$endn) { + Error::add('end', T_('Invalid IPv4 / IPv6 Address Entered')); + } + + if (strlen(bin2hex($startn)) != strlen(bin2hex($endn))) { + Error::add('start', T_('IP Address Version Mismatch')); + Error::add('end', T_('IP Address Version Mismatch')); + return false; + } + + return true; + } + + /** + * update + * + * This function takes a named array as a datasource and updates the current + * access list entry. + */ + public function update($data) + { + if (!self::_verify_range($data['start'], $data['end'])) { + return false; + } + + $start = @inet_pton($data['start']); + $end = @inet_pton($data['end']); + $name = $data['name']; + $type = self::validate_type($data['type']); + $level = intval($data['level']); + $user = $data['user'] ?: '-1'; + $enabled = make_bool($data['enabled']) ? 1 : 0; + + $sql = 'UPDATE `access_list` SET `start` = ?, `end` = ?, `level` = ?, ' . + '`user` = ?, `name` = ?, `type` = ?, `enabled` = ? WHERE `id` = ?'; + Dba::write($sql, + array($start, $end, $level, $user, $name, $type, $enabled, $this->id)); + + return true; + } + + /** + * create + * + * This takes a keyed array of data and trys to insert it as a + * new ACL entry + */ + public static function create($data) + { + if (!self::_verify_range($data['start'], $data['end'])) { + return false; + } + + // Check existing ACLs to make sure we're not duplicating values here + if (self::exists($data)) { + debug_event('ACL Create', 'Error: An ACL equal to the created one already exists. Not adding another one: ' . $data['start'] . ' - ' . $data['end'], 1); + Error::add('general', T_('Duplicate ACL defined')); + return false; + } + + $start = @inet_pton($data['start']); + $end = @inet_pton($data['end']); + $name = $data['name']; + $user = $data['user'] ?: '-1'; + $level = intval($data['level']); + $type = self::validate_type($data['type']); + $enabled = make_bool($data['enabled']) ? 1 : 0; + + $sql = 'INSERT INTO `access_list` (`name`, `level`, `start`, `end`, ' . + '`user`,`type`,`enabled`) VALUES (?, ?, ?, ?, ?, ?, ?)'; + Dba::write($sql, array($name, $level, $start, $end, $user, $type, $enabled)); + + return true; + + } + + /** + * exists + * + * This sees if the ACL that we've specified already exists in order to + * prevent duplicates. The name is ignored. + */ + public static function exists($data) + { + $start = inet_pton($data['start']); + $end = inet_pton($data['end']); + $type = self::validate_type($data['type']); + $user = $data['user'] ?: '-1'; + + $sql = 'SELECT * FROM `access_list` WHERE `start` = ? AND `end` = ? ' . + 'AND `type` = ? AND `user` = ?'; + $db_results = Dba::read($sql, array($start, $end, $type, $user)); + + if (Dba::fetch_assoc($db_results)) { + return true; + } + + return false; + } + + /** + * delete + * + * deletes the specified access_list entry + */ + public static function delete($id) + { + Dba::write('DELETE FROM `access_list` WHERE `id` = ?', array($id)); + } + + /** + * check_function + * + * This checks if specific functionality is enabled. + */ + public static function check_function($type) + { + switch ($type) { + case 'download': + return AmpConfig::get('download'); + case 'batch_download': + if (!function_exists('gzcompress')) { + debug_event('access', 'ZLIB extension not loaded, batch download disabled', 3); + return false; + } + if (AmpConfig::get('allow_zip_download') AND $GLOBALS['user']->has_access('25')) { + return AmpConfig::get('download'); + } + break; + default: + return false; + } + } + + /** + * check_network + * + * This takes a type, ip, user, level and key and then returns whether they + * are allowed. The IP is passed as a dotted quad. + */ + public static function check_network($type, $user, $level, $ip=null) + { + if (!AmpConfig::get('access_control')) { + switch ($type) { + case 'interface': + case 'stream': + return true; + default: + return false; + } + } + + // Clean incoming variables + $ip = $ip ?: $_SERVER['REMOTE_ADDR']; + $ip = inet_pton($ip); + + switch ($type) { + case 'init-api': + if ($user) { + $user = User::get_from_username($user); + $user = $user->id; + } + case 'api': + $type = 'rpc'; + case 'network': + case 'interface': + case 'stream': + break; + default: + return false; + } // end switch on type + + $sql = 'SELECT `id` FROM `access_list` ' . + 'WHERE `start` <= ? AND `end` >= ? ' . + 'AND `level` >= ? AND `type` = ?'; + + $params = array($ip, $ip, $level, $type); + + if (strlen($user) && $user != '-1') { + $sql .= " AND `user` IN(?, '-1')"; + $params[] = $user; + } else { + $sql .= " AND `user` = '-1'"; + } + + $db_results = Dba::read($sql, $params); + + if (Dba::fetch_row($db_results)) { + // Yah they have access they can use the mojo + return true; + } + + return false; + } + + /** + * check_access + * + * This is the global 'has_access' function.(t can check for any 'type' + * of object. + * + * Everything uses the global 0,5,25,50,75,100 stuff. GLOBALS['user'] is + * always used. + */ + public static function check($type, $level) + { + if (AmpConfig::get('demo_mode')) { + return true; + } + if (defined('INSTALL')) { + return true; + } + + $level = intval($level); + + // Switch on the type + switch ($type) { + case 'localplay': + // Check their localplay_level + return (AmpConfig::get('localplay_level') >= $level + || $GLOBALS['user']->access >= 100); + case 'interface': + // Check their standard user level + return ($GLOBALS['user']->access >= $level); + default: + return false; + } + } + + /** + * validate_type + * + * This validates the specified type; it will always return a valid type, + * even if you pass in an invalid one. + */ + public static function validate_type($type) + { + switch ($type) { + case 'rpc': + case 'interface': + case 'network': + return $type; + default: + return 'stream'; + } + } + + /** + * get_access_lists + * returns a full listing of all access rules on this server + */ + public static function get_access_lists() + { + $sql = 'SELECT `id` FROM `access_list`'; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + + /** + * get_level_name + * + * take the int level and return a named level + */ + public function get_level_name() + { + if ($this->level >= '75') { + return T_('All'); + } + if ($this->level == '5') { + return T_('View'); + } + if ($this->level == '25') { + return T_('Read'); + } + if ($this->level == '50') { + return T_('Read/Write'); + } + } + + /** + * get_user_name + * + * Return a name for the users covered by this ACL. + */ + public function get_user_name() + { + if ($this->user == '-1') { return T_('All'); } + + $user = new User($this->user); + return $user->fullname . " (" . $user->username . ")"; + } + + /** + * get_type_name + * + * This function returns the pretty name for our current type. + */ + public function get_type_name() + { + switch ($this->type) { + case 'rpc': + return T_('API/RPC'); + case 'network': + return T_('Local Network Definition'); + case 'interface': + return T_('Web Interface'); + case 'stream': + default: + return T_('Stream Access'); + } + } +} diff --git a/sources/lib/class/ajax.class.php b/sources/lib/class/ajax.class.php new file mode 100644 index 0000000..83a6cbd --- /dev/null +++ b/sources/lib/class/ajax.class.php @@ -0,0 +1,217 @@ +"; + $methodact = (($method == 'click') ? "update_action();" : ""); + if (!empty($confirm)) { + $observe .= "$(".$source_txt.").on('".$method."', function(){ ".$methodact." if (confirm(\"".$confirm."\")) { ".$action." }});"; + } else { + $observe .= "$(".$source_txt.").on('".$method."', function(){ ".$methodact." ".$action.";});"; + } + $observe .= ""; + + return $observe; + + } // observe + + /** + * url + * This takes a string and makes an URL + */ + public static function url($action) + { + return AmpConfig::get('ajax_url') . $action; + } + + /** + * action + * This takes the action, the source and the post (if passed) and + * generates the full ajax link + */ + public static function action($action, $source, $post='') + { + $url = self::url($action); + + $non_quoted = array('document','window'); + + if (in_array($source,$non_quoted)) { + $source_txt = $source; + } else { + $source_txt = "'$source'"; + } + + if ($post) { + $ajax_string = "ajaxPost('$url','$post',$source_txt)"; + } else { + $ajax_string = "ajaxPut('$url',$source_txt)"; + } + + return $ajax_string; + + } // action + + /** + * button + * This prints out an img of the specified icon with the specified alt + * text and then sets up the required ajax for it. + */ + public static function button($action, $icon, $alt, $source='', $post='', $class='', $confirm='') + { + // Get the correct action + $ajax_string = self::action($action, $source, $post); + + // If they passed a span class + if ($class) { + $class = ' class="'.$class.'"'; + } + + $string = UI::get_icon($icon, $alt); + + // Generate an so that it's more compliant with older + // browsers (ie :hover actions) and also to unify linkbuttons + // (w/o ajax) display + $string = "".$string."\n"; + + $string .= self::observe($source, 'click', $ajax_string, $confirm); + + return $string; + + } // button + + /** + * text + * This prints out the specified text as a link and sets up the required + * ajax for the link so it works correctly + */ + public static function text($action, $text, $source, $post='', $class='') + { + // Avoid duplicate id + $source .= '_' . time(); + + // Format the string we wanna use + $ajax_string = self::action($action, $source, $post); + + // If they passed a span class + if ($class) { + $class = ' class="' . $class . '"'; + } + + $string = "$text\n"; + + $string .= self::observe($source, 'click', $ajax_string); + + return $string; + + } // text + + /** + * run + * This runs the specified action no questions asked + */ + public static function run($action) + { + echo ""; + + } // run + + /** + * set_include_override + * This sets the including div override, used only one place. Kind of a + * hack. + */ + public static function set_include_override($value) + { + self::$include_override = make_bool($value); + + } // set_include_override + + /** + * start_container + * This checks to see if we're AJAXin'. If we aren't then it echoes out + * the html needed to start a container that can be replaced by Ajax. + */ + public static function start_container($name, $class = '') + { + if (defined('AJAX_INCLUDE') && !self::$include_override) { return true; } + + echo '
'; + + } // start_container + + /** + * end_container + * This ends the container if we're not doing the AJAX thing + */ + public static function end_container() + { + if (defined('AJAX_INCLUDE') && !self::$include_override) { return true; } + + echo "
"; + + self::$include_override = false; + + } // end_container + +} // end Ajax class diff --git a/sources/lib/class/album.class.php b/sources/lib/class/album.class.php new file mode 100644 index 0000000..c5bd40e --- /dev/null +++ b/sources/lib/class/album.class.php @@ -0,0 +1,596 @@ +get_info($id); + + // Foreach what we've got + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + // Little bit of formatting here + $this->full_name = trim(trim($info['prefix']) . ' ' . trim($info['name'])); + + // Looking for other albums with same mbid, ordering by disk ascending + if ($this->disk && !empty($this->mbid) && AmpConfig::get('album_group')) { + $this->album_suite = $this->get_album_suite(); + } + + return true; + + } // constructor + + /** + * construct_from_array + * This is often used by the metadata class, it fills out an album object from a + * named array, _fake is set to true + */ + public static function construct_from_array($data) + { + $album = new Album(0); + foreach ($data as $key=>$value) { + $album->$key = $value; + } + + $album->_fake = true; // Make sure that we tell em it's fake + + return $album; + + } // construct_from_array + + /** + * gc + * + * Cleans out unused albums + */ + public static function gc() + { + Dba::write('DELETE FROM `album` USING `album` LEFT JOIN `song` ON `song`.`album` = `album`.`id` WHERE `song`.`id` IS NULL'); + } + + /** + * build_cache + * This takes an array of object ids and caches all of their information + * with a single query + */ + public static function build_cache($ids) + { + // Nothing to do if they pass us nothing + if (!is_array($ids) OR !count($ids)) { + return false; + } + + $idlist = '(' . implode(',', $ids) . ')'; + + $sql = "SELECT * FROM `album` WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('album',$row['id'],$row); + } + + return true; + + } // build_cache + + /** + * _get_extra_info + * This pulls the extra information from our tables, this is a 3 table join, which is why we don't normally + * do it + */ + private function _get_extra_info() + { + if (parent::is_cached('album_extra', $this->id)) { + return parent::get_from_cache('album_extra', $this->id); + } + + $sql = "SELECT " . + "COUNT(DISTINCT(`song`.`artist`)) AS `artist_count`, " . + "COUNT(`song`.`id`) AS `song_count`, " . + "SUM(`song`.`time`) as `total_duration`," . + "`song`.`catalog` as `catalog_id`,". + "`artist`.`name` AS `artist_name`, " . + "`artist`.`prefix` AS `artist_prefix`, " . + "`artist`.`id` AS `artist_id` " . + "FROM `song` INNER JOIN `artist` " . + "ON `artist`.`id`=`song`.`artist` "; + + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + + $suite_array = array(); + if ($this->allow_group_disks) { + $suite_array = $this->album_suite; + } + if (!count($suite_array)) { + $suite_array[] = $this->id; + } + + $idlist = '(' . implode(',', $suite_array) . ')'; + $sql .= "WHERE `song`.`album` IN $idlist "; + + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + if (!count($this->album_suite)) { + $sql .= "GROUP BY `song`.`album`"; + } else { + $sql .= "GROUP BY `song`.`artist`"; + } + + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + $art = new Art($this->id, 'album'); + $art->get_db(); + $results['has_art'] = make_bool($art->raw); + $results['has_thumb'] = make_bool($art->thumb); + + if (AmpConfig::get('show_played_times')) { + $results['object_cnt'] = Stats::get_object_count('album', $this->id); + } + + parent::add_to_cache('album_extra', $this->id, $results); + + return $results; + + } // _get_extra_info + + /** + * check + * + * Searches for an album; if none is found, insert a new one. + */ + public static function check($name, $year = 0, $disk = 0, $mbid = null, $readonly = false) + { + if ($mbid == '') $mbid = null; + + $trimmed = Catalog::trim_prefix(trim($name)); + $name = $trimmed['string']; + $prefix = $trimmed['prefix']; + + // Not even sure if these can be negative, but better safe than llama. + $year = abs(intval($year)); + $disk = abs(intval($disk)); + + if (!$name) { + $name = T_('Unknown (Orphaned)'); + $year = 0; + $disk = 0; + } + + if (isset(self::$_mapcache[$name][$year][$disk][$mbid])) { + return self::$_mapcache[$name][$year][$disk][$mbid]; + } + + $sql = 'SELECT `id` FROM `album` WHERE `name` = ? AND `disk` = ? AND `year` = ? AND `mbid` '; + $params = array($name, $disk, $year); + + if ($mbid) { + $sql .= '= ? '; + $params[] = $mbid; + } else { + $sql .= 'IS NULL '; + } + + $sql .= 'AND `prefix` '; + if ($prefix) { + $sql .= '= ?'; + $params[] = $prefix; + } else { + $sql .= 'IS NULL'; + } + + $db_results = Dba::read($sql, $params); + + if ($row = Dba::fetch_assoc($db_results)) { + $id = $row['id']; + self::$_mapcache[$name][$year][$disk][$mbid] = $id; + return $id; + } + + if ($readonly) { + return null; + } + + $sql = 'INSERT INTO `album` (`name`, `prefix`, `year`, `disk`, `mbid`) VALUES (?, ?, ?, ?, ?)'; + + $db_results = Dba::write($sql, array($name, $prefix, $year, $disk, $mbid)); + if (!$db_results) { + return null; + } + + $id = Dba::insert_id(); + + // Remove from wanted album list if any request on it + if (!empty($mbid) && AmpConfig::get('wanted')) { + try { + Wanted::delete_wanted_release($mbid); + } catch (Exception $e) { + debug_event('wanted', 'Cannot process wanted releases auto-removal check: ' . $e->getMessage(), '1'); + } + } + + self::$_mapcache[$name][$year][$disk][$mbid] = $id; + return $id; + } + + /** + * get_songs + * gets the songs for this album takes an optional limit + * and an optional artist, if artist is passed it only gets + * songs with this album + specified artist + */ + public function get_songs($limit = 0,$artist='') + { + $results = array(); + + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `song`.`album` = ? "; + $params = array($this->id); + if (strlen($artist)) { + $sql .= "AND `artist` = ? "; + $params[] = $artist; + } + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY `song`.`track`, `song`.`title`"; + if ($limit) { + $sql .= " LIMIT " . intval($limit); + } + $db_results = Dba::read($sql, $params); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + + } // get_songs + + /** + * get_http_album_query_ids + * return the html album parameters with all album suite ids + */ + public function get_http_album_query_ids($url_param_name) + { + if ($this->allow_group_disks) { + $suite_array = $this->get_group_disks_ids(); + } else { + $suite_array = array($this->id); + } + + return http_build_query(array($url_param_name => $suite_array)); + } + + /** + * get_group_disks_ids + * return all album suite ids or current album if no albums + */ + public function get_group_disks_ids() + { + $suite_array = $this->album_suite; + if (!count($suite_array)) { + $suite_array[] = $this->id; + } + + return $suite_array; + } + + /** + * get_album_suite + * gets the album ids with the same musicbrainz identifier + */ + public function get_album_suite($catalog = '') + { + $results = array(); + + $catalog_where = ""; + $catalog_join = "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog`"; + if (!empty($catalog)) { + $catalog_where .= " AND `catalog`.`id` = '$catalog'"; + } + if (AmpConfig::get('catalog_disable')) { + $catalog_where .= " AND `catalog`.`enabled` = '1'"; + } + + $sql = "SELECT DISTINCT `album`.`id` FROM album LEFT JOIN `song` ON `song`.`album`=`album`.`id` $catalog_join " . + "WHERE `album`.`mbid`='$this->mbid' $catalog_where ORDER BY `album`.`disk` ASC"; + + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + + } // get_album_suite + + /** + * has_track + * This checks to see if this album has a track of the specified title + */ + public function has_track($title) + { + $sql = "SELECT `id` FROM `song` WHERE `album` = ? AND `title` = ?"; + $db_results = Dba::read($sql, array($this->id, $title)); + + $data = Dba::fetch_assoc($db_results); + + return $data; + + } // has_track + + /** + * format + * This is the format function for this object. It sets cleaned up + * album information with the base required + * f_link, f_name + */ + public function format() + { + $web_path = AmpConfig::get('web_path'); + + /* Pull the advanced information */ + $data = $this->_get_extra_info(); + foreach ($data as $key=>$value) { $this->$key = $value; } + + /* Truncate the string if it's to long */ + $this->f_name = $this->full_name; + + $this->f_link_src = $web_path . '/albums.php?action=show&album=' . scrub_out($this->id); + $this->f_name_link = "f_link_src . "\" title=\"" . scrub_out($this->full_name) . "\">" . scrub_out($this->f_name); + + // Looking if we need to combine or display disks + if ($this->disk && (!$this->allow_group_disks || ($this->allow_group_disks && !AmpConfig::get('album_group')))) { + $this->f_name_link .= " [" . T_('Disk') . " " . $this->disk . "]"; + } + + $this->f_name_link .=""; + + $this->f_link = $this->f_name_link; + $this->f_title = $this->full_name; + if ($this->artist_count == '1') { + $artist = trim(trim($this->artist_prefix) . ' ' . trim($this->artist_name)); + $this->f_artist_name = $artist; + $this->f_artist_link = "artist_id . "\" title=\"" . scrub_out($this->artist_name) . "\">" . $artist . ""; + $this->f_artist = $artist; + } else { + $this->f_artist_link = "artist_count " . T_('Artists') . "\">" . T_('Various') . ""; + $this->f_artist = T_('Various'); + $this->f_artist_name = $this->f_artist; + } + + if ($this->year == '0') { + $this->year = "N/A"; + } + + $this->tags = Tag::get_top_tags('album', $this->id); + $this->f_tags = Tag::get_display($this->tags); + + } // format + + /** + * get_random_songs + * gets a random number, and a random assortment of songs from this album + */ + public function get_random_songs() + { + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `song`.`album` = ? "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY RAND()"; + $db_results = Dba::read($sql, array($this->id)); + + $results = array(); + while ($r = Dba::fetch_row($db_results)) { + $results[] = $r['0']; + } + + return $results; + + } // get_random_songs + + /** + * update + * This function takes a key'd array of data and updates this object + * as needed + */ + public function update($data) + { + $year = $data['year']; + $artist = $data['artist']; + $name = $data['name']; + $disk = $data['disk']; + $mbid = $data['mbid']; + + $current_id = $this->id; + + $updated = false; + $songs = null; + if ($artist != $this->artist_id AND $artist) { + // Update every song + $songs = $this->get_songs(); + foreach ($songs as $song_id) { + Song::update_artist($artist,$song_id); + } + $updated = true; + Artist::gc(); + } + + $album_id = self::check($name, $year, $disk, $mbid); + if ($album_id != $this->id) { + if (!is_array($songs)) { $songs = $this->get_songs(); } + foreach ($songs as $song_id) { + Song::update_album($album_id,$song_id); + Song::update_year($year,$song_id); + } + $current_id = $album_id; + $updated = true; + self::gc(); + } + + if ($updated && is_array($songs)) { + foreach ($songs as $song_id) { + Song::update_utime($song_id); + } // foreach song of album + Stats::gc(); + Rating::gc(); + Userflag::gc(); + } // if updated + + $override_songs = false; + if ($data['apply_childs'] == 'checked') { + $override_songs = true; + } + $this->update_tags($data['edit_tags'], $override_songs, $current_id); + + return $current_id; + + } // update + + /** + * update_tags + * + * Update tags of albums and/or songs + */ + public function update_tags($tags_comma, $override_songs, $current_id = null) + { + if ($current_id == null) { + $current_id = $this->id; + } + + Tag::update_tag_list($tags_comma, 'album', $current_id); + + if ($override_songs) { + $songs = $this->get_songs(); + foreach ($songs as $song_id) { + Tag::update_tag_list($tags_comma, 'song', $song_id); + } + } + } + + /** + * get_random + * + * This returns a number of random albums. + */ + public static function get_random($count = 1, $with_art = false) + { + $results = array(); + + if (!$count) { + $count = 1; + } + + $sql = "SELECT `album`.`id` FROM `album` " . + "LEFT JOIN `song` ON `song`.`album` = `album`.`id` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + $where = "WHERE `catalog`.`enabled` = '1' "; + } else { + $where = "WHERE '1' = '1' "; + } + if ($with_art) { + $sql .= "LEFT JOIN `image` ON (`image`.`object_type` = 'album' AND `image`.`object_id` = `album`.`id`) "; + $where .="AND `image`.`id` IS NOT NULL "; + } + + $sql .= $where; + $sql .= "ORDER BY RAND() LIMIT " . intval($count); + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + +} //end of album class diff --git a/sources/lib/class/ampache_rss.class.php b/sources/lib/class/ampache_rss.class.php new file mode 100644 index 0000000..3c00107 --- /dev/null +++ b/sources/lib/class/ampache_rss.class.php @@ -0,0 +1,254 @@ +type = self::validate_type($type); + + } // constructor + + /** + * get_xml + * This returns the xmldocument for the current rss type, it calls a sub function that gathers the data + * and then uses the xmlDATA class to build the document + */ + public function get_xml() + { + // Function call name + $data_function = 'load_' . $this->type; + $pub_date_function = 'pubdate_' . $this->type; + + $data = call_user_func(array('Ampache_RSS',$data_function)); + $pub_date = call_user_func(array('Ampache_RSS',$pub_date_function)); + + XML_Data::set_type('rss'); + $xml_document = XML_Data::rss_feed($data,$this->get_title(),$this->get_description(),$pub_date); + + return $xml_document; + + } // get_xml + + /** + * get_title + * This returns the standardized title for the rss feed based on this->type + */ + public function get_title() + { + $titles = array('now_playing' => T_('Now Playing'), + 'recently_played' => T_('Recently Played'), + 'latest_album' => T_('Newest Albums'), + 'latest_artist' => T_('Newest Artists')); + + return scrub_out(AmpConfig::get('site_title')) . ' - ' . $titles[$this->type]; + + } // get_title + + /** + * get_description + * This returns the standardized description for the rss feed based on this->type + */ + public function get_description() + { + //FIXME: For now don't do any kind of translating + return 'Ampache RSS Feeds'; + + } // get_description + + /** + * validate_type + * this returns a valid type for an rss feed, if the specified type is invalid it returns a default value + */ + public static function validate_type($type) + { + $valid_types = array('now_playing','recently_played','latest_album','latest_artist','latest_song', + 'popular_song','popular_album','popular_artist'); + + if (!in_array($type,$valid_types)) { + return 'now_playing'; + } + + return $type; + + } // validate_type + + /** + * get_display + * This dumps out some html and an icon for the type of rss that we specify + */ + public static function get_display($type='now_playing') + { + // Default to now playing + $type = self::validate_type($type); + + $string = '' . UI::get_icon('feed', T_('RSS Feed')) . ''; + + return $string; + + } // get_display + + // type specific functions below, these are called semi-dynamically based on the current type // + + /** + * load_now_playing + * This loads in the now playing information. This is just the raw data with key=>value pairs that could be turned + * into an xml document if we so wished + */ + public static function load_now_playing() + { + $data = Stream::get_now_playing(); + + $results = array(); + $format = AmpConfig::get('rss_format') ?: '%t - %a - %A'; + $string_map = array( + '%t' => 'title', + '%a' => 'artist', + '%A' => 'album' + ); + foreach ($data as $element) { + $song = $element['media']; + $client = $element['user']; + $title = $format; + $description = $format; + foreach ($string_map as $search => $replace) { + $trep = 'f_' . $replace; + $drep = 'f_' . $replace . '_full'; + $title = str_replace($search, $song->$trep, $title); + $description = str_replace($search, $song->$drep, $description); + } + $xml_array = array( + 'title' => $title, + 'link' => $song->link, + 'description' => $description, + 'comments' => $client->fullname . ' - ' . $element['agent'], + 'pubDate' => date('r', $element['expire']) + ); + $results[] = $xml_array; + } // end foreach + + return $results; + + } // load_now_playing + + /** + * pubdate_now_playing + * this is the pub date we should use for the now playing information, + * this is a little specific as it uses the 'newest' expire we can find + */ + public static function pubdate_now_playing() + { + // Little redundent, should be fixed by an improvement in the get_now_playing stuff + $data = Stream::get_now_playing(); + + $element = array_shift($data); + + return $element['expire']; + + } // pubdate_now_playing + + /** + * load_recently_played + * This loads in the recently played information and formats it up real nice like + */ + public static function load_recently_played() + { + //FIXME: The time stuff should be centralized, it's currently in two places, lame + + $time_unit = array('', T_('seconds ago'), T_('minutes ago'), T_('hours ago'), T_('days ago'), T_('weeks ago'), T_('months ago'), T_('years ago')); + $data = Song::get_recently_played(); + + $results = array(); + + foreach ($data as $item) { + $client = new User($item['user']); + $song = new Song($item['object_id']); + $song->format(); + $amount = intval(time() - $item['date']+2); + $final = '0'; + $time_place = '0'; + while ($amount >= 1) { + $final = $amount; + $time_place++; + if ($time_place <= 2) { + $amount = floor($amount/60); + } + if ($time_place == '3') { + $amount = floor($amount/24); + } + if ($time_place == '4') { + $amount = floor($amount/7); + } + if ($time_place == '5') { + $amount = floor($amount/4); + } + if ($time_place == '6') { + $amount = floor ($amount/12); + } + if ($time_place > '6') { + $final = $amount . '+'; + break; + } + } // end while + + $time_string = $final . ' ' . $time_unit[$time_place]; + + $xml_array = array('title'=>$song->f_title . ' - ' . $song->f_artist . ' - ' . $song->f_album, + 'link'=>str_replace('&', '&', $song->link), + 'description'=>$song->title . ' - ' . $song->f_artist_full . ' - ' . $song->f_album_full . ' - ' . $time_string, + 'comments'=>$client->username, + 'pubDate'=>date("r",$item['date'])); + $results[] = $xml_array; + + } // end foreach + + return $results; + + } // load_recently_played + + /** + * pubdate_recently_played + * This just returns the 'newest' recently played entry + */ + public static function pubdate_recently_played() + { + $data = Song::get_recently_played(); + + $element = array_shift($data); + + return $element['date']; + + } // pubdate_recently_played + +} // end Ampache_RSS class diff --git a/sources/lib/class/ampconfig.class.php b/sources/lib/class/ampconfig.class.php new file mode 100644 index 0000000..665cfaf --- /dev/null +++ b/sources/lib/class/ampconfig.class.php @@ -0,0 +1,97 @@ + $value) { + self::set($name, $value, $clobber); + } + } +} diff --git a/sources/lib/class/api.class.php b/sources/lib/class/api.class.php new file mode 100644 index 0000000..6e22ca5 --- /dev/null +++ b/sources/lib/class/api.class.php @@ -0,0 +1,825 @@ +set_filter('add_lt',strtotime($elements['1'])); + self::$browse->set_filter('add_gt',strtotime($elements['0'])); + } else { + self::$browse->set_filter('add_gt',strtotime($value)); + } + break; + case 'update': + // Check for a range, if no range default to gt + if (strpos($value,'/')) { + $elements = explode('/',$value); + self::$browse->set_filter('update_lt',strtotime($elements['1'])); + self::$browse->set_filter('update_gt',strtotime($elements['0'])); + } else { + self::$browse->set_filter('update_gt',strtotime($value)); + } + break; + case 'alpha_match': + self::$browse->set_filter('alpha_match',$value); + break; + case 'exact_match': + self::$browse->set_filter('exact_match',$value); + break; + default: + // Rien a faire + break; + } // end filter + + return true; + + } // set_filter + + /** + * handshake + * + * This is the function that handles verifying a new handshake + * Takes a timestamp, auth key, and username. + */ + public static function handshake($input) + { + $timestamp = preg_replace('/[^0-9]/', '', $input['timestamp']); + $passphrase = $input['auth']; + if (empty($passphrase)) { + $passphrase = $_POST['auth']; + } + $username = trim($input['user']); + $ip = $_SERVER['REMOTE_ADDR']; + $version = $input['version']; + + // Log the attempt + debug_event('API', "Handshake Attempt, IP:$ip User:$username Version:$version", 5); + + // Version check shouldn't be soo restrictive... only check with initial version to not break clients compatibility + if (intval($version) < self::$auth_version) { + debug_event('API', 'Login Failed: version too old', 1); + Error::add('api', T_('Login Failed: version too old')); + return false; + } + + $user_id = -1; + // Grab the correct userid + if (!$username) { + $client = User::get_from_apikey($passphrase); + if ($client) { + $user_id = $client->id; + } + } else { + $client = User::get_from_username($username); + $user_id = $client->id; + } + + // Log this attempt + debug_event('API', "Login Attempt, IP:$ip Time: $timestamp User:$username ($user_id) Auth:$passphrase", 1); + + if ($user_id > 0 && Access::check_network('api', $user_id, 5, $ip)) { + + // Authentication with user/password, we still need to check the password + if ($username) { + + // If the timestamp isn't within 30 minutes sucks to be them + if (($timestamp < (time() - 1800)) || + ($timestamp > (time() + 1800))) { + debug_event('API', 'Login Failed: timestamp out of range ' . $timestamp . '/' . time(), 1); + Error::add('api', T_('Login Failed: timestamp out of range')); + return false; + } + + // Now we're sure that there is an ACL line that matches + // this user or ALL USERS, pull the user's password and + // then see what we come out with + $realpwd = $client->get_password(); + + if (!$realpwd) { + debug_event('API', 'Unable to find user with userid of ' . $user_id, 1); + Error::add('api', T_('Invalid Username/Password')); + return false; + } + + $sha1pass = hash('sha256', $timestamp . $realpwd); + + if ($sha1pass !== $passphrase) { + $client = null; + } + } else { + $timestamp = time(); + } + + if ($client) { + // Create the session + $data = array(); + $data['username'] = $client->username; + $data['type'] = 'api'; + $data['value'] = $timestamp; + $token = Session::create($data); + + debug_event('API', 'Login Success, passphrase matched', 1); + + // We need to also get the 'last update' of the + // catalog information in an RFC 2822 Format + $sql = 'SELECT MAX(`last_update`) AS `update`, MAX(`last_add`) AS `add`, MAX(`last_clean`) AS `clean` FROM `catalog`'; + $db_results = Dba::read($sql); + $row = Dba::fetch_assoc($db_results); + + // Now we need to quickly get the song totals + $sql = 'SELECT COUNT(`id`) AS `song`, ' . + 'COUNT(DISTINCT(`album`)) AS `album`, '. + 'COUNT(DISTINCT(`artist`)) AS `artist` ' . + 'FROM `song`'; + $db_results = Dba::read($sql); + $counts = Dba::fetch_assoc($db_results); + + // Next the video counts + $sql = "SELECT COUNT(`id`) AS `video` FROM `video`"; + $db_results = Dba::read($sql); + $vcounts = Dba::fetch_assoc($db_results); + + $sql = "SELECT COUNT(`id`) AS `playlist` FROM `playlist`"; + $db_results = Dba::read($sql); + $playlist = Dba::fetch_assoc($db_results); + + $sql = "SELECT COUNT(`id`) AS `catalog` FROM `catalog` WHERE `catalog_type`='local'"; + $db_results = Dba::read($sql); + $catalog = Dba::fetch_assoc($db_results); + + echo XML_Data::keyed_array(array('auth'=>$token, + 'api'=>self::$version, + 'session_expire'=>date("c",time()+AmpConfig::get('session_length')-60), + 'update'=>date("c",$row['update']), + 'add'=>date("c",$row['add']), + 'clean'=>date("c",$row['clean']), + 'songs'=>$counts['song'], + 'albums'=>$counts['album'], + 'artists'=>$counts['artist'], + 'playlists'=>$playlist['playlist'], + 'videos'=>$vcounts['video'], + 'catalogs'=>$catalog['catalog'])); + return true; + } // match + + } // end while + + debug_event('API','Login Failed, unable to match passphrase','1'); + XML_Data::error('401', T_('Error Invalid Handshake - ') . T_('Invalid Username/Password')); + + } // handshake + + /** + * ping + * This can be called without being authenticated, it is useful for determining if what the status + * of the server is, and what version it is running/compatible with + */ + public static function ping($input) + { + $xmldata = array('server'=>AmpConfig::get('version'),'version'=>Api::$version,'compatible'=>'350001'); + + // Check and see if we should extend the api sessions (done if valid sess is passed) + if (Session::exists('api', $input['auth'])) { + Session::extend($input['auth']); + $xmldata = array_merge(array('session_expire'=>date("c",time()+AmpConfig::get('session_length')-60)),$xmldata); + } + + debug_event('API','Ping Received from ' . $_SERVER['REMOTE_ADDR'] . ' :: ' . $input['auth'],'5'); + + ob_end_clean(); + echo XML_Data::keyed_array($xmldata); + + } // ping + + /** + * artists + * This takes a collection of inputs and returns + * artist objects. This function is deprecated! + * //DEPRECATED + */ + public static function artists($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('artist'); + self::$browse->set_sort('name','ASC'); + + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + Api::set_filter('add',$input['add']); + Api::set_filter('update',$input['update']); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + $artists = self::$browse->get_objects(); + // echo out the resulting xml document + ob_end_clean(); + echo XML_Data::artists($artists); + + } // artists + + /** + * artist + * This returns a single artist based on the UID of said artist + * //DEPRECATED + */ + public static function artist($input) + { + $uid = scrub_in($input['filter']); + echo XML_Data::artists(array($uid)); + + } // artist + + /** + * artist_albums + * This returns the albums of an artist + */ + public static function artist_albums($input) + { + $artist = new Artist($input['filter']); + + $albums = $artist->get_albums(null, true); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + ob_end_clean(); + echo XML_Data::albums($albums); + + } // artist_albums + + /** + * artist_songs + * This returns the songs of the specified artist + */ + public static function artist_songs($input) + { + $artist = new Artist($input['filter']); + $songs = $artist->get_songs(); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + ob_end_clean(); + echo XML_Data::songs($songs); + + } // artist_songs + + /** + * albums + * This returns albums based on the provided search filters + */ + public static function albums($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('album'); + self::$browse->set_sort('name','ASC'); + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + Api::set_filter('add',$input['add']); + Api::set_filter('update',$input['update']); + + $albums = self::$browse->get_objects(); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + ob_end_clean(); + echo XML_Data::albums($albums); + + } // albums + + /** + * album + * This returns a single album based on the UID provided + */ + public static function album($input) + { + $uid = scrub_in($input['filter']); + echo XML_Data::albums(array($uid)); + + } // album + + /** + * album_songs + * This returns the songs of a specified album + */ + public static function album_songs($input) + { + $album = new Album($input['filter']); + $songs = $album->get_songs(); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::songs($songs); + + } // album_songs + + /** + * tags + * This returns the tags based on the specified filter + */ + public static function tags($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('tag'); + self::$browse->set_sort('name','ASC'); + + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + $tags = self::$browse->get_objects(); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::tags($tags); + + } // tags + + /** + * tag + * This returns a single tag based on UID + */ + public static function tag($input) + { + $uid = scrub_in($input['filter']); + ob_end_clean(); + echo XML_Data::tags(array($uid)); + + } // tag + + /** + * tag_artists + * This returns the artists associated with the tag in question as defined by the UID + */ + public static function tag_artists($input) + { + $artists = Tag::get_tag_objects('artist',$input['filter']); + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::artists($artists); + + } // tag_artists + + /** + * tag_albums + * This returns the albums associated with the tag in question + */ + public static function tag_albums($input) + { + $albums = Tag::get_tag_objects('album',$input['filter']); + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::albums($albums); + + } // tag_albums + + /** + * tag_songs + * returns the songs for this tag + */ + public static function tag_songs($input) + { + $songs = Tag::get_tag_objects('song',$input['filter']); + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::songs($songs); + + } // tag_songs + + /** + * songs + * Returns songs based on the specified filter + */ + public static function songs($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('song'); + self::$browse->set_sort('title','ASC'); + + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + Api::set_filter('add',$input['add']); + Api::set_filter('update',$input['update']); + + $songs = self::$browse->get_objects(); + + // Set the offset + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::songs($songs); + + } // songs + + /** + * song + * returns a single song + */ + public static function song($input) + { + $uid = scrub_in($input['filter']); + + ob_end_clean(); + echo XML_Data::songs(array($uid)); + + } // song + + /** + * url_to_song + * + * This takes a url and returns the song object in question + */ + public static function url_to_song($input) + { + // Don't scrub, the function needs her raw and juicy + $data = Stream_URL::parse($input['url']); + ob_end_clean(); + echo XML_Data::songs(array($data['id'])); + } + + /** + * playlists + * This returns playlists based on the specified filter + */ + public static function playlists($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('playlist'); + self::$browse->set_sort('name','ASC'); + + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + self::$browse->set_filter('playlist_type', '1'); + + $playlist_ids = self::$browse->get_objects(); + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + ob_end_clean(); + echo XML_Data::playlists($playlist_ids); + + } // playlists + + /** + * playlist + * This returns a single playlist + */ + public static function playlist($input) + { + $uid = scrub_in($input['filter']); + + ob_end_clean(); + echo XML_Data::playlists(array($uid)); + + } // playlist + + /** + * playlist_songs + * This returns the songs for a playlist + */ + public static function playlist_songs($input) + { + $playlist = new Playlist($input['filter']); + $items = $playlist->get_items(); + + $songs = array(); + foreach ($items as $object) { + if ($object['object_type'] == 'song') { + $songs[] = $object['object_id']; + } + } // end foreach + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + ob_end_clean(); + echo XML_Data::songs($songs); + + } // playlist_songs + + /** + * playlist_create + * This create a new playlist and return it + */ + public static function playlist_create($input) + { + $name = $input['name']; + $type = $input['type']; + if ($type != 'private') { + $type = 'public'; + } + + $uid = Playlist::create($name, $type); + echo XML_Data::playlists(array($uid)); + } + + /** + * playlist_delete + * This delete a playlist + */ + public static function playlist_delete($input) + { + ob_end_clean(); + $playlist = new Playlist($input['filter']); + if (!$playlist->has_access()) { + echo XML_Data::error('401', T_('Access denied to this playlist.')); + } else { + $playlist->delete(); + echo XML_Data::single_string('success'); + } + } // playlist_delete + + /** + * playlist_add_song + * This add a song to a playlist + */ + public static function playlist_add_song($input) + { + ob_end_clean(); + $playlist = new Playlist($input['filter']); + $song = new Playlist($input['song']); + if (!$playlist->has_access()) { + echo XML_Data::error('401', T_('Access denied to this playlist.')); + } else { + $playlist->add_songs(array($song)); + echo XML_Data::single_string('success'); + } + + } // playlist_add_song + + /** + * playlist_remove_song + * This remove a song from a playlist + */ + public static function playlist_remove_song($input) + { + ob_end_clean(); + $playlist = new Playlist($input['filter']); + $track = new Playlist($input['track']); + if (!$playlist->has_access()) { + echo XML_Data::error('401', T_('Access denied to this playlist.')); + } else { + $playlist->delete_track_number($track); + echo XML_Data::single_string('success'); + } + + } // playlist_remove_song + + /** + * search_songs + * This searches the songs and returns... songs + */ + public static function search_songs($input) + { + $array = array(); + $array['type'] = 'song'; + $array['rule_1'] = 'anywhere'; + $array['rule_1_input'] = $input['filter']; + $array['rule_1_operator'] = 0; + + ob_end_clean(); + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + $results = Search::run($array); + + echo XML_Data::songs($results); + + } // search_songs + + /** + * videos + * This returns video objects! + */ + public static function videos($input) + { + self::$browse->reset_filters(); + self::$browse->set_type('video'); + self::$browse->set_sort('title','ASC'); + + $method = $input['exact'] ? 'exact_match' : 'alpha_match'; + Api::set_filter($method,$input['filter']); + + $video_ids = self::$browse->get_objects(); + + XML_Data::set_offset($input['offset']); + XML_Data::set_limit($input['limit']); + + echo XML_Data::videos($video_ids); + + } // videos + + /** + * video + * This returns a single video + */ + public static function video($input) + { + $video_id = scrub_in($input['filter']); + + echo XML_Data::videos(array($video_id)); + + + } // video + + /** + * localplay + * This is for controling localplay + */ + public static function localplay($input) + { + // Load their localplay instance + $localplay = new Localplay(AmpConfig::get('localplay_controller')); + $localplay->connect(); + + switch ($input['command']) { + case 'next': + case 'prev': + case 'play': + case 'stop': + $result_status = $localplay->$input['command'](); + $xml_array = array('localplay'=>array('command'=>array($input['command']=>make_bool($result_status)))); + echo XML_Data::keyed_array($xml_array); + break; + default: + // They are doing it wrong + echo XML_Data::error('405', T_('Invalid Request')); + break; + } // end switch on command + + } // localplay + + /** + * democratic + * This is for controlling democratic play + */ + public static function democratic($input) + { + // Load up democratic information + $democratic = Democratic::get_current_playlist(); + $democratic->set_parent(); + + switch ($input['method']) { + case 'vote': + $type = 'song'; + $media = new $type($input['oid']); + if (!$media->id) { + echo XML_Data::error('400', T_('Media Object Invalid or Not Specified')); + break; + } + $democratic->add_vote(array( + array( + 'object_type' => 'song', + 'object_id' => $media->id + ) + )); + + // If everything was ok + $xml_array = array('action'=>$input['action'],'method'=>$input['method'],'result'=>true); + echo XML_Data::keyed_array($xml_array); + break; + case 'devote': + $type = 'song'; + $media = new $type($input['oid']); + if (!$media->id) { + echo XML_Data::error('400', T_('Media Object Invalid or Not Specified')); + } + + $uid = $democratic->get_uid_from_object_id($media->id,$type); + $democratic->remove_vote($uid); + + // Everything was ok + $xml_array = array('action'=>$input['action'],'method'=>$input['method'],'result'=>true); + echo XML_Data::keyed_array($xml_array); + break; + case 'playlist': + $objects = $democratic->get_items(); + Song::build_cache($democratic->object_ids); + Democratic::build_vote_cache($democratic->vote_ids); + XML_Data::democratic($objects); + break; + case 'play': + $url = $democratic->play_url(); + $xml_array = array('url'=>$url); + echo XML_Data::keyed_array($xml_array); + break; + default: + echo XML_Data::error('405', T_('Invalid Request')); + break; + } // switch on method + + } // democratic + + public static function stats($input) + { + $type = $input['type']; + $offset = $input['offset']; + $limit = $input['limit']; + + if ($type == "newest") { + $albums = Stats::get_newest("album", $limit, $offset); + } else if ($type == "highest") { + $albums = Rating::get_highest("album", $limit, $offset); + } else if ($type == "frequent") { + $albums = Stats::get_top("album", $limit, '', $offset); + } else if ($type == "recent") { + $albums = Stats::get_recent("album", $limit, $offset); + } else if ($type == "flagged") { + $albums = Userflag::get_latest('album'); + } else { + if (!$limit) { + $limit = AmpConfig::get('popular_threshold'); + } + $albums = Album::get_random($limit); + } + + ob_end_clean(); + echo XML_Data::albums($albums); + } + +} // API class diff --git a/sources/lib/class/art.class.php b/sources/lib/class/art.class.php new file mode 100644 index 0000000..41c371f --- /dev/null +++ b/sources/lib/class/art.class.php @@ -0,0 +1,1231 @@ +type = Art::validate_type($type); + $this->uid = $uid; + + } // constructor + + /** + * build_cache + * This attempts to reduce # of queries by asking for everything in the + * browse all at once and storing it in the cache, this can help if the + * db connection is the slow point + */ + public static function build_cache($object_ids) + { + if (!is_array($object_ids) || !count($object_ids)) { return false; } + $uidlist = '(' . implode(',', $object_ids) . ')'; + $sql = "SELECT `object_type`, `object_id`, `mime`, `size` FROM `image` WHERE `object_id` IN $uidlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('art', $row['object_type'] . + $row['object_id'] . $row['size'], $row); + } + + return true; + + } // build_cache + + /** + * _auto_init + * Called on creation of the class + */ + public static function _auto_init() + { + if (!isset($_SESSION['art_enabled'])) { + /*if (isset($_COOKIE['art_enabled'])) { + $_SESSION['art_enabled'] = $_COOKIE['art_enabled']; + } else {*/ + $_SESSION['art_enabled'] = true; + //} + } + + self::$enabled = make_bool($_SESSION['art_enabled']); + //setcookie('art_enabled', self::$enabled, time() + 31536000, "/"); + } + + /** + * is_enabled + * Checks whether the user currently wants art + */ + public static function is_enabled() + { + if (self::$enabled) { + return true; + } + + return false; + } + + /** + * set_enabled + * Changes the value of enabled + */ + public static function set_enabled($value = null) + { + if (is_null($value)) { + self::$enabled = self::$enabled ? false : true; + } else { + self::$enabled = make_bool($value); + } + + $_SESSION['art_enabled'] = self::$enabled; + //setcookie('art_enabled', self::$enabled, time() + 31536000, "/"); + } + + /** + * validate_type + * This validates the type + */ + public static function validate_type($type) + { + switch ($type) { + case 'album': + case 'artist': + case 'video': + case 'user': + return $type; + default: + return 'album'; + } + + } // validate_type + + /** + * extension + * This returns the file extension for the currently loaded art + */ + public static function extension($mime) + { + $data = explode("/", $mime); + $extension = $data['1']; + + if ($extension == 'jpeg') { $extension = 'jpg'; } + + return $extension; + + } // extension + + /** + * test_image + * Runs some sanity checks on the putative image + */ + public static function test_image($source) + { + if (strlen($source) < 10) { + debug_event('Art', 'Invalid image passed', 1); + return false; + } + + // Check to make sure PHP:GD exists. If so, we can sanity check + // the image. + if (function_exists('ImageCreateFromString')) { + $image = ImageCreateFromString($source); + if (!$image || imagesx($image) < 5 || imagesy($image) < 5) { + debug_event('Art', 'Image failed PHP-GD test',1); + return false; + } + } + + return true; + } //test_image + + /** + * get + * This returns the art for our current object, this can + * look in the database and will return the thumb if it + * exists, if it doesn't depending on settings it will try + * to create it. + */ + public function get($raw=false) + { + // Get the data either way + if (!$this->get_db()) { + return false; + } + + if ($raw || !$this->thumb) { + return $this->raw; + } else { + return $this->thumb; + } + + } // get + + + /** + * get_db + * This pulls the information out from the database, depending + * on if we want to resize and if there is not a thumbnail go + * ahead and try to resize + */ + public function get_db() + { + $type = Dba::escape($this->type); + $id = Dba::escape($this->uid); + + $sql = "SELECT `image`, `mime`, `size` FROM `image` WHERE `object_type`='$type' AND `object_id`='$id'"; + $db_results = Dba::read($sql); + + while ($results = Dba::fetch_assoc($db_results)) { + if ($results['size'] == 'original') { + $this->raw = $results['image']; + $this->raw_mime = $results['mime']; + } else if (AmpConfig::get('resize_images') && + $results['size'] == '275x275') { + $this->thumb = $results['image']; + $this->raw_mime = $results['mime']; + } + } + // If we get nothing return false + if (!$this->raw) { return false; } + + // If there is no thumb and we want thumbs + if (!$this->thumb && AmpConfig::get('resize_images')) { + $data = $this->generate_thumb($this->raw, array('width' => 275, 'height' => 275), $this->raw_mime); + // If it works save it! + if ($data) { + $this->save_thumb($data['thumb'], $data['thumb_mime'], '275x275'); + $this->thumb = $data['thumb']; + $this->thumb_mime = $data['thumb_mime']; + } else { + debug_event('Art','Unable to retrieve or generate thumbnail for ' . $type . '::' . $id,1); + } + } // if no thumb, but art and we want to resize + + return true; + + } // get_db + + /** + * insert + * This takes the string representation of an image and inserts it into + * the database. You must also pass the mime type. + */ + public function insert($source, $mime) + { + // Disabled in demo mode cause people suck and upload porn + if (AmpConfig::get('demo_mode')) { return false; } + + // Check to make sure we like this image + if (!self::test_image($source)) { + debug_event('Art', 'Not inserting image, invalid data passed', 1); + return false; + } + + // Default to image/jpeg if they don't pass anything + $mime = $mime ? $mime : 'image/jpeg'; + + $image = Dba::escape($source); + $mime = Dba::escape($mime); + $uid = Dba::escape($this->uid); + $type = Dba::escape($this->type); + + // Blow it away! + $this->reset(); + + // Insert it! + $sql = "INSERT INTO `image` (`image`, `mime`, `size`, `object_type`, `object_id`) VALUES('$image', '$mime', 'original', '$type', '$uid')"; + Dba::write($sql); + + return true; + + } // insert + + /** + * reset + * This resets the art in the database + */ + public function reset() + { + $sql = "DELETE FROM `image` WHERE `object_id` = ? AND `object_type` = ?"; + Dba::write($sql, array($this->uid, $this->type)); + } // reset + + /** + * save_thumb + * This saves the thumbnail that we're passed + */ + public function save_thumb($source, $mime, $size) + { + // Quick sanity check + if (!self::test_image($source)) { + debug_event('Art', 'Not inserting thumbnail, invalid data passed', 1); + return false; + } + + $source = Dba::escape($source); + $mime = Dba::escape($mime); + $size = Dba::escape($size); + $uid = Dba::escape($this->uid); + $type = Dba::escape($this->type); + + $sql = "DELETE FROM `image` WHERE `object_id`='$uid' AND `object_type`='$type' AND `size`='$size'"; + Dba::write($sql); + + $sql = "INSERT INTO `image` (`image`, `mime`, `size`, `object_type`, `object_id`) VALUES('$source', '$mime', '$size', '$type', '$uid')"; + Dba::write($sql); + } // save_thumb + + /** + * get_thumb + * Returns the specified resized image. If the requested size doesn't + * already exist, create and cache it. + */ + public function get_thumb($size) + { + $sizetext = $size['width'] . 'x' . $size['height']; + $sizetext = Dba::escape($sizetext); + $type = Dba::escape($this->type); + $uid = Dba::escape($this->uid); + + $sql = "SELECT `image`, `mime` FROM `image` WHERE `size`='$sizetext' AND `object_type`='$type' AND `object_id`='$uid'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + if (count($results)) { + return array('thumb' => $results['image'], + 'thumb_mime' => $results['mime']); + } + + // If we didn't get a result + $results = $this->generate_thumb($this->raw, $size, $this->raw_mime); + if ($results) { + $this->save_thumb($results['thumb'], $results['thumb_mime'], $sizetext); + } + + return $results; + + } // get_thumb + + /** + * generate_thumb + * Automatically resizes the image for thumbnail viewing. + * Only works on gif/jpg/png/bmp. Fails if PHP-GD isn't available + * or lacks support for the requested image type. + */ + public function generate_thumb($image,$size,$mime) + { + $data = explode("/",$mime); + $type = strtolower($data['1']); + + if (!self::test_image($image)) { + debug_event('Art', 'Not trying to generate thumbnail, invalid data passed', 1); + return false; + } + + if (!function_exists('gd_info')) { + debug_event('Art','PHP-GD Not found - unable to resize art',1); + return false; + } + + // Check and make sure we can resize what you've asked us to + if (($type == 'jpg' OR $type == 'jpeg') AND !(imagetypes() & IMG_JPG)) { + debug_event('Art','PHP-GD Does not support JPGs - unable to resize',1); + return false; + } + if ($type == 'png' AND !imagetypes() & IMG_PNG) { + debug_event('Art','PHP-GD Does not support PNGs - unable to resize',1); + return false; + } + if ($type == 'gif' AND !imagetypes() & IMG_GIF) { + debug_event('Art','PHP-GD Does not support GIFs - unable to resize',1); + return false; + } + if ($type == 'bmp' AND !imagetypes() & IMG_WBMP) { + debug_event('Art','PHP-GD Does not support BMPs - unable to resize',1); + return false; + } + + $source = imagecreatefromstring($image); + + if (!$source) { + debug_event('Art','Failed to create Image from string - Source Image is damaged / malformed',1); + return false; + } + + $source_size = array('height' => imagesy($source), 'width' => imagesx($source)); + + // Create a new blank image of the correct size + $thumbnail = imagecreatetruecolor($size['width'], $size['height']); + + if (!imagecopyresampled($thumbnail, $source, 0, 0, 0, 0, $size['width'], $size['height'], $source_size['width'], $source_size['height'])) { + debug_event('Art','Unable to create resized image',1); + return false; + } + + // Start output buffer + ob_start(); + + // Generate the image to our OB + switch ($type) { + case 'jpg': + case 'jpeg': + imagejpeg($thumbnail, null, 75); + $mime_type = image_type_to_mime_type(IMAGETYPE_JPEG); + break; + case 'gif': + imagegif($thumbnail); + $mime_type = image_type_to_mime_type(IMAGETYPE_GIF); + break; + // Turn bmps into pngs + case 'bmp': + case 'png': + imagepng($thumbnail); + $mime_type = image_type_to_mime_type(IMAGETYPE_PNG); + break; + } // resized + + if (!isset($mime_type)) { + debug_event('Art', 'Eror: No mime type found.', 1); + return false; + } + + $data = ob_get_contents(); + ob_end_clean(); + + if (!strlen($data)) { + debug_event('Art', 'Unknown Error resizing art', 1); + return false; + } + + return array('thumb' => $data, 'thumb_mime' => $mime_type); + + } // generate_thumb + + /** + * get_from_source + * This gets an image for the album art from a source as + * defined in the passed array. Because we don't know where + * it's coming from we are a passed an array that can look like + * ['url'] = URL *** OPTIONAL *** + * ['file'] = FILENAME *** OPTIONAL *** + * ['raw'] = Actual Image data, already captured + */ + public static function get_from_source($data, $type = 'album') + { + // Already have the data, this often comes from id3tags + if (isset($data['raw'])) { + return $data['raw']; + } + + // If it came from the database + if (isset($data['db'])) { + // Repull it + $uid = Dba::escape($data['db']); + $type = Dba::escape($type); + + $sql = "SELECT * FROM `image` WHERE `object_type`='$type' AND `object_id`='$uid' AND `size`='original'"; + $db_results = Dba::read($sql); + $row = Dba::fetch_assoc($db_results); + return $row['art']; + } // came from the db + + // Check to see if it's a URL + if (isset($data['url'])) { + $options = array(); + if (AmpConfig::get('proxy_host') AND AmpConfig::get('proxy_port')) { + $proxy = array(); + $proxy[] = AmpConfig::get('proxy_host') . ':' . AmpConfig::get('proxy_port'); + if (AmpConfig::get('proxy_user')) { + $proxy[] = AmpConfig::get('proxy_user'); + $proxy[] = AmpConfig::get('proxy_pass'); + } + $options['proxy'] = $proxy; + } + $request = Requests::get($data['url'], array(), $options); + return $request->body; + } + + // Check to see if it's a FILE + if (isset($data['file'])) { + $handle = fopen($data['file'],'rb'); + $image_data = fread($handle,filesize($data['file'])); + fclose($handle); + return $image_data; + } + + // Check to see if it is embedded in id3 of a song + if (isset($data['song'])) { + // If we find a good one, stop looking + $getID3 = new getID3(); + $id3 = $getID3->analyze($data['song']); + + if ($id3['format_name'] == "WMA") { + return $id3['asf']['extended_content_description_object']['content_descriptors']['13']['data']; + } elseif (isset($id3['id3v2']['APIC'])) { + // Foreach in case they have more then one + foreach ($id3['id3v2']['APIC'] as $image) { + return $image['data']; + } + } + } // if data song + + return false; + + } // get_from_source + + /** + * url + * This returns the constructed URL for the art in question + */ + public static function url($uid,$type,$sid=false) + { + $sid = $sid ? scrub_out($sid) : scrub_out(session_id()); + $type = self::validate_type($type); + + $key = $type . $uid; + + if (parent::is_cached('art', $key . '275x275') && AmpConfig::get('resize_images')) { + $row = parent::get_from_cache('art', $key . '275x275'); + $mime = $row['mime']; + } + if (parent::is_cached('art', $key . 'original')) { + $row = parent::get_from_cache('art', $key . 'original'); + $thumb_mime = $row['mime']; + } + if (!isset($mime) && !isset($thumb_mime)) { + $sql = "SELECT `object_type`, `object_id`, `mime`, `size` FROM `image` WHERE `object_type` = ? AND `object_id` = ?"; + $db_results = Dba::read($sql, array($type, $uid)); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('art', $key . $row['size'], $row); + if ($row['size'] == 'original') { + $mime = $row['mime']; + } else if ($row['size'] == '275x275' && AmpConfig::get('resize_images')) { + $thumb_mime = $row['mime']; + } + } + } + + $mime = isset($thumb_mime) ? $thumb_mime : (isset($mime) ? $mime : null); + $extension = self::extension($mime); + + $name = 'art.' . $extension; + $url = AmpConfig::get('web_path') . '/image.php?id=' . scrub_out($uid) . '&object_type=' . scrub_out($type) . '&auth=' . $sid . '&name=' . $name; + + return $url; + + } // url + + /** + * gc + * This cleans up art that no longer has a corresponding object + */ + public static function gc() + { + // iterate over our types and delete the images + foreach (array('album', 'artist') as $type) { + $sql = "DELETE FROM `image` USING `image` LEFT JOIN `" . + $type . "` ON `" . $type . "`.`id`=" . + "`image`.`object_id` WHERE `object_type`='" . + $type . "' AND `" . $type . "`.`id` IS NULL"; + Dba::write($sql); + } // foreach + } + + /** + * gather + * This tries to get the art in question + */ + public function gather($options = array(), $limit = false) + { + // Define vars + $results = array(); + + switch ($this->type) { + case 'album': + $allowed_methods = array('db','lastfm','folder','amazon','google','musicbrainz','tags'); + break; + case 'artist': + case 'video': + default: + $allowed_methods = array(); + break; + } + + $config = AmpConfig::get('art_order'); + $methods = get_class_methods('Art'); + + /* If it's not set */ + if (empty($config)) { + // They don't want art! + debug_event('Art', 'art_order is empty, skipping art gathering', 3); + return array(); + } elseif (!is_array($config)) { + $config = array($config); + } + + debug_event('Art','Searching using:' . json_encode($config), 3); + + foreach ($config as $method) { + + if (!in_array($method, $allowed_methods)) { + debug_event('Art', "$method not in allowed_methods, skipping", 3); + continue; + } + + $method_name = "gather_" . $method; + + if (in_array($method_name, $methods)) { + debug_event('Art', "Method used: $method_name", 3); + // Some of these take options! + switch ($method_name) { + case 'gather_amazon': + $data = $this->{$method_name}($limit, $options['keyword']); + break; + case 'gather_lastfm': + $data = $this->{$method_name}($limit, $options); + break; + default: + $data = $this->{$method_name}($limit); + break; + } + + // Add the results we got to the current set + $results = array_merge($results, (array) $data); + + if ($limit && count($results) >= $limit) { + return array_slice($results, 0, $limit); + } + + } // if the method exists + else { + debug_event("Art", "$method_name not defined", 1); + } + + } // end foreach + + return $results; + + } // gather + + /////////////////////////////////////////////////////////////////////// + // Art Methods + /////////////////////////////////////////////////////////////////////// + + /** + * gather_db + * This function retrieves art that's already in the database + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function gather_db($limit = null) + { + if ($this->get_db()) { + return array('db' => true); + } + return array(); + } + + /** + * gather_musicbrainz + * This function retrieves art based on MusicBrainz' Advanced + * Relationships + */ + public function gather_musicbrainz($limit = 5) + { + $images = array(); + $num_found = 0; + + if ($this->type == 'album') { + $album = new Album($this->uid); + } else { + return $images; + } + + if ($album->mbid) { + debug_event('mbz-gatherart', "Album MBID: " . $album->mbid, '5'); + } else { + return $images; + } + + $mb = new MusicBrainz(new RequestsMbClient()); + $includes = array( + 'url-rels' + ); + try { + $release = $mb->lookup('release', $album->mbid, $includes); + } catch (Exception $e) { + return $images; + } + + $asin = $release->asin; + + if ($asin) { + debug_event('mbz-gatherart', "Found ASIN: " . $asin, '5'); + $base_urls = array( + "01" => "ec1.images-amazon.com", + "02" => "ec1.images-amazon.com", + "03" => "ec2.images-amazon.com", + "08" => "ec1.images-amazon.com", + "09" => "ec1.images-amazon.com", + ); + foreach ($base_urls as $server_num => $base_url) { + // to avoid complicating things even further, we only look for large cover art + $url = 'http://' . $base_url . '/images/P/' . $asin . '.' . $server_num . '.LZZZZZZZ.jpg'; + debug_event('mbz-gatherart', "Evaluating Amazon URL: " . $url, '5'); + $options = array(); + if (AmpConfig::get('proxy_host') AND AmpConfig::get('proxy_port')) { + $proxy = array(); + $proxy[] = AmpConfig::get('proxy_host') . ':' . AmpConfig::get('proxy_port'); + if (AmpConfig::get('proxy_user')) { + $proxy[] = AmpConfig::get('proxy_user'); + $proxy[] = AmpConfig::get('proxy_pass'); + } + $options['proxy'] = $proxy; + } + $request = Requests::get($url, array(), $options); + if ($request->status_code == 200) { + $num_found++; + debug_event('mbz-gatherart', "Amazon URL added: " . $url, '5'); + $images[] = array( + 'url' => $url, + 'mime' => 'image/jpeg', + ); + if ($num_found >= $limit) { + return $images; + } + } + } + } + // The next bit is based directly on the MusicBrainz server code + // that displays cover art. + // I'm leaving in the releaseuri info for the moment, though + // it's not going to be used. + $coverartsites = array(); + $coverartsites[] = array( + 'name' => "CD Baby", + 'domain' => "cdbaby.com", + 'regexp' => '@http://cdbaby\.com/cd/(\w)(\w)(\w*)@', + 'imguri' => 'http://cdbaby.name/$matches[1]/$matches[2]/$matches[1]$matches[2]$matches[3].jpg', + 'releaseuri' => 'http://cdbaby.com/cd/$matches[1]$matches[2]$matches[3]/from/musicbrainz', + ); + $coverartsites[] = array( + 'name' => "CD Baby", + 'domain' => "cdbaby.name", + 'regexp' => "@http://cdbaby\.name/([a-z0-9])/([a-z0-9])/([A-Za-z0-9]*).jpg@", + 'imguri' => 'http://cdbaby.name/$matches[1]/$matches[2]/$matches[3].jpg', + 'releaseuri' => 'http://cdbaby.com/cd/$matches[3]/from/musicbrainz', + ); + $coverartsites[] = array( + 'name' => 'archive.org', + 'domain' => 'archive.org', + 'regexp' => '/^(.*\.(jpg|jpeg|png|gif))$/', + 'imguri' => '$matches[1]', + 'releaseuri' => '', + ); + $coverartsites[] = array( + 'name' => "Jamendo", + 'domain' => "www.jamendo.com", + 'regexp' => '/http://www\.jamendo\.com/(\w\w/)?album/(\d+)/', + 'imguri' => 'http://img.jamendo.com/albums/$matches[2]/covers/1.200.jpg', + 'releaseuri' => 'http://www.jamendo.com/album/$matches[2]', + ); + $coverartsites[] = array( + 'name' => '8bitpeoples.com', + 'domain' => '8bitpeoples.com', + 'regexp' => '/^(.*)$/', + 'imguri' => '$matches[1]', + 'releaseuri' => '', + ); + $coverartsites[] = array( + 'name' => 'Encyclopédisque', + 'domain' => 'encyclopedisque.fr', + 'regexp' => '/http://www.encyclopedisque.fr/images/imgdb/(thumb250|main)/(\d+).jpg/', + 'imguri' => 'http://www.encyclopedisque.fr/images/imgdb/thumb250/$matches[2].jpg', + 'releaseuri' => 'http://www.encyclopedisque.fr/', + ); + $coverartsites[] = array( + 'name' => 'Thastrom', + 'domain' => 'www.thastrom.se', + 'regexp' => '/^(.*)$/', + 'imguri' => '$matches[1]', + 'releaseuri' => '', + ); + $coverartsites[] = array( + 'name' => 'Universal Poplab', + 'domain' => 'www.universalpoplab.com', + 'regexp' => '/^(.*)$/', + 'imguri' => '$matches[1]', + 'releaseuri' => '', + ); + foreach ($release->relations as $ar) { + $arurl = $ar->url->resource; + debug_event('mbz-gatherart', "Found URL AR: " . $arurl , '5'); + foreach ($coverartsites as $casite) { + if (strpos($arurl, $casite['domain']) !== false) { + debug_event('mbz-gatherart', "Matched coverart site: " . $casite['name'], '5'); + if (preg_match($casite['regexp'], $arurl, $matches)) { + $num_found++; + $url = ''; + eval("\$url = \"$casite[imguri]\";"); + debug_event('mbz-gatherart', "Generated URL added: " . $url, '5'); + $images[] = array( + 'url' => $url, + 'mime' => 'image/jpeg', + ); + if ($num_found >= $limit) { + return $images; + } + } + } + } // end foreach coverart sites + } // end foreach + + return $images; + + } // gather_musicbrainz + + /** + * gather_amazon + * This takes keywords and performs a search of the Amazon website + * for the art. It returns an array of found objects with mime/url keys + */ + public function gather_amazon($limit = 5, $keywords = '') + { + $images = array(); + $final_results = array(); + $possible_keys = array( + 'LargeImage', + 'MediumImage', + 'SmallImage' + ); + + if ($this->type == 'album') { + $album = new Album($this->uid); + } else { + return $images; + } + + // Prevent the script from timing out + set_time_limit(0); + + if (empty($keywords)) { + $keywords = $album->full_name; + /* If this isn't a various album combine with artist name */ + if ($album->artist_count == '1') { $keywords .= ' ' . $album->artist_name; } + } + + /* Attempt to retrieve the album art order */ + $amazon_base_urls = AmpConfig::get('amazon_base_urls'); + + /* If it's not set */ + if (!count($amazon_base_urls)) { + $amazon_base_urls = array('http://webservices.amazon.com'); + } + + /* Foreach through the base urls that we should check */ + foreach ($amazon_base_urls as $amazon_base) { + + // Create the Search Object + $amazon = new AmazonSearch(AmpConfig::get('amazon_developer_public_key'), AmpConfig::get('amazon_developer_private_key'), AmpConfig::get('amazon_developer_associate_tag'), $amazon_base); + if (AmpConfig::get('proxy_host') AND AmpConfig::get('proxy_port')) { + $proxyhost = AmpConfig::get('proxy_host'); + $proxyport = AmpConfig::get('proxy_port'); + $proxyuser = AmpConfig::get('proxy_user'); + $proxypass = AmpConfig::get('proxy_pass'); + debug_event('amazon', 'setProxy', 5); + $amazon->setProxy($proxyhost, $proxyport, $proxyuser, $proxypass); + } + + $search_results = array(); + + /* Set up the needed variables */ + $max_pages_to_search = max(AmpConfig::get('max_amazon_results_pages'),$amazon->_default_results_pages); + // while we have pages to search + do { + $raw_results = $amazon->search(array('artist'=>'', 'album'=>'', 'keywords'=>$keywords)); + $total = count($raw_results) + count($search_results); + + // If we've gotten more then we wanted + if ($limit && $total > $limit) { + $raw_results = array_slice($raw_results, 0, -($total - $limit), true); + + debug_event('amazon-xml', "Found $total, limit $limit; reducing and breaking from loop", 5); + // Merge the results and BREAK! + $search_results = array_merge($search_results,$raw_results); + break; + } // if limit defined + + $search_results = array_merge($search_results,$raw_results); + $pages_to_search = min($max_pages_to_search, $amazon->_maxPage); + debug_event('amazon-xml', "Searched results page " . ($amazon->_currentPage+1) . "/" . $pages_to_search,'5'); + $amazon->_currentPage++; + + } while ($amazon->_currentPage < $pages_to_search); + + + // Only do the second search if the first actually returns something + if (count($search_results)) { + $final_results = $amazon->lookup($search_results); + } + + /* Log this if we're doin debug */ + debug_event('amazon-xml',"Searched using $keywords with " . AmpConfig::get('amazon_developer_key') . " as key, results: " . count($final_results), 5); + + // If we've hit our limit + if (!empty($limit) && count($final_results) >= $limit) { + break; + } + + } // end foreach + + /* Foreach through what we've found */ + foreach ($final_results as $result) { + + $key = ''; + /* Recurse through the images found */ + foreach ($possible_keys as $k) { + if (strlen($result[$k])) { + $key = $k; + break; + } + } // foreach + + // Rudimentary image type detection, only JPG and GIF allowed. + if (substr($result[$key], -4) == '.jpg') { + $mime = "image/jpeg"; + } elseif (substr($result[$key], -4) == '.gif') { + $mime = "image/gif"; + } elseif (substr($result[$key], -4) == '.png') { + $mime = "image/png"; + } else { + /* Just go to the next result */ + continue; + } + + $data = array(); + $data['url'] = $result[$key]; + $data['mime'] = $mime; + + $images[] = $data; + + if (!empty($limit)) { + if (count($images) >= $limit) { + return $images; + } + } + + } // if we've got something + + return $images; + + } // gather_amazon + + /** + * gather_folder + * This returns the art from the folder of the files + * If a limit is passed or the preferred filename is found the current + * results set is returned + */ + public function gather_folder($limit = 5) + { + $media = new Album($this->uid); + $songs = $media->get_songs(); + $results = array(); + $preferred = false; + // For storing which directories we've already done + $processed = array(); + + /* See if we are looking for a specific filename */ + $preferred_filename = AmpConfig::get('album_art_preferred_filename'); + + // Array of valid extensions + $image_extensions = array( + 'bmp', + 'gif', + 'jp2', + 'jpeg', + 'jpg', + 'png' + ); + + foreach ($songs as $song_id) { + $song = new Song($song_id); + $dir = dirname($song->file); + + if (isset($processed[$dir])) { + continue; + } + + debug_event('folder_art', "Opening $dir and checking for Album Art", 3); + + /* Open up the directory */ + $handle = opendir($dir); + + if (!$handle) { + Error::add('general', T_('Error: Unable to open') . ' ' . $dir); + debug_event('folder_art', "Error: Unable to open $dir for album art read", 2); + continue; + } + + $processed[$dir] = true; + + // Recurse through this dir and create the files array + while ($file = readdir($handle)) { + $extension = pathinfo($file); + $extension = $extension['extension']; + + // Make sure it looks like an image file + if (!in_array($extension, $image_extensions)) { + continue; + } + + $full_filename = $dir . '/' . $file; + + // Make sure it's got something in it + if (!filesize($full_filename)) { + debug_event('folder_art', "Empty file, rejecting $file", 5); + continue; + } + + // Regularise for mime type + if ($extension == 'jpg') { + $extension = 'jpeg'; + } + + // Take an md5sum so we don't show duplicate + // files. + $index = md5($full_filename); + + if ($file == $preferred_filename) { + // We found the preferred filename and + // so we're done. + debug_event('folder_art', "Found preferred image file: $file", 5); + $preferred[$index] = array( + 'file' => $full_filename, + 'mime' => 'image/' . $extension + ); + break; + } + + debug_event('folder_art', "Found image file: $file", 5); + $results[$index] = array( + 'file' => $full_filename, + 'mime' => 'image/' . $extension + ); + + } // end while reading dir + closedir($handle); + + } // end foreach songs + + if (is_array($preferred)) { + // We found our favourite filename somewhere, so we need + // to dump the other, less sexy ones. + $results = $preferred; + } + + debug_event('folder_art', 'Results: ' . json_encode($results), 5); + if ($limit && count($results) > $limit) { + $results = array_slice($results, 0, $limit); + } + + return array_values($results); + + } // gather_folder + + /** + * gather_tags + * This looks for the art in the meta-tags of the file + * itself + */ + public function gather_tags($limit = 5) + { + // We need the filenames + $album = new Album($this->uid); + + // grab the songs and define our results + $songs = $album->get_songs(); + $data = array(); + + // Foreach songs in this album + foreach ($songs as $song_id) { + $song = new Song($song_id); + // If we find a good one, stop looking + $getID3 = new getID3(); + try { $id3 = $getID3->analyze($song->file); } catch (Exception $error) { + debug_event('getid3', $error->getMessage(), 1); + } + + if (isset($id3['asf']['extended_content_description_object']['content_descriptors']['13'])) { + $image = $id3['asf']['extended_content_description_object']['content_descriptors']['13']; + $data[] = array( + 'song' => $song->file, + 'raw' => $image['data'], + 'mime' => $image['mime']); + } + + if (isset($id3['id3v2']['APIC'])) { + // Foreach in case they have more then one + foreach ($id3['id3v2']['APIC'] as $image) { + $data[] = array( + 'song' => $song->file, + 'raw' => $image['data'], + 'mime' => $image['mime']); + } + } + + if ($limit && count($data) >= $limit) { + return array_slice($data, 0, $limit); + } + + } // end foreach + + return $data; + + } // gather_tags + + /** + * gather_google + * Raw google search to retrieve the art, not very reliable + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function gather_google($limit = 5) + { + $images = array(); + $media = new $this->type($this->uid); + $media->format(); + + $search = $media->full_name; + + if ($media->artist_count == '1') + $search = $media->artist_name . ', ' . $search; + + $search = rawurlencode($search); + + $size = '&imgsz=m'; // Medium + //$size = '&imgsz=l'; // Large + + $html = file_get_contents("http://images.google.com/images?source=hp&q=$search&oq=&um=1&ie=UTF-8&sa=N&tab=wi&start=0&tbo=1$size"); + + if(preg_match_all("|\ssrc\=\"(http.+?)\"|", $html, $matches, PREG_PATTERN_ORDER)) + foreach ($matches[1] as $match) { + $extension = "image/jpeg"; + + if (strrpos($extension, '.') !== false) $extension = substr($extension, strrpos($extension, '.') + 1); + + $images[] = array('url' => $match, 'mime' => $extension); + } + + return $images; + + } // gather_google + + /** + * gather_lastfm + * This returns the art from lastfm. It doesn't currently require an + * account but may in the future. + */ + public function gather_lastfm($limit, $options = false) + { + $data = array(); + // Create the parser object + $lastfm = new LastFMSearch(); + + switch ($this->type) { + case 'album': + if (is_array($options)) { + $artist = $options['artist']; + $album = $options['album_name']; + } else { + $media = new Album($this->uid); + $media->format(); + $artist = $media->artist_name; + $album = $media->full_name; + } + break; + default: + return $data; + } + + if (AmpConfig::get('proxy_host') AND AmpConfig::get('proxy_port')) { + $proxyhost = AmpConfig::get('proxy_host'); + $proxyport = AmpConfig::get('proxy_port'); + $proxyuser = AmpConfig::get('proxy_user'); + $proxypass = AmpConfig::get('proxy_pass'); + debug_event('LastFM', 'proxy set', 5); + $lastfm->setProxy($proxyhost, $proxyport, $proxyuser, $proxypass); + } + + $raw_data = $lastfm->album_search($artist, $album); + + if (!count($raw_data)) { return array(); } + + $coverart = $raw_data['coverart']; + if (!is_array($coverart)) { return array(); } + + ksort($coverart); + foreach ($coverart as $url) { + // We need to check the URL for the /noimage/ stuff + if (strpos($url, '/noimage/') !== false) { + debug_event('LastFM', 'Detected as noimage, skipped ' . $url, 3); + continue; + } + + // HACK: we shouldn't rely on the extension to determine file type + $results = pathinfo($url); + $mime = 'image/' . $results['extension']; + $data[] = array('url' => $url, 'mime' => $mime); + if ($limit && count($data) >= $limit) { + return $data; + } + } // end foreach + + return $data; + + } // gather_lastfm + +} // Art diff --git a/sources/lib/class/artist.class.php b/sources/lib/class/artist.class.php new file mode 100644 index 0000000..ee286a4 --- /dev/null +++ b/sources/lib/class/artist.class.php @@ -0,0 +1,515 @@ +catalog_id = $catalog_init; + /* Get the information from the db */ + $info = $this->get_info($id); + + foreach ($info as $key=>$value) { + $this->$key = $value; + } // foreach info + + return true; + + } //constructor + + /** + * construct_from_array + * This is used by the metadata class specifically but fills out a Artist object + * based on a key'd array, it sets $_fake to true + */ + public static function construct_from_array($data) + { + $artist = new Artist(0); + foreach ($data as $key=>$value) { + $artist->$key = $value; + } + + //Ack that this is not a real object from the DB + $artist->_fake = true; + + return $artist; + + } // construct_from_array + + /** + * gc + * + * This cleans out unused artists + */ + public static function gc() + { + Dba::write('DELETE FROM `artist` USING `artist` LEFT JOIN `song` ON `song`.`artist` = `artist`.`id` LEFT JOIN `wanted` ON `wanted`.`artist` = `artist`.`id` WHERE `song`.`id` IS NULL AND `wanted`.`id` IS NULL'); + } + + /** + * this attempts to build a cache of the data from the passed albums all in one query + */ + public static function build_cache($ids,$extra=false) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + $idlist = '(' . implode(',', $ids) . ')'; + + $sql = "SELECT * FROM `artist` WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('artist',$row['id'],$row); + } + + // If we need to also pull the extra information, this is normally only used when we are doing the human display + if ($extra) { + $sql = "SELECT `song`.`artist`, COUNT(`song`.`id`) AS `song_count`, COUNT(DISTINCT `song`.`album`) AS `album_count`, SUM(`song`.`time`) AS `time` FROM `song` WHERE `song`.`artist` IN $idlist GROUP BY `song`.`artist`"; + + debug_event("Artist", "build_cache sql: " . $sql, "6"); + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + if (AmpConfig::get('show_played_times')) { + $row['object_cnt'] = Stats::get_object_count('artist', $row['artist']); + } + parent::add_to_cache('artist_extra',$row['artist'],$row); + } + + } // end if extra + + return true; + + } // build_cache + + /** + * get_from_name + * This gets an artist object based on the artist name + */ + public static function get_from_name($name) + { + $sql = "SELECT `id` FROM `artist` WHERE `name` = ?'"; + $db_results = Dba::read($sql, array($name)); + + $row = Dba::fetch_assoc($db_results); + + $object = new Artist($row['id']); + + return $object; + + } // get_from_name + + /** + * get_albums + * gets the album ids that this artist is a part + * of + */ + public function get_albums($catalog = null, $ignoreAlbumGroups = false) + { + $catalog_where = ""; + $catalog_join = "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog`"; + if ($catalog) { + $catalog_where .= " AND `catalog`.`id` = '$catalog'"; + } + if (AmpConfig::get('catalog_disable')) { + $catalog_where .= " AND `catalog`.`enabled` = '1'"; + } + + $results = array(); + + $sort_type = AmpConfig::get('album_sort'); + $sql_sort = '`album`.`name`,`album`.`disk`,`album`.`year`'; + if ($sort_type == 'year_asc') { + $sql_sort = '`album`.`year` ASC'; + } elseif ($sort_type == 'year_desc') { + $sql_sort = '`album`.`year` DESC'; + } elseif ($sort_type == 'name_asc') { + $sql_sort = '`album`.`name` ASC'; + } elseif ($sort_type == 'name_desc') { + $sql_sort = '`album`.`name` DESC'; + } + + $sql_group_type = '`album`.`id`'; + if (!$ignoreAlbumGroups && AmpConfig::get('album_group')) { + $sql_group_type = '`album`.`mbid`'; + } + $sql_group = "COALESCE($sql_group_type, `album`.`id`)"; + + $sql = "SELECT `album`.`id` FROM album LEFT JOIN `song` ON `song`.`album`=`album`.`id` $catalog_join " . + "WHERE `song`.`artist`='$this->id' $catalog_where GROUP BY $sql_group ORDER BY $sql_sort"; + + debug_event("Artist", "$sql", "6"); + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + + } // get_albums + + /** + * get_songs + * gets the songs for this artist + */ + public function get_songs() + { + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `song`.`artist`='" . Dba::escape($this->id) . "' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY album, track"; + $db_results = Dba::read($sql); + + $results = array(); + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + + } // get_songs + + /** + * get_random_songs + * Gets the songs from this artist in a random order + */ + public function get_random_songs() + { + $results = array(); + + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `song`.`artist`='$this->id' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY RAND()"; + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + + } // get_random_songs + + /** + * _get_extra info + * This returns the extra information for the artist, this means totals etc + */ + private function _get_extra_info($catalog=FALSE) + { + // Try to find it in the cache and save ourselves the trouble + if (parent::is_cached('artist_extra',$this->id) ) { + $row = parent::get_from_cache('artist_extra',$this->id); + } else { + $uid = Dba::escape($this->id); + $sql = "SELECT `song`.`artist`,COUNT(`song`.`id`) AS `song_count`, COUNT(DISTINCT `song`.`album`) AS `album_count`, SUM(`song`.`time`) AS `time` FROM `song` LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` " . + "WHERE `song`.`artist`='$uid' "; + if ($catalog) { + $sql .= "AND (`song`.`catalog` = '$catalog') "; + } + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND `catalog`.`enabled` = '1'"; + } + + $sql .= "GROUP BY `song`.`artist`"; + + $db_results = Dba::read($sql); + $row = Dba::fetch_assoc($db_results); + if (AmpConfig::get('show_played_times')) { + $row['object_cnt'] = Stats::get_object_count('artist', $row['artist']); + } + parent::add_to_cache('artist_extra',$row['artist'],$row); + } + + /* Set Object Vars */ + $this->songs = $row['song_count']; + $this->albums = $row['album_count']; + $this->time = $row['time']; + + return $row; + + } // _get_extra_info + + /** + * format + * this function takes an array of artist + * information and reformats the relevent values + * so they can be displayed in a table for example + * it changes the title into a full link. + */ + public function format() + { + /* Combine prefix and name, trim then add ... if needed */ + $name = trim($this->prefix . " " . $this->name); + $this->f_name = $name; + $this->f_full_name = trim(trim($this->prefix) . ' ' . trim($this->name)); + + // If this is a fake object, we're done here + if ($this->_fake) { return true; } + + if ($this->catalog_id) { + $this->f_link = AmpConfig::get('web_path') . '/artists.php?action=show&catalog=' . $this->catalog_id . '&artist=' . $this->id; + $this->f_name_link = "f_link . "\" title=\"" . $this->f_full_name . "\">" . $name . ""; + } else { + $this->f_link = AmpConfig::get('web_path') . '/artists.php?action=show&artist=' . $this->id; + $this->f_name_link = "f_link . "\" title=\"" . $this->f_full_name . "\">" . $name . ""; + } + // Get the counts + $extra_info = $this->_get_extra_info($this->catalog_id); + + //Format the new time thingy that we just got + $min = sprintf("%02d",(floor($extra_info['time']/60)%60)); + + $sec = sprintf("%02d",($extra_info['time']%60)); + $hours = floor($extra_info['time']/3600); + + $this->f_time = ltrim($hours . ':' . $min . ':' . $sec,'0:'); + + $this->tags = Tag::get_top_tags('artist', $this->id); + $this->f_tags = Tag::get_display($this->tags); + + $this->object_cnt = $extra_info['object_cnt']; + + return true; + + } // format + + /** + * check + * + * Checks for an existing artist; if none exists, insert one. + */ + public static function check($name, $mbid = null, $readonly = false) + { + $trimmed = Catalog::trim_prefix(trim($name)); + $name = $trimmed['string']; + $prefix = $trimmed['prefix']; + + if ($mbid == '') $mbid = null; + + if (!$name) { + $name = T_('Unknown (Orphaned)'); + $prefix = null; + } + + if (isset(self::$_mapcache[$name][$mbid])) { + return self::$_mapcache[$name][$mbid]; + } + + $id = 0; + $exists = false; + + if ($mbid) { + $sql = 'SELECT `id` FROM `artist` WHERE `mbid` = ?'; + $db_results = Dba::read($sql, array($mbid)); + + if ($row = Dba::fetch_assoc($db_results)) { + $id = $row['id']; + $exists = true; + } + } + + if (!$exists) { + $sql = 'SELECT `id`, `mbid` FROM `artist` WHERE `name` LIKE ?'; + $db_results = Dba::read($sql, array($name)); + + $id_array = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $key = $row['mbid'] ?: 'null'; + $id_array[$key] = $row['id']; + } + + if (count($id_array)) { + if ($mbid) { + if (isset($id_array['null']) && !$readonly) { + $sql = 'UPDATE `artist` SET `mbid` = ? WHERE `id` = ?'; + Dba::write($sql, array($mbid, $id_array['null'])); + } + if (isset($id_array['null'])) { + $id = $id_array['null']; + $exists = true; + } + } else { + // Pick one at random + $id = array_shift($id_array); + $exists = true; + } + } + } + + if ($exists) { + self::$_mapcache[$name][$mbid] = $id; + return $id; + } + + if ($readonly) { + return null; + } + + $sql = 'INSERT INTO `artist` (`name`, `prefix`, `mbid`) ' . + 'VALUES(?, ?, ?)'; + + $db_results = Dba::write($sql, array($name, $prefix, $mbid)); + if (!$db_results) { + return null; + } + $id = Dba::insert_id(); + + self::$_mapcache[$name][$mbid] = $id; + return $id; + + } + + /** + * update + * This takes a key'd array of data and updates the current artist + */ + public function update($data) + { + // Save our current ID + $current_id = $this->id; + + // Check if name is different than current name + if ($this->name != $data['name']) { + $artist_id = self::check($data['name'], $this->mbid); + + $updated = false; + $songs = array(); + + // If it's changed we need to update + if ($artist_id != $this->id) { + $songs = $this->get_songs(); + foreach ($songs as $song_id) { + Song::update_artist($artist_id,$song_id); + } + $updated = true; + $current_id = $artist_id; + self::gc(); + } // end if it changed + + if ($updated) { + foreach ($songs as $song_id) { + Song::update_utime($song_id); + } + Stats::gc(); + Rating::gc(); + Userflag::gc(); + } // if updated + } else if ($this->mbid != $data['mbid']) { + $sql = 'UPDATE `artist` SET `mbid` = ? WHERE `id` = ?'; + Dba::write($sql, array($data['mbid'], $current_id)); + } + + // Update artist name (if we don't want to use the MusicBrainz name) + $trimmed = Catalog::trim_prefix(trim($data['name'])); + $name = $trimmed['string']; + if ($name != '' && $name != $this->name) { + $sql = 'UPDATE `artist` SET `name` = ? WHERE `id` = ?'; + Dba::write($sql, array($name, $current_id)); + } + + $override_childs = false; + if ($data['apply_childs'] == 'checked') { + $override_childs = true; + } + $this->update_tags($data['edit_tags'], $override_childs, $current_id); + + return $current_id; + + } // update + + /** + * update_tags + * + * Update tags of artists and/or albums + */ + public function update_tags($tags_comma, $override_childs, $current_id = null) + { + if ($current_id == null) { + $current_id = $this->id; + } + + Tag::update_tag_list($tags_comma, 'artist', $current_id); + + if ($override_childs) { + $albums = $this->get_albums(null, true); + foreach ($albums as $album_id) { + $album = new Album($album_id); + $album->update_tags($tags_comma, $override_childs); + } + } + } + + public function update_artist_info($summary, $placeformed, $yearformed) + { + $sql = "UPDATE `artist` SET `summary` = ?, `placeformed` = ?, `yearformed` = ?, `last_update` = ? WHERE `id` = ?"; + return Dba::write($sql, array($summary, $placeformed, $yearformed, time(), $this->id)); + } + +} // end of artist class diff --git a/sources/lib/class/artist_event.class.php b/sources/lib/class/artist_event.class.php new file mode 100644 index 0000000..5ddd68c --- /dev/null +++ b/sources/lib/class/artist_event.class.php @@ -0,0 +1,85 @@ +mbid)) { + $query = 'mbid=' . rawurlencode($artist->mbid); + } else { + $query = 'artist=' . rawurlencode($artist->name); + } + + $limit = AmpConfig::get('concerts_limit_future'); + if ($limit) { + $query .= '&limit=' . $limit; + } + + $xml = Recommendation::get_lastfm_results('artist.getevents', $query); + + if ($xml->events) { + return $xml->events; + } + + return false; + } + + /** + * get_past_events + * Returns a list of past events + */ + public static function get_past_events($artist) + { + if (isset($artist->mbid)) { + $query = 'mbid=' . rawurlencode($artist->mbid); + } else { + $query = 'artist=' . rawurlencode($artist->name); + } + + $limit = AmpConfig::get('concerts_limit_past'); + if ($limit) { + $query .= '&limit=' . $limit; + } + + $xml = Recommendation::get_lastfm_results('artist.getpastevents', $query); + + if ($xml->events) { + return $xml->events; + } + + return false; + } + +} // end of recommendation class diff --git a/sources/lib/class/auth.class.php b/sources/lib/class/auth.class.php new file mode 100644 index 0000000..3d22a6e --- /dev/null +++ b/sources/lib/class/auth.class.php @@ -0,0 +1,537 @@ +reloadRedirect("' . $target . '")'; + echo xoutput_from_array($results); + } else { + /* Redirect them to the login page */ + header('Location: ' . $target); + } + + exit; + } + + /** + * login + * + * This takes a username and password and then returns the results + * based on what happens when we try to do the auth. + */ + public static function login($username, $password, $allow_ui = false) + { + $results = array(); + foreach (AmpConfig::get('auth_methods') as $method) { + $function_name = $method . '_auth'; + + if (!method_exists('Auth', $function_name)) { + continue; + } + + $results = self::$function_name($username, $password); + if ($results['success'] || ($allow_ui && !empty($results['ui_required']))) { break; } + } + + return $results; + } + + /** + * login_step2 + * + * This process authenticate step2 for an auth module + */ + public static function login_step2($auth_mod) + { + $results = null; + if (in_array($auth_mod, AmpConfig::get('auth_methods'))) { + $function_name = $auth_mod . '_auth_2'; + if (method_exists('Auth', $function_name)) { + $results = self::$function_name(); + } + } + + return $results; + } + + /** + * mysql_auth + * + * This is the core function of our built-in authentication. + */ + private static function mysql_auth($username, $password) + { + if (strlen($password) && strlen($username)) { + $sql = 'SELECT `password` FROM `user` WHERE `username` = ?'; + $db_results = Dba::read($sql, array($username)); + + if ($row = Dba::fetch_assoc($db_results)) { + // Use SHA2 now... cooking with fire. + // For backwards compatibility we hash a couple of different + // variations of the password. Increases collision chances, but + // doesn't break things. + // FIXME: Break things in the future. + $hashed_password = array(); + $hashed_password[] = hash('sha256', $password); + $hashed_password[] = hash('sha256', + Dba::escape(stripslashes(htmlspecialchars(strip_tags($password))))); + + // Automagically update the password if it's old and busted. + if ($row['password'] == $hashed_password[1] && + $hashed_password[0] != $hashed_password[1]) { + $user = User::get_from_username($username); + $user->update_password($password); + } + + if (in_array($row['password'], $hashed_password)) { + return array( + 'success' => true, + 'type' => 'mysql', + 'username' => $username + ); + } + } + } + + return array( + 'success' => false, + 'error' => 'MySQL login attempt failed' + ); + } + + /** + * pam_auth + * + * Check to make sure the pam_auth function is implemented (module is + * installed), then check the credentials. + */ + private static function pam_auth($username, $password) + { + $results = array(); + if (!function_exists('pam_auth')) { + $results['success'] = false; + $results['error'] = 'The PAM PHP module is not installed'; + return $results; + } + + $password = scrub_in($password); + + if (pam_auth($username, $password)) { + $results['success'] = true; + $results['type'] = 'pam'; + $results['username'] = $username; + } else { + $results['success'] = false; + $results['error'] = 'PAM login attempt failed'; + } + + return $results; + } + + /** + * external_auth + * + * Calls an external program compatible with mod_authnz_external + * such as pwauth. + */ + private static function external_auth($username, $password) + { + $authenticator = AmpConfig::get('external_authenticator'); + if (!$authenticator) { + return array( + 'success' => false, + 'error' => 'No external authenticator configured' + ); + } + + //FIXME: should we do input sanitization? + $proc = proc_open($authenticator, + array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ), $pipes); + + if (is_resource($proc)) { + fwrite($pipes[0], $username."\n".$password."\n"); + fclose($pipes[0]); + fclose($pipes[1]); + if ($stderr = fread($pipes[2], 8192)) { + debug_event('external_auth', $stderr, 5); + } + fclose($pipes[2]); + } else { + return array( + 'success' => false, + 'error' => 'Failed to run external authenticator' + ); + } + + if (proc_close($proc) == 0) { + return array( + 'success' => true, + 'type' => 'external', + 'username' => $username + ); + } + + return array( + 'success' => false, + 'error' => 'The external authenticator did not accept the login' + ); + } + + /** + * ldap_auth + * Step one, connect to the LDAP server and perform a search for the + * username provided. + * Step two, attempt to bind using that username and the password + * provided. + * Step three, figure out if they are authorized to use ampache: + * TODO: in config but unimplemented: + * * require-dn "Grant access if the DN in the directive matches + * the DN fetched from the LDAP directory" + * * require-attribute "an attribute fetched from the LDAP + * directory matches the given value" + */ + private static function ldap_auth($username, $password) + { + $ldap_username = AmpConfig::get('ldap_username'); + $ldap_password = AmpConfig::get('ldap_password'); + + $require_group = AmpConfig::get('ldap_require_group'); + + // This is the DN for the users (required) + $ldap_dn = AmpConfig::get('ldap_search_dn'); + + // This is the server url (required) + $ldap_url = AmpConfig::get('ldap_url'); + + // This is the ldap filter string (required) + $ldap_filter = AmpConfig::get('ldap_filter'); + + //This is the ldap objectclass (required) + $ldap_class = AmpConfig::get('ldap_objectclass'); + + $results = array(); + if (!($ldap_dn && $ldap_url && $ldap_filter && $ldap_class)) { + debug_event('ldap_auth', 'Required config value missing', 1); + $results['success'] = false; + $results['error'] = 'Incomplete LDAP config'; + return $results; + } + + $ldap_name_field = AmpConfig::get('ldap_name_field'); + $ldap_email_field = AmpConfig::get('ldap_email_field'); + + if ($ldap_link = ldap_connect($ldap_url) ) { + + /* Set to Protocol 3 */ + ldap_set_option($ldap_link, LDAP_OPT_PROTOCOL_VERSION, 3); + + // bind using our auth if we need to for initial search + if (!ldap_bind($ldap_link, $ldap_username, $ldap_password)) { + $results['success'] = false; + $results['error'] = 'Could not bind to LDAP server.'; + return $results; + } // If bind fails + + $sr = ldap_search($ldap_link, $ldap_dn, "(&(objectclass=$ldap_class)($ldap_filter=$username))"); + $info = ldap_get_entries($ldap_link, $sr); + + if ($info["count"] == 1) { + $user_entry = ldap_first_entry($ldap_link, $sr); + $user_dn = ldap_get_dn($ldap_link, $user_entry); + // bind using the user.. + $retval = ldap_bind($ldap_link, $user_dn, $password); + + if ($retval) { + // When the current user needs to be in + // a specific group to access Ampache, + // check whether the 'member' list of + // the group contains the DN + if ($require_group) { + $group_result = ldap_read($ldap_link, $require_group, 'objectclass=*', array('member')); + if (!$group_result) { + debug_event('ldap_auth', "Failure reading $require_group", 1); + $results['success'] = false; + $results['error'] = 'The LDAP group could not be read'; + return $results; + } + + $group_info = ldap_get_entries($ldap_link, $group_result); + + if ($group_info['count'] < 1) { + debug_event('ldap_auth', "No members found in $require_group", 1); + $results['success'] = false; + $results['error'] = 'Empty LDAP group'; + return $results; + } + + $group_match = preg_grep("/^$user_dn\$/i", $group_info[0]['member']); + if (!$group_match) { + debug_event('ldap_auth', "$user_dn is not a member of $require_group",1); + $results['success'] = false; + $results['error'] = 'LDAP login attempt failed'; + return $results; + } + } + ldap_close($ldap_link); + $results['success'] = true; + $results['type'] = "ldap"; + $results['username'] = $username; + $results['name'] = $info[0][$ldap_name_field][0]; + $results['email'] = $info[0][$ldap_email_field][0]; + + return $results; + + } // if we get something good back + + } // if something was sent back + + } // if failed connect + + /* Default to bad news */ + $results['success'] = false; + $results['error'] = 'LDAP login attempt failed'; + + return $results; + + } // ldap_auth + + /** + * http_auth + * This auth method relies on HTTP auth from the webserver + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private static function http_auth($username, $password) + { + $results = array(); + if (($_SERVER['REMOTE_USER'] == $username) || + ($_SERVER['HTTP_REMOTE_USER'] == $username)) { + $results['success'] = true; + $results['type'] = 'http'; + $results['username'] = $username; + $results['name'] = $username; + $results['email'] = ''; + } else { + $results['success'] = false; + $results['error'] = 'HTTP auth login attempt failed'; + } + return $results; + } // http_auth + + /** + * openid_auth + * Authenticate user with OpenID + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private static function openid_auth($username, $password) + { + $results = array(); + // Username contains the openid url. We don't care about password here. + $website = $username; + if (strpos($website, 'http://') === 0 || strpos($website, 'https://') === 0) { + $consumer = Openid::get_consumer(); + if ($consumer) { + $auth_request = $consumer->begin($website); + if ($auth_request) { + $sreg_request = Auth_OpenID_SRegRequest::build( + // Required + array('nickname'), + // Optional + array('fullname', 'email') + ); + if ($sreg_request) { + $auth_request->addExtension($sreg_request); + } + $pape_request = new Auth_OpenID_PAPE_Request(Openid::get_policies()); + if ($pape_request) { + $auth_request->addExtension($pape_request); + } + + // Redirect the user to the OpenID server for authentication. + // Store the token for this authentication so we can verify the response. + + // For OpenID 1, send a redirect. For OpenID 2, use a Javascript + // form to send a POST request to the server. + if ($auth_request->shouldSendRedirect()) { + $redirect_url = $auth_request->redirectURL(AmpConfig::get('web_path'), Openid::get_return_url()); + if (Auth_OpenID::isFailure($redirect_url)) { + $results['success'] = false; + $results['error'] = 'Could not redirect to server: ' . $redirect_url->message; + } else { + // Send redirect. + debug_event('auth', 'OpenID 1: redirecting to ' . $redirect_url, '5'); + header("Location: " . $redirect_url); + } + } else { + // Generate form markup and render it. + $form_id = 'openid_message'; + $form_html = $auth_request->htmlMarkup(AmpConfig::get('web_path'), Openid::get_return_url(), false, array('id' => $form_id)); + + if (Auth_OpenID::isFailure($form_html)) { + $results['success'] = false; + $results['error'] = 'Could not render authentication form.'; + } else { + debug_event('auth', 'OpenID 2: javascript redirection code to OpenID form.', '5'); + // First step is a success, UI interaction required. + $results['success'] = false; + $results['ui_required'] = $form_html; + } + } + } else { + debug_event('auth', $website . ' is not a valid OpenID.', '3'); + $results['success'] = false; + $results['error'] = 'Not a valid OpenID.'; + } + } else { + debug_event('auth', 'Cannot initialize OpenID resources.', '3'); + $results['success'] = false; + $results['error'] = 'Cannot initialize OpenID resources.'; + } + } else { + debug_event('auth', 'Skipped OpenID authentication: missing scheme in ' . $website . '.', '3'); + $results['success'] = false; + $results['error'] = 'Missing scheme in OpenID.'; + } + + return $results; + } + + /** + * openid_auth_2 + * Authenticate user with OpenID, step 2 + */ + private static function openid_auth_2() + { + $results = array(); + $results['type'] = 'openid'; + $consumer = Openid::get_consumer(); + if ($consumer) { + $response = $consumer->complete(Openid::get_return_url()); + + if ($response->status == Auth_OpenID_CANCEL) { + $results['success'] = false; + $results['error'] = 'OpenID verification cancelled.'; + } else if ($response->status == Auth_OpenID_FAILURE) { + $results['success'] = false; + $results['error'] = 'OpenID authentication failed: ' . $response->message; + } else if ($response->status == Auth_OpenID_SUCCESS) { + // Extract the identity URL and Simple Registration data (if it was returned). + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response); + $sreg = $sreg_resp->contents(); + + $results['website'] = $response->getDisplayIdentifier(); + if (@$sreg['email']) { + $results['email'] = $sreg['email']; + } + + if (@$sreg['nickname']) { + $results['username'] = $sreg['nickname']; + } + + if (@$sreg['fullname']) { + $results['name'] = $sreg['fullname']; + } + + $users = User::get_from_website($results['website']); + if (count($users) > 0) { + if (count($users) == 1) { + $user = new User($users[0]); + $results['success'] = true; + $results['username'] = $user->username; + } else { + // Several users for the same website/openid? Allowed but stupid, try to get a match on username. + // Should we make website field unique? + foreach ($users as $id) { + $user = new User($id); + if ($user->username == $results['username']) { + $results['success'] = true; + $results['username'] = $user->username; + } + } + } + } else { + // Don't return success if an user already exists for this username but don't have this openid identity as website + $user = User::get_from_username($results['username']); + if ($user->id) { + $results['success'] = false; + $results['error'] = 'No user associated to this OpenID and username already taken.'; + } else { + $results['success'] = true; + $results['error'] = 'No user associated to this OpenID.'; + } + } + } + } + + return $results; + } +} diff --git a/sources/lib/class/autoupdate.class.php b/sources/lib/class/autoupdate.class.php new file mode 100644 index 0000000..2593124 --- /dev/null +++ b/sources/lib/class/autoupdate.class.php @@ -0,0 +1,196 @@ +status_code != 200) { + debug_event('autoupdate', 'Github API request ' . $url . ' failed with http code ' . $request->status_code, '1'); + return; + } + return json_decode($request->body); + } catch (Exception $e) { + debug_event('autoupdate', 'Request error: ' . $e->getMessage(), '1'); + return ""; + } + } + + protected static function lastcheck_expired() + { + $lastcheck = AmpConfig::get('autoupdate_lastcheck'); + if (!$lastcheck) { + User::rebuild_all_preferences(); + Preference::update('autoupdate_lastcheck', $GLOBALS['user']->id, '1'); + AmpConfig::set('autoupdate_lastcheck', '1', true); + } + + return ((time() - (3600 * 3)) > $lastcheck); + } + + public static function get_latest_version($force = false) + { + $lastversion = ''; + // Forced or last check expired, check latest version from Github + if ($force || (self::lastcheck_expired() && AmpConfig::get('autoupdate'))) { + // Development version, get latest commit on develop branch + if (self::is_develop()) { + $commits = self::github_request('/commits/develop'); + if (!empty($commits)) { + $lastversion = $commits->sha; + Preference::update('autoupdate_lastversion', $GLOBALS['user']->id, $lastversion); + AmpConfig::set('autoupdate_lastversion', $lastversion, true); + $time = time(); + Preference::update('autoupdate_lastcheck', $GLOBALS['user']->id, $time); + AmpConfig::set('autoupdate_lastcheck', $time, true); + $available = self::is_update_available(true); + Preference::update('autoupdate_lastversion_new', $GLOBALS['user']->id, $available); + AmpConfig::set('autoupdate_lastversion_new', $available, true); + } + } + // Otherwise it is stable version, get latest tag + else { + $tags = self::github_request('/tags'); + if (!empty($tags)) { + $lastversion = $tags[0]->name; + Preference::update('autoupdate_lastversion', $GLOBALS['user']->id, $lastversion); + AmpConfig::set('autoupdate_lastversion', $lastversion, true); + $time = time(); + Preference::update('autoupdate_lastcheck', $GLOBALS['user']->id, $time); + AmpConfig::set('autoupdate_lastcheck', $time, true); + $available = self::is_update_available(true); + Preference::update('autoupdate_lastversion_new', $GLOBALS['user']->id, $available); + AmpConfig::set('autoupdate_lastversion_new', $available, true); + } + } + } + // Otherwise retrieve the cached version number + else { + $lastversion = AmpConfig::get('autoupdate_lastversion'); + } + + return $lastversion; + } + + public static function get_current_version() + { + if (self::is_develop()) { + return self::get_current_commit(); + } else { + return AmpConfig::get('version'); + } + } + + public static function get_current_commit() + { + if (self::is_branch_develop_exists()) { + return trim(file_get_contents(AmpConfig::get('prefix') . '/.git/refs/heads/develop')); + } + + return false; + } + + public static function is_update_available($force = false) + { + if (!$force && (!self::lastcheck_expired() || !AmpConfig::get('autoupdate'))) { + return AmpConfig::get('autoupdate_lastversion_new'); + } + + debug_event('autoupdate', 'Checking latest version online...', '5'); + + $available = false; + $current = self::get_current_version(); + $latest = self::get_latest_version(); + + if ($current != $latest && !empty($current)) { + if (self::is_develop()) { + $ccommit = self::github_request('/commits/' . $current); + $lcommit = self::github_request('/commits/' . $latest); + + if (!empty($ccommit) && !empty($lcommit)) { + // Comparison based on commit date + $ctime = strtotime($ccommit->commit->author->date); + $ltime = strtotime($lcommit->commit->author->date); + + $available = ($ctime < $ltime); + } + } else { + $cpart = explode('-', $current); + $lpart = explode('-', $latest); + + $available = (version_compare($cpart[0], $lpart[0]) < 0); + } + } + + return $available; + } + + public static function show_new_version() + { + echo '
'; + echo '' . T_('Update available') . ''; + echo ' (' . self::get_latest_version() . ').
'; + + echo T_('See') . ' ' . T_('changes') . ' '; + echo T_('or') . ' ' . T_('download') . '.'; + echo '
'; + } +} diff --git a/sources/lib/class/broadcast.class.php b/sources/lib/class/broadcast.class.php new file mode 100644 index 0000000..7007a19 --- /dev/null +++ b/sources/lib/class/broadcast.class.php @@ -0,0 +1,204 @@ +get_info($id); + + // Foreach what we've got + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + } //constructor + + public function update_state($started, $key='') + { + $sql = "UPDATE `broadcast` SET `started` = ?, `key` = ?, `song` = '0', `listeners` = '0' WHERE `id` = ?"; + Dba::write($sql, array($started, $key, $this->id)); + + $this->started = $started; + } + + public function update_listeners($listeners) + { + $sql = "UPDATE `broadcast` SET `listeners` = ? " . + "WHERE `id` = ?"; + Dba::write($sql, array($listeners, $this->id)); + $this->listeners = $listeners; + } + + public function update_song($song_id) + { + $sql = "UPDATE `broadcast` SET `song` = ? " . + "WHERE `id` = ?"; + Dba::write($sql, array($song_id, $this->id)); + $this->song = $song_id; + $this->song_position = 0; + } + + public function delete() + { + $sql = "DELETE FROM `broadcast` WHERE `id` = ?"; + return Dba::write($sql, array($this->id)); + } + + public static function create($name, $description='') + { + if (!empty($name)) { + $sql = "INSERT INTO `broadcast` (`user`, `name`, `description`, `is_private`) VALUES (?, ?, ?, '1')"; + $params = array($GLOBALS['user']->id, $name, $description); + Dba::write($sql, $params); + return Dba::insert_id(); + } + + return 0; + } + + public function update($data) + { + if (isset($data['edit_tags'])) { + Tag::update_tag_list($data['edit_tags'], 'broadcast', $this->id); + } + + $sql = "UPDATE `broadcast` SET `name` = ?, `description` = ?, `is_private` = ? " . + "WHERE `id` = ?"; + $params = array($data['name'], $data['description'], !empty($data['private']), $this->id); + return Dba::write($sql, $params); + } + + public function format() + { + $this->f_name = $this->name; + $this->f_link = '' . scrub_out($this->f_name) . ''; + $this->tags = Tag::get_top_tags('broadcast', $this->id); + $this->f_tags = Tag::get_display($this->tags); + } + + public static function get_broadcast_list_sql() + { + $sql = "SELECT `id` FROM `broadcast` WHERE `started` = '1' "; + + return $sql; + } + + public static function get_broadcast_list() + { + $sql = self::get_broadcast_list_sql(); + $db_results = Dba::read($sql); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + public static function generate_key() + { + // Should be improved for security reasons! + return md5(uniqid(rand(), true)); + } + + public static function get_broadcast($key) + { + $sql = "SELECT `id` FROM `broadcast` WHERE `key` = ?"; + $db_results = Dba::read($sql, array($key)); + + if ($results = Dba::fetch_assoc($db_results)) { + return new Broadcast($results['id']); + } + + return null; + } + + public function show_action_buttons() + { + if ($this->id) { + if ($GLOBALS['user']->has_access('75')) { + echo "id . "\" onclick=\"showEditDialog('broadcast_row', '" . $this->id . "', 'edit_broadcast_" . $this->id . "', '" . T_('Broadcast edit') . "', 'broadcast_row_', 'refresh_broadcast')\">" . UI::get_icon('edit', T_('Edit')) . ""; + echo " id ."\">" . UI::get_icon('delete', T_('Delete')) . ""; + } + } + } + + public static function get_broadcast_link() + { + $link = ""; + return $link; + } + + public static function get_unbroadcast_link($id) + { + $link = "
"; + $link .= Ajax::button('?page=player&action=unbroadcast&broadcast_id=' . $id, 'broadcast', T_('Unbroadcast'), 'broadcast_action'); + $link .= "
"; + $link .= "
(0)
"; + return $link; + } + + public static function get_broadcasts($user_id) + { + $sql = "SELECT `id` FROM `broadcast` WHERE `user` = ?"; + $db_results = Dba::read($sql, array($user_id)); + + $broadcasts = array(); + while ($results = Dba::fetch_assoc($db_results)) { + $broadcasts[] = $results['id']; + } + return $broadcasts; + } + + /* + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function play_url($oid, $additional_params='') + { + return $oid; + } + +} // end of broadcast class diff --git a/sources/lib/class/broadcast_server.class.php b/sources/lib/class/broadcast_server.class.php new file mode 100644 index 0000000..5ded480 --- /dev/null +++ b/sources/lib/class/broadcast_server.class.php @@ -0,0 +1,321 @@ +verbose = false; + $this->clients = array(); + $this->sids = array(); + $this->listeners = array(); + $this->broadcasters = array(); + } + + public function onOpen(ConnectionInterface $conn) + { + $this->clients[$conn->resourceId] = $conn; + } + + public function onMessage(ConnectionInterface $from, $msg) + { + $commands = explode(';', $msg); + foreach ($commands as $command) { + $command = trim($command); + if (!empty($command)) { + $cmdinfo = explode(':', $command, 2); + + if (count($cmdinfo) == 2) { + switch ($cmdinfo[0]) { + case self::BROADCAST_SONG: + $this->notifySong($from, $cmdinfo[1]); + break; + case self::BROADCAST_SONG_POSITION: + $this->notifySongPosition($from, $cmdinfo[1]); + break; + case self::BROADCAST_PLAYER_PLAY: + $this->notifyPlayerPlay($from, $cmdinfo[1]); + break; + case self::BROADCAST_ENDED: + $this->notifyEnded($from); + break; + case self::BROADCAST_REGISTER_BROADCAST: + $this->registerBroadcast($from, $cmdinfo[1]); + break; + case self::BROADCAST_REGISTER_LISTENER: + $this->registerListener($from, $cmdinfo[1]); + break; + case self::BROADCAST_AUTH_SID: + $this->authSid($from, $cmdinfo[1]); + break; + default: + if ($this->verbose) { + echo "[" . time() ."][warning]Unknown message code." . "\r\n"; + } + break; + } + } else { + if ($this->verbose) { + echo "[" . time() ."][error]Wrong message format (" . $command . ")." . "\r\n"; + } + } + } + } + } + + protected function getSongJS($song_id) + { + $media = array(); + $media[] = array( + 'object_type' => 'song', + 'object_id' => $song_id + ); + $item = Stream_Playlist::media_to_urlarray($media); + + return WebPlayer::get_media_js_param($item[0]); + } + + protected function notifySong($from, $song_id) + { + if ($this->isBroadcaster($from)) { + $broadcast = $this->broadcasters[$from->resourceId]; + $clients = $this->getListeners($broadcast); + + Session::extend(Stream::$session, 'stream'); + + $broadcast->update_song($song_id); + $this->broadcastMessage($clients, self::BROADCAST_SONG, base64_encode($this->getSongJS($song_id))); + + if ($this->verbose) { + echo "[" . time() ."][info]Broadcast " . $broadcast->id . " now playing song " . $song_id . "." . "\r\n"; + } + } else { + debug_event('broadcast', 'Action unauthorized.', '3'); + } + } + + protected function notifySongPosition($from, $song_position) + { + if ($this->isBroadcaster($from)) { + $broadcast = $this->broadcasters[$from->resourceId]; + $seekdiff = $broadcast->song_position - $song_position; + if ($seekdiff > 2 || $seekdiff < -2) { + $clients = $this->getListeners($broadcast); + $this->broadcastMessage($clients, self::BROADCAST_SONG_POSITION, $song_position); + } + $broadcast->song_position = $song_position; + + if ($this->verbose) { + echo "[" . time() ."][info]Broadcast " . $broadcast->id . " has song position to " . $song_position . "." . "\r\n"; + } + } else { + debug_event('broadcast', 'Action unauthorized.', '3'); + } + } + + protected function notifyPlayerPlay($from, $play) + { + if ($this->isBroadcaster($from)) { + $broadcast = $this->broadcasters[$from->resourceId]; + $clients = $this->getListeners($broadcast); + $this->broadcastMessage($clients, self::BROADCAST_PLAYER_PLAY, $play); + + if ($this->verbose) { + echo "[" . time() ."][info]Broadcast " . $broadcast->id . " player state: " . $play . "." . "\r\n"; + } + } else { + debug_event('broadcast', 'Action unauthorized.', '3'); + } + } + + protected function registerBroadcast($from, $broadcast_key) + { + $broadcast = Broadcast::get_broadcast($broadcast_key); + if ($broadcast) { + $this->broadcasters[$from->resourceId] = $broadcast; + $this->listeners[$broadcast->id] = array(); + + if ($this->verbose) { + echo "[info]Broadcast " . $broadcast->id . " registered." . "\r\n"; + } + } + } + + protected function unregisterBroadcast($conn) + { + $broadcast = $this->broadcasters[$conn->resourceId]; + $clients = $this->getListeners($broadcast); + $this->broadcastMessage($clients, self::BROADCAST_ENDED); + $broadcast->update_state(false); + + unset($this->listeners[$broadcast->id]); + unset($this->broadcasters[$conn->resourceId]); + + if ($this->verbose) { + echo "[" . time() ."][info]Broadcast " . $broadcast->id . " unregistered." . "\r\n"; + } + } + + protected function getRunningBroadcast($broadcast_id) + { + $broadcast = null; + foreach ($this->broadcasters as $conn_id => $br) { + if ($br->id == $broadcast_id) { + $broadcast = $br; + break; + } + } + return $broadcast; + } + + protected function registerListener($from, $broadcast_id) + { + $broadcast = $this->getRunningBroadcast($broadcast_id); + + if (!$broadcast->is_private || !AmpConfig::get('require_session') || Session::exists('stream', $this->sids[$from->resourceId])) { + $this->listeners[$broadcast->id][] = $from; + + // Send current song and song position to + $this->broadcastMessage(array($from), self::BROADCAST_SONG, base64_encode($this->getSongJS($broadcast->song))); + $this->broadcastMessage(array($from), self::BROADCAST_SONG_POSITION, $broadcast->song_position); + $this->notifyNbListeners($broadcast); + + if ($this->verbose) { + echo "[info]New listener on broadcast " . $broadcast->id . "." . "\r\n"; + } + } else { + debug_event('broadcast', 'Listener unauthorized.', '3'); + } + } + + protected function authSid($conn, $sid) + { + if (Session::exists('stream', $sid)) { + $this->sids[$conn->resourceId] = $sid; + } else { + if ($this->verbose) { + echo "Wrong listener session " . $sid . "\r\n"; + } + } + } + + protected function unregisterListener($conn) + { + foreach ($this->listeners as $broadcast_id => $brlisteners) { + $lindex = array_search($conn, $brlisteners); + if ($lindex) { + unset($this->listeners[$broadcast_id][$lindex]); + echo "[info]Listener leaved broadcast " . $broadcast_id . "." . "\r\n"; + + foreach ($this->broadcasters as $broadcaster_id => $broadcast) { + if ($broadcast->id == $broadcast_id) { + $this->notifyNbListeners($broadcast); + break; + } + } + + break; + } + } + } + + protected function notifyNbListeners($broadcast) + { + $broadcaster_id = array_search($broadcast, $this->broadcasters); + if ($broadcaster_id) { + $clients = $this->listeners[$broadcast->id]; + $clients[] = $this->clients[$broadcaster_id]; + $nb_listeners = count($this->listeners[$broadcast->id]); + $broadcast->update_listeners($nb_listeners); + $this->broadcastMessage($clients, self::BROADCAST_NB_LISTENERS, $nb_listeners); + } + } + + protected function getListeners($broadcast) + { + return $this->listeners[$broadcast->id]; + } + + protected function isBroadcaster($conn) + { + return array_key_exists($conn->resourceId, $this->broadcasters); + } + + protected function broadcastMessage($clients, $cmd, $value='') + { + $msg = $cmd . ':' . $value . ';'; + foreach ($clients as $client) { + $sid = $this->sids[$client->resourceId]; + if ($sid) { + Session::extend($sid, 'stream'); + } + $client->send($msg); + } + } + + public function onClose(ConnectionInterface $conn) + { + if ($this->isBroadcaster($conn)) { + $this->unregisterBroadcast($conn); + } else { + $this->unregisterListener($conn); + } + + unset($this->clients[$conn->resourceId]); + unset($this->sids[$conn->resourceId]); + } + + public function onError(ConnectionInterface $conn, \Exception $e) + { + $conn->close(); + } + + public static function get_address() + { + $websocket_address = AmpConfig::get('websocket_address'); + if (empty($websocket_address)) { + $websocket_address = 'ws://' . $_SERVER['HTTP_HOST'] . ':8100'; + } + + return $websocket_address . '/broadcast'; + } + +} // end of broadcast_server class diff --git a/sources/lib/class/browse.class.php b/sources/lib/class/browse.class.php new file mode 100644 index 0000000..2df265e --- /dev/null +++ b/sources/lib/class/browse.class.php @@ -0,0 +1,363 @@ +set_use_pages(true); + $this->set_use_alpha(false); + } + $this->show_header = true; + } + + /** + * set_simple_browse + * This sets the current browse object to a 'simple' browse method + * which means use the base query provided and expand from there + */ + public function set_simple_browse($value) + { + $this->set_is_simple($value); + + } // set_simple_browse + + /** + * add_supplemental_object + * Legacy function, need to find a better way to do that + */ + public function add_supplemental_object($class, $uid) + { + $_SESSION['browse']['supplemental'][$this->id][$class] = intval($uid); + + return true; + + } // add_supplemental_object + + /** + * get_supplemental_objects + * This returns an array of 'class','id' for additional objects that + * need to be created before we start this whole browsing thing. + */ + public function get_supplemental_objects() + { + $objects = isset($_SESSION['browse']['supplemental'][$this->id]) ? $_SESSION['browse']['supplemental'][$this->id] : ''; + + if (!is_array($objects)) { + $objects = array(); + } + + return $objects; + + } // get_supplemental_objects + + /** + * show_objects + * This takes an array of objects + * and requires the correct template based on the + * type that we are currently browsing + */ + public function show_objects($object_ids = null, $argument = null) + { + if ($this->is_simple() || !is_array($object_ids)) { + $object_ids = $this->get_saved(); + } else { + $this->save_objects($object_ids); + } + + // Limit is based on the user's preferences if this is not a + // simple browse because we've got too much here + if ((count($object_ids) > $this->get_start()) && + ! $this->is_simple() && + ! $this->is_static_content()) { + $object_ids = array_slice( + $object_ids, + $this->get_start(), + $this->get_offset(), + true + ); + } else if (!count($object_ids)) { + $this->set_total(0); + } + + // Load any additional object we need for this + $extra_objects = $this->get_supplemental_objects(); + $browse = $this; + + foreach ($extra_objects as $class_name => $id) { + ${$class_name} = new $class_name($id); + } + + $match = ''; + // Format any matches we have so we can show them to the masses + if ($filter_value = $this->get_filter('alpha_match')) { + $match = ' (' . $filter_value . ')'; + } elseif ($filter_value = $this->get_filter('starts_with')) { + $match = ' (' . $filter_value . ')'; + /*} elseif ($filter_value = $this->get_filter('regex_match')) { + $match = ' (' . $filter_value . ')'; + } elseif ($filter_value = $this->get_filter('regex_not_match')) { + $match = ' (' . $filter_value . ')';*/ + } elseif ($filter_value = $this->get_filter('catalog')) { + // Get the catalog title + $catalog = Catalog::create_from_id($filter_value); + $match = ' (' . $catalog->name . ')'; + } + + $type = $this->get_type(); + + // Set the correct classes based on type + $class = "box browse_" . $type; + + debug_event('browse', 'Called for type {'.$type.'}', '5'); + + // Switch on the type of browsing we're doing + switch ($type) { + case 'song': + $box_title = T_('Songs') . $match; + Song::build_cache($object_ids); + $box_req = AmpConfig::get('prefix') . '/templates/show_songs.inc.php'; + break; + case 'album': + $box_title = T_('Albums') . $match; + Album::build_cache($object_ids); + $allow_group_disks = $argument; + $box_req = AmpConfig::get('prefix') . '/templates/show_albums.inc.php'; + break; + case 'user': + $box_title = T_('Manage Users') . $match; + $box_req = AmpConfig::get('prefix') . '/templates/show_users.inc.php'; + break; + case 'artist': + $box_title = T_('Artists') . $match; + Artist::build_cache($object_ids, 'extra'); + $box_req = AmpConfig::get('prefix') . '/templates/show_artists.inc.php'; + break; + case 'live_stream': + require_once AmpConfig::get('prefix') . '/templates/show_live_stream.inc.php'; + $box_title = T_('Radio Stations') . $match; + $box_req = AmpConfig::get('prefix') . '/templates/show_live_streams.inc.php'; + break; + case 'playlist': + Playlist::build_cache($object_ids); + $box_title = T_('Playlists') . $match; + $box_req = AmpConfig::get('prefix') . '/templates/show_playlists.inc.php'; + break; + case 'playlist_song': + $box_title = T_('Playlist Songs') . $match; + $box_req = AmpConfig::get('prefix') . '/templates/show_playlist_songs.inc.php'; + break; + case 'playlist_localplay': + $box_title = T_('Current Playlist'); + $box_req = AmpConfig::get('prefix') . '/templates/show_localplay_playlist.inc.php'; + UI::show_box_bottom(); + break; + case 'smartplaylist': + $box_title = T_('Smart Playlists') . $match; + $box_req = AmpConfig::get('prefix') . '/templates/show_smartplaylists.inc.php'; + break; + case 'catalog': + $box_title = T_('Catalogs'); + $box_req = AmpConfig::get('prefix') . '/templates/show_catalogs.inc.php'; + break; + case 'shoutbox': + $box_title = T_('Shoutbox Records'); + $box_req = AmpConfig::get('prefix') . '/templates/show_manage_shoutbox.inc.php'; + break; + case 'tag': + Tag::build_cache($object_ids); + $box_title = T_('Tag Cloud'); + $box_req = AmpConfig::get('prefix') . '/templates/show_tagcloud.inc.php'; + break; + case 'video': + Video::build_cache($object_ids); + $box_title = T_('Videos'); + $box_req = AmpConfig::get('prefix') . '/templates/show_videos.inc.php'; + break; + case 'democratic': + $box_title = T_('Democratic Playlist'); + $box_req = AmpConfig::get('prefix') . '/templates/show_democratic_playlist.inc.php'; + break; + case 'wanted': + $box_title = T_('Wanted Albums'); + $box_req = AmpConfig::get('prefix') . '/templates/show_wanted_albums.inc.php'; + break; + case 'share': + $box_title = T_('Shared Objects'); + $box_req = AmpConfig::get('prefix') . '/templates/show_shared_objects.inc.php'; + break; + case 'song_preview': + $box_title = T_('Songs'); + $box_req = AmpConfig::get('prefix') . '/templates/show_song_previews.inc.php'; + break; + case 'channel': + $box_title = T_('Channels'); + $box_req = AmpConfig::get('prefix') . '/templates/show_channels.inc.php'; + break; + case 'broadcast': + $box_title = T_('Broadcasts'); + $box_req = AmpConfig::get('prefix') . '/templates/show_broadcasts.inc.php'; + break; + default: + // Rien a faire + break; + } // end switch on type + + Ajax::start_container('browse_content_' . $type, 'browse_content'); + if ($this->get_show_header()) { + if (isset($box_req) && isset($box_title)) { + UI::show_box_top($box_title, $class); + } + } + + if (isset($box_req)) { + require $box_req; + } + + if ($this->get_show_header()) { + if (isset($box_req)) { + UI::show_box_bottom(); + } + echo ''; + } else { + if (!$this->get_use_pages()) { + $this->show_next_link(); + } + } + Ajax::end_container(); + + } // show_object + + public function show_next_link() + { + $limit = $this->get_offset(); + $start = $this->get_start(); + $total = $this->get_total(); + $next_offset = $start + $limit; + if ($next_offset <= $total) { + echo '' . T_('More') . ''; + } + } + + /** + * set_filter_from_request + * //FIXME + */ + public function set_filter_from_request($request) + { + foreach ($request as $key => $value) { + //reinterpret v as a list of int + $list = explode(',', $value); + $ok = true; + foreach ($list as $item) { + if (!is_numeric($item)) { + $ok = false; + break; + } + } + if ($ok) { + if (sizeof($list) == 1) { + $this->set_filter($key, $list[0]); + } + } else { + $this->set_filter($key, $list); + } + } + } // set_filter_from_request + + public function set_type($type, $custom_base = '') + { + $cn = 'browse_' . $type . '_pages'; + if (isset($_COOKIE[$cn])) { + $this->set_use_pages($_COOKIE[$cn] == 'true'); + } + $cn = 'browse_' . $type . '_alpha'; + if (isset($_COOKIE[$cn])) { + $this->set_use_alpha($_COOKIE[$cn] == 'true'); + if ($this->get_use_alpha()) { + if (count($this->_state['filter']) == 0) { + $this->set_filter('regex_match', '^A'); + } + } else { + $this->set_filter('regex_not_match', ''); + } + } + + parent::set_type($type, $custom_base); + } + + public function save_cookie_params($option, $value) + { + if ($this->get_type()) { + setcookie('browse_' . $this->get_type() . '_' . $option, $value, time() + 31536000, "/"); + } + } + + public function set_use_pages($use_pages) + { + $this->save_cookie_params('pages', $use_pages ? 'true' : 'false'); + $this->_state['use_pages'] = $use_pages; + } + + public function get_use_pages() + { + return $this->_state['use_pages']; + } + + public function set_use_alpha($use_alpha) + { + $this->save_cookie_params('alpha', $use_alpha ? 'true' : 'false'); + $this->_state['use_alpha'] = $use_alpha; + } + + public function get_use_alpha() + { + return $this->_state['use_alpha']; + } + + public function set_show_header($show_header) + { + $this->show_header = $show_header; + } + + public function get_show_header() + { + return $this->show_header; + } + +} // browse diff --git a/sources/lib/class/catalog.class.php b/sources/lib/class/catalog.class.php new file mode 100644 index 0000000..478848d --- /dev/null +++ b/sources/lib/class/catalog.class.php @@ -0,0 +1,1434 @@ +get_type())); + + $sql = "DROP TABLE `catalog_" . $this->get_type() ."`"; + Dba::query($sql); + + return true; + + } // uninstall + + public static function create_from_id($id) + { + $sql = 'SELECT `catalog_type` FROM `catalog` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($id)); + if ($results = Dba::fetch_assoc($db_results)) { + return self::create_catalog_type($results['catalog_type'], $id); + } + + return null; + } + + /** + * create_catalog_type + * This function attempts to create a catalog type + * all Catalog modules should be located in /modules/catalog/.class.php + */ + public static function create_catalog_type($type, $id=0) + { + if (!$type) { return false; } + + $filename = AmpConfig::get('prefix') . '/modules/catalog/' . $type . '.catalog.php'; + $include = require_once $filename; + + if (!$include) { + /* Throw Error Here */ + debug_event('catalog', 'Unable to load ' . $type . ' catalog type', '2'); + return false; + } // include + else { + $class_name = "Catalog_" . $type; + if ($id > 0) { + $catalog = new $class_name($id); + } else { + $catalog = new $class_name(); + } + if (!($catalog instanceof Catalog)) { + debug_event('catalog', $type . ' not an instance of Catalog abstract, unable to load', '1'); + return false; + } + return $catalog; + } + + } // create_catalog_type + + public static function show_catalog_types($divback = 'catalog_type_fields') + { + echo "" . + ""; + } + + /** + * get_catalog_types + * This returns the catalog types that are available + */ + public static function get_catalog_types() + { + /* First open the dir */ + $handle = opendir(AmpConfig::get('prefix') . '/modules/catalog'); + + if (!is_resource($handle)) { + debug_event('catalog', 'Error: Unable to read catalog types directory', '1'); + return array(); + } + + $results = array(); + + while ($file = readdir($handle)) { + + if (substr($file, -11, 11) != 'catalog.php') { continue; } + + /* Make sure it isn't a dir */ + if (!is_dir($file)) { + /* Get the basename and then everything before catalog */ + $filename = basename($file, '.catalog.php'); + $results[] = $filename; + } + } // end while + + return $results; + + } // get_catalog_types + + public static function is_audio_file($file) + { + $pattern = "/\.(" . AmpConfig::get('catalog_file_pattern') . ")$/i"; + $match = preg_match($pattern, $file); + + return $match; + } + + public static function is_video_file($file) + { + $video_pattern = "/\.(" . AmpConfig::get('catalog_video_pattern') . ")$/i"; + return preg_match($video_pattern, $file); + } + + public static function is_playlist_file($file) + { + $playlist_pattern = "/\.(" . AmpConfig::get('catalog_playlist_pattern') . ")$/i"; + return preg_match($playlist_pattern, $file); + } + + public function get_info($id, $table = 'catalog') + { + $info = parent::get_info($id, $table); + + $table = 'catalog_' . $this->get_type(); + $sql = "SELECT `id` FROM $table WHERE `catalog_id` = ?"; + $db_results = Dba::read($sql, array($id)); + + if ($results = Dba::fetch_assoc($db_results)) { + + $info_type = parent::get_info($results['id'], $table); + foreach ($info_type as $key => $value) { + if (!$info[$key]) { + $info[$key] = $value; + } + } + } + + return $info; + } + + public static function get_enable_filter($type, $id) + { + $sql = ""; + if ($type == "song" || $type == "album" || $type == "artist") { + if ($type == "song") $type = "id"; + $sql = "(SELECT COUNT(`song_dis`.`id`) FROM `song` AS `song_dis` LEFT JOIN `catalog` AS `catalog_dis` ON `catalog_dis`.`id` = `song_dis`.`catalog` " . + "WHERE `song_dis`.`" . $type . "`=" . $id . " AND `catalog_dis`.`enabled` = '1' GROUP BY `song_dis`.`" . $type . "`) > 0"; + } else if ($type == "video") { + $sql = "(SELECT COUNT(`video_dis`.`id`) FROM `video` AS `video_dis` LEFT JOIN `catalog` AS `catalog_dis` ON `catalog_dis`.`id` = `video_dis`.`catalog` " . + "WHERE `video_dis`.`id`=" . $id . " AND `catalog_dis`.`enabled` = '1' GROUP BY `video_dis`.`id`) > 0"; + } + + return $sql; + } + + /** + * _create_filecache + * + * This populates an array which is used to speed up the add process. + */ + protected function _create_filecache() + { + if (count($this->_filecache) == 0) { + // Get _EVERYTHING_ + $sql = 'SELECT `id`, `file` FROM `song` WHERE `catalog` = ?'; + $db_results = Dba::read($sql, array($this->id)); + + // Populate the filecache + while ($results = Dba::fetch_assoc($db_results)) { + $this->_filecache[strtolower($results['file'])] = $results['id']; + } + + $sql = 'SELECT `id`,`file` FROM `video` WHERE `catalog` = ?'; + $db_results = Dba::read($sql, array($this->id)); + + while ($results = Dba::fetch_assoc($db_results)) { + $this->_filecache[strtolower($results['file'])] = 'v_' . $results['id']; + } + } + + return true; + } + + /** + * update_enabled + * sets the enabled flag + */ + public static function update_enabled($new_enabled, $catalog_id) + { + self::_update_item('enabled', $new_enabled, $catalog_id, '75'); + + } // update_enabled + + /** + * _update_item + * This is a private function that should only be called from within the catalog class. + * It takes a field, value, catalog id and level. first and foremost it checks the level + * against $GLOBALS['user'] to make sure they are allowed to update this record + * it then updates it and sets $this->{$field} to the new value + */ + private static function _update_item($field, $value, $catalog_id, $level) + { + /* Check them Rights! */ + if (!Access::check('interface', $level)) { return false; } + + /* Can't update to blank */ + if (!strlen(trim($value))) { return false; } + + $value = Dba::escape($value); + + $sql = "UPDATE `catalog` SET `$field`='$value' WHERE `id`='$catalog_id'"; + Dba::write($sql); + + return true; + + } // _update_item + + /** + * format + * + * This makes the object human-readable. + */ + public function format() + { + $this->f_name = $this->name; + $this->f_name_link = '' . + scrub_out($this->f_name) . ''; + $this->f_update = $this->last_update + ? date('d/m/Y h:i', $this->last_update) + : T_('Never'); + $this->f_add = $this->last_add + ? date('d/m/Y h:i', $this->last_add) + : T_('Never'); + $this->f_clean = $this->last_clean + ? date('d/m/Y h:i', $this->last_clean) + : T_('Never'); + } + + /** + * get_catalogs + * + * Pull all the current catalogs and return an array of ids + * of what you find + */ + public static function get_catalogs() + { + $sql = "SELECT `id` FROM `catalog` ORDER BY `name`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + /** + * get_stats + * + * This returns an hash with the #'s for the different + * objects that are associated with this catalog. This is used + * to build the stats box, it also calculates time. + */ + public static function get_stats($catalog_id = null) + { + $results = self::count_songs($catalog_id); + $results = array_merge(User::count(), $results); + $results['tags'] = self::count_tags(); + $results['videos'] = self::count_videos($catalog_id); + + $hours = floor($results['time'] / 3600); + + $results['formatted_size'] = UI::format_bytes($results['size']); + + $days = floor($hours / 24); + $hours = $hours % 24; + + $time_text = "$days "; + $time_text .= ngettext('day','days',$days); + $time_text .= ", $hours "; + $time_text .= ngettext('hour','hours',$hours); + + $results['time_text'] = $time_text; + + return $results; + } + + /** + * create + * + * This creates a new catalog entry and associate it to current instance + */ + public static function create($data) + { + $name = $data['name']; + $type = $data['type']; + $rename_pattern = $data['rename_pattern']; + $sort_pattern = $data['sort_pattern']; + + $insert_id = 0; + $filename = AmpConfig::get('prefix') . '/modules/catalog/' . $type . '.catalog.php'; + $include = require_once $filename; + + if ($include) { + $sql = 'INSERT INTO `catalog` (`name`, `catalog_type`, ' . + '`rename_pattern`, `sort_pattern`) VALUES (?, ?, ?, ?)'; + Dba::write($sql, array( + $name, + $type, + $rename_pattern, + $sort_pattern + )); + + $insert_id = Dba::insert_id(); + + if (!$insert_id) { + Error::add('general', T_('Catalog Insert Failed check debug logs')); + debug_event('catalog', 'Insert failed: ' . json_encode($data), 2); + return false; + } + + $classname = 'Catalog_' . $type; + if (!$classname::create_type($insert_id, $data)) { + $sql = 'DELETE FROM `catalog` WHERE `id` = ?'; + Dba::write($sql, array($insert_id)); + $insert_id = 0; + } + } + + return $insert_id; + } + + /** + * count_videos + * + * This returns the current number of video files in the database. + */ + public static function count_videos($id = null) + { + $sql = 'SELECT COUNT(`id`) FROM `video` '; + if ($id) { + $sql .= 'WHERE `catalog` = ?'; + } + $db_results = Dba::read($sql, $id ? array($id) : null); + + $row = Dba::fetch_assoc($db_results); + return $row[0]; + } + + /** + * count_tags + * + * This returns the current number of unique tags in the database. + */ + public static function count_tags() + { + // FIXME: Ignores catalog_id + $sql = "SELECT COUNT(`id`) FROM `tag`"; + $db_results = Dba::read($sql); + + $row = Dba::fetch_row($db_results); + return $row[0]; + } + + /** + * count_songs + * + * This returns the current number of songs, albums, and artists + * in this catalog. + */ + public static function count_songs($id = null) + { + $where_sql = $id ? 'WHERE `catalog` = ?' : ''; + $params = $id ? array($id) : null; + + $sql = 'SELECT COUNT(`id`), SUM(`time`), SUM(`size`) FROM `song` ' . + $where_sql; + + $db_results = Dba::read($sql, $params); + $data = Dba::fetch_row($db_results); + $songs = $data[0]; + $time = $data[1]; + $size = $data[2]; + + $sql = 'SELECT COUNT(DISTINCT(`album`)) FROM `song` ' . $where_sql; + $db_results = Dba::read($sql, $params); + $data = Dba::fetch_row($db_results); + $albums = $data[0]; + + $sql = 'SELECT COUNT(DISTINCT(`artist`)) FROM `song` ' . $where_sql; + $db_results = Dba::read($sql, $params); + $data = Dba::fetch_row($db_results); + $artists = $data[0]; + + $results = array(); + $results['songs'] = $songs; + $results['albums'] = $albums; + $results['artists'] = $artists; + $results['size'] = $size; + $results['time'] = $time; + + return $results; + } + + /** + * get_album_ids + * + * This returns an array of ids of albums that have songs in this + * catalog + */ + public function get_album_ids() + { + $results = array(); + + $sql = 'SELECT DISTINCT(`song`.`album`) FROM `song` WHERE `song`.`catalog` = ?'; + $db_results = Dba::read($sql, array($this->id)); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['album']; + } + + return $results; + } + + /** + * get_artist + * + * This returns an array of ids of artists that have songs in the catalogs parameter + */ + public static function get_artists($catalogs = null) + { + $sql_where = ""; + if (is_array($catalogs) && count($catalogs)) { + $catlist = '(' . implode(',', $catalogs) . ')'; + $sql_where = "WHERE `song`.`catalog` IN $catlist"; + } + + $sql = "SELECT `artist`.id, `artist`.`name`, `artist`.`summary` FROM `song` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` $sql_where GROUP BY `song`.artist ORDER BY `artist`.`name`"; + + $results = array(); + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = Artist::construct_from_array($r); + } + + return $results; + } + + /** + * get_albums + * + * Returns an array of ids of albums that have songs in the catalogs parameter + */ + public static function get_albums($size = 0, $offset = 0, $catalogs = null) + { + $sql_where = ""; + if (is_array($catalogs) && count($catalogs)) { + $catlist = '(' . implode(',', $catalogs) . ')'; + $sql_where = "WHERE `song`.`catalog` IN $catlist"; + } + + $sql_limit = ""; + if ($offset > 0 && $size > 0) { + $sql_limit = "LIMIT $offset, $size"; + } else if ($size > 0) { + $sql_limit = "LIMIT $size"; + } else if ($offset > 0) { + // MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value + // https://dev.mysql.com/doc/refman/5.0/en/select.html + $sql_limit = "LIMIT $offset, 18446744073709551615"; + } + + $sql = "SELECT `album`.`id` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` $sql_where GROUP BY `song`.`album` ORDER BY `album`.`name` $sql_limit"; + + $db_results = Dba::read($sql); + $results = array(); + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + } + + /** + * get_albums_by_artist + * + * Returns an array of ids of albums that have songs in the catalogs parameter, grouped by artist + */ + public static function get_albums_by_artist($size = 0, $offset = 0, $catalogs = null) + { + $sql_where = ""; + if (is_array($catalogs) && count($catalogs)) { + $catlist = '(' . implode(',', $catalogs) . ')'; + $sql_where = "WHERE `song`.`catalog` IN $catlist"; + } + + $sql_limit = ""; + if ($offset > 0 && $size > 0) { + $sql_limit = "LIMIT $offset, $size"; + } else if ($size > 0) { + $sql_limit = "LIMIT $size"; + } else if ($offset > 0) { + // MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value + // https://dev.mysql.com/doc/refman/5.0/en/select.html + $sql_limit = "LIMIT $offset, 18446744073709551615"; + } + + $sql = "SELECT `album`.`id` FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` " . + "LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` $sql_where GROUP BY `song`.`album` ORDER BY `artist`.`name`, `artist`.`id`, `album`.`name` $sql_limit"; + + $db_results = Dba::read($sql); + $results = array(); + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['id']; + } + + return $results; + } + + /** + * gather_art + * + * This runs through all of the albums and finds art for them + * This runs through all of the needs art albums and trys + * to find the art for them from the mp3s + */ + public function gather_art() + { + // Make sure they've actually got methods + $art_order = AmpConfig::get('art_order'); + if (!count($art_order)) { + debug_event('gather_art', 'art_order not set, Catalog::gather_art aborting', 3); + return true; + } + + // Prevent the script from timing out + set_time_limit(0); + + $search_count = 0; + $albums = $this->get_album_ids(); + + // Run through them and get the art! + foreach ($albums as $album_id) { + $art = new Art($album_id, 'album'); + $album = new Album($album_id); + // We're going to need the name here + $album->format(); + + debug_event('gather_art', 'Gathering art for ' . $album->name, 5); + + $options = array( + 'album_name' => $album->full_name, + 'artist' => $album->artist_name, + 'keyword' => $album->artist_name . ' ' . $album->full_name + ); + + $results = $art->gather($options, 1); + + if (count($results)) { + // Pull the string representation from the source + $image = Art::get_from_source($results[0], 'album'); + if (strlen($image) > '5') { + $art->insert($image, $results[0]['mime']); + // If they've enabled resizing of images generate a thumbnail + if (AmpConfig::get('resize_images')) { + $thumb = $art->generate_thumb($image, array( + 'width' => 275, + 'height' => 275), + $results[0]['mime']); + if (is_array($thumb)) { + $art->save_thumb($thumb['thumb'], $thumb['thumb_mime'], '275x275'); + } + } + + } else { + debug_event('gather_art', 'Image less than 5 chars, not inserting', 3); + } + } + + // Stupid little cutesie thing + $search_count++; + if (UI::check_ticker()) { + UI::update_text('count_art_' . $this->id, $search_count); + UI::update_text('read_art_' . $this->id, scrub_out($album->name)); + } + + unset($found); + } // foreach albums + + // One last time for good measure + UI::update_text('count_art_' . $this->id, $search_count); + } + + /** + * get_songs + * + * Returns an array of song objects. + */ + public function get_songs() + { + $results = array(); + + $sql = "SELECT `id` FROM `song` WHERE `catalog` = ? AND `enabled`='1'"; + $db_results = Dba::read($sql, array($this->id)); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = new Song($row['id']); + } + + return $results; + } + + /** + * dump_album_art + * + * This runs through all of the albums and tries to dump the + * art for them into the 'folder.jpg' file in the appropriate dir. + */ + public function dump_album_art($methods = array()) + { + // Get all of the albums in this catalog + $albums = $this->get_album_ids(); + + echo "Starting Dump Album Art...\n"; + $i = 0; + + // Run through them and get the art! + foreach ($albums as $album_id) { + + $album = new Album($album_id); + $art = new Art($album_id, 'album'); + + if (!$art->get_db()) { + continue; + } + + // Get the first song in the album + $songs = $album->get_songs(1); + $song = new Song($songs[0]); + $dir = dirname($song->file); + + $extension = Art::extension($art->raw_mime); + + // Try the preferred filename, if that fails use folder.??? + $preferred_filename = AmpConfig::get('album_art_preferred_filename'); + if (!$preferred_filename || + strpos($preferred_filename, '%') !== false) { + $preferred_filename = "folder.$extension"; + } + + $file = "$dir/$preferred_filename"; + if ($file_handle = fopen($file,"w")) { + if (fwrite($file_handle, $art->raw)) { + + // Also check and see if we should write + // out some metadata + if ($methods['metadata']) { + switch ($methods['metadata']) { + case 'windows': + $meta_file = $dir . '/desktop.ini'; + $string = "[.ShellClassInfo]\nIconFile=$file\nIconIndex=0\nInfoTip=$album->full_name"; + break; + case 'linux': + default: + $meta_file = $dir . '/.directory'; + $string = "Name=$album->full_name\nIcon=$file"; + break; + } + + $meta_handle = fopen($meta_file,"w"); + fwrite($meta_handle,$string); + fclose($meta_handle); + + } // end metadata + $i++; + if (!($i%100)) { + echo "Written: $i. . .\n"; + debug_event('art_write',"$album->name Art written to $file",'5'); + } + } else { + debug_event('art_write',"Unable to open $file for writing", 5); + echo "Error: unable to open file for writing [$file]\n"; + } + } + fclose($file_handle); + } + + echo "Album Art Dump Complete\n"; + } + + /** + * update_last_update + * updates the last_update of the catalog + */ + protected function update_last_update() + { + $date = time(); + $sql = "UPDATE `catalog` SET `last_update` = ? WHERE `id` = ?"; + Dba::write($sql, array($date, $this->id)); + + } // update_last_update + + /** + * update_last_add + * updates the last_add of the catalog + */ + public function update_last_add() + { + $date = time(); + $sql = "UPDATE `catalog` SET `last_add` = ? WHERE `id` = ?"; + Dba::write($sql, array($date, $this->id)); + + } // update_last_add + + /** + * update_last_clean + * This updates the last clean information + */ + public function update_last_clean() + { + $date = time(); + $sql = "UPDATE `catalog` SET `last_clean` = ? WHERE `id` = ?"; + Dba::write($sql, array($date, $this->id)); + + } // update_last_clean + + /** + * update_settings + * This function updates the basic setting of the catalog + */ + public static function update_settings($data) + { + $sql = "UPDATE `catalog` SET `name` = ?, `rename_pattern` = ?, `sort_pattern` = ? WHERE `id` = ?"; + $params = array($data['name'], $data['rename_pattern'], $data['sort_pattern'], $data['catalog_id']); + Dba::write($sql, $params); + + return true; + + } // update_settings + + /** + * update_single_item + * updates a single album,artist,song from the tag data + * this can be done by 75+ + */ + public static function update_single_item($type,$id) + { + // Because single items are large numbers of things too + set_time_limit(0); + + $songs = array(); + + switch ($type) { + case 'album': + $album = new Album($id); + $songs = $album->get_songs(); + break; + case 'artist': + $artist = new Artist($id); + $songs = $artist->get_songs(); + break; + case 'song': + $songs[] = $id; + break; + } // end switch type + + foreach ($songs as $song_id) { + $song = new Song($song_id); + $info = self::update_media_from_tags($song,'',''); + + if ($info['change']) { + $file = scrub_out($song->file); + echo "
\n\t
"; + echo "$file " . T_('Updated') . "\n"; + echo $info['text']; + echo "\t
\n

"; + flush(); + } // if change + else { + echo"
\n\t
"; + echo "" . scrub_out($song->file) . "
" . T_('No Update Needed') . "\n"; + echo "\t
\n

"; + flush(); + } + } // foreach songs + + self::gc(); + + } // update_single_item + + /** + * update_media_from_tags + * This is a 'wrapper' function calls the update function for the media + * type in question + */ + public static function update_media_from_tags($media, $sort_pattern='', $rename_pattern='') + { + // Check for patterns + if (!$sort_pattern OR !$rename_pattern) { + $catalog = Catalog::create_from_id($media->catalog); + $sort_pattern = $catalog->sort_pattern; + $rename_pattern = $catalog->rename_pattern; + } + + debug_event('tag-read', 'Reading tags from ' . $media->file, 5); + + $vainfo = new vainfo($media->file,'','','',$sort_pattern,$rename_pattern); + $vainfo->get_info(); + + $key = vainfo::get_tag_type($vainfo->tags); + + $results = vainfo::clean_tag_info($vainfo->tags,$key,$media->file); + + // Figure out what type of object this is and call the right + // function, giving it the stuff we've figured out above + $name = (get_class($media) == 'Song') ? 'song' : 'video'; + + $function = 'update_' . $name . '_from_tags'; + + $return = call_user_func(array('Catalog',$function),$results,$media); + + return $return; + + } // update_media_from_tags + + /** + * update_video_from_tags + * Updates the video info based on tags + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function update_video_from_tags($results,$video) + { + // Pretty sweet function here + return $results; + + } // update_video_from_tags + + /** + * update_song_from_tags + * Updates the song info based on tags; this is called from a bunch of + * different places and passes in a full fledged song object, so it's a + * static function. + * FIXME: This is an ugly mess, this really needs to be consolidated and + * cleaned up. + */ + public static function update_song_from_tags($results,$song) + { + /* Setup the vars */ + $new_song = new Song(); + $new_song->file = $results['file']; + $new_song->title = $results['title']; + $new_song->year = $results['year']; + $new_song->comment = $results['comment']; + $new_song->language = $results['language']; + $new_song->lyrics = $results['lyrics']; + $new_song->bitrate = $results['bitrate']; + $new_song->rate = $results['rate']; + $new_song->mode = ($results['mode'] == 'cbr') ? 'cbr' : 'vbr'; + $new_song->size = $results['size']; + $new_song->time = $results['time']; + $new_song->mime = $results['mime']; + $new_song->track = intval($results['track']); + $new_song->mbid = $results['mb_trackid']; + $artist = $results['artist']; + $artist_mbid = $results['mb_artistid']; + $album = $results['album']; + $album_mbid = $results['mb_albumid']; + $disk = $results['disk']; + $tags = $results['genre']; // multiple genre support makes this an array + + /* + * We have the artist/genre/album name need to check it in the tables + * If found then add & return id, else return id + */ + $new_song->artist = Artist::check($artist, $artist_mbid); + $new_song->f_artist = $artist; + $new_song->album = Album::check($album, $new_song->year, $disk, $album_mbid); + $new_song->f_album = $album . " - " . $new_song->year; + $new_song->title = self::check_title($new_song->title,$new_song->file); + + // Nothing to assign here this is a multi-value doodly + // multiple genre support + if (is_array($tags)) { + foreach ($tags as $tag) { + $tag = trim($tag); + //self::check_tag($tag,$song->id); + //self::check_tag($tag,$new_song->album,'album'); + //self::check_tag($tag,$new_song->artist,'artist'); + } + } + + /* Since we're doing a full compare make sure we fill the extended information */ + $song->fill_ext_info(); + + $info = Song::compare_song_information($song,$new_song); + + if ($info['change']) { + debug_event('update', "$song->file : differences found, updating database", 5); + $song->update_song($song->id,$new_song); + // Refine our reference + //$song = $new_song; + } else { + debug_event('update', "$song->file : no differences found", 5); + } + + return $info; + + } // update_song_from_tags + + /** + * clean_catalog + * + * Cleans the catalog of files that no longer exist. + */ + public function clean_catalog() + { + // We don't want to run out of time + set_time_limit(0); + + debug_event('clean', 'Starting on ' . $this->name, 5); + + require AmpConfig::get('prefix') . '/templates/show_clean_catalog.inc.php'; + ob_flush(); + flush(); + + $dead_total = $this->clean_catalog_proc(); + + debug_event('clean', 'clean finished, ' . $dead_total . ' removed from '. $this->name, 5); + + // Remove any orphaned artists/albums/etc. + self::gc(); + + UI::show_box_top(); + echo ""; + printf (ngettext('Catalog Clean Done. %d file removed.', 'Catalog Clean Done. %d files removed.', $dead_total), $dead_total); + echo "
\n\n"; + echo "
\n"; + UI::show_box_bottom(); + ob_flush(); + flush(); + + $this->update_last_clean(); + } // clean_catalog + + /** + * verify_catalog + * This function verify the catalog + */ + public function verify_catalog() + { + + require AmpConfig::get('prefix') . '/templates/show_verify_catalog.inc.php'; + ob_flush(); + flush(); + + $verified = $this->verify_catalog_proc();+ + + UI::show_box_top(); + echo ''; + printf(T_('Catalog Verify Done. %d of %d files updated.'), $verified['updated'], $verified['total']); + echo "
\n"; + echo "
\n"; + UI::show_box_bottom(); + ob_flush(); + flush(); + + return true; + } // verify_catalog + + /** + * gc + * + * This is a wrapper function for all of the different cleaning + * functions, it runs them in an order that resembles correctness. + */ + public static function gc() + { + debug_event('catalog', 'Database cleanup started', 5); + Song::gc(); + Album::gc(); + Artist::gc(); + Art::gc(); + Stats::gc(); + Rating::gc(); + Userflag::gc(); + Playlist::gc(); + Tmp_Playlist::gc(); + Shoutbox::gc(); + Tag::gc(); + debug_event('catalog', 'Database cleanup ended', 5); + } + + /** + * trim_prefix + * Splits the prefix from the string + */ + public static function trim_prefix($string) + { + $prefix_pattern = '/^(' . implode('\\s|',explode('|',AmpConfig::get('catalog_prefix_pattern'))) . '\\s)(.*)/i'; + preg_match($prefix_pattern, $string, $matches); + + if (count($matches)) { + $string = trim($matches[2]); + $prefix = trim($matches[1]); + } else { + $prefix = null; + } + + return array('string' => $string, 'prefix' => $prefix); + } // trim_prefix + + /** + * check_title + * this checks to make sure something is + * set on the title, if it isn't it looks at the + * filename and trys to set the title based on that + */ + public static function check_title($title,$file=0) + { + if (strlen(trim($title)) < 1) { + $title = Dba::escape($file); + } + + return $title; + + } // check_title + + /** + * playlist_import + * Attempts to create a Public Playlist based on the playlist file + */ + public static function import_playlist($playlist) + { + $data = file_get_contents($playlist); + if (substr($playlist, -3,3) == 'm3u') { + $files = self::parse_m3u($data); + } elseif (substr($playlist, -3,3) == 'pls') { + $files = self::parse_pls($data); + } elseif (substr($playlist, -3,3) == 'asx') { + $files = self::parse_asx($data); + } elseif (substr($playlist, -4,4) == 'xspf') { + $files = self::parse_xspf($data); + } + + $songs = array(); + $pinfo = pathinfo($playlist); + if (isset($files)) { + foreach ($files as $file) { + $file = trim($file); + // Check to see if it's a url from this ampache instance + if (substr($file, 0, strlen(AmpConfig::get('web_path'))) == AmpConfig::get('web_path')) { + $data = Stream_URL::parse($file); + $sql = 'SELECT COUNT(*) FROM `song` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($data['id'])); + if (Dba::num_rows($db_results)) { + $songs[] = $data['id']; + } + } // end if it's an http url + else { + // Remove file:// prefix if any + if (strpos($file, "file://") !== false) { + $file = urldecode(substr($file, 7)); + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + // Removing starting / on Windows OS. + if (substr($file, 0, 1) == '/') { + $file = substr($file, 1); + } + // Restore real directory separator + $file = str_replace("/", DIRECTORY_SEPARATOR, $file); + } + } + debug_event('catalog', 'Add file ' . $file . ' to playlist.', '5'); + + // First, try to found the file as absolute path + $sql = "SELECT `id` FROM `song` WHERE `file` = ?"; + $db_results = Dba::read($sql, array($file)); + $results = Dba::fetch_assoc($db_results); + + if (isset($results['id'])) { + $songs[] = $results['id']; + } else { + // Not found in absolute path, create it from relative path + $file = $pinfo['dirname'] . DIRECTORY_SEPARATOR . $file; + // Normalize the file path. realpath requires the files to exists. + $file = realpath($file); + if ($file) { + $sql = "SELECT `id` FROM `song` WHERE `file` = ?"; + $db_results = Dba::read($sql, array($file)); + $results = Dba::fetch_assoc($db_results); + + if (isset($results['id'])) { + $songs[] = $results['id']; + } + } + } + } // if it's a file + } + } + + debug_event('import_playlist', "Parsed " . $playlist . ", found " . count($songs) . " songs", 5); + + if (count($songs)) { + $name = $pinfo['extension'] . " - " . $pinfo['filename']; + $playlist_id = Playlist::create($name, 'public'); + + if (!$playlist_id) { + return array( + 'success' => false, + 'error' => T_('Failed to create playlist.'), + ); + } + + /* Recreate the Playlist */ + $playlist = new Playlist($playlist_id); + $playlist->add_songs($songs, true); + + return array( + 'success' => true, + 'id' => $playlist_id, + 'count' => count($songs) + ); + } + + return array( + 'success' => false, + 'error' => T_('No valid songs found in playlist file.') + ); + } + + /** + * parse_m3u + * this takes m3u filename and then attempts to found song filenames listed in the m3u + */ + public static function parse_m3u($data) + { + $files = array(); + $results = explode("\n", $data); + + foreach ($results as $value) { + $value = trim($value); + if (!empty($value) && substr($value, 0, 1) != '#') { + $files[] = $value; + } + } + + return $files; + } // parse_m3u + + /** + * parse_pls + * this takes pls filename and then attempts to found song filenames listed in the pls + */ + public static function parse_pls($data) + { + $files = array(); + $results = explode("\n", $data); + + foreach ($results as $value) { + $value = trim($value); + if (preg_match("/file[0-9]+[\s]*\=(.*)/i", $value, $matches)) { + $file = trim($matches[1]); + if (!empty($file)) { + $files[] = $file; + } + } + } + + return $files; + } // parse_pls + + /** + * parse_asx + * this takes asx filename and then attempts to found song filenames listed in the asx + */ + public static function parse_asx($data) + { + $files = array(); + $xml = simplexml_load_string($data); + + if ($xml) { + foreach ($xml->entry as $entry) { + $file = trim($entry->ref['href']); + if (!empty($file)) { + $files[] = $file; + } + } + } + + return $files; + } // parse_asx + + /** + * parse_xspf + * this takes xspf filename and then attempts to found song filenames listed in the xspf + */ + public static function parse_xspf($data) + { + $files = array(); + $xml = simplexml_load_string($data); + if ($xml) { + foreach ($xml->trackList->track as $track) { + $file = trim($track->location); + if (!empty($file)) { + $files[] = $file; + } + } + } + + return $files; + } // parse_xspf + + /** + * delete + * Deletes the catalog and everything associated with it + * it takes the catalog id + */ + public static function delete($catalog_id) + { + // Large catalog deletion can take time + set_time_limit(0); + + // First remove the songs in this catalog + $sql = "DELETE FROM `song` WHERE `catalog` = ?"; + $db_results = Dba::write($sql, array($catalog_id)); + + // Only if the previous one works do we go on + if (!$db_results) { return false; } + + $sql = "DELETE FROM `video` WHERE `catalog` = ?"; + $db_results = Dba::write($sql, array($catalog_id)); + + if (!$db_results) { return false; } + + $catalog = self::create_from_id($catalog_id); + + $sql = 'DELETE FROM `catalog_' . $catalog->get_type() . '` WHERE catalog_id = ?'; + $db_results = Dba::write($sql, array($catalog_id)); + + if (!$db_results) { return false; } + + // Next Remove the Catalog Entry it's self + $sql = "DELETE FROM `catalog` WHERE `id` = ?"; + Dba::write($sql, array($catalog_id)); + + // Run the cleaners... + self::gc(); + + } // delete + + /** + * exports the catalog + * it exports all songs in the database to the given export type. + */ + public static function export($type, $catalog_id = '') + { + // Select all songs in catalog + $params = array(); + if ($catalog_id) { + $sql = 'SELECT `id` FROM `song` ' . + "WHERE `catalog`= ? " . + 'ORDER BY `album`, `track`'; + $params[] = $catalog_id; + } else { + $sql = 'SELECT `id` FROM `song` ORDER BY `album`, `track`'; + } + $db_results = Dba::read($sql, $params); + + switch ($type) { + case 'itunes': + echo xml_get_header('itunes'); + while ($results = Dba::fetch_assoc($db_results)) { + $song = new Song($results['id']); + $song->format(); + + $xml = array(); + $xml['key']= $results['id']; + $xml['dict']['Track ID']= intval($results['id']); + $xml['dict']['Name'] = $song->title; + $xml['dict']['Artist'] = $song->f_artist_full; + $xml['dict']['Album'] = $song->f_album_full; + $xml['dict']['Total Time'] = intval($song->time) * 1000; // iTunes uses milliseconds + $xml['dict']['Track Number'] = intval($song->track); + $xml['dict']['Year'] = intval($song->year); + $xml['dict']['Date Added'] = date("Y-m-d\TH:i:s\Z",$song->addition_time); + $xml['dict']['Bit Rate'] = intval($song->bitrate/1000); + $xml['dict']['Sample Rate'] = intval($song->rate); + $xml['dict']['Play Count'] = intval($song->played); + $xml['dict']['Track Type'] = "URL"; + $xml['dict']['Location'] = Song::play_url($song->id); + echo xoutput_from_array($xml, 1, 'itunes'); + // flush output buffer + } // while result + echo xml_get_footer('itunes'); + + break; + case 'csv': + echo "ID,Title,Artist,Album,Length,Track,Year,Date Added,Bitrate,Played,File\n"; + while ($results = Dba::fetch_assoc($db_results)) { + $song = new Song($results['id']); + $song->format(); + echo '"' . $song->id . '","' . + $song->title . '","' . + $song->f_artist_full . '","' . + $song->f_album_full .'","' . + $song->f_time . '","' . + $song->f_track . '","' . + $song->year .'","' . + date("Y-m-d\TH:i:s\Z", $song->addition_time) . '","' . + $song->f_bitrate .'","' . + $song->played . '","' . + $song->file . "\n"; + } + break; + } // end switch + + } // export + +} // end of catalog class diff --git a/sources/lib/class/channel.class.php b/sources/lib/class/channel.class.php new file mode 100644 index 0000000..4b3f4a5 --- /dev/null +++ b/sources/lib/class/channel.class.php @@ -0,0 +1,390 @@ +get_info($id); + + // Foreach what we've got + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + } //constructor + + public function update_start($start_date, $address, $port, $pid) + { + $sql = "UPDATE `channel` SET `start_date` = ?, `interface` = ?, `port` = ?, `pid` = ?, `listeners` = '0' WHERE `id` = ?"; + Dba::write($sql, array($start_date, $address, $port, $pid, $this->id)); + + $this->start_date = $start_date; + $this->interface = $address; + $this->port = $port; + $this->pid = $pid; + } + + public function update_listeners($listeners, $addition=false) + { + $sql = "UPDATE `channel` SET `listeners` = ? "; + $params = array($listeners); + $this->listeners = $listeners; + if ($listeners > $this->peak_listeners) { + $this->peak_listeners = $listeners; + $sql .= ", `peak_listeners` = ? "; + $params[] = $listeners; + } + if ($addition) { + $sql .= ", `connections`=`connections`+1 "; + } + $sql .= "WHERE `id` = ?"; + $params[] = $this->id; + Dba::write($sql, $params); + } + + public function get_genre() + { + $tags = Tag::get_object_tags('channel', $this->id); + $genre = ""; + foreach ($tags as $tag) { + $genre .= $tag['name'] . ' '; + } + $genre = trim($genre); + + return $genre; + } + + public function delete() + { + $sql = "DELETE FROM `channel` WHERE `id` = ?"; + return Dba::write($sql, array($this->id)); + } + + public static function get_next_port() + { + $port = 8200; + $sql = "SELECT MAX(`port`) AS `max_port` FROM `channel`"; + $db_results = Dba::read($sql); + + if ($results = Dba::fetch_assoc($db_results)) { + if ($results['max_port'] > 0) { + $port = $results['max_port'] + 1; + } + } + + return $port; + } + + public static function create($name, $description, $url, $object_type, $object_id, $interface, $port, $admin_password, $private, $max_listeners, $random, $loop, $stream_type, $bitrate) + { + if (!empty($name)) { + $sql = "INSERT INTO `channel` (`name`, `description`, `url`, `object_type`, `object_id`, `interface`, `port`, `fixed_endpoint`, `admin_password`, `is_private`, `max_listeners`, `random`, `loop`, `stream_type`, `bitrate`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $params = array($name, $description, $url, $object_type, $object_id, $interface, $port, (!empty($interface) && !empty($port)), $admin_password, !empty($private), $max_listeners, $random, $loop, $stream_type, $bitrate); + return Dba::write($sql, $params); + } + + return false; + } + + public function update($data) + { + if (isset($data['edit_tags'])) { + Tag::update_tag_list($data['edit_tags'], 'channel', $this->id); + } + + $sql = "UPDATE `channel` SET `name` = ?, `description` = ?, `url` = ?, `interface` = ?, `port` = ?, `fixed_endpoint` = ?, `admin_password` = ?, `is_private` = ?, `max_listeners` = ?, `random` = ?, `loop` = ?, `stream_type` = ?, `bitrate` = ?, `object_id` = ? " . + "WHERE `id` = ?"; + $params = array($data['name'], $data['description'], $data['url'], $data['interface'], $data['port'], (!empty($data['interface']) && !empty($data['port'])), $data['admin_password'], !empty($data['private']), $data['max_listeners'], $data['random'], $data['loop'], $data['stream_type'], $data['bitrate'], $data['object_id'], $this->id); + return Dba::write($sql, $params); + } + + public static function format_type($type) + { + switch ($type) { + case 'playlist': + $ftype = $type; + break; + default: + $ftype = ''; + break; + } + + return $ftype; + } + + public function show_action_buttons() + { + if ($this->id) { + if ($GLOBALS['user']->has_access('75')) { + echo Ajax::button('?page=index&action=start_channel&id=' . $this->id,'run', T_('Start Channel'),'channel_start_' . $this->id); + echo " " . Ajax::button('?page=index&action=stop_channel&id=' . $this->id,'stop', T_('Stop Channel'),'channel_stop_' . $this->id); + echo " id . "\" onclick=\"showEditDialog('channel_row', '" . $this->id . "', 'edit_channel_" . $this->id . "', '" . T_('Channel edit') . "', 'channel_row_', 'refresh_channel')\">" . UI::get_icon('edit', T_('Edit')) . ""; + echo " id ."\">" . UI::get_icon('delete', T_('Delete')) . ""; + } + } + } + + public function format() + { + $this->tags = Tag::get_top_tags('channel', $this->id); + $this->f_tags = Tag::get_display($this->tags); + } + + public function get_target_object() + { + $object = null; + if ($this->object_type == 'playlist') { + $object = new Playlist($this->object_id); + $object->format(); + } + + return $object; + } + + public function get_stream_url() + { + return "http://" . $this->interface . ":" . $this->port . "/stream." . $this->stream_type; + } + + public function get_stream_proxy_url() + { + return AmpConfig::get('web_path') . '/channel/' . $this->id . '/stream.' . $this->stream_type; + } + + public static function get_channel_list_sql() + { + $sql = "SELECT `id` FROM `channel` "; + + return $sql; + } + + public static function get_channel_list() + { + $sql = self::get_channel_list_sql(); + $db_results = Dba::read($sql); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + public function start_channel() + { + exec("php " . AmpConfig::get('prefix') . '/bin/channel_run.inc -c ' . $this->id . ' > /dev/null &'); + } + + public function stop_channel() + { + if ($this->pid) { + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + exec("taskkill /F /PID " . $this->pid); + } else { + exec("kill -9 " . $this->pid); + } + + $sql = "UPDATE `channel` SET `start_date` = '0', `listeners` = '0', `pid` = '0' WHERE `id` = ?"; + Dba::write($sql, array($this->id)); + + $this->pid = 0; + } + } + + public function check_channel() + { + $check = false; + if ($this->interface && $this->port) { + $connection = @fsockopen($this->interface, $this->port); + if (is_resource($connection)) { + $check = true; + fclose($connection); + } + } + return $check; + } + + public function get_channel_state() + { + if ($this->check_channel()) { + $state = T_("Running"); + } else { + $state = T_("Stopped"); + } + + return $state; + } + + protected function init_channel_songs() + { + $this->song_pos = 0; + $this->songs = array(); + $this->playlist = $this->get_target_object(); + if ($this->playlist) { + if (!$this->random) { + $this->songs = $this->playlist->get_songs(); + } + } + $this->is_init = true; + } + + public function get_chunk() + { + $chunk = null; + + if (!$this->is_init) { + $this->init_channel_songs(); + } + + if ($this->is_init) { + // Move to next song + while ($this->media == null && ($this->random || $this->song_pos < count($this->songs))) { + if ($this->random) { + $randsongs = $this->playlist->get_random_items(1); + $this->media = new Song($randsongs[0]['object_id']); + } else { + $this->media = new Song($this->songs[$this->song_pos]); + } + $this->media->format(); + + if ($this->media->catalog) { + $catalog = Catalog::create_from_id($this->media->catalog); + if (make_bool($this->media->enabled)) { + if (AmpConfig::get('lock_songs')) { + if (!Stream::check_lock_media($this->media->id, 'song')) { + debug_event('channel', 'Media ' . $this->media->id . ' locked, skipped.', '3'); + $this->media = null; + } + } + } + + if ($this->media != null) { + $this->media = $catalog->prepare_media($this->media); + + if (!$this->media->file || !Core::is_readable(Core::conv_lc_file($this->media->file))) { + debug_event('channel', 'Cannot read media ' . $this->media->id . ' file, skipped.', '3'); + $this->media = null; + } else { + $valid_types = $this->media->get_stream_types(); + if (!in_array('transcode', $valid_types)) { + debug_event('channel', 'Missing settings to transcode ' . $this->media->file . ', skipped.', '3'); + $this->media = null; + } else { + debug_event('channel', 'Now listening to ' . $this->media->file . '.', '5'); + } + } + } + } else { + debug_event('channel', 'Media ' . $this->media->id . ' doesn\'t have catalog, skipped.', '3'); + $this->media = null; + } + + $this->song_pos++; + // Restart from beginning for next song if the channel is 'loop' enabled + // and load fresh data from database + if ($this->media != null && $this->song_pos == count($this->songs) && $this->loop) { + $this->init_channel_songs(); + } + } + + if ($this->media != null) { + // Stream not yet initialized for this media, start it + if (!$this->transcoder) { + $this->transcoder = Stream::start_transcode($this->media, $this->stream_type, $this->bitrate); + $this->media_bytes_streamed = 0; + } + + if (is_resource($this->transcoder['handle'])) { + + $chunk = fread($this->transcoder['handle'], 4096); + $this->media_bytes_streamed += strlen($chunk); + + // End of file, prepare to move on for next call + if (feof($this->transcoder['handle'])) { + $this->media->set_played(); + if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + fread($this->transcoder['stderr'], 4096); + fclose($this->transcoder['stderr']); + } + fclose($this->transcoder['handle']); + proc_close($this->transcoder['process']); + + $this->media = null; + $this->transcoder = null; + } + } else { + $this->media = null; + $this->transcoder = null; + } + + if (!strlen($chunk)) { + $chunk = $this->get_chunk(); + } + } + } + + return $chunk; + } + + public static function play_url($oid, $additional_params='') + { + $channel = new Channel($oid); + return $channel->get_stream_proxy_url() . '?rt=' . time() . '&filename=' . urlencode($channel->name) . '.' . $channel->stream_type . $additional_params; + } + +} // end of channel class diff --git a/sources/lib/class/core.class.php b/sources/lib/class/core.class.php new file mode 100644 index 0000000..b5df1f9 --- /dev/null +++ b/sources/lib/class/core.class.php @@ -0,0 +1,249 @@ + $name, 'expire' => $expire); + debug_event('Core', "Registered $type form $name with SID $sid and expiration $expire ($window seconds from now)", 5); + + switch ($type) { + case 'get': + $string = $sid; + break; + case 'post': + default: + $string = ''; + break; + } // end switch on type + + return $string; + + } // form_register + + /** + * form_verify + * + * This takes a form name and then compares it with the posted sid, if + * they don't match then it returns false and doesn't let the person + * continue + */ + public static function form_verify($name, $type = 'post') + { + switch ($type) { + case 'post': + $sid = $_POST['form_validation']; + break; + case 'get': + $sid = $_GET['form_validation']; + break; + case 'cookie': + $sid = $_COOKIE['form_validation']; + break; + case 'request': + $sid = $_REQUEST['form_validation']; + break; + default: + return false; + } + + if (!isset($_SESSION['forms'][$sid])) { + debug_event('Core', "Form $sid not found in session, rejecting request", 2); + return false; + } + + $form = $_SESSION['forms'][$sid]; + unset($_SESSION['forms'][$sid]); + + if ($form['name'] == $name) { + debug_event('Core', "Verified SID $sid for $type form $name", 5); + if ($form['expire'] < time()) { + debug_event('Core', "Form $sid is expired, rejecting request", 2); + return false; + } + + return true; + } + + // OMG HAX0RZ + debug_event('Core', "$type form $sid failed consistency check, rejecting request", 2); + return false; + + } // form_verify + + /** + * image_dimensions + * This returns the dimensions of the passed song of the passed type + * returns an empty array if PHP-GD is not currently installed, returns + * false on error + */ + public static function image_dimensions($image_data) + { + if (!function_exists('ImageCreateFromString')) { return false; } + + $image = ImageCreateFromString($image_data); + + if (!$image) { return false; } + + $width = imagesx($image); + $height = imagesy($image); + + if (!$width || !$height) { return false; } + + return array('width'=>$width,'height'=>$height); + + } // image_dimensions + + /* + * is_readable + * + * Replacement function because PHP's is_readable is buggy: + * https://bugs.php.net/bug.php?id=49620 + */ + public static function is_readable($path) + { + if (is_dir($path)) { + $handle = opendir($path); + if ($handle === false) { + return false; + } + closedir($handle); + return true; + } + + $handle = @fopen($path, 'rb'); + if ($handle === false) { + return false; + } + fclose($handle); + return true; + } + + /* + * conv_lc_file + * + * Convert site charset filename to local charset filename for file operations + */ + public static function conv_lc_file($filename) + { + $lc_filename = $filename; + $site_charset = AmpConfig::get('site_charset'); + $lc_charset = AmpConfig::get('lc_charset'); + if ($lc_charset && $lc_charset != $site_charset) { + if (function_exists('iconv')) { + $lc_filename = iconv($site_charset, $lc_charset, $filename); + } + } + + return $lc_filename; + } + + /* + * is_session_started + * + * Universal function for checking session status. + */ + public static function is_session_started() + { + if (php_sapi_name() !== 'cli' ) { + if (version_compare(phpversion(), '5.4.0', '>=') ) { + return session_status() === PHP_SESSION_ACTIVE ? true : false; + } else { + return session_id() === '' ? false : true; + } + } + return false; + } +} // Core diff --git a/sources/lib/class/database_object.abstract.php b/sources/lib/class/database_object.abstract.php new file mode 100644 index 0000000..ec4b7f9 --- /dev/null +++ b/sources/lib/class/database_object.abstract.php @@ -0,0 +1,140 @@ +0); + + private static $_sql; + private static $_error; + + /** + * constructor + * This does nothing with the DBA class + */ + private function __construct() + { + // Rien a faire + + } // construct + + /** + * query + */ + public static function query($sql, $params = array()) + { + // json_encode throws errors about UTF-8 cleanliness, which we don't + // care about here. + debug_event('Query', $sql . ' ' . @json_encode($params), 6); + + // Be aggressive, be strong, be dumb + $tries = 0; + do { + $stmt = self::_query($sql, $params); + } while (!$stmt && $tries++ < 3); + + return $stmt; + } + + private static function _query($sql, $params) + { + $dbh = self::dbh(); + if (!$dbh) { + debug_event('Dba', 'Error: failed to get database handle', 1); + return false; + } + + // Run the query + if ($params) { + $stmt = $dbh->prepare($sql); + $stmt->execute($params); + } else { + $stmt = $dbh->query($sql); + } + + // Save the query, to make debug easier + self::$_sql = $sql; + self::$stats['query']++; + + if (!$stmt) { + self::$_error = json_encode($dbh->errorInfo()); + debug_event('Dba', 'Error: ' . json_encode($dbh->errorInfo()), 1); + self::disconnect(); + } else if ($stmt->errorCode() && $stmt->errorCode() != '00000') { + self::$_error = json_encode($stmt->errorInfo()); + debug_event('Dba', 'Error: ' . json_encode($stmt->errorInfo()), 1); + self::disconnect(); + return false; + } + + return $stmt; + } + + /** + * read + */ + public static function read($sql, $params = null) + { + return self::query($sql, $params); + } + + /** + * write + */ + public static function write($sql, $params = null) + { + return self::query($sql, $params); + } + + /** + * escape + * + * This runs a escape on a variable so that it can be safely inserted + * into the sql + */ + public static function escape($var) + { + $var = self::dbh()->quote($var); + // This is slightly less ugly than it was, but still ugly + return substr($var, 1, -1); + } + + /** + * fetch_assoc + * + * This emulates the mysql_fetch_assoc. + * We force it to always return an array, albeit an empty one + * The optional finish parameter affects whether we automatically clean + * up the result set after the last row is read. + */ + public static function fetch_assoc($resource, $finish = true) + { + if (!$resource) { + return array(); + } + + $result = $resource->fetch(PDO::FETCH_ASSOC); + + if (!$result) { + if ($finish) { + self::finish($resource); + } + return array(); + } + + return $result; + } + + /** + * fetch_row + * + * This emulates the mysql_fetch_row + * we force it to always return an array, albeit an empty one + * The optional finish parameter affects whether we automatically clean + * up the result set after the last row is read. + */ + public static function fetch_row($resource, $finish = true) + { + if (!$resource) { + return array(); + } + + $result = $resource->fetch(PDO::FETCH_NUM);; + + if (!$result) { + if ($finish) { + self::finish($resource); + } + return array(); + } + + return $result; + } + + /** + * num_rows + * + * This emulates the mysql_num_rows function which is really + * just a count of rows returned by our select statement, this + * doesn't work for updates or inserts. + */ + public static function num_rows($resource) + { + if ($resource) { + $result = $resource->rowCount(); + if ($result) { + return $result; + } + } + + return 0; + } + + /** + * finish + * + * This closes a result handle and clears the memory associated with it + */ + public static function finish($resource) + { + if ($resource) { + $resource->closeCursor(); + } + } + + /** + * affected_rows + * + * This emulates the mysql_affected_rows function + */ + public static function affected_rows($resource) + { + if ($resource) { + $result = $resource->rowCount(); + if ($result) { + return $result; + } + } + + return 0; + } + + /** + * _connect + * + * This connects to the database, used by the DBH function + */ + private static function _connect() + { + $username = AmpConfig::get('database_username'); + $hostname = AmpConfig::get('database_hostname'); + $password = AmpConfig::get('database_password'); + $port = AmpConfig::get('database_port'); + + // Build the data source name + if (strpos($hostname, '/') === 0) { + $dsn = 'mysql:unix_socket=' . $hostname; + } else { + $dsn = 'mysql:host=' . $hostname ?: 'localhost'; + } + if ($port) { + $dsn .= ';port=' . intval($port); + } + + try { + $dbh = new PDO($dsn, $username, $password); + } catch (PDOException $e) { + self::$_error = $e->getMessage(); + debug_event('Dba', 'Connection failed: ' . $e->getMessage(), 1); + return null; + } + + return $dbh; + } + + private static function _setup_dbh($dbh, $database) + { + if (!$dbh) { + return false; + } + + $charset = self::translate_to_mysqlcharset(AmpConfig::get('site_charset')); + $charset = $charset['charset']; + if ($dbh->exec('SET NAMES ' . $charset) === false) { + debug_event('Dba', 'Unable to set connection charset to ' . $charset, 1); + } + + if ($dbh->exec('USE `' . $database . '`') === false) { + self::$_error = json_encode($dbh->errorInfo()); + debug_event('Dba', 'Unable to select database ' . $database . ': ' . json_encode($dbh->errorInfo()), 1); + } + + if (AmpConfig::get('sql_profiling')) { + $dbh->exec('SET profiling=1'); + $dbh->exec('SET profiling_history_size=50'); + $dbh->exec('SET query_cache_type=0'); + } + } + + /** + * check_database + * + * Make sure that we can connect to the database + */ + public static function check_database() + { + $dbh = self::_connect(); + + if (!$dbh || $dbh->errorCode()) { + if ($dbh) { + self::$_error = json_encode($dbh->errorInfo()); + } + return false; + } + + return true; + } + + /** + * check_database_inserted + * + * Checks to make sure that you have inserted the database + * and that the user you are using has access to it. + */ + public static function check_database_inserted() + { + $sql = "DESCRIBE session"; + $db_results = Dba::read($sql); + + if (!$db_results) { + return false; + } + + // Make sure the whole table is there + if (Dba::num_rows($db_results) != '7') { + return false; + } + + return true; + } + + /** + * show_profile + * + * This function is used for debug, helps with profiling + */ + public static function show_profile() + { + if (AmpConfig::get('sql_profiling')) { + print '
Profiling data:
'; + $res = Dba::read('SHOW PROFILES'); + print ''; + while ($r = Dba::fetch_row($res)) { + print ''; + } + print '
' . implode('', $r) . '
'; + } + } + + /** + * dbh + * + * This is called by the class to return the database handle + * for the specified database, if none is found it connects + */ + public static function dbh($database='') + { + if (!$database) { + $database = AmpConfig::get('database_name'); + } + + // Assign the Handle name that we are going to store + $handle = 'dbh_' . $database; + + if (!is_object(AmpConfig::get($handle))) { + $dbh = self::_connect(); + self::_setup_dbh($dbh, $database); + AmpConfig::set($handle, $dbh, true); + return $dbh; + } else { + return AmpConfig::get($handle); + } + } + + /** + * disconnect + * + * This nukes the dbh connection, this isn't used very often... + */ + public static function disconnect($database = '') + { + if (!$database) { + $database = AmpConfig::get('database_name'); + } + + $handle = 'dbh_' . $database; + + // Nuke it + AmpConfig::set($handle, null, true); + + return true; + } + + /** + * insert_id + */ + public static function insert_id() + { + $dbh = self::dbh(); + if ($dbh) { + return $dbh->lastInsertId(); + } + return null; + } + + /** + * error + * this returns the error of the db + */ + public static function error() + { + return self::$_error; + } + + /** + * translate_to_mysqlcharset + * + * This translates the specified charset to a mysql charset. + */ + public static function translate_to_mysqlcharset($charset) + { + // Translate real charset names into fancy MySQL land names + switch (strtoupper($charset)) { + case 'CP1250': + case 'WINDOWS-1250': + $target_charset = 'cp1250'; + $target_collation = 'cp1250_general_ci'; + break; + case 'ISO-8859': + case 'ISO-8859-2': + $target_charset = 'latin2'; + $target_collation = 'latin2_general_ci'; + break; + case 'ISO-8859-1': + case 'CP1252': + case 'WINDOWS-1252': + $target_charset = 'latin1'; + $target_collation = 'latin1_general_ci'; + break; + case 'EUC-KR': + $target_charset = 'euckr'; + $target_collation = 'euckr_korean_ci'; + break; + case 'CP932': + $target_charset = 'sjis'; + $target_collation = 'sjis_japanese_ci'; + break; + case 'KOI8-U': + $target_charset = 'koi8u'; + $target_collation = 'koi8u_general_ci'; + break; + case 'KOI8-R': + $target_charset = 'koi8r'; + $target_collation = 'koi8r_general_ci'; + break; + case 'UTF-8': + default: + $target_charset = 'utf8'; + $target_collation = 'utf8_unicode_ci'; + break; + } + + return array( + 'charset' => $target_charset, + 'collation' => $target_collation + ); + } + + /** + * reset_db_charset + * + * This cruises through the database and trys to set the charset to the + * current site charset. This is an admin function that can be run by an + * administrator only. This can mess up data if you switch between charsets + * that are not overlapping. + */ + public static function reset_db_charset() + { + $translated_charset = self::translate_to_mysqlcharset(AmpConfig::get('site_charset')); + $target_charset = $translated_charset['charset']; + $target_collation = $translated_charset['collation']; + + // Alter the charset for the entire database + $sql = "ALTER DATABASE `" . AmpConfig::get('database_name') . "` DEFAULT CHARACTER SET $target_charset COLLATE $target_collation"; + Dba::write($sql); + + $sql = "SHOW TABLES"; + $db_results = Dba::read($sql); + + // Go through the tables! + while ($row = Dba::fetch_row($db_results)) { + $sql = "DESCRIBE `" . $row['0'] . "`"; + $describe_results = Dba::read($sql); + + // Change the tables default charset and colliation + $sql = "ALTER TABLE `" . $row['0'] . "` DEFAULT CHARACTER SET $target_charset COLLATE $target_collation"; + Dba::write($sql); + + // Iterate through the columns of the table + while ($table = Dba::fetch_assoc($describe_results)) { + if ( + (strpos($table['Type'], 'varchar') !== false) || + (strpos($table['Type'], 'enum') !== false) || + (strpos($table['Table'],'text') !== false)) { + $sql = "ALTER TABLE `" . $row['0'] . "` MODIFY `" . $table['Field'] . "` " . $table['Type'] . " CHARACTER SET " . $target_charset; + $charset_results = Dba::write($sql); + if (!$charset_results) { + debug_event('CHARSET','Unable to update the charset of ' . $table['Field'] . '.' . $table['Type'] . ' to ' . $target_charset,'3'); + } // if it fails + } + } + } + } + + /** + * optimize_tables + * + * This runs an optimize on the tables and updates the stats to improve + * join speed. + * This can be slow, but is a good idea to do from time to time. We do + * it in case the dba isn't doing it... which we're going to assume they + * aren't. + */ + public static function optimize_tables() + { + $sql = "SHOW TABLES"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_row($db_results)) { + $sql = "OPTIMIZE TABLE `" . $row[0] . "`"; + Dba::write($sql); + + $sql = "ANALYZE TABLE `" . $row[0] . "`"; + Dba::write($sql); + } + } +} diff --git a/sources/lib/class/democratic.class.php b/sources/lib/class/democratic.class.php new file mode 100644 index 0000000..63e5f4f --- /dev/null +++ b/sources/lib/class/democratic.class.php @@ -0,0 +1,643 @@ +get_info($id); + + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + } // constructor + + /** + * build_vote_cache + * This builds a vote cache of the objects we've got in the playlist + */ + public static function build_vote_cache($ids) + { + if (!is_array($ids) || !count($ids)) { return false; } + + $idlist = '(' . implode(',', $ids) . ')'; + + $sql = 'SELECT `object_id`, COUNT(`user`) AS `count` ' . + 'FROM `user_vote` ' . + "WHERE `object_id` IN $idlist GROUP BY `object_id`"; + + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('democratic_vote', $row['object_id'], $row['count']); + } + + return true; + + } // build_vote_cache + + /** + * is_enabled + * This function just returns true / false if the current democratic + * playlist is currently enabled / configured + */ + public function is_enabled() + { + if ($this->tmp_playlist) { return true; } + + return false; + + } // is_enabled + + /** + * set_parent + * This returns the Tmp_Playlist for this democratic play instance + */ + public function set_parent() + { + $demo_id = Dba::escape($this->id); + + $sql = "SELECT * FROM `tmp_playlist` WHERE `session`='$demo_id'"; + $db_results = Dba::read($sql); + + $row = Dba::fetch_assoc($db_results); + + $this->tmp_playlist = $row['id']; + + + } // set_parent + + /** + * set_user_preferences + * This sets up a (or all) user(s) to use democratic play. This sets + * their play method and playlist method (clear on send) If no user is + * passed it does it for everyone and also locks down the ability to + * change to admins only + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function set_user_preferences($user = null) + { + //FIXME: Code in single user stuff + + $preference_id = Preference::id_from_name('play_type'); + Preference::update_level($preference_id,'75'); + Preference::update_all($preference_id,'democratic'); + + $allow_demo = Preference::id_from_name('allow_democratic_playback'); + Preference::update_all($allow_demo,'1'); + + $play_method = Preference::id_from_name('playlist_method'); + Preference::update_all($play_method,'clear'); + + return true; + + } // set_user_preferences + + /** + * format + * This makes the variables all purrty so that they can be displayed + */ + public function format() + { + $this->f_cooldown = $this->cooldown . ' ' . T_('minutes'); + $this->f_primary = $this->primary ? T_('Primary') : ''; + + switch ($this->level) { + case '5': + $this->f_level = T_('Guest'); + break; + case '25': + $this->f_level = T_('User'); + break; + case '50': + $this->f_level = T_('Content Manager'); + break; + case '75': + $this->f_level = T_('Catalog Manager'); + break; + case '100': + $this->f_level = T_('Admin'); + break; + } + + } // format + + /** + * get_playlists + * This returns all of the current valid 'Democratic' Playlists + * that have been created. + */ + public static function get_playlists() + { + $sql = "SELECT `id` FROM `democratic` ORDER BY `name`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_playlists + + /** + * get_current_playlist + * This returns the curren users current playlist, or if specified + * this current playlist of the user + */ + public static function get_current_playlist() + { + $democratic_id = AmpConfig::get('democratic_id'); + + if (!$democratic_id) { + $level = Dba::escape($GLOBALS['user']->access); + $sql = "SELECT `id` FROM `democratic` WHERE `level` <= '$level' " . + " ORDER BY `level` DESC,`primary` DESC"; + $db_results = Dba::read($sql); + $row = Dba::fetch_assoc($db_results); + $democratic_id = $row['id']; + } + + $object = new Democratic($democratic_id); + + return $object; + + } // get_current_playlist + + /** + * get_items + * This returns a sorted array of all object_ids in this Tmp_Playlist. + * The array is multidimensional; the inner array needs to contain the + * keys 'id', 'object_type' and 'object_id'. + * + * Sorting is highest to lowest vote count, then by oldest to newest + * vote activity. + */ + public function get_items($limit = null) + { + $sql = 'SELECT `tmp_playlist_data`.`object_type`, ' . + '`tmp_playlist_data`.`object_id`, ' . + '`tmp_playlist_data`.`id` ' . + 'FROM `tmp_playlist_data` INNER JOIN `user_vote` ' . + 'ON `user_vote`.`object_id` = `tmp_playlist_data`.`id` ' . + "WHERE `tmp_playlist_data`.`tmp_playlist` = '" . + Dba::escape($this->tmp_playlist) . "' " . + 'GROUP BY 1, 2 ' . + 'ORDER BY COUNT(*) DESC, MAX(`user_vote`.`date`) '; + + if ($limit) { + $sql .= 'LIMIT ' . intval($limit); + } + + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + if ($row['id']) { + $results[] = $row; + } + } + + return $results; + + } // get_items + + /** + * play_url + * This returns the special play URL for democratic play, only open to ADMINs + */ + public function play_url() + { + $link = Stream::get_base_url() . 'uid=' . scrub_out($GLOBALS['user']->id) . '&demo_id=' . scrub_out($this->id); + + return Stream_URL::format($link); + + } // play_url + + /** + * get_next_object + * This returns the next object in the tmp_playlist. + * Most of the time this will just be the top entry, but if there is a + * base_playlist and no items in the playlist then it returns a random + * entry from the base_playlist + */ + public function get_next_object($offset = 0) + { + // FIXME: Shouldn't this return object_type? + + $offset = intval($offset); + + $items = $this->get_items($offset + 1); + + if (count($items) > $offset) { + return $items[$offset]['object_id']; + } + + + // If nothing was found and this is a voting playlist then get + // from base_playlist + if ($this->base_playlist) { + $base_playlist = new Playlist($this->base_playlist); + $data = $base_playlist->get_random_items(1); + return $data[0]['object_id']; + } else { + $sql = "SELECT `id` FROM `song` WHERE `enabled`='1' ORDER BY RAND() LIMIT 1"; + $db_results = Dba::read($sql); + $results = Dba::fetch_assoc($db_results); + return $results['id']; + } + + } // get_next_object + + /** + * get_uid_from_object_id + * This takes an object_id and an object type and returns the ID for the row + */ + public function get_uid_from_object_id($object_id, $object_type = 'song') + { + $object_id = Dba::escape($object_id); + $object_type = Dba::escape($object_type); + $tmp_id = Dba::escape($this->tmp_playlist); + + $sql = 'SELECT `id` FROM `tmp_playlist_data` ' . + "WHERE `object_type`='$object_type' AND " . + "`tmp_playlist`='$tmp_id' AND `object_id`='$object_id'"; + $db_results = Dba::read($sql); + + $row = Dba::fetch_assoc($db_results); + + return $row['id']; + + } // get_uid_from_object_id + + /** + * get_cool_songs + * This returns all of the song_ids for songs that have happened within + * the last 'cooldown' for this user. + */ + public function get_cool_songs() + { + // Convert cooldown time to a timestamp in the past + $cool_time = time() - ($this->cooldown * 60); + + $song_ids = Stats::get_object_history($GLOBALS['user']->id, $cool_time); + + return $song_ids; + + } // get_cool_songs + + /** + * vote + * This function is called by users to vote on a system wide playlist + * This adds the specified objects to the tmp_playlist and adds a 'vote' + * by this user, naturally it checks to make sure that the user hasn't + * already voted on any of these objects + */ + public function add_vote($items) + { + /* Iterate through the objects if no vote, add to playlist and vote */ + foreach ($items as $element) { + $type = array_shift($element); + $object_id = array_shift($element); + if (!$this->has_vote($object_id, $type)) { + $this->_add_vote($object_id, $type); + } + } // end foreach + + } // vote + + /** + * has_vote + * This checks to see if the current user has already voted on this object + */ + public function has_vote($object_id, $type = 'song') + { + $tmp_id = Dba::escape($this->tmp_playlist); + $object_id = Dba::escape($object_id); + $type = Dba::escape($type); + $user_id = Dba::escape($GLOBALS['user']->id); + + /* Query vote table */ + $sql = 'SELECT `tmp_playlist_data`.`object_id` ' . + 'FROM `user_vote` INNER JOIN `tmp_playlist_data` ' . + 'ON `tmp_playlist_data`.`id`=`user_vote`.`object_id` ' . + "WHERE `user_vote`.`user`='$user_id' " . + "AND `tmp_playlist_data`.`object_type`='$type' " . + "AND `tmp_playlist_data`.`object_id`='$object_id' " . + "AND `tmp_playlist_data`.`tmp_playlist`='$tmp_id'"; + $db_results = Dba::read($sql); + + /* If we find row, they've voted!! */ + if (Dba::num_rows($db_results)) { + return true; + } + + return false; + + } // has_vote + + /** + * _add_vote + * This takes a object id and user and actually inserts the row + */ + private function _add_vote($object_id, $object_type = 'song') + { + $object_id = Dba::escape($object_id); + $tmp_playlist = Dba::escape($this->tmp_playlist); + $object_type = Dba::escape($object_type); + $media = new $object_type($object_id); + $track = isset($media->track) ? "'" . intval($media->track) . "'" : "NULL"; + + /* If it's on the playlist just vote */ + $sql = "SELECT `id` FROM `tmp_playlist_data` " . + "WHERE `tmp_playlist_data`.`object_id`='$object_id' AND `tmp_playlist_data`.`tmp_playlist`='$tmp_playlist'"; + $db_results = Dba::write($sql); + + /* If it's not there, add it and pull ID */ + if (!$results = Dba::fetch_assoc($db_results)) { + $sql = "INSERT INTO `tmp_playlist_data` (`tmp_playlist`,`object_id`,`object_type`,`track`) " . + "VALUES ('$tmp_playlist','$object_id','$object_type',$track)"; + Dba::write($sql); + $results['id'] = Dba::insert_id(); + } + + /* Vote! */ + $time = time(); + $sql = "INSERT INTO user_vote (`user`,`object_id`,`date`) " . + "VALUES ('" . Dba::escape($GLOBALS['user']->id) . "','" . $results['id'] . "','$time')"; + Dba::write($sql); + + return true; + } + + /** + * remove_vote + * This is called to remove a vote by a user for an object, it uses the object_id + * As that's what we'll have most the time, no need to check if they've got an existing + * vote for this, just remove anything that is there + */ + public function remove_vote($row_id) + { + $object_id = Dba::escape($row_id); + $user_id = Dba::escape($GLOBALS['user']->id); + + $sql = "DELETE FROM `user_vote` WHERE `object_id`='$object_id' AND `user`='$user_id'"; + Dba::write($sql); + + /* Clean up anything that has no votes */ + self::prune_tracks(); + + return true; + + } // remove_vote + + /** + * delete_votes + * This removes the votes for the specified object on the current playlist + */ + public function delete_votes($row_id) + { + $row_id = Dba::escape($row_id); + + $sql = "DELETE FROM `user_vote` WHERE `object_id`='$row_id'"; + Dba::write($sql); + + $sql = "DELETE FROM `tmp_playlist_data` WHERE `id`='$row_id'"; + Dba::write($sql); + + return true; + + } // delete_votes + + /** + * delete_from_oid + * This takes an OID and type and removes the object from the democratic playlist + */ + public function delete_from_oid($oid,$object_type) + { + $row_id = $this->get_uid_from_object_id($oid,$object_type); + if ($row_id) { + debug_event('Democratic','Removing Votes for ' . $oid . ' of type ' . $object_type,'5'); + $this->delete_votes($row_id); + } else { debug_event('Democratic','Unable to find Votes for ' . $oid . ' of type ' . $object_type,'3'); } + + return true; + + } // delete_from_oid + + /** + * delete + * This deletes a democratic playlist + */ + public static function delete($democratic_id) + { + $democratic_id = Dba::escape($democratic_id); + + $sql = "DELETE FROM `democratic` WHERE `id`='$democratic_id'"; + Dba::write($sql); + + $sql = "DELETE FROM `tmp_playlist` WHERE `session`='$democratic_id'"; + Dba::write($sql); + + self::prune_tracks(); + + return true; + + } // delete + + /** + * update + * This updates an existing democratic playlist item. It takes a key'd array just like the create + */ + public function update($data) + { + $name = Dba::escape($data['name']); + $base = Dba::escape($data['democratic']); + $cool = Dba::escape($data['cooldown']); + $level = Dba::escape($data['level']); + $default = Dba::escape($data['make_default']); + $id = Dba::escape($this->id); + + $sql = "UPDATE `democratic` SET `name` = ?, `base_playlist` = ?,`cooldown` = ?, `primary` = ?, `level` = ? WHERE `id` = ?"; + Dba::write($sql, array($name, $base, $cool, $default, $level, $id)); + + return true; + + } // update + + /** + * create + * This is the democratic play create function it inserts this into the democratic table + */ + public static function create($data) + { + // Clean up the input + $name = Dba::escape($data['name']); + $base = Dba::escape($data['democratic']); + $cool = Dba::escape($data['cooldown']); + $level = Dba::escape($data['level']); + $default = Dba::escape($data['make_default']); + $user = Dba::escape($GLOBALS['user']->id); + + $sql = "INSERT INTO `democratic` (`name`,`base_playlist`,`cooldown`,`level`,`user`,`primary`) " . + "VALUES ('$name','$base','$cool','$level','$user','$default')"; + $db_results = Dba::write($sql); + + if ($db_results) { + $insert_id = Dba::insert_id(); + parent::create(array( + 'session_id' => $insert_id, + 'type' => 'vote', + 'object_type' => 'song' + )); + } + + return $db_results; + + } // create + + /** + * prune_tracks + * This replaces the normal prune tracks and correctly removes the votes + * as well + */ + public static function prune_tracks() + { + // This deletes data without votes, if it's a voting democratic playlist + $sql = "DELETE FROM `tmp_playlist_data` USING `tmp_playlist_data` " . + "LEFT JOIN `user_vote` ON `tmp_playlist_data`.`id`=`user_vote`.`object_id` " . + "LEFT JOIN `tmp_playlist` ON `tmp_playlist`.`id`=`tmp_playlist_data`.`tmp_playlist` " . + "WHERE `user_vote`.`object_id` IS NULL AND `tmp_playlist`.`type` = 'vote'"; + Dba::write($sql); + + return true; + + } // prune_tracks + + /** + * clear + * This is really just a wrapper function, it clears the entire playlist + * including all votes etc. + */ + public function clear() + { + $tmp_id = Dba::escape($this->tmp_playlist); + + /* Clear all votes then prune */ + $sql = "DELETE FROM `user_vote` USING `user_vote` " . + "LEFT JOIN `tmp_playlist_data` ON `user_vote`.`object_id` = `tmp_playlist_data`.`id` " . + "WHERE `tmp_playlist_data`.`tmp_playlist`='$tmp_id'"; + Dba::write($sql); + + // Prune! + self::prune_tracks(); + + // Clean the votes + self::clear_votes(); + + return true; + + } // clear_playlist + + /** + * clean_votes + * This removes in left over garbage in the votes table + */ + public function clear_votes() + { + $sql = "DELETE FROM `user_vote` USING `user_vote` " . + "LEFT JOIN `tmp_playlist_data` ON `user_vote`.`object_id`=`tmp_playlist_data`.`id` " . + "WHERE `tmp_playlist_data`.`id` IS NULL"; + Dba::write($sql); + + return true; + + } // clear_votes + + /** + * get_vote + * This returns the current count for a specific song + */ + public function get_vote($id) + { + if (parent::is_cached('democratic_vote', $id)) { + return parent::get_from_cache('democratic_vote', $id); + } + + $sql = 'SELECT COUNT(`user`) AS `count` FROM `user_vote` ' . + "WHERE `object_id`='" . Dba::escape($id) . "'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + parent::add_to_cache('democratic_vote', $id, $results['count']); + return $results['count']; + + } // get_vote + + /** + * get_voters + * This returns the users that voted for the specified object + * This is an array of user ids + */ + public function get_voters($object_id) + { + return parent::get_from_cache('democratic_voters',$object_id); + + } // get_voters + + +} // Democratic class diff --git a/sources/lib/class/error.class.php b/sources/lib/class/error.class.php new file mode 100644 index 0000000..d2ed811 --- /dev/null +++ b/sources/lib/class/error.class.php @@ -0,0 +1,138 @@ +$error) { + $_SESSION['errors'][$key] = $error; + } + + } // __destruct + + /** + * add + * This is a public static function it adds a new error message to the array + * It can optionally clobber rather then adding to the error message + */ + public static function add($name,$message,$clobber=0) + { + // Make sure its set first + if (!isset(Error::$errors[$name])) { + Error::$errors[$name] = $message; + Error::$state = 1; + $_SESSION['errors'][$name] = $message; + } + // They want us to clobber it + elseif ($clobber) { + Error::$state = 1; + Error::$errors[$name] = $message; + $_SESSION['errors'][$name] = $message; + } + // They want us to append the error, add a BR\n and then the message + else { + Error::$state = 1; + Error::$errors[$name] .= "
\n" . $message; + $_SESSION['errors'][$name] .= "
\n" . $message; + } + + } // add + + /** + * occurred + * This returns true / false if an error has occured anywhere + */ + public static function occurred() + { + if (self::$state == '1') { return true; } + + return false; + + } // occurred + + /** + * get + * This returns an error by name + */ + public static function get($name) + { + if (!isset(Error::$errors[$name])) { return ''; } + + return Error::$errors[$name]; + + } // get + + /** + * display + * This prints the error out with a standard Error class span + * Ben Goska: Renamed from print to display, print is reserved + */ + public static function display($name) + { + // Be smart about this, if no error don't print + if (!isset(Error::$errors[$name])) { return ''; } + + echo '

' . T_(Error::$errors[$name]) . '

'; + + } // display + + /** + * auto_init + * This loads the errors from the session back into Ampache + */ + public static function auto_init() + { + if (!is_array($_SESSION['errors'])) { return false; } + + // Re-insert them + foreach ($_SESSION['errors'] as $key=>$error) { + self::add($key,$error); + } + + } // auto_init + +} // Error diff --git a/sources/lib/class/localplay.class.php b/sources/lib/class/localplay.class.php new file mode 100644 index 0000000..4a53088 --- /dev/null +++ b/sources/lib/class/localplay.class.php @@ -0,0 +1,664 @@ +type = $type; + + $this->_get_info(); + + } // Localplay + + /** + * _get_info + * This functions takes the type and attempts to get all the + * information needed to load it. Will log errors if there are + * any failures, fatal errors will actually return something to the + * gui + */ + private function _get_info() + { + $this->_load_player(); + + } // _get_info + + /** + * player_loaded + * This returns true / false if the player load + * failed / worked + */ + public function player_loaded() + { + if (is_object($this->_player)) { + return true; + } else { + return false; + } + + } // player_loaded + + /** + * format + * This makes the localplay/plugin information + * human readable + */ + public function format() + { + if (!is_object($this->_player)) { return false; } + + $this->f_name = ucfirst($this->type); + $this->f_description = $this->_player->get_description(); + $this->f_version = $this->_player->get_version(); + + + } // format + + /** + * _load_player + * This function attempts to load the player class that localplay + * Will interface with in order to make all this magical stuf work + * all LocalPlay modules should be located in /modules//.class.php + */ + private function _load_player() + { + if (!$this->type) { return false; } + + $filename = AmpConfig::get('prefix') . '/modules/localplay/' . $this->type . '.controller.php'; + $include = require_once $filename; + + if (!$include) { + /* Throw Error Here */ + debug_event('localplay','Unable to load ' . $this->type . ' controller','2'); + return false; + } // include + else { + $class_name = "Ampache" . $this->type; + $this->_player = new $class_name(); + if (!($this->_player instanceof localplay_controller)) { + debug_event('Localplay',$this->type . ' not an instance of controller abstract, unable to load','1'); + unset($this->_player); + return false; + } + } + + } // _load_player + + /** + * format_name + * This function takes the track name and checks to see if 'skip' + * is supported in the current player, if so it returns a 'skip to' + * link, otherwise it returns just the text + */ + public function format_name($name,$id) + { + $name = scrub_out($name); + $name = Ajax::text('?page=localplay&action=command&command=skip&id=' . $id,$name,'localplay_skip_' . $id); + return $name; + + } // format_name + + /** + * get_controllers + * This returns the controllers that are currently loaded into this instance + */ + public static function get_controllers() + { + /* First open the dir */ + $handle = opendir(AmpConfig::get('prefix') . '/modules/localplay'); + + if (!is_resource($handle)) { + debug_event('Localplay','Error: Unable to read localplay controller directory','1'); + return array(); + } + + $results = array(); + + while ($file = readdir($handle)) { + + if (substr($file,-14,14) != 'controller.php') { continue; } + + /* Make sure it isn't a dir */ + if (!is_dir($file)) { + /* Get the basename and then everything before controller */ + $filename = basename($file,'.controller.php'); + $results[] = $filename; + } + } // end while + + return $results; + + } // get_controllers + + /** + * is_enabled + * This returns true or false depending on if the specified controller + * is currently enabled + */ + public static function is_enabled($controller) + { + // Load the controller and then check for its preferences + $localplay = new Localplay($controller); + // If we can't even load it no sense in going on + if (!isset($localplay->_player)) { return false; } + + return $localplay->_player->is_installed(); + + } // is_enabled + + /** + * install + * This runs the install for the localplay controller we've + * currently got pimped out + */ + public function install() + { + // Run the player's installer + $installed = $this->_player->install(); + + return $installed; + + } // install + + /** + * uninstall + * This runs the uninstall for the localplay controller we've + * currently pimped out + */ + public function uninstall() + { + // Run the players uninstaller + $this->_player->uninstall(); + + // If its our current player, reset player to nothing + if (AmpConfig::get('localplay_controller') == $this->type) { + Preference::update('localplay_controller',$GLOBALS['user']->id,''); + } + + return true; + + } // uninstall + + /** + * connect + * This function attempts to connect to the localplay + * player that we are using + */ + public function connect() + { + if (!$this->_player->connect()) { + debug_event('localplay','Error Unable to connect, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // connect + + /** + * play + * This function passes NULL and calls the play function of the player + * object + */ + public function play() + { + if (!$this->_player->play()) { + debug_event('localplay','Error Unable to start playback, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // play + + /** + * stop + * This functions passes NULl and calls the stop function of the player + * object, it should recieve a true/false boolean value + */ + public function stop() + { + if (!$this->_player->stop()) { + debug_event('localplay','Error Unable to stop playback, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // stop + + /** + * add + */ + public function add($object) + { + debug_event('localplay', 'Deprecated add method called: ' . json_encode($object), 5); + return false; + + } // add + + /** + * add_url + * This directly adds an URL to the localplay module. Is more betterer. + */ + public function add_url(Stream_URL $url) + { + if (!$this->_player->add_url($url)) { + debug_event('localplay', 'Unable to add url ' . $url . ', check ' . $this->type . ' controller', 1); + return false; + } + + return true; + + } // add_url + + /** + * repeat + * This turns the repeat feature of a localplay method on or + * off, takes a 0/1 value + */ + public function repeat($state) + { + $data = $this->_player->repeat($state); + + if (!$data) { + debug_event('localplay',"Error Unable to set Repeat to $state",'1'); + } + + return $data; + + } // repeat + + /** + * random + * This turns on the random feature of a localplay method + * It takes a 0/1 value + */ + public function random($state) + { + $data = $this->_player->random($state); + + if (!$data) { + debug_event('localplay',"Error Unable to set Random to $state",'1'); + } + + return $data; + + } // random + + /** + * status + * This returns current information about the state of the player + * There is an expected array format + */ + public function status() + { + $data = $this->_player->status(); + + if (!count($data)) { + debug_event('localplay','Error Unable to get status, check ' . $this->type . ' controller','1'); + return false; + } + + return $data; + + } // status + + /** + * get + * This calls the get function of the player and then returns + * the array of current songs for display or whatever + * an empty array is passed on failure + */ + public function get() + { + $data = $this->_player->get(); + + if (!count($data) OR !is_array($data)) { + debug_event('localplay','Error Unable to get song info, check ' . $this->type . ' controller','1'); + return array(); + } + + return $data; + + } // get + + /** + * volume_set + * This isn't a required function, it sets the volume to a specified value + * as passed in the variable it is a 0 - 100 scale the controller is + * responsible for adjusting the scale if nessecary + */ + public function volume_set($value) + { + /* Make sure it's int and 0 - 100 */ + $value = int($value); + + /* Make sure that it's between 0 and 100 */ + if ($value > 100 OR $value < 0) { return false; } + + if (!$this->_player->volume($value)) { + debug_event('localplay','Error: Unable to set volume, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // volume_set + + /** + * volume_up + * This function isn't required. It tells the daemon to increase the volume + * by a pre-defined amount controlled by the controller + */ + public function volume_up() + { + if (!$this->_player->volume_up()) { + debug_event('localplay','Error: Unable to increase volume, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // volume_up + + /** + * volume_down + * This function isn't required. It tells the daemon to decrese the volume + * by a pre-defined amount controlled by the controller. + */ + public function volume_down() + { + if (!$this->_player->volume_down()) { + debug_event('localplay','Error: Unable to decrese volume, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // volume_down + + /** + * volume_mute + * This function isn't required, It tells the daemon to mute all output + * It's up to the controller to decide what that actually entails + */ + public function volume_mute() + { + if (!$this->_player->volume(0)) { + debug_event('localplay','Error: Unable to mute volume, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // volume_mute + + /** + * skip + * This isn't a required function, it tells the daemon to skip to the specified song + */ + public function skip($track_id) + { + if (!$this->_player->skip($track_id)) { + debug_event('localplay','Error: Unable to skip to next song, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // skip + + /** + * next + * This isn't a required function, it tells the daemon to go to the next + * song + */ + public function next() + { + if (!$this->_player->next()) { + debug_event('localplay','Error: Unable to skip to next song, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // next + + /** + * prev + * This isn't a required function, it tells the daemon to go the the previous + * song + */ + public function prev() + { + if (!$this->_player->prev()) { + debug_event('localplay','Error: Unable to skip to previous song, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // prev + + /** + * pause + * This isn't a required function, it tells the daemon to pause the + * song + */ + public function pause() + { + if (!$this->_player->pause()) { + debug_event('localplay','Error: Unable to pause song, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // pause + + /** + * get_instances + * This returns the instances of the current type + */ + public function get_instances() + { + $instances = $this->_player->get_instances(); + + return $instances; + + } // get_instances + + /** + * current_instance + * This returns the UID of the current Instance + */ + public function current_instance() + { + $data = $this->_player->get_instance(); + + return $data['id']; + + } // current_instance + + /** + * get_instance + * This returns the specified instance + */ + public function get_instance($uid) + { + $data = $this->_player->get_instance($uid); + + return $data; + + } // get_instance + + /** + * update_instance + * This updates the specified instance with a named array of data (_POST most likely) + */ + public function update_instance($uid,$data) + { + $data = $this->_player->update_instance($uid,$data); + + return $data; + + } // update_instance + + /** + * add_instance + * This adds a new instance for the current controller type + */ + public function add_instance($data) + { + $this->_player->add_instance($data); + + } // add_instance + + /** + * delete_instance + * This removes an instance (it actually calls the players function) + */ + public function delete_instance($instance_uid) + { + $this->_player->delete_instance($instance_uid); + + } // delete_instance + + /** + * set_active_instance + * This sets the active instance of the localplay controller + */ + public function set_active_instance($instance) + { + $this->_player->set_active_instance($instance); + + } // set_active_instance + + /** + * delete_track + * This removes songs from the players playlist it takes a single ID as provided + * by the get command + */ + public function delete_track($object_id) + { + if (!$this->_player->delete_track($object_id)) { + debug_event('localplay','Error: Unable to remove songs, check ' . $this->type . ' controller','1'); + return false; + } + + + return true; + + } // delete + + /** + * delete_all + * This removes every song from the players playlist as defined by the delete_all function + * map + */ + public function delete_all() + { + if (!$this->_player->clear_playlist()) { + debug_event('localplay','Error: Unable to delete entire playlist, check ' . $this->type . ' controller','1'); + return false; + } + + return true; + + } // delete_all + + /** + * get_instance_fields + * This loads the fields from the localplay + * player and returns them + */ + public function get_instance_fields() + { + $fields = $this->_player->instance_fields(); + + return $fields; + + } // get_instance_fields + + /** + * get_user_state + * This function returns a user friendly version + * of the current player state + */ + public function get_user_state($state) + { + switch ($state) { + case 'play': + return T_('Now Playing'); + case 'stop': + return T_('Stopped'); + case 'pause': + return T_('Paused'); + default: + return T_('Unknown'); + } // switch on state + + } // get_user_state + + /** + * get_user_playing + * This attempts to return a nice user friendly + * currently playing string + */ + public function get_user_playing() + { + $status = $this->status(); + + /* Format the track name */ + $track_name = $status['track_artist'] . ' - ' . $status['track_album'] . ' - ' . $status['track_title']; + + /* This is a cheezball fix for when we were unable to find a + * artist/album (or one wasn't provided) + */ + $track_name = ltrim(ltrim($track_name,' - '),' - '); + + $track_name = "[" . $status['track'] . "] - " . $track_name; + + return $track_name; + + } // get_user_playing + + +} // end localplay class diff --git a/sources/lib/class/localplay_controller.abstract.php b/sources/lib/class/localplay_controller.abstract.php new file mode 100644 index 0000000..2dfa9b4 --- /dev/null +++ b/sources/lib/class/localplay_controller.abstract.php @@ -0,0 +1,113 @@ +id); + + return $url; + + } // get_url + + /** + * get_file + * This returns the Filename for the passed object, not + * always possible + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function get_file($object) + { + } // get_file + + /** + * parse_url + * This takes an Ampache URL and then returns the 'primary' part of it + * So that it's easier for localplay modules to return valid song information + */ + public function parse_url($url) + { + // Define possible 'primary' keys + $primary_array = array('oid','demo_id','random'); + $data = array(); + + $variables = parse_url($url,PHP_URL_QUERY); + if ($variables) { + parse_str($variables,$data); + + foreach ($primary_array as $pkey) { + if ($data[$pkey]) { + $data['primary_key'] = $pkey; + return $data; + } + + } // end foreach + } + + return $data; + + } // parse_url + +} // end localplay_controller interface diff --git a/sources/lib/class/mailer.class.php b/sources/lib/class/mailer.class.php new file mode 100644 index 0000000..f88c319 --- /dev/null +++ b/sources/lib/class/mailer.class.php @@ -0,0 +1,213 @@ +sender = $user . '@' . $domain; + $this->sender_name = $fromname; + } // set_default_sender + + /** + * get_users + * This returns an array of userids for people who have e-mail + * addresses based on the passed filter + */ + public static function get_users($filter) + { + switch ($filter) { + case 'users': + $sql = "SELECT * FROM `user` WHERE `access`='25' AND `email` IS NOT NULL"; + break; + case 'admins': + $sql = "SELECT * FROM `user` WHERE `access`='100' AND `email` IS NOT NULL"; + break ; + case 'inactive': + $inactive = time() - (30 * 86400); + $sql = 'SELECT * FROM `user` WHERE `last_seen` <= ? AND `email` IS NOT NULL'; + break; + case 'all': + default: + $sql = "SELECT * FROM `user` WHERE `email` IS NOT NULL"; + break; + } // end filter switch + + $db_results = Dba::read($sql, isset($inactive) ? array($inactive) : null); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array('id'=>$row['id'],'fullname'=>$row['fullname'],'email'=>$row['email']); + } + + return $results; + + } // get_users + + /** + * send + * This actually sends the mail, how amazing + */ + public function send($phpmailer = null) + { + $mailtype = AmpConfig::get('mail_type'); + + if ($phpmailer == null) { + $mail = new PHPMailer(); + + $recipient_name = $this->recipient_name; + if (function_exists('mb_encode_mimeheader')) { + $recipient_name = mb_encode_mimeheader($recipient_name); + } + $mail->AddAddress($this->recipient, $recipient_name); + } else { + $mail = $phpmailer; + } + + $mail->CharSet = AmpConfig::get('site_charset'); + $mail->Encoding = 'base64'; + $mail->From = $this->sender; + $mail->Sender = $this->sender; + $mail->FromName = $this->sender_name; + $mail->Subject = $this->subject; + + if (function_exists('mb_eregi_replace')) { + $this->message = mb_eregi_replace("\r\n", "\n", $this->message); + } + $mail->Body = $this->message; + + $sendmail = AmpConfig::get('sendmail_path'); + $sendmail = $sendmail ? $sendmail : '/usr/sbin/sendmail'; + $mailhost = AmpConfig::get('mail_host'); + $mailhost = $mailhost ? $mailhost : 'localhost'; + $mailport = AmpConfig::get('mail_port'); + $mailport = $mailport ? $mailport : 25; + $mailauth = AmpConfig::get('mail_auth'); + $mailuser = AmpConfig::get('mail_auth_user'); + $mailuser = $mailuser ? $mailuser : ''; + $mailpass = AmpConfig::get('mail_auth_pass'); + $mailpass = $mailpass ? $mailpass : ''; + + switch ($mailtype) { + case 'smtp': + $mail->IsSMTP(); + $mail->Host = $mailhost; + $mail->Port = $mailport; + if ($mailauth == true) { + $mail->SMTPAuth = true; + $mail->Username = $mailuser; + $mail->Password = $mailpass; + } + if ($mailsecure = AmpConfig::get('mail_secure_smtp')) { + $mail->SMTPSecure = ($mailsecure == 'ssl') ? 'ssl' : 'tls'; + } + break; + case 'sendmail': + $mail->IsSendmail(); + $mail->Sendmail = $sendmail; + break; + case 'php': + default: + $mail->IsMail(); + break; + } + + $retval = $mail->send(); + if ($retval == true) { + return true; + } else { + return false; + } + } // send + + public function send_to_group($group_name) + { + $mail = new PHPMailer(); + + foreach (self::get_users($group_name) as $member) { + if (function_exists('mb_encode_mimeheader')) { + $member['fullname'] = mb_encode_mimeheader($member['fullname']); + } + $mail->AddBCC($member['email'], $member['fullname']); + } + + return $this->send($mail); + } + +} // Mailer class diff --git a/sources/lib/class/media.interface.php b/sources/lib/class/media.interface.php new file mode 100644 index 0000000..e9a28c0 --- /dev/null +++ b/sources/lib/class/media.interface.php @@ -0,0 +1,64 @@ + $value) { + if (in_array($key, $this->properties)) { + $this->_data[$key] = $value; + } + } + + } + + public function __set($name, $value) + { + if (!in_array($name, $this->properties)) { + return false; + } + $this->_data[$name] = $value; + } + + public function __get($name) + { + if (!in_array($name, $this->properties)) { + return false; + } + + return isset($this->_data[$name]) ? $this->_data[$name] : null; + } +} diff --git a/sources/lib/class/openid.class.php b/sources/lib/class/openid.class.php new file mode 100644 index 0000000..a31be6b --- /dev/null +++ b/sources/lib/class/openid.class.php @@ -0,0 +1,97 @@ +get_info($id); + + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + } // Playlist + + /** + * gc + * + * Clean dead items out of playlists + */ + public static function gc() + { + Dba::write("DELETE FROM `playlist_data` USING `playlist_data` LEFT JOIN `song` ON `song`.`id` = `playlist_data`.`object_id` WHERE `song`.`file` IS NULL AND `playlist_data`.`object_type`='song'"); + Dba::write("DELETE FROM `playlist` USING `playlist` LEFT JOIN `playlist_data` ON `playlist_data`.`playlist` = `playlist`.`id` WHERE `playlist_data`.`object_id` IS NULL"); + } + + /** + * build_cache + * This is what builds the cache from the objects + */ + public static function build_cache($ids) + { + if (!count($ids)) { return false; } + + $idlist = '(' . implode(',',$ids) . ')'; + + $sql = "SELECT * FROM `playlist` WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('playlist',$row['id'],$row); + } + + } // build_cache + + /** + * get_playlists + * Returns a list of playlists accessible by the current user. + */ + public static function get_playlists() + { + $sql = 'SELECT `id` from `playlist`'; + $sql_order = ' ORDER BY `name`'; + + if (!Access::check('interface','100')) { + $sql .= " WHERE `type`='public' OR " . + "`user`='" . $GLOBALS['user']->id . "'"; + } + + $sql .= $sql_order; + + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } // get_playlists + + /** + * format + * This takes the current playlist object and gussies it up a little + * bit so it is presentable to the users + */ + public function format() + { + parent::format(); + $this->f_link = AmpConfig::get('web_path') . '/playlist.php?action=show_playlist&playlist_id=' . $this->id; + $this->f_name_link = '' . $this->f_name . ''; + + } // format + + /** + * get_track + * Returns the single item on the playlist and all of it's information, restrict + * it to this Playlist + */ + public function get_track($track_id) + { + $sql = "SELECT * FROM `playlist_data` WHERE `id` = ? AND `playlist` = ?"; + $db_results = Dba::read($sql, array($track_id, $this->id)); + + $row = Dba::fetch_assoc($db_results); + + return $row; + + } // get_track + + /** + * get_items + * This returns an array of playlist songs that are in this playlist. + * Because the same song can be on the same playlist twice they are + * keyed by the uid from playlist_data + */ + public function get_items() + { + $results = array(); + + $sql = "SELECT `id`,`object_id`,`object_type`,`track` FROM `playlist_data` WHERE `playlist`= ? ORDER BY `track`"; + $db_results = Dba::read($sql, array($this->id)); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array( + 'object_type' => $row['object_type'], + 'object_id' => $row['object_id'], + 'track' => $row['track'], + 'track_id' => $row['id'] + ); + } // end while + + return $results; + + } // get_items + + /** + * get_random_items + * This is the same as before but we randomize the buggers! + */ + public function get_random_items($limit='') + { + $results = array(); + + $limit_sql = $limit ? 'LIMIT ' . intval($limit) : ''; + + $sql = "SELECT `object_id`,`object_type` FROM `playlist_data` " . + "WHERE `playlist` = ? ORDER BY RAND() $limit_sql"; + $db_results = Dba::read($sql, array($this->id)); + + while ($row = Dba::fetch_assoc($db_results)) { + + $results[] = array( + 'object_type' => $row['object_type'], + 'object_id' => $row['object_id'] + ); + } // end while + + return $results; + + } // get_random_items + + /** + * get_songs + * This is called by the batch script, because we can't pass in Dynamic objects they pulled once and then their + * target song.id is pushed into the array + */ + public function get_songs() + { + $results = array(); + + $sql = "SELECT * FROM `playlist_data` WHERE `playlist` = ? ORDER BY `track`"; + $db_results = Dba::read($sql, array($this->id)); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['object_id']; + } // end while + + return $results; + + } // get_songs + + /** + * get_song_count + * This simply returns a int of how many song elements exist in this playlist + * For now let's consider a dyn_song a single entry + */ + public function get_song_count() + { + $sql = "SELECT COUNT(`id`) FROM `playlist_data` WHERE `playlist` = ?"; + $db_results = Dba::read($sql, array($this->id)); + + $results = Dba::fetch_row($db_results); + + return $results['0']; + + } // get_song_count + + /** + * get_total_duration + * Get the total duration of all songs. + */ + public function get_total_duration() + { + $songs = self::get_songs(); + $idlist = '(' . implode(',', $songs) . ')'; + + $sql = "SELECT SUM(`time`) FROM `song` WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_row($db_results); + + return $results['0']; + + } // get_total_duration + + /** + * get_users + * This returns the specified users playlists as an array of + * playlist ids + */ + public static function get_users($user_id) + { + $results = array(); + + $sql = "SELECT `id` FROM `playlist` WHERE `user` = ? ORDER BY `name`"; + $db_results = Dba::read($sql, array($user_id)); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_users + + /** + * update + * This function takes a key'd array of data and runs updates + */ + public function update($data) + { + if ($data['name'] != $this->name) { + $this->update_name($data['name']); + } + if ($data['pl_type'] != $this->type) { + $this->update_type($data['pl_type']); + } + + } // update + + /** + * update_type + * This updates the playlist type, it calls the generic update_item function + */ + private function update_type($new_type) + { + if ($this->_update_item('type',$new_type,'50')) { + $this->type = $new_type; + } + + } // update_type + + /** + * update_name + * This updates the playlist name, it calls the generic update_item function + */ + private function update_name($new_name) + { + if ($this->_update_item('name',$new_name,'50')) { + $this->name = $new_name; + } + + } // update_name + + /** + * _update_item + * This is the generic update function, it does the escaping and error checking + */ + private function _update_item($field,$value,$level) + { + if ($GLOBALS['user']->id != $this->user AND !Access::check('interface',$level)) { + return false; + } + + $sql = "UPDATE `playlist` SET `$field` = ? WHERE `id` = ?"; + $db_results = Dba::write($sql, array($value, $this->id)); + + return $db_results; + + } // update_item + + /** + * update_track_number + * This takes a playlist_data.id and a track (int) and updates the track value + */ + public function update_track_number($track_id, $index) + { + $sql = "UPDATE `playlist_data` SET `track` = ? WHERE `id` = ?"; + Dba::write($sql, array($index, $track_id)); + + } // update_track_number + + /** + * add_songs + * This takes an array of song_ids and then adds it to the playlist + */ + public function add_songs($song_ids=array(),$ordered=false) + { + /* We need to pull the current 'end' track and then use that to + * append, rather then integrate take end track # and add it to + * $song->track add one to make sure it really is 'next' + */ + $sql = "SELECT `track` FROM `playlist_data` WHERE `playlist` = ? ORDER BY `track` DESC LIMIT 1"; + $db_results = Dba::read($sql, array($this->id)); + $data = Dba::fetch_assoc($db_results); + $base_track = $data['track']; + debug_event('add_songs', 'Track number: '.$base_track, '5'); + + $i = 0; + foreach ($song_ids as $song_id) { + /* We need the songs track */ + $song = new Song($song_id); + + // Based on the ordered prop we use track + base or just $i++ + if (!$ordered) { + $track = $song->track + $base_track; + } else { + $i++; + $track = $base_track + $i; + } + + /* Don't insert dead songs */ + if ($song->id) { + $sql = "INSERT INTO `playlist_data` (`playlist`,`object_id`,`object_type`,`track`) " . + " VALUES (?, ?, 'song', ?)"; + Dba::write($sql, array($this->id, $song->id, $track)); + } // if valid id + + } // end foreach songs + + } // add_songs + + /** + * create + * This function creates an empty playlist, gives it a name and type + * Assumes $GLOBALS['user']->id as the user + */ + public static function create($name,$type) + { + $sql = "INSERT INTO `playlist` (`name`,`user`,`type`,`date`) VALUES (?, ?, ?, ?)"; + Dba::write($sql, array($name, $GLOBALS['user']->id, $type, time())); + + $insert_id = Dba::insert_id(); + return $insert_id; + + } // create + + /** + * set_items + * This calls the get_items function and sets it to $this->items which is an array in this object + */ + public function set_items() + { + $this->items = $this->get_items(); + + } // set_items + + /** + * delete_track + * this deletes a single track, you specify the playlist_data.id here + */ + public function delete_track($id) + { + $sql = "DELETE FROM `playlist_data` WHERE `playlist_data`.`playlist` = ? AND `playlist_data`.`id` = ? LIMIT 1"; + Dba::write($sql, array($this->id, $id)); + + return true; + + } // delete_track + + /** + * delete_track_number + * this deletes a single track by it's track #, you specify the playlist_data.track here + */ + public function delete_track_number($track) + { + $sql = "DELETE FROM `playlist_data` WHERE `playlist_data`.`playlist` = ? AND `playlist_data`.`track` = ? LIMIT 1"; + Dba::write($sql, array($this->id, $track)); + + return true; + + } // delete_track_number + + /** + * delete + * This deletes the current playlist and all associated data + */ + public function delete() + { + $sql = "DELETE FROM `playlist_data` WHERE `playlist` = ?"; + Dba::write($sql, array($this->id)); + + $sql = "DELETE FROM `playlist` WHERE `id` = ?"; + Dba::write($sql, array($this->id)); + + $sql = "DELETE FROM `object_count` WHERE `object_type`='playlist' AND `object_id` = ?"; + Dba::write($sql, array($this->id)); + + return true; + + } // delete + +} // class Playlist diff --git a/sources/lib/class/playlist_object.abstract.php b/sources/lib/class/playlist_object.abstract.php new file mode 100644 index 0000000..5da43cd --- /dev/null +++ b/sources/lib/class/playlist_object.abstract.php @@ -0,0 +1,74 @@ +f_name = $this->name; + $this->f_type = ($this->type == 'private') ? UI::get_icon('lock', T_('Private')) : ''; + + $client = new User($this->user); + + $this->f_user = $client->fullname; + + } // format + + /** + * has_access + * This function returns true or false if the current user + * has access to this playlist + */ + public function has_access() + { + if (!Access::check('interface','25')) { + return false; + } + if ($this->user == $GLOBALS['user']->id) { + return true; + } else { + return Access::check('interface','100'); + } + + } // has_access + + +} // end playlist_object diff --git a/sources/lib/class/plex_api.class.php b/sources/lib/class/plex_api.class.php new file mode 100644 index 0000000..b41fb31 --- /dev/null +++ b/sources/lib/class/plex_api.class.php @@ -0,0 +1,974 @@ +access_token as $tk) { + if ((string) $tk['token'] == $myplex_token) { + $username = (string) $tk['username']; + // We should apply filter and access restriction to shared sections only, but that's not easily possible with current Ampache architecture + $validToken = true; + break; + } + } + + if (!$validToken) { + debug_event('Access Control', 'Auth-Token ' . $myplex_token . ' invalid for this server.', '3'); + self::createError(401); + } + } + + // Need to get a match between Plex and Ampache users + if ($match_users) { + if (!AmpConfig::get('access_control')) { + debug_event('Access Control', 'Error Attempted to use Plex with Access Control turned off and plex/ampache link enabled.','3'); + self::createError(401); + } + + if (empty($email)) { + $xml = self::get_users_account(); + if ((string) $xml->username == $username) { + $email = (string) $xml->email; + } else { + $xml = self::get_server_friends(); + foreach ($xml->user as $xuser) { + if ((string) $xml['username'] == $username) { + $email = (string) $xml['email']; + } + } + } + } + + if (!empty($email)) { + $user = User::get_from_email($email); + } + if (!isset($user) || !$user->id) { + debug_event('Access Denied', 'Unable to get an Ampache user match for email ' . $email, '3'); + self::createError(401); + } else { + $username = $user->username; + if (!Access::check_network('init-api', $username, 5)) { + debug_event('Access Denied', 'Unauthorized access attempt to Plex [' . $_SERVER['REMOTE_ADDR'] . ']', '3'); + self::createError(401); + } else { + $GLOBALS['user'] = $user; + } + } + } else { + $email = $username; + $username = null; + } + + if ($createSession) { + // Create an Ampache session from Plex authtoken + Session::create(array( + 'type' => 'api', + 'sid' => $myplex_token, + 'username' => $username, + 'value' => $email + )); + } + } + } + + protected static function check_access($level) + { + if (!self::is_local() && $GLOBALS['user']->access < $level) { + debug_event('plex', 'User ' . $GLOBALS['user']->username . ' is unauthorized to complete the action.', '3'); + self::createError(401); + } + } + + public static function setHeader($f) + { + header("HTTP/1.1 200 OK", true, 200); + header("Connection: close", true); + + header_remove("x-powered-by"); + + if (strtolower($f) == "xml") { + header("Cache-Control: no-cache", true); + header("Content-type: text/xml; charset=" . AmpConfig::get('site_charset'), true); + } elseif (substr(strtolower($f), 0, 6) == "image/") { + header("Cache-Control: public, max-age=604800", true); + header("Content-type: " . $f, true); + } else { + header("Content-type: " . $f, true); + } + } + + public static function setPlexHeader($reqheaders) + { + header("X-Plex-Protocol: 1.0"); + + header('Access-Control-Allow-Origin: *'); + $acm = $reqheaders['Access-Control-Request-Method']; + if ($acm) { + header('Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE, PUT, HEAD'); + } + $ach = $reqheaders['Access-Control-Request-Headers']; + if ($ach) { + $headers = self::getPlexHeaders(true, $ach); + $headerkeys = array(); + foreach ($headers as $key => $value) { + $headerkeys[] = strtolower($key); + } + header('Access-Control-Allow-Headers: ' . implode(',', $headerkeys)); + } + + if ($acm || $ach) { + header('Access-Control-Max-Age: 1209600'); + } else { + header('Access-Control-Expose-Headers: Location'); + } + } + public static function apiOutput($string) + { + if ($_SERVER['REQUEST_METHOD'] != 'OPTIONS') { + ob_start('ob_gzhandler'); + echo $string; + ob_end_flush(); + $reqheaders = getallheaders(); + if ($reqheaders['Accept-Encoding']) { + header("X-Plex-Content-Compressed-Length: " . ob_get_length(), true); + header("X-Plex-Content-Original-Length: " . strlen($string), true); + } + } else { + header("Content-type: text/plain", true); + header("Content-length: 0", true); + } + } + + public static function createError($code) + { + $error = ""; + switch ($code) { + case 404: + $error = "Not Found"; + break; + + case 401: + $error = "Unauthorized"; + break; + } + header("Content-type: text/html", true); + header("HTTP/1.0 ". $code . " " . $error, true, $code); + + $html = "" . $error . "

" . $code . " " . $error . "

"; + self::apiOutput($html); + exit(); + } + + public static function validateMyPlex($myplex_username, $myplex_password) + { + $options = array( + CURLOPT_USERPWD => $myplex_username . ':' . $myplex_password, + //CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_POST => true, + ); + $headers = array( + 'Content-Length: 0' + ); + $action = 'users/sign_in.xml'; + + $res = self::myPlexRequest($action, $options, $headers);; + return $res['xml']['authenticationToken']; + } + + public static function getPublicIp() + { + $action = 'pms/:/ip'; + + $res = self::myPlexRequest($action); + return trim($res['raw']); + } + + public static function registerMyPlex($authtoken) + { + $headers = array ( + 'Content-Type: text/xml' + ); + $action = 'servers.xml?auth_token=' . $authtoken; + + $r = Plex_XML_Data::createContainer(); + Plex_XML_Data::setServerInfo($r, Catalog::get_catalogs()); + Plex_XML_Data::setContainerSize($r); + + $curlopts = array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $r->asXML() + ); + + return self::myPlexRequest($action, $curlopts, $headers, true); + } + + public static function publishDeviceConnection($authtoken) + { + $headers = array (); + $action = 'devices/' . Plex_XML_Data::getMachineIdentifier() . '?Connection[][uri]=' . Plex_XML_Data::getServerUri() . '&X-Plex-Token=' . $authtoken; + $curlopts = array( + CURLOPT_CUSTOMREQUEST => "PUT" + ); + + return self::myPlexRequest($action, $curlopts, $headers); + } + + public static function unregisterMyPlex($authtoken) + { + $headers = array ( + 'Content-Type: text/xml' + ); + $action = 'servers/' . Plex_XML_Data::getMachineIdentifier() . '.xml?auth_token=' . $authtoken; + $curlopts = array( + CURLOPT_CUSTOMREQUEST => "DELETE" + ); + + return self::myPlexRequest($action, $curlopts, $headers); + } + + protected static function get_server_authtokens() + { + $action = 'servers/' . Plex_XML_Data::getMachineIdentifier() . '/access_tokens.xml?auth_token=' . Plex_XML_Data::getMyPlexAuthToken(); + + $res = self::myPlexRequest($action); + return $res['xml']; + } + + protected static function get_server_friends() + { + $action = 'pms/friends/all?auth_token=' . Plex_XML_Data::getMyPlexAuthToken(); + + $res = self::myPlexRequest($action); + return $res['xml']; + } + + protected static function getPlexHeaders($private = false, $filters = null) + { + $headers = array( + 'X-Plex-Client-Identifier' => Plex_XML_Data::getClientIdentifier(), + 'X-Plex-Product' => 'Plex Media Server', + 'X-Plex-Version' => Plex_XML_Data::getPlexVersion(), + 'X-Plex-Platform' => Plex_XML_Data::getPlexPlatform(), + 'X-Plex-Platform-Version' => Plex_XML_Data::getPlexPlatformVersion(), + 'X-Plex-Client-Platform' => Plex_XML_Data::getPlexPlatform(), + 'X-Plex-Protocol' => 1.0, + 'X-Plex-Device' => 'Ampache', + 'X-Plex-Device-Name' => 'Ampache', + 'X-Plex-Provides' => 'server' + ); + + if ($private) { + if (Plex_XML_Data::getMyPlexUsername()) { + $headers['X-Plex-Username'] = Plex_XML_Data::getMyPlexUsername(); + } + if (Plex_XML_Data::getMyPlexUsername()) { + $headers['X-Plex-Token'] = Plex_XML_Data::getMyPlexAuthToken(); + } + } + + if ($filters) { + $fheaders = array(); + foreach ($headers as $key => $value) { + if (array_search(strtolower($key), $filters)) { + $fheaders[$key] = $value; + } + } + $headers = $fheaders; + } + + return $headers; + } + + static $request_headers = array(); + public static function request_output_header($ch, $header) + { + self::$request_headers[] = $header; + return strlen($header); + } + + public static function replay_header($ch, $header) + { + $rheader = trim($header); + $rhpart = explode(':', $rheader); + if (!empty($rheader) && count($rhpart) > 1) { + if ($rhpart[0] != "Transfer-Encoding") { + header($rheader); + } + } + return strlen($header); + } + + public static function replay_body($ch, $data) + { + echo $data; + ob_flush(); + + return strlen($data); + } + + protected static function myPlexRequest($action, $curlopts = array(), $headers = array(), $proxy = false) + { + $server = Plex_XML_Data::getServerUri(); + $allheaders = array(); + if (!$proxy) { + $allheadersarr = self::getPlexHeaders(); + foreach ($allheadersarr as $key => $value) { + $allheaders[] = $key . ': ' . $value; + } + $allheaders += array( + 'Origin: ' . $server, + 'Referer: ' . $server . '/web/index.html', + ); + + if (!$curlopts[CURLOPT_POST]) { + $allheaders[] = 'Content-length: 0'; + } + } + $allheaders = array_merge($allheaders, $headers); + + $url = 'https://my.plexapp.com/' . $action; + debug_event('plex', 'Calling ' . $url, '5'); + + $options = array( + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADERFUNCTION => array('Plex_Api', 'request_output_header'), + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_HTTPHEADER => $allheaders, + ); + $options += $curlopts; + + $ch = curl_init($url); + curl_setopt_array($ch, $options); + $r = curl_exec($ch); + $res = array(); + $res['status'] = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + $res['headers'] = self::$request_headers; + $res['raw'] = $r; + try { + $res['xml'] = simplexml_load_string($r); + } catch (Exception $e) { + // If exception, wrong data returned (Plex API changes?) + } + return $res; + } + + public static function root() + { + $r = Plex_XML_Data::createContainer(); + Plex_XML_Data::setRootContent($r, Catalog::get_catalogs()); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function library($params) + { + $r = Plex_XML_Data::createLibContainer(); + Plex_XML_Data::setLibraryContent($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function system($params) + { + $r = Plex_XML_Data::createSysContainer(); + Plex_XML_Data::setSystemContent($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function clients($params) + { + $r = Plex_XML_Data::createContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function channels($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function photos($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function photo($params) + { + if (count($params) == 2) { + if ($params[0] == ':' && $params[1] == 'transcode') { + + $width = $_REQUEST['width']; + $height = $_REQUEST['height']; + $url = $_REQUEST['url']; + + // Replace 32400 request port with the real listening port + // *** To `Plex Inc`: *** + // Please allow listening port server configuration for your Plex server + // and fix your clients to not request resources so hard-coded on 127.0.0.1:32400. + // May be ok on Apple & UPnP world but that's really ugly for a server... + // Yes, it's a little hack but it works. + $localrs = "http://127.0.0.1:32400/"; + if (strpos($url, $localrs) !== false) { + $url = "http://127.0.0.1:" . Plex_XML_Data::getServerPort() . "/" . substr($url, strlen($localrs)); + } + + if ($width && $height && $url) { + $request = Requests::get($url); + if ($request->status_code == 200) { + $mime = $request->headers['content-type']; + self::setHeader($mime); + $art = new Art(0); + $art->raw = $request->body; + $thumb = $art->generate_thumb($art->raw, array('width' => $width, 'height' => $height), $mime); + echo $thumb['thumb']; + exit(); + } + } + } + } + } + + public static function music($params) + { + if (count($params) > 2) { + if ($params[0] == ':' && $params[1] == 'transcode') { + if (count($params) == 3) { + $format = $_REQUEST['format'] ?: pathinfo($params[2], PATHINFO_EXTENSION); + $url = $_REQUEST['url']; + $br = $_REQUEST['audioBitrate']; + if (preg_match("/\/parts\/([0-9]+)\//", $url, $matches)) { + $song_id = Plex_XML_Data::getAmpacheId($matches[1]); + } + } elseif (count($params) == 4 && $params[2] == 'universal') { + $format = pathinfo($params[3], PATHINFO_EXTENSION); + $path = $_REQUEST['path']; + // Should be the maximal allowed bitrate, not necessary the bitrate used but Ampache doesn't support this kind of option yet + $br = $_REQUEST['maxAudioBitrate']; + if (preg_match("/\/metadata\/([0-9]+)/", $path, $matches)) { + $song_id = Plex_XML_Data::getAmpacheId($matches[1]); + } + } + + if (!empty($format) && !empty($song_id)) { + $urlparams = '&transcode_to=' . $format; + if (!empty($br)) { + $urlparams .= '&bitrate=' . $br; + } + + $url = Song::play_url($song_id, $urlparams); + self::stream_url($url); + } + } + } + } + + public static function video($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function applications($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function library_sections($params) + { + $r = Plex_XML_Data::createLibContainer(); + $n = count($params); + if ($n == 0) { + Plex_XML_Data::setSections($r, Catalog::get_catalogs()); + } else { + $key = $params[0]; + $catalog = Catalog::create_from_id($key); + if (!$catalog) { + self::createError(404); + } + if ($n == 1) { + Plex_XML_Data::setSectionContent($r, $catalog); + } elseif ($n == 2) { + $view = $params[1]; + if ($view == "all") { + Plex_XML_Data::setSectionAll($r, $catalog); + } elseif ($view == "albums") { + Plex_XML_Data::setSectionAlbums($r, $catalog); + } elseif ($view == "recentlyadded") { + Plex_XML_Data::setCustomSectionView($r, $catalog, Stats::get_recent('album', 25, $key)); + } + } + } + + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function library_metadata($params) + { + $r = Plex_XML_Data::createLibContainer(); + $n = count($params); + if ($n > 0) { + $key = $params[0]; + + $id = Plex_XML_Data::getAmpacheId($key); + + if ($n == 1) { + // Should we check that files still exists here? + $checkFiles = $_REQUEST['checkFiles']; + + if (Plex_XML_Data::isArtist($key)) { + $artist = new Artist($id); + $artist->format(); + Plex_XML_Data::addArtist($r, $artist); + } elseif (Plex_XML_Data::isAlbum($key)) { + $album = new Album($id); + $album->format(); + Plex_XML_Data::addAlbum($r, $album); + } elseif (Plex_XML_Data::isTrack($key)) { + $song = new Song($id); + $song->format(); + Plex_XML_Data::addSong($r, $song); + } + } else { + $subact = $params[1]; + if ($subact == "children") { + if (Plex_XML_Data::isArtist($key)) { + $artist = new Artist($id); + $artist->format(); + Plex_XML_Data::setArtistRoot($r, $artist); + } else if (Plex_XML_Data::isAlbum($key)) { + $album = new Album($id); + $album->format(); + Plex_XML_Data::setAlbumRoot($r, $album); + } + } elseif ($subact == "thumb") { + if ($n == 3) { + // Ignore thumb id as we can only have 1 thumb + $art = null; + if (Plex_XML_Data::isArtist($key)) { + $art = new Art($id, "artist"); + } else if (Plex_XML_Data::isAlbum($key)) { + $art = new Art($id, "album"); + } else if (Plex_XML_Data::isTrack($key)) { + $art = new Art($id, "song"); + } + + if ($art != null) { + $art->get_db(); + + if (!isset($size)) { + self::setHeader($art->raw_mime); + echo $art->raw; + } else { + $dim = array(); + $dim['width'] = $size; + $dim['height'] = $size; + $thumb = $art->get_thumb($dim); + self::setHeader($art->thumb_mime); + echo $thumb['thumb']; + } + exit(); + } + } + } + } + } + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + protected static function stream_url($url) + { + // header("Location: " . $url); + set_time_limit(0); + + $ch = curl_init($url); + curl_setopt_array($ch, array( + CURLOPT_HTTPHEADER => array("User-Agent: Plex"), + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_WRITEFUNCTION => array('Plex_Api', 'replay_body'), + CURLOPT_HEADERFUNCTION => array('Plex_Api', 'replay_header'), + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_TIMEOUT => 0 + )); + curl_exec($ch); + curl_close($ch); + } + + public static function library_parts($params) + { + $n = count($params); + + if ($n == 2) { + $key = $params[0]; + $file = $params[1]; + + $id = Plex_XML_Data::getAmpacheId($key); + $song = new Song($id); + if ($song->id) { + $url = Song::play_url($id); + self::stream_url($url); + } else { + self::createError(404); + } + } + } + + public static function library_recentlyadded($params) + { + $data = array(); + $data['album'] = Stats::get_newest('album', 25); + $r = Plex_XML_Data::createLibContainer(); + Plex_XML_Data::setCustomView($r, $data); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function library_ondeck($params) + { + $data = array(); + $data['album'] = Stats::get_recent('album', 25); + $r = Plex_XML_Data::createLibContainer(); + Plex_XML_Data::setCustomView($r, $data); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function system_library_sections($params) + { + $r = Plex_XML_Data::createSysContainer(); + Plex_XML_Data::setSysSections($r, Catalog::get_catalogs()); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function manage_frameworks_ekspinner_resources($params) + { + // Image file used to 'ping' the server + if ($params[0] == "small_black_7.png") { + header("Content-type: image/png", true); + echo file_get_contents(AmpConfig::get('prefix') . '/plex/resources/small_black_7.png'); + exit; + } + } + + public static function myplex_account($params) + { + $r = Plex_XML_Data::createMyPlexAccount(); + self::apiOutput($r->asXML()); + } + + public static function system_agents($params) + { + $r = Plex_XML_Data::createSysContainer(); + $addcontributors = false; + $mediaType = $_REQUEST['mediaType']; + if (count($params) >= 3 && $params[1] == 'config') { + $mediaType = $params[2]; + $addcontributors = true; + } + + if ($mediaType) { + switch ($mediaType) { + case '1': + Plex_XML_Data::setSysMovieAgents($r); + break; + case '2': + Plex_XML_Data::setSysTVShowAgents($r); + break; + case '13': + Plex_XML_Data::setSysPhotoAgents($r); + break; + case '8': + Plex_XML_Data::setSysMusicAgents($r); + break; + case '9': + Plex_XML_Data::setSysMusicAgents($r, 'Albums'); + break; + default: + self::createError(404); + break; + } + } else { + Plex_XML_Data::setSysAgents($r); + } + if ($addcontributors) { + Plex_XML_Data::setAgentsContributors($r, $mediaType, 'com.plexapp.agents.none'); + } + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function system_agents_contributors($params) + { + $mediaType = $_REQUEST['mediaType']; + $primaryAgent = $_REQUEST['primaryAgent']; + + $r = Plex_XML_Data::createSysContainer(); + Plex_XML_Data::setAgentsContributors($r, $mediaType, $primaryAgent); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function system_agents_attribution($params) + { + $identifier = $_REQUEST['identifier']; + + self::createError(404); + } + + public static function system_scanners($params) + { + if (count($params) > 0) { + if ($params[0] == '8' || $params[0] == '9') { + $r = Plex_XML_Data::createSysContainer(); + Plex_XML_Data::setMusicScanners($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + } else { + self::createError(404); + } + } + + public static function system_appstore($params) + { + $r = Plex_XML_Data::createAppStore(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function accounts($params) + { + $userid = ''; + if (isset($params[0])) { + $userid = $params[0]; + } + // Not supported yet + if ($userid > 1) { self::createError(404); } + + $r = Plex_XML_Data::createAccountContainer(); + Plex_XML_Data::setAccounts($r, $userid); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function status($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setStatus($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function status_sessions($params) + { + self::createError(403); + } + + public static function prefs($params) + { + $r = Plex_XML_Data::createContainer(); + Plex_XML_Data::setPrefs($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function help($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function plexonline($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function plugins($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function services($params) + { + $r = Plex_XML_Data::createPluginContainer(); + Plex_XML_Data::setServices($r); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function services_browse($params) + { + self::check_access(75); + + $r = Plex_XML_Data::createContainer(); + Plex_XML_Data::setBrowseService($r, $params[0]); + Plex_XML_Data::setContainerSize($r); + self::apiOutput($r->asXML()); + } + + public static function timeline($params) + { + $ratingKey = $_REQUEST['ratingKey']; + $key = $_REQUEST['key']; + $state = $_REQUEST['state']; + $time = $_REQUEST['time']; + $duration = $_REQUEST['duration']; + + // Not supported right now (maybe in a future for broadcast?) + if ($_SERVER['REQUEST_METHOD'] != 'OPTIONS') { + self::apiOutput(''); + } else { + self::createError(400); + } + } + + public static function rate($params) + { + $id = $_REQUEST['key']; + $identifier = $_REQUEST['identifier']; + $rating = $_REQUEST['rating']; + + if ($identifier == 'com.plexapp.plugins.library') { + $robj = null; + if (Plex_XML_Data::isArtist($id)) { + $robj = new Rating(Plex_XML_Data::getAmpacheId($id), "artist"); + } else if (Plex_XML_Data::isAlbum($id)) { + $robj = new Rating(Plex_XML_Data::getAmpacheId($id), "album"); + } else if (Plex_XML_Data::isTrack($id)) { + $robj = new Rating(Plex_XML_Data::getAmpacheId($id), "song"); + } + + if ($robj != null) { + $robj->set_rating($rating / 2); + } + } + } + + protected static function get_users_account($authtoken='') + { + if (empty($authtoken)) { + $authtoken = Plex_XML_Data::getMyPlexAuthToken(); + } + + $action = 'users/account?auth_token=' . $authtoken; + $res = self::myPlexRequest($action); + return $res['xml']; + } +} diff --git a/sources/lib/class/plex_xml_data.class.php b/sources/lib/class/plex_xml_data.class.php new file mode 100644 index 0000000..da1dce1 --- /dev/null +++ b/sources/lib/class/plex_xml_data.class.php @@ -0,0 +1,1131 @@ += Plex_XML_Data::AMPACHEID_ARTIST && $id < Plex_XML_Data::AMPACHEID_ALBUM); + } + + public static function isAlbum($id) + { + return ($id >= Plex_XML_Data::AMPACHEID_ALBUM && $id < Plex_XML_Data::AMPACHEID_TRACK); + } + + public static function isTrack($id) + { + return ($id >= Plex_XML_Data::AMPACHEID_TRACK && $id < Plex_XML_Data::AMPACHEID_MEDIA); + } + + public static function isMedia($id) + { + return ($id >= Plex_XML_Data::AMPACHEID_MEDIA && $id < Plex_XML_Data::AMPACHEID_PART); + } + + public static function isPart($id) + { + return ($id >= Plex_XML_Data::AMPACHEID_PART); + } + + public static function getPlexVersion() + { + return "0.9.8.18.290-11b7fdd"; + } + + public static function getServerAddress() + { + return $_SERVER['SERVER_ADDR']; + } + + public static function getServerPort() + { + $port = $_SERVER['SERVER_PORT']; + return $port?:'32400'; + } + + public static function getServerPublicAddress() + { + $address = AmpConfig::get('plex_public_address'); + if (!$address) { + $address = self::getServerAddress(); + } + return $address; + } + + public static function getServerPublicPort() + { + $port = AmpConfig::get('plex_public_port'); + if (!$port) { + $port = self::getServerPort(); + } + return $port; + } + + public static function getServerUri() + { + return 'http://' . self::getServerPublicAddress() . ':' . self::getServerPublicPort(); + } + + public static function getServerName() + { + return AmpConfig::get('plex_servername') ?: 'Ampache'; + } + + public static function getMyPlexUsername() + { + return AmpConfig::get('myplex_username'); + } + + public static function getMyPlexAuthToken() + { + return AmpConfig::get('myplex_authtoken'); + } + + public static function getMyPlexPublished() + { + return AmpConfig::get('myplex_published'); + } + + public static function createContainer() + { + $response = new SimpleXMLElement(''); + return $response; + } + + public static function createLibContainer() + { + $response = self::createContainer(); + $response->addAttribute('identifier', 'com.plexapp.plugins.library'); + $response->addAttribute('mediaTagPrefix', '/system/bundle/media/flags/'); + $response->addAttribute('mediaTagVersion', '1365384731'); + return $response; + } + + public static function createPluginContainer() + { + $response = self::createContainer(); + $response->addAttribute('content', 'plugins'); + return $response; + } + + public static function createSysContainer() + { + $response = self::createContainer(); + $response->addAttribute('noHistory', '0'); + $response->addAttribute('replaceParent', '0'); + $response->addAttribute('identifier', 'com.plexapp.system'); + return $response; + } + + public static function createAccountContainer() + { + $response = self::createContainer(); + $response->addAttribute('identifier', 'com.plexapp.system.accounts'); + return $response; + } + + public static function setContainerSize($container) + { + $container->addAttribute('size', $container->count()); + } + + public static function setContainerTitle($container, $title) + { + $container->addAttribute('title1', $title); + } + + public static function getResourceUri($resource) + { + return '/resources/' . $resource; + } + + public static function getMetadataUri($key) + { + return '/library/metadata/' . $key; + } + + public static function getSectionUri($key) + { + return '/library/sections/' . $key; + } + + public static function getPartUri($key, $type) + { + return '/library/parts/' . $key . '/file.' . $type; + } + + public static function uuidFromKey($key) + { + return hash('sha1', $key); + } + + public static function uuidFromSubKey($key) + { + return self::uuidFromKey(self::getMachineIdentifier() . '-' . $key); + } + + public static function getMachineIdentifier() + { + $uniqid = AmpConfig::get('plex_uniqid'); + if (!$uniqid) { + $uniqid = self::getServerAddress(); + } + return self::uuidFromKey($uniqid); + } + + public static function getClientIdentifier() + { + return self::getMachineIdentifier(); + } + + public static function getPlexPlatform() + { + if (PHP_OS == 'WINNT') { + return 'Windows'; + } else { + return "Linux"; + } + } + + public static function getPlexPlatformVersion() + { + if (PHP_OS == 'WINNT') { + return '6.2 (Build 9200)'; + } else { + return '(#1 SMP Debian 3.2.54-2)'; + } + } + + public static function setRootContent($xml, $catalogs) + { + $xml->addAttribute('friendlyName', self::getServerName()); + $xml->addAttribute('machineIdentifier', self::getMachineIdentifier()); + + $myplex_username = self::getMyPlexUsername(); + $myplex_authtoken = self::getMyPlexAuthToken(); + $myplex_published = self::getMyPlexPublished(); + if ($myplex_username) { + $xml->addAttribute('myPlex', '1'); + $xml->addAttribute('myPlexUsername', $myplex_username); + if ($myplex_authtoken) { + $xml->addAttribute('myPlexSigninState', 'ok'); + if ($myplex_published) { + $xml->addAttribute('myPlexMappingState', 'mapped'); + } else { + $xml->addAttribute('myPlexMappingState', 'unknown'); + } + } else { + $xml->addAttribute('myPlexSigninState', 'none'); + } + } else { + $xml->addAttribute('myPlex', '0'); + } + + $xml->addAttribute('platform', self::getPlexPlatform()); + $xml->addAttribute('platformVersion', self::getPlexPlatformVersion()); + $xml->addAttribute('requestParametersInCookie', '1'); + $xml->addAttribute('sync', '1'); + $xml->addAttribute('transcoderActiveVideoSessions', '0'); + $xml->addAttribute('transcoderAudio', '1'); + $xml->addAttribute('transcoderVideo', '0'); + + $xml->addAttribute('updatedAt', self::getLastUpdate($catalogs)); + $xml->addAttribute('version', self::getPlexVersion()); + + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'channels'); + $dir->addAttribute('title', 'channels'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'clients'); + $dir->addAttribute('title', 'clients'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'library'); + $dir->addAttribute('title', 'library'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'music'); + $dir->addAttribute('title', 'music'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'playQueues'); + $dir->addAttribute('title', 'playQueues'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'player'); + $dir->addAttribute('title', 'player'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'playlists'); + $dir->addAttribute('title', 'playlists'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'search'); + $dir->addAttribute('title', 'search'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'servers'); + $dir->addAttribute('title', 'servers'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'system'); + $dir->addAttribute('title', 'system'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'transcode'); + $dir->addAttribute('title', 'transcode'); + /*$dir = $xml->addChild('Directory'); + $dir->addAttribute('count', '1'); + $dir->addAttribute('key', 'video'); + $dir->addAttribute('title', 'video');*/ + } + + public static function getLastUpdate($catalogs) + { + $last_update = 0; + foreach ($catalogs as $id) { + $catalog = Catalog::create_from_id($id); + if ($catalog->last_add > $last_update) { + $last_update = $catalog->last_add; + } + if ($catalog->last_update > $last_update) { + $last_update = $catalog->last_update; + } + if ($catalog->last_clean > $last_update) { + $last_update = $catalog->last_clean; + } + } + + return $last_update; + } + + public static function setSysSections($xml, $catalogs) + { + foreach ($catalogs as $id) { + $catalog = Catalog::create_from_id($id); + $catalog->format(); + + $dir = $xml->addChild('Directory'); + $key = base64_encode(self::getMachineIdentifier() . '-' . $id); + $dir->addAttribute('type', 'music'); + $dir->addAttribute('key', $key); + $dir->addAttribute('uuid', self::uuidFromSubKey($id)); + $dir->addAttribute('name', $catalog->name); + $dir->addAttribute('unique', '1'); + $dir->addAttribute('serverVersion', self::getPlexVersion()); + $dir->addAttribute('machineIdentifier', self::getMachineIdentifier()); + $dir->addAttribute('serverName', self::getServerName()); + $dir->addAttribute('path', self::getSectionUri($id)); + $ip = self::getServerAddress(); + $port = self::getServerPort(); + $dir->addAttribute('host', $ip); + $dir->addAttribute('local', ($ip == "127.0.0.1") ? '1' : '0'); + $dir->addAttribute('port', $port); + self::setSectionXContent($dir, $catalog, 'title'); + } + } + + public static function setSections($xml, $catalogs) + { + foreach ($catalogs as $id) { + $catalog = Catalog::create_from_id($id); + $catalog->format(); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('filters', '1'); + $dir->addAttribute('refreshing', '0'); + $dir->addAttribute('key', $id); + $dir->addAttribute('type', 'artist'); + $dir->addAttribute('agent', 'com.plexapp.agents.none'); // com.plexapp.agents.lastfm + $dir->addAttribute('scanner', 'Plex Music Scanner'); + $dir->addAttribute('language', 'en'); + $dir->addAttribute('uuid', self::uuidFromSubKey($id)); + $dir->addAttribute('updatedAt', self::getLastUpdate($catalogs)); + self::setSectionXContent($dir, $catalog, 'title'); + //$date = new DateTime("2013-01-01"); + //$dir->addAttribute('createdAt', $date->getTimestamp()); + + $location = $dir->addChild('Location'); + $location->addAttribute('id', $id); + $location->addAttribute('path', $catalog->f_full_info); + } + + $xml->addAttribute('allowSync', '0'); + self::setContainerTitle($xml, 'Plex Library'); + } + + public static function setLibraryContent($xml) + { + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'sections'); + $dir->addAttribute('title', 'Library Sections'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'recentlyAdded'); + $dir->addAttribute('title', 'Recently Added Content'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'onDeck'); + $dir->addAttribute('title', 'On Deck Content'); + + $xml->addAttribute('allowSync', '0'); + self::setContainerTitle($xml, 'Plex Library'); + } + + public static function setSectionContent($xml, $catalog) + { + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'all'); + $dir->addAttribute('title', 'All Artists'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'albums'); + $dir->addAttribute('title', 'By Albums'); + /*$dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'genre'); + $dir->addAttribute('secondary', '1'); + $dir->addAttribute('title', 'By Genre');*/ + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'recentlyAdded'); + $dir->addAttribute('title', 'Recently Added'); + + /*$dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'search?type=8'); + $dir->addAttribute('prompt', 'Search for Artists'); + $dir->addAttribute('search', '1'); + $dir->addAttribute('title', 'Search Artists...'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'search?type=9'); + $dir->addAttribute('prompt', 'Search for Albums'); + $dir->addAttribute('search', '1'); + $dir->addAttribute('title', 'Search Albums...'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'search?type=10'); + $dir->addAttribute('prompt', 'Search for Tracks'); + $dir->addAttribute('search', '1'); + $dir->addAttribute('title', 'Search Tracks...');*/ + + $xml->addAttribute('allowSync', '0'); + $xml->addAttribute('content', 'secondary'); + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('viewGroup', 'secondary'); + $xml->addAttribute('viewMode', '65592'); + self::setSectionXContent($xml, $catalog); + } + + public static function setSectionXContent($xml, $catalog, $title = 'title1') + { + $xml->addAttribute('art', self::getResourceUri('artist-fanart.jpg')); + $xml->addAttribute('thumb', self::getResourceUri('artist.png')); + $xml->addAttribute($title, $catalog->name); + } + + public static function setSystemContent($xml) + { + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'plexonline'); + $dir->addAttribute('title', 'Channel Directory'); + $dir->addAttribute('name', 'Channel Directory'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'help'); + $dir->addAttribute('title', 'Plex Help'); + $dir->addAttribute('name', 'Plex Help'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'library'); + $dir->addAttribute('title', 'Library Sections'); + $dir->addAttribute('name', 'Library Sections'); + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'plugins'); + $dir->addAttribute('title', 'Plug-ins'); + $dir->addAttribute('name', 'Plug-ins'); + } + + public static function setSectionAll($xml, $catalog) + { + $artists = Catalog::get_artists(array($catalog->id)); + + $xml->addAttribute('allowSync', '1'); + self::setSectionXContent($xml, $catalog); + $xml->addAttribute('title2', 'All Artists'); + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('viewGroup', 'artist'); + $xml->addAttribute('viewMode', '65592'); + $xml->addAttribute('librarySectionID', $catalog->id); + $xml->addAttribute('librarySectionUUID', self::uuidFromSubKey($catalog->id)); + + foreach ($artists as $artist) { + self::addArtist($xml, $artist); + } + } + + public static function setSectionAlbums($xml, $catalog) + { + $albums = $catalog->get_album_ids(); + + $xml->addAttribute('allowSync', '0'); + self::setSectionXContent($xml, $catalog); + $xml->addAttribute('title2', 'By Album'); + $xml->addAttribute('mixedParents', '1'); + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('viewGroup', 'album'); + $xml->addAttribute('viewMode', '65592'); + + foreach ($albums as $id) { + $album = new Album($id); + $album->format(); + self::addAlbum($xml, $album); + } + } + + public static function setCustomSectionView($xml, $catalog, $albums) + { + $xml->addAttribute('allowSync', '1'); + self::setSectionXContent($xml, $catalog); + $xml->addAttribute('title2', 'Recently Added'); + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('mixedParents', '1'); + $xml->addAttribute('viewGroup', 'album'); + $xml->addAttribute('viewMode', '65592'); + $xml->addAttribute('librarySectionID', $catalog->id); + $xml->addAttribute('librarySectionUUID', self::uuidFromSubKey($catalog->id)); + self::setSectionXContent($xml, $catalog); + + $data = array(); + $data['album'] = $albums; + self::_setCustomView($xml, $data); + } + + public static function setCustomView($xml, $data) + { + $xml->addAttribute('allowSync', '0'); + $xml->addAttribute('mixedParents', '1'); + self::_setCustomView($xml, $data); + } + + protected static function _setCustomView($xml, $data) + { + foreach ($data as $key => $value) { + foreach ($value as $id) { + if ($key == 'artist') { + $artist = new Artist($id); + $artist->format(); + self::addArtist($xml, $artist); + } elseif ($key == 'album') { + $album = new Album($id); + $album->format(); + self::addAlbum($xml, $album); + } elseif ($key == 'song') { + $song = new Song($id); + $song->format(); + self::addSong($xml, $song); + } + } + } + } + + public static function setServerInfo($xml, $catalogs) + { + $server = $xml->addChild('Server'); + $server->addAttribute('name', self::getServerName()); + $server->addAttribute('host', self::getServerPublicAddress()); + $server->addAttribute('localAddresses', self::getServerAddress()); + $server->addAttribute('port', self::getServerPublicPort()); + $server->addAttribute('machineIdentifier', self::getMachineIdentifier()); + $server->addAttribute('version', self::getPlexVersion()); + + self::setSections($xml, $catalogs); + } + + public static function addArtist($xml, $artist) + { + $xdir = $xml->addChild('Directory'); + $id = self::getArtistId($artist->id); + $xdir->addAttribute('ratingKey', $id); + $xdir->addAttribute('type', 'artist'); + $xdir->addAttribute('title', $artist->name); + $xdir->addAttribute('index', '1'); + $xdir->addAttribute('addedAt', ''); + $xdir->addAttribute('updatedAt', ''); + + $rating = new Rating($artist->id, "artist"); + $rating_value = $rating->get_average_rating(); + if ($rating_value > 0) { + $xdir->addAttribute('rating', intval($rating_value * 2)); + } + + self::addArtistMeta($xdir, $artist); + + $tags = Tag::get_top_tags('artist', $artist->id); + if (is_array($tags)) { + foreach ($tags as $tag_id=>$value) { + $tag = new Tag($tag_id); + $xgenre = $xdir->addChild('Genre'); + $xgenre->addAttribute('tag', $tag->name); + } + } + } + + public static function addArtistMeta($xml, $artist) + { + $id = self::getArtistId($artist->id); + if (!isset($xml['key'])) { + $xml->addAttribute('key', self::getMetadataUri($id) . '/children'); + } + $xml->addAttribute('summary', $artist->summary); + self::addArtistThumb($xml, $artist->id); + } + + protected static function addArtistThumb($xml, $artist_id, $attrthumb = 'thumb') + { + $id = self::getArtistId($artist_id); + $art = new Art($artist_id, 'artist'); + $thumb = ''; + if ($art->get_db()) { + $thumb = self::getMetadataUri($id) . '/thumb/' . $id; + } + $xml->addAttribute($attrthumb, $thumb); + } + + public static function addAlbum($xml, $album) + { + $id = self::getAlbumId($album->id); + $xdir = $xml->addChild('Directory'); + self::addAlbumMeta($xdir, $album); + $xdir->addAttribute('ratingKey', $id); + $xdir->addAttribute('key', self::getMetadataUri($id) . '/children'); + $xdir->addAttribute('title', $album->f_title); + $artistid = self::getArtistId($album->artist_id); + $xdir->addAttribute('parentRatingKey', $artistid); + $xdir->addAttribute('parentKey', self::getMetadataUri($artistid)); + $xdir->addAttribute('parentTitle', $album->f_artist); + $xdir->addAttribute('leafCount', $album->song_count); + if ($album->year != 0 && $album->year != 'N/A') { + $xdir->addAttribute('year', $album->year); + } + + $rating = new Rating($album->id, "album"); + $rating_value = $rating->get_average_rating(); + if ($rating_value > 0) { + $xdir->addAttribute('rating', intval($rating_value * 2)); + } + + $tags = Tag::get_top_tags('album', $album->id); + if (is_array($tags)) { + foreach ($tags as $tag_id=>$value) { + $tag = new Tag($tag_id); + $xgenre = $xdir->addChild('Genre'); + $xgenre->addAttribute('tag', $tag->name); + } + } + } + + public static function addAlbumMeta($xml, $album) + { + $id = self::getAlbumId($album->id); + $xml->addAttribute('allowSync', '1'); + $xml->addAttribute('librarySectionID', $album->catalog_id); + $xml->addAttribute('librarySectionUUID', self::uuidFromkey($album->catalog_id)); + $xml->addAttribute('type', 'album'); + $xml->addAttribute('summary', ''); + $xml->addAttribute('index', '1'); + if ($album->has_art || $album->has_thumb) { + $xml->addAttribute('art', self::getMetadataUri($id) . '/thumb/' . $id); + $xml->addAttribute('thumb', self::getMetadataUri($id) . '/thumb/' . $id); + } + if ($album->artist_id) { + self::addArtistThumb($xml, $album->artist_id, 'parentThumb'); + } + $xml->addAttribute('originallyAvailableAt', ''); + $xml->addAttribute('addedAt', ''); + $xml->addAttribute('updatedAt', ''); + } + + public static function setArtistRoot($xml, $artist) + { + $id = self::getAlbumId($artist->id); + $xml->addAttribute('key', $id); + self::addArtistMeta($xml, $artist); + $xml->addAttribute('allowSync', '1'); + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('parentIndex', '1'); // ?? + $xml->addAttribute('parentTitle', $artist->name); + $xml->addAttribute('title1', ''); // Should be catalog name + $xml->addAttribute('title2', $artist->name); + $xml->addAttribute('viewGroup', 'album'); + $xml->addAttribute('viewMode', '65592'); + + $allalbums = $artist->get_albums(null, true); + foreach ($allalbums as $id) { + $album = new Album($id); + $album->format(); + self::addAlbum($xml, $album); + } + } + + public static function setAlbumRoot($xml, $album) + { + $id = self::getAlbumId($album->id); + self::addAlbumMeta($xml, $album); + if (!isset($xml['key'])) { + $xml->addAttribute('key', $id); + } + $xml->addAttribute('grandparentTitle', $album->f_artist); + $xml->addAttribute('title1', $album->f_artist); + if (!isset($xml['allowSync'])) { + $xml->addAttribute('allowSync', '1'); + } + $xml->addAttribute('nocache', '1'); + $xml->addAttribute('parentIndex', '1'); // ?? + $xml->addAttribute('parentTitle', $album->f_title); + $xml->addAttribute('title2', $album->f_title); + if ($album->year != 0 && $album->year != 'N/A') { + $xml->addAttribute('parentYear', $album->year); + } + $xml->addAttribute('viewGroup', 'track'); + $xml->addAttribute('viewMode', '65593'); + + $allsongs = $album->get_songs(); + foreach ($allsongs as $sid) { + $song = new Song($sid); + self::addSong($xml, $song); + } + } + + public static function addSong($xml, $song) + { + $xdir = $xml->addChild('Track'); + self::addSongMeta($xdir, $song); + $time = $song->time * 1000; + $xdir->addAttribute('title', $song->title); + $albumid = self::getAlbumId($song->album); + $album = new Album($song->album); + $xdir->addAttribute('parentRatingKey', $albumid); + $xdir->addAttribute('parentKey', self::getMetadataUri($albumid)); + $xdir->addAttribute('originalTitle', $album->f_name); + $xdir->addAttribute('summary', ''); + $xdir->addAttribute('index', $song->track); + $xdir->addAttribute('duration', $time); + $xdir->addAttribute('type', 'track'); + $xdir->addAttribute('addedAt', ''); + $xdir->addAttribute('updatedAt', ''); + + $rating = new Rating($song->id, "song"); + $rating_value = $rating->get_average_rating(); + if ($rating_value > 0) { + $xdir->addAttribute('rating', intval($rating_value * 2)); + } + + $xmedia = $xdir->addChild('Media'); + $mediaid = self::getMediaId($song->id); + $xmedia->addAttribute('id', $mediaid); + $xmedia->addAttribute('duration', $time); + $xmedia->addAttribute('bitrate', intval($song->bitrate / 1000)); + $xmedia->addAttribute('audioChannels', ''); + // Type != Codec != Container, but that's how Ampache works today... + $xmedia->addAttribute('audioCodec', $song->type); + $xmedia->addAttribute('container', $song->type); + + $xpart = $xmedia->addChild('Part'); + $partid = self::getPartId($song->id); + $xpart->addAttribute('id', $partid); + $xpart->addAttribute('key', self::getPartUri($partid, $song->type)); + $xpart->addAttribute('duration', $time); + $xpart->addAttribute('file', $song->file); + $xpart->addAttribute('size', $song->size); + $xpart->addAttribute('container', $song->type); + } + + public static function addSongMeta($xml, $song) + { + $id = self::getTrackId($song->id); + $xml->addAttribute('ratingKey', $id); + $xml->addAttribute('key', self::getMetadataUri($id)); + + return $xml; + } + + public static function createMyPlexAccount() + { + $xml = new SimpleXMLElement(''); + $myplex_username = self::getMyPlexUsername(); + $myplex_authtoken = self::getMyPlexAuthToken(); + $myplex_published = self::getMyPlexPublished(); + if ($myplex_username) { + $xml->addAttribute('myPlex', '1'); + $xml->addAttribute('username', $myplex_username); + if ($myplex_authtoken) { + $xml->addAttribute('authToken', $myplex_authtoken); + $xml->addAttribute('signInState', 'ok'); + if ($myplex_published) { + $xml->addAttribute('mappingState', 'mapped'); + } else { + $xml->addAttribute('mappingState', 'unknown'); + } + } else { + $xml->addAttribute('signInState', 'none'); + } + } else { + $xml->addAttribute('signInState', 'none'); + } + $xml->addAttribute('mappingError', ''); + $xml->addAttribute('mappingErrorMessage', '1'); + + $xml->addAttribute('publicAddress', '1'); + $xml->addAttribute('publicPort', self::getServerPublicPort()); + $xml->addAttribute('privateAddress', '1'); + $xml->addAttribute('privatePort', self::getServerPort()); + + $xml->addAttribute('subscriptionActive', '1'); + $xml->addAttribute('subscriptionFeatures', 'cloudsync,pass,sync'); + $xml->addAttribute('subscriptionState', 'Active'); + + return $xml; + } + + public static function setSysAgents($xml) + { + /*$agent = $xml->addChild('Agent'); + $agent->addAttribute('primary', '0'); + $agent->addAttribute('hasPrefs', '0'); + $agent->addAttribute('hasAttribution', '1'); + $agent->addAttribute('identifier', 'com.plexapp.agents.wikipedia');*/ + + $agent = $xml->addChild('Agent'); + $agent->addAttribute('primary', '1'); + $agent->addAttribute('hasPrefs', '0'); + $agent->addAttribute('hasAttribution', '0'); + $agent->addAttribute('identifier', 'com.plexapp.agents.none'); + self::addNoneAgentMediaType($agent, 'Personal Media Artists', '8'); + self::addNoneAgentMediaType($agent, 'Personal Media', '1'); + self::addNoneAgentMediaType($agent, 'Personal Media Shows', '2'); + self::addNoneAgentMediaType($agent, 'Photos', '13'); + self::addNoneAgentMediaType($agent, 'Personal Media Albums', '9'); + } + + protected static function addNoneAgentMediaType($xml, $name, $type) + { + $media = $xml->addChild('MediaType'); + $media->addAttribute('name', $name); + $media->addAttribute('mediaType', $type); + self::addLanguages($media, 'xn'); + } + + protected static function addLanguages($xml, $languages) + { + $langs = explode(',', $languages); + foreach ($langs as $lang) { + $lg = $xml->addChild('Language'); + $lg->addAttribute('code', $lang); + } + } + + protected static function addNoneAgent($xml, $name) + { + self::addAgent($xml, $name, '0', 'com.plexapp.agents.none', true, 'xn'); + } + + protected static function addAgent($xml, $name, $hasPrefs, $identifier, $enabled = false, $langs='') + { + $agent = $xml->addChild('Agent'); + $agent->addAttribute('name', $name); + if ($enabled) { + $agent->addAttribute('enabled', ($enabled) ? '1' : '0'); + } + $agent->addAttribute('hasPrefs', $hasPrefs); + $agent->addAttribute('identifier', $identifier); + if (!empty($langs)) { + self::addLanguages($agent, $langs); + } + return $agent; + } + + public static function setSysMovieAgents($xml) + { + self::addNoneAgent($xml, 'Personal Media'); + } + + public static function setSysTVShowAgents($xml) + { + self::addNoneAgent($xml, 'Personal Media Shows'); + } + + public static function setSysPhotoAgents($xml) + { + self::addNoneAgent($xml, 'Photos'); + } + + public static function setSysMusicAgents($xml, $category = 'Artists') + { + self::addNoneAgent($xml, 'Personal Media ' . $category); + //self::addAgent($xml, 'Last.fm', '1', 'com.plexapp.agents.lastfm', 'true', 'en,sv,fr,es,de,pl,it,pt,ja,tr,ru,zh'); + } + + public static function setAgentsContributors($xml, $mediaType, $primaryAgent) + { + if ($primaryAgent == 'com.plexapp.agents.none') { + $type = ''; + switch ($mediaType) { + case '1': + $type = 'Movies'; + break; + case '2': + $type = 'TV'; + break; + case '13': + $type = 'Photos'; + break; + case '8': + $type = 'Artists'; + break; + case '9': + $type = 'Albums'; + break; + } + + self::addAgent($xml, 'Local Media Assets (' . $type . ')', '0', 'com.plexapp.agents.localmedia', true); + } + } + + /** + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function setAccounts($xml, $userid) + { + // Not sure how to handle Plex accounts vs Ampache accounts, return only 1 for now. + + $account = $xml->addChild('Account'); + $account->addAttribute('key', '/accounts/1'); + $account->addAttribute('name', 'Administrator'); + $account->addAttribute('defaultAudioLanguage', 'en'); + $account->addAttribute('autoSelectAudio', '1'); + $account->addAttribute('defaultSubtitleLanguage', 'en'); + $account->addAttribute('subtitleMode', '0'); + } + + public static function setStatus($xml) + { + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'sessions'); + $dir->addAttribute('title', 'sessions'); + } + + public static function setPrefs($xml) + { + self::addSettings($xml, 'AcceptedEULA', 'Has the user accepted the EULA', 'false', '', 'bool', 'true', '1', '0', ''); + //self::addSettings($xml, 'ApertureLibraryXmlPath', 'Aperture library XML path', '', '', 'text', '', '0', '1', 'channels'); + //self::addSettings($xml, 'ApertureSharingEnabled', 'Enable Aperture sharing', 'true', '', 'bool', 'true', '0', '0', 'channels'); + //self::addSettings($xml, 'AppCastUrl', 'AppCast URL', 'https://www.plexapp.com/appcast/mac/pms.xml', '', 'text', 'https://www.plexapp.com/appcast/mac/pms.xml', '0', '1', 'network'); + self::addSettings($xml, 'ConfigurationUrl', 'Web Manager URL', 'http://127.0.0.1:32400/web', '', 'text', self::getServerUri() . '/web', '1', '0', 'network'); + //self::addSettings($xml, 'DisableHlsAuthorization', 'Disable HLS authorization', 'false', '', 'bool', 'false', '1', '0', ''); + //self::addSettings($xml, 'DlnaAnnouncementLeaseTime', 'DLNA server announcement lease time', '1800', 'Duration of DLNA Server SSDP announcement lease time, in seconds', 'int', '1800', '0', '1', 'dlna'); + self::addSettings($xml, 'FirstRun', 'First run of PMS on this machine', 'true', '', 'bool', 'false', '1', '0', ''); + self::addSettings($xml, 'ForceSecureAccess', 'Force secure access', 'false', 'Disallow access on the local network except to authorized users', 'bool', 'false', '1', '0', 'general'); + self::addSettings($xml, 'FriendlyName', 'Friendly name', '', 'This name will be used to identify this media server to other computers on your network. If you leave it blank, your computer\'s name will be used instead.', 'text', self::getServerName(), '0', '0', 'general'); + self::addSettings($xml, 'LogVerbose', 'Plex Media Server verbose logging', 'false', 'Enable Plex Media Server verbose logging', 'bool', AmpConfig::get('debug_level') == '5', '0', '1', 'general'); + self::addSettings($xml, 'MachineIdentifier', 'A unique identifier for the machine', '', '', 'text', self::getMachineIdentifier(), '1', '0', ''); + self::addSettings($xml, 'ManualPortMappingMode', 'Disable Automatic Port Mapping', 'false', 'When enabled, PMS is not trying to set-up a port mapping through your Router automatically', 'bool', 'false', '1', '0', ''); + self::addSettings($xml, 'ManualPortMappingPort', 'External Port', '32400', 'When Automatic Port Mapping is disabled, you need to specify the external port that is mapped to this machine.', 'int', self::getServerPublicPort(), '1', '0', ''); + self::addSettings($xml, 'PlexOnlineMail', 'myPlex email', '', 'The email address you use to login to myPlex.', 'text', self::getMyPlexUsername(), '1', '0', ''); + self::addSettings($xml, 'PlexOnlineUrl', 'myPlex service URL', 'https://my.plexapp.com', 'The URL of the myPlex service', 'text', 'https://my.plexapp.com', '1', '0', ''); + self::addSettings($xml, 'allowMediaDeletion', 'Allow clients to delete media', 'false', 'Clients will be able to delete media', 'bool', 'false', '0', '1', 'library'); + self::addSettings($xml, 'logDebug', 'Plex Media Server debug logging', 'false', 'Enable Plex Media Server debug logging', 'bool', AmpConfig::get('debug'), '0', '1', 'general'); + } + + protected static function addSettings($xml, $id, $label, $default, $summary, $type, $value, $hidden, $advanced, $group) + { + $setting = $xml->addChild('Setting'); + $setting->addAttribute('id', $id); + $setting->addAttribute('label', $label); + $setting->addAttribute('default', $default); + $setting->addAttribute('summary', $summary); + $setting->addAttribute('type', $type); + $setting->addAttribute('value', $value); + $setting->addAttribute('hidden', $hidden); + $setting->addAttribute('advanced', $advanced); + $setting->addAttribute('group', $group); + } + + public static function setMusicScanners($xml) + { + $scanner = $xml->addChild('Scanner'); + $scanner->addAttribute('name', 'Plex Music Scanner'); + } + + public static function createAppStore() + { + // Maybe we can setup our own store here? Ignore for now. + $xml = new SimpleXMLElement(''); + $xml->addAttribute('art', self::getResourceUri('store-art.png')); + $xml->addAttribute('noCache', '1'); + $xml->addAttribute('noHistory', '0'); + $xml->addAttribute('title1', 'Channel Directory'); + $xml->addAttribute('replaceParent', '0'); + $xml->addAttribute('identifier', 'com.plexapp.system'); + + return $xml; + } + + public static function setMyPlexSubscription($xml) + { + $subscription = $xml->addChild('subscription'); + $subscription->addAttribute('active', '1'); + $subscription->addAttribute('status', 'Active'); + $subscription->addAttribute('plan', 'lifetime'); + $features = array('cloudsync', 'pass', 'sync'); + foreach ($features as $feature) { + $fxml = $subscription->addChild('feature'); + $fxml->addAttribute('id', $feature); + } + } + + public static function setServices($xml) + { + $dir = $xml->addChild('Directory'); + $dir->addAttribute('key', 'browse'); + $dir->addAttribute('title', 'browse'); + } + + protected static function getPathDelimiter() + { + if (strpos(PHP_OS, 'WIN') === 0) + return '\\'; + else + return '/'; + } + + public static function setBrowseService($xml, $path) + { + $delim = self::getPathDelimiter(); + if (!empty($path)) { + $dir = base64_decode($path); + debug_event('plex', 'Dir: ' . $dir, '5'); + } else { + self::addDirPath($xml, AmpConfig::get('prefix') . $delim . 'plex', 'plex', true); + + if ($delim == '/') { + $dir = '/'; + } else { + $dir = ''; + // TODO: found a better way to list Windows drive + $letters = str_split("CDEFGHIJ"); + foreach ($letters as $letter) { + self::addDirPath($xml, $letter . ':'); + } + } + } + + if (!empty($dir)) { + $dh = opendir($dir); + if (is_resource($dh)) { + while (false !== ($filename = readdir($dh))) { + $path = $dir . $delim . $filename; + if ($filename != '.' && $filename != '..' && is_dir($path)) { + self::addDirPath($xml, $path); + } + } + } + } + } + + public static function addDirPath($xml, $path, $title='', $isHome=false) + { + $delim = self::getPathDelimiter(); + $dir = $xml->addChild('Path'); + if ($isHome) { + $dir->addAttribute('isHome', '1'); + } + if (empty($title)) { + $pp = explode($delim, $path); + $title = $pp[count($pp)-1]; + if (empty($title)) { + $title = $path; + } + } + $key = '/services/browse/' . base64_encode($path); + $dir->addAttribute('key', $key); + $dir->addAttribute('title', $title); + $dir->addAttribute('path', $path); + } +} diff --git a/sources/lib/class/plugin.class.php b/sources/lib/class/plugin.class.php new file mode 100644 index 0000000..f536cf9 --- /dev/null +++ b/sources/lib/class/plugin.class.php @@ -0,0 +1,296 @@ +_get_info($name)) { + return false; + } + + return true; + + } // Constructor + + + /** + * _get_info + * This actually loads the config file for the plugin the name of the + * class contained within the config file must be Plugin[NAME OF FILE] + */ + public function _get_info($name) + { + /* Require the file we want */ + require_once AmpConfig::get('prefix') . '/modules/plugins/' . $name . '.plugin.php'; + + $plugin_name = "Ampache$name"; + + $this->_plugin = new $plugin_name(); + + if (!$this->is_valid()) { + return false; + } + + return true; + + } // _get_info + + /** + * get_plugins + * This returns an array of plugin names + */ + public static function get_plugins($type='') + { + $results = array(); + + // Open up the plugin dir + $handle = opendir(AmpConfig::get('prefix') . '/modules/plugins'); + + if (!is_resource($handle)) { + debug_event('Plugins','Unable to read plugins directory','1'); + } + + // Recurse the directory + while ($file = readdir($handle)) { + // Ignore non-plugin files + if (substr($file,-10,10) != 'plugin.php') { continue; } + if (is_dir($file)) { continue; } + $plugin_name = basename($file,'.plugin.php'); + if ($type != '') { + $plugin = new Plugin($plugin_name); + if (! Plugin::is_installed($plugin->_plugin->name)) { + debug_event('Plugins', 'Plugin ' . $plugin->_plugin->name . ' is not installed, skipping', 5); + continue; + } + if (! $plugin->is_valid()) { + debug_event('Plugins', 'Plugin ' . $plugin_name . ' is not valid, skipping', 5); + continue; + } + if (! method_exists($plugin->_plugin, $type)) { + debug_event('Plugins', 'Plugin ' . $plugin_name . ' does not support ' . $type . ', skipping', 5); + continue; + } + } + // It's a plugin record it + $results[$plugin_name] = $plugin_name; + } // end while + + // Little stupid but hey + ksort($results); + + return $results; + + } // get_plugins + + /** + * is_valid + * This checks to make sure the plugin has the required functions and + * settings. Ampache requires public variables name, description, and + * version (as an int), and methods install, uninstall, and load. We + * also check that Ampache's database version falls within the min/max + * version specified by the plugin. + */ + public function is_valid() + { + /* Check the plugin to make sure it's got the needed vars */ + if (!strlen($this->_plugin->name)) { + return false; + } + if (!strlen($this->_plugin->description)) { + return false; + } + if (!strlen($this->_plugin->version)) { + return false; + } + + /* Make sure we've got the required methods */ + if (!method_exists($this->_plugin,'install')) { + return false; + } + + if (!method_exists($this->_plugin,'uninstall')) { + return false; + } + + if (!method_exists($this->_plugin,'load')) { + return false; + } + + /* Make sure it's within the version confines */ + $db_version = $this->get_ampache_db_version(); + + if ($db_version < $this->_plugin->min_ampache) { + return false; + } + + if ($db_version > $this->_plugin->max_ampache) { + return false; + } + + // We've passed all of the tests + return true; + + } // is_valid + + /** + * is_installed + * This checks to see if the specified plugin is currently installed in + * the database, it doesn't check the files for integrity + */ + public static function is_installed($plugin_name) + { + /* All we do is check the version */ + return self::get_plugin_version($plugin_name); + + } // is_installed + + /** + * install + * This runs the install function of the plugin and inserts a row into + * the update_info table to indicate that it's installed. + */ + public function install() + { + if ($this->_plugin->install() && + $this->set_plugin_version($this->_plugin->version)) { + return true; + } + + return false; + } // install + + /** + * uninstall + * This runs the uninstall function of the plugin and removes the row + * from the update_info table to indicate that it isn't installed. + */ + public function uninstall() + { + $this->_plugin->uninstall(); + + $this->remove_plugin_version(); + + } // uninstall + + /** + * upgrade + * This runs the upgrade function of the plugin (if it exists) and + * updates the database to indicate our new version. + */ + public function upgrade() + { + if (method_exists($this->_plugin, 'upgrade')) { + if ($this->_plugin->upgrade()) { + $this->set_plugin_version($this->_plugin->version); + } + } + } // upgrade + + /** + * load + * This calls the plugin's load function + */ + public function load($user) + { + $user->set_preferences(); + return $this->_plugin->load($user); + } + + /** + * get_plugin_version + * This returns the version of the specified plugin + */ + public static function get_plugin_version($plugin_name) + { + $name = Dba::escape('Plugin_' . $plugin_name); + + $sql = "SELECT * FROM `update_info` WHERE `key`='$name'"; + $db_results = Dba::read($sql); + + if ($results = Dba::fetch_assoc($db_results)) { + return $results['value']; + } + + return false; + + } // get_plugin_version + + /** + * get_ampache_db_version + * This function returns the Ampache database version + */ + public function get_ampache_db_version() + { + $sql = "SELECT * FROM `update_info` WHERE `key`='db_version'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + return $results['value']; + + } // get_ampache_db_version + + /** + * set_plugin_version + * This sets the plugin version in the update_info table + */ + public function set_plugin_version($version) + { + $name = Dba::escape('Plugin_' . $this->_plugin->name); + $version = Dba::escape($version); + + $sql = "REPLACE INTO `update_info` SET `key`='$name', `value`='$version'"; + Dba::write($sql); + + return true; + + } // set_plugin_version + + /** + * remove_plugin_version + * This removes the version row from the db done on uninstall + */ + public function remove_plugin_version() + { + $name = Dba::escape('Plugin_' . $this->_plugin->name); + + $sql = "DELETE FROM `update_info` WHERE `key`='$name'"; + Dba::write($sql); + + return true; + + } // remove_plugin_version + +} //end plugin class diff --git a/sources/lib/class/preference.class.php b/sources/lib/class/preference.class.php new file mode 100644 index 0000000..fbf522c --- /dev/null +++ b/sources/lib/class/preference.class.php @@ -0,0 +1,486 @@ +username : '???' . ' attempted to update ' . $name . ' but does not have sufficient permissions','3'); + } + + return false; + } // update + + /** + * update_level + * This takes a preference ID and updates the level required to update it (performed by an admin) + */ + public static function update_level($preference,$level) + { + // First prepare + if (!is_numeric($preference)) { + $preference_id = self::id_from_name($preference); + } else { + $preference_id = $preference; + } + + $preference_id = Dba::escape($preference_id); + $level = Dba::escape($level); + + $sql = "UPDATE `preference` SET `level`='$level' WHERE `id`='$preference_id'"; + Dba::write($sql); + + return true; + + } // update_level + + /** + * update_all + * This takes a preference id and a value and updates all users with the new info + */ + public static function update_all($preference_id,$value) + { + $preference_id = Dba::escape($preference_id); + $value = Dba::escape($value); + + $sql = "UPDATE `user_preference` SET `value`='$value' WHERE `preference`='$preference_id'"; + Dba::write($sql); + + parent::clear_cache(); + + return true; + + } // update_all + + /** + * exists + * This just checks to see if a preference currently exists + */ + public static function exists($preference) + { + // We assume it's the name + $name = Dba::escape($preference); + $sql = "SELECT * FROM `preference` WHERE `name`='$name'"; + $db_results = Dba::read($sql); + + return Dba::num_rows($db_results); + + } // exists + + /** + * has_access + * This checks to see if the current user has access to modify this preference + * as defined by the preference name + */ + public static function has_access($preference) + { + // Nothing for those demo thugs + if (AmpConfig::get('demo_mode')) { return false; } + + $preference = Dba::escape($preference); + + $sql = "SELECT `level` FROM `preference` WHERE `name`='$preference'"; + $db_results = Dba::read($sql); + $data = Dba::fetch_assoc($db_results); + + if (Access::check('interface',$data['level'])) { + return true; + } + + return false; + + } // has_access + + /** + * id_from_name + * This takes a name and returns the id + */ + public static function id_from_name($name) + { + $name = Dba::escape($name); + + if (parent::is_cached('id_from_name', $name)) { + return parent::get_from_cache('id_from_name', $name); + } + + $sql = "SELECT `id` FROM `preference` WHERE `name`='$name'"; + $db_results = Dba::read($sql); + $row = Dba::fetch_assoc($db_results); + + parent::add_to_cache('id_from_name', $name, $row['id']); + + return $row['id']; + + } // id_from_name + + /** + * name_from_id + * This returns the name from an id, it's the exact opposite + * of the function above it, amazing! + */ + public static function name_from_id($id) + { + $id = Dba::escape($id); + + $sql = "SELECT `name` FROM `preference` WHERE `id`='$id'"; + $db_results = Dba::read($sql); + + $row = Dba::fetch_assoc($db_results); + + return $row['name']; + + } // name_from_id + + /** + * get_catagories + * This returns an array of the names of the different possible sections + * it ignores the 'internal' catagory + */ + public static function get_catagories() + { + $sql = "SELECT `preference`.`catagory` FROM `preference` GROUP BY `catagory` ORDER BY `catagory`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + if ($row['catagory'] != 'internal') { + $results[] = $row['catagory']; + } + } // end while + + return $results; + + } // get_catagories + + /** + * get_all + * This returns a nice flat array of all of the possible preferences for the specified user + */ + public static function get_all($user_id) + { + $user_id = Dba::escape($user_id); + + $user_limit = ""; + if ($user_id != '-1') { + $user_limit = "AND `preference`.`catagory` != 'system'"; + } + + $sql = "SELECT `preference`.`name`,`preference`.`description`,`user_preference`.`value` FROM `preference` " . + " INNER JOIN `user_preference` ON `user_preference`.`preference`=`preference`.`id` " . + " WHERE `user_preference`.`user`='$user_id' AND `preference`.`catagory` != 'internal' $user_limit " . + " ORDER BY `preference`.`description`"; + + $db_results = Dba::read($sql); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array('name'=>$row['name'],'level'=>$row['level'],'description'=>$row['description'],'value'=>$row['value']); + } + + return $results; + + } // get_all + + /** + * insert + * This inserts a new preference into the preference table + * it does NOT sync up the users, that should be done independently + */ + public static function insert($name,$description,$default,$level,$type,$catagory) + { + // Clean em up + $name = Dba::escape($name); + $description = Dba::escape($description); + $default = Dba::escape($default); + $level = Dba::escape($level); + $type = Dba::escape($type); + $catagory = Dba::escape($catagory); + + $sql = "INSERT INTO `preference` (`name`,`description`,`value`,`level`,`type`,`catagory`) " . + "VALUES ('$name','$description','$default','$level','$type','$catagory')"; + $db_results = Dba::write($sql); + + if (!$db_results) { return false; } + + return true; + + } // insert + + /** + * delete + * This deletes the specified preference, a name or a ID can be passed + */ + public static function delete($preference) + { + // First prepare + if (!is_numeric($preference)) { + $name = Dba::escape($preference); + $sql = "DELETE FROM `preference` WHERE `name`='$name'"; + } else { + $id = Dba::escape($preference); + $sql = "DELETE FROM `preference` WHERE `id`='$id'"; + } + + Dba::write($sql); + + self::rebuild_preferences(); + + } // delete + + /** + * rename + * This renames a preference in the database + */ + public static function rename($old, $new) + { + $old = Dba::escape($old); + $new = Dba::escape($new); + + $sql = "UPDATE `preference` SET `name`='$new' WHERE `name`='$old'"; + Dba::write($sql); + } + + /** + * rebuild_preferences + * This removes any garbage and then adds back in anything missing preferences wise + */ + public static function rebuild_preferences() + { + // First remove garbage + $sql = "DELETE FROM `user_preference` USING `user_preference` LEFT JOIN `preference` ON `preference`.`id`=`user_preference`.`preference` " . + "WHERE `preference`.`id` IS NULL"; + Dba::write($sql); + + // Now add anything that we are missing back in, except System + //$sql = "SELECT * FROM `preference` WHERE `type`!='system'"; + //FIXME: Uhh WTF shouldn't there be something here?? + + } // rebuild_preferences + + /** + * fix_preferences + * This takes the preferences, explodes what needs to + * become an array and boolean everythings + */ + public static function fix_preferences($results) + { + $arrays = array('auth_methods', 'getid3_tag_order', + 'metadata_order', 'art_order', 'amazon_base_urls'); + + foreach ($arrays as $item) { + $results[$item] = trim($results[$item]) + ? explode(',', $results[$item]) + : array(); + } + + foreach ($results as $key=>$data) { + if (!is_array($data)) { + if (strcasecmp($data,"true") == "0") { $results[$key] = 1; } + if (strcasecmp($data,"false") == "0") { $results[$key] = 0; } + } + } + + return $results; + + } // fix_preferences + + /** + * load_from_session + * This loads the preferences from the session rather then creating a connection to the database + */ + public static function load_from_session($uid=-1) + { + if (isset($_SESSION['userdata']['preferences']) && is_array($_SESSION['userdata']['preferences']) AND $_SESSION['userdata']['uid'] == $uid) { + AmpConfig::set_by_array($_SESSION['userdata']['preferences'], true); + return true; + } + + return false; + + } // load_from_session + + /** + * clear_from_session + * This clears the users preferences, this is done whenever modifications are made to the preferences + * or the admin resets something + */ + public static function clear_from_session() + { + unset($_SESSION['userdata']['preferences']); + + } // clear_from_session + + /** + * is_boolean + * This returns true / false if the preference in question is a boolean preference + * This is currently only used by the debug view, could be used other places.. wouldn't be a half + * bad idea + */ + public static function is_boolean($key) + { + $boolean_array = array('session_cookiesecure','require_session', + 'access_control','require_localnet_session', + 'downsample_remote','track_user_ip', + 'xml_rpc','allow_zip_download', + 'file_zip_download','ratings', + 'shoutbox','resize_images', + 'show_album_art','allow_public_registration', + 'captcha_public_reg','admin_notify_reg', + 'use_rss','download','force_http_play','cookie_secure', + 'allow_stream_playback','allow_democratic_playback', + 'use_auth','allow_localplay_playback','debug','lock_songs', + 'transcode_m4a','transcode_mp3','transcode_ogg','transcode_flac', + 'shoutcast_active','httpq_active','show_lyrics'); + + if (in_array($key,$boolean_array)) { + return true; + } + + return false; + + } // is_boolean + + /** + * init + * This grabs the preferences and then loads them into conf it should be run on page load + * to initialize the needed variables + */ + public static function init() + { + $user_id = $GLOBALS['user']->id ? Dba::escape($GLOBALS['user']->id) : '-1'; + + // First go ahead and try to load it from the preferences + if (self::load_from_session($user_id)) { + return true; + } + + /* Get Global Preferences */ + $sql = "SELECT `preference`.`name`,`user_preference`.`value`,`syspref`.`value` AS `system_value` FROM `preference` " . + "LEFT JOIN `user_preference` `syspref` ON `syspref`.`preference`=`preference`.`id` AND `syspref`.`user`='-1' AND `preference`.`catagory`='system' " . + "LEFT JOIN `user_preference` ON `user_preference`.`preference`=`preference`.`id` AND `user_preference`.`user`='$user_id' AND `preference`.`catagory`!='system'"; + $db_results = Dba::read($sql); + + $results = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $value = $row['system_value'] ? $row['system_value'] : $row['value']; + $name = $row['name']; + $results[$name] = $value; + } // end while sys prefs + + /* Set the Theme mojo */ + if (strlen($results['theme_name']) > 0) { + $results['theme_path'] = '/themes/' . $results['theme_name']; + } + // Default theme if we don't get anything from their + // preferences because we're going to want at least something otherwise + // the page is going to be really ugly + else { + $results['theme_path'] = '/themes/reborn'; + } + + AmpConfig::set_by_array($results, true); + $_SESSION['userdata']['preferences'] = $results; + $_SESSION['userdata']['uid'] = $user_id; + + } // init + + +} // end Preference class diff --git a/sources/lib/class/query.class.php b/sources/lib/class/query.class.php new file mode 100644 index 0000000..a03a724 --- /dev/null +++ b/sources/lib/class/query.class.php @@ -0,0 +1,1771 @@ +reset(); + if ($cached) { + $data = serialize($this->_state); + + $sql = 'INSERT INTO `tmp_browse` (`sid`, `data`) ' . + 'VALUES(?, ?)'; + Dba::write($sql, array($sid, $data)); + $this->id = Dba::insert_id(); + + } else { + $this->id = 'nocache'; + } + return true; + } + + $this->id = $id; + + $sql = 'SELECT `data` FROM `tmp_browse` ' . + 'WHERE `id` = ? AND `sid` = ?'; + + $db_results = Dba::read($sql, array($id, $sid)); + + if ($results = Dba::fetch_assoc($db_results)) { + $this->_state = unserialize($results['data']); + return true; + } + + Error::add('browse', T_('Browse not found or expired, try reloading the page')); + return false; + } + + /** + * _auto_init + * Automatically called when the class is loaded. + * Populate static arrays if necessary + */ + public static function _auto_init() + { + if (is_array(self::$allowed_filters)) { + return true; + } + + self::$allowed_filters = array( + 'album' => array( + 'add_lt', + 'add_gt', + 'update_lt', + 'update_gt', + 'show_art', + 'starts_with', + 'exact_match', + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'catalog', + 'catalog_enabled' + ), + 'artist' => array( + 'add_lt', + 'add_gt', + 'update_lt', + 'update_gt', + 'exact_match', + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'starts_with', + 'tag', + 'catalog', + 'catalog_enabled' + ), + 'song' => array( + 'add_lt', + 'add_gt', + 'update_lt', + 'update_gt', + 'exact_match', + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'starts_with', + 'tag', + 'catalog', + 'catalog_enabled' + ), + 'live_stream' => array( + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'starts_with', + 'catalog_enabled' + ), + 'playlist' => array( + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'starts_with' + ), + 'smartplaylist' => array( + 'alpha_match', + 'regex_match', + 'regex_not_match', + 'starts_with' + ), + 'tag' => array( + 'tag', + 'object_type', + 'exact_match', + 'alpha_match', + 'regex_match', + 'regex_not_match' + ), + 'video' => array( + 'starts_with', + 'exact_match', + 'alpha_match', + 'regex_match', + 'regex_not_match' + ) + ); + + if (Access::check('interface','50')) { + array_push(self::$allowed_filters['playlist'], 'playlist_type'); + } + + self::$allowed_sorts = array( + 'playlist_song' => array( + 'title', + 'year', + 'track', + 'time', + 'album', + 'artist' + ), + 'song' => array( + 'title', + 'year', + 'track', + 'time', + 'album', + 'artist' + ), + 'artist' => array( + 'name', + 'album' + ), + 'tag' => array( + 'tag' + ), + 'album' => array( + 'name', + 'year', + 'artist' + ), + 'playlist' => array( + 'name', + 'user' + ), + 'smartplaylist' => array( + 'name', + 'user' + ), + 'shoutbox' => array( + 'date', + 'user', + 'sticky' + ), + 'live_stream' => array( + 'name', + 'call_sign', + 'frequency' + ), + 'video' => array( + 'title', + 'resolution', + 'length', + 'codec' + ), + 'user' => array( + 'fullname', + 'username', + 'last_seen', + 'create_date' + ), + 'wanted' => array( + 'user', + 'accepted', + 'artist', + 'name', + 'year' + ), + 'share' => array( + 'object', + 'object_type', + 'user', + 'creation_date', + 'lastvisit_date', + 'counter', + 'max_counter', + 'allow_stream', + 'allow_download', + 'expire' + ), + 'channel' => array( + 'id', + 'name', + 'interface', + 'port', + 'max_listeners', + 'listeners' + ), + 'broadcast' => array( + 'name', + 'user', + 'started', + 'listeners' + ), + ); + } + + /** + * gc + * This cleans old data out of the table + */ + public static function gc() + { + $sql = 'DELETE FROM `tmp_browse` USING `tmp_browse` LEFT JOIN ' . + '`session` ON `session`.`id` = `tmp_browse`.`sid` ' . + 'WHERE `session`.`id` IS NULL'; + Dba::write($sql); + } + + /** + * _serialize + * + * Attempts to produce a more compact representation for large result + * sets by collapsing ranges. + */ + private static function _serialize($data) + { + if (count($data) > 1000 && is_int($data[0])) { + $last = -17; + $in_range = false; + $idx = -1; + $cooked = array(); + foreach ($data as $id) { + if ($id == ($last + 1)) { + if ($in_range) { + $cooked[$idx][1] = $id; + } else { + $in_range = true; + $cooked[$idx] = array($last, $id); + } + } else { + $in_range = false; + $idx++; + $cooked[$idx] = $id; + } + $last = $id; + } + $data = json_encode($cooked); + debug_event('Query', 'cooked serialize length: ' . strlen($data), 5); + } else { + $data = json_encode($data); + } + + return $data; + } + + /* + * _unserialize + * + * Reverses serialization. + */ + private static function _unserialize($data) + { + $raw = array(); + $cooked = json_decode($data); + if ($cooked) { + foreach ($cooked as $grain) { + if (is_array($grain)) { + foreach (range($grain[0], $grain[1]) as $id) { + $raw[] = $id; + } + } else { + $raw[] = $grain; + } + } + } + return $raw; + } + + /** + * set_filter + * This saves the filter data we pass it. + */ + public function set_filter($key, $value) + { + switch ($key) { + case 'tag': + if (is_array($value)) { + $this->_state['filter'][$key] = $value; + } elseif (is_numeric($value)) { + $this->_state['filter'][$key] = array($value); + } else { + $this->_state['filter'][$key] = array(); + } + break; + case 'artist': + case 'catalog': + case 'album': + $this->_state['filter'][$key] = $value; + break; + case 'min_count': + case 'unplayed': + case 'rated': + + break; + case 'add_lt': + case 'add_gt': + case 'update_lt': + case 'update_gt': + case 'catalog_enabled': + $this->_state['filter'][$key] = intval($value); + break; + case 'exact_match': + case 'alpha_match': + case 'regex_match': + case 'regex_not_match': + case 'starts_with': + if ($this->is_static_content()) { return false; } + $this->_state['filter'][$key] = $value; + if ($key == 'regex_match') unset($this->_state['filter']['regex_not_match']); + if ($key == 'regex_not_match') unset($this->_state['filter']['regex_match']); + break; + case 'playlist_type': + // Must be a content manager to turn this off + if (Access::check('interface','100')) { unset($this->_state['filter'][$key]); } else { $this->_state['filter'][$key] = '1'; } + break; + default: + // Rien a faire + return false; + } // end switch + + // If we've set a filter we need to reset the totals + $this->reset_total(); + $this->set_start(0); + + return true; + + } // set_filter + + /** + * reset + * Reset everything, this should only be called when we are starting + * fresh + */ + public function reset() + { + $this->reset_base(); + $this->reset_filters(); + $this->reset_total(); + $this->reset_join(); + $this->reset_select(); + $this->reset_having(); + $this->set_static_content(false); + $this->set_is_simple(false); + $this->set_start(0); + $this->set_offset(AmpConfig::get('offset_limit') ? AmpConfig::get('offset_limit') : '25'); + + } // reset + + /** + * reset_base + * this resets the base string + */ + public function reset_base() + { + $this->_state['base'] = NULL; + + } // reset_base + + /** + * reset_select + * This resets the select fields that we've added so far + */ + public function reset_select() + { + $this->_state['select'] = array(); + + } // reset_select + + /** + * reset_having + * Null out the having clause + */ + public function reset_having() + { + unset($this->_state['having']); + + } // reset_having + + /** + * reset_join + * clears the joins if there are any + */ + public function reset_join() + { + unset($this->_state['join']); + + } // reset_join + + /** + * reset_filter + * This is a wrapper function that resets the filters + */ + public function reset_filters() + { + $this->_state['filter'] = array(); + + } // reset_filters + + /** + * reset_total + * This resets the total for the browse type + */ + public function reset_total() + { + unset($this->_state['total']); + + } // reset_total + + /** + * get_filter + * returns the specified filter value + */ + public function get_filter($key) + { + // Simple enough, but if we ever move this crap + // If we ever move this crap what? + return isset($this->_state['filter'][$key]) + ? $this->_state['filter'][$key] + : false; + + } // get_filter + + /** + * get_start + * This returns the current value of the start + */ + public function get_start() + { + return $this->_state['start']; + + } // get_start + + /** + * get_offset + * This returns the current offset + */ + public function get_offset() + { + if ($this->is_static_content()) { + return $this->get_total(); + } + + return $this->_state['offset']; + } // get_offset + + /** + * set_total + * This sets the total number of objects + */ + public function set_total($total) + { + $this->_state['total'] = $total; + } + + /** + * get_total + * This returns the total number of objects for this current sort type. + * If it's already cached used it. if they pass us an array then use + * that. + */ + public function get_total($objects = null) + { + // If they pass something then just return that + if (is_array($objects) and !$this->is_simple()) { + return count($objects); + } + + // See if we can find it in the cache + if (isset($this->_state['total'])) { + return $this->_state['total']; + } + + $db_results = Dba::read($this->get_sql(false)); + $num_rows = Dba::num_rows($db_results); + + $this->_state['total'] = $num_rows; + + return $num_rows; + + } // get_total + + /** + * get_allowed_filters + * This returns an array of the allowed filters based on the type of + * object we are working with, this is used to display the 'filter' + * sidebar stuff. + */ + public static function get_allowed_filters($type) + { + return isset(self::$allowed_filters[$type]) + ? self::$allowed_filters[$type] + : array(); + } // get_allowed_filters + + /** + * set_type + * This sets the type of object that we want to browse by + * we do this here so we only have to maintain a single whitelist + * and if I want to change the location I only have to do it here + */ + public function set_type($type, $custom_base = '') + { + switch ($type) { + case 'user': + case 'video': + case 'playlist': + case 'playlist_song': + case 'smartplaylist': + case 'song': + case 'catalog': + case 'album': + case 'artist': + case 'tag': + case 'playlist_localplay': + case 'shoutbox': + case 'live_stream': + case 'democratic': + case 'wanted': + case 'share': + case 'song_preview': + case 'channel': + case 'broadcast': + // Set it + $this->_state['type'] = $type; + $this->set_base_sql(true, $custom_base); + break; + default: + // Rien a faire + break; + } // end type whitelist + } // set_type + + /** + * get_type + * This returns the type of the browse we currently are using + */ + public function get_type() + { + return $this->_state['type']; + + } // get_type + + /** + * set_sort + * This sets the current sort(s) + */ + public function set_sort($sort,$order='') + { + // If it's not in our list, smeg off! + if (!in_array($sort, self::$allowed_sorts[$this->get_type()])) { + return false; + } + + if ($order) { + $order = ($order == 'DESC') ? 'DESC' : 'ASC'; + $this->_state['sort'] = array(); + $this->_state['sort'][$sort] = $order; + } elseif ($this->_state['sort'][$sort] == 'DESC') { + // Reset it till I can figure out how to interface the hotness + $this->_state['sort'] = array(); + $this->_state['sort'][$sort] = 'ASC'; + } else { + // Reset it till I can figure out how to interface the hotness + $this->_state['sort'] = array(); + $this->_state['sort'][$sort] = 'DESC'; + } + + $this->resort_objects(); + + } // set_sort + + /** + * set_offset + * This sets the current offset of this query + */ + public function set_offset($offset) + { + $this->_state['offset'] = abs($offset); + + } // set_offset + + public function set_catalog( $catalog_number ) + { + $this->catalog = $catalog_number; + debug_event("Catalog", "set catalog id: " . $this->catalog, "5"); + } + + /** + * set_select + * This appends more information to the select part of the SQL + * statement, we're going to move to the %%SELECT%% style queries, as I + * think it's the only way to do this... + */ + public function set_select($field) + { + $this->_state['select'][] = $field; + + } // set_select + + /** + * set_join + * This sets the joins for the current browse object + */ + public function set_join($type, $table, $source, $dest, $priority) + { + $this->_state['join'][$priority][$table] = strtoupper($type) . ' JOIN ' . $table . ' ON ' . $source . '=' . $dest; + + } // set_join + + /** + * set_having + * This sets the "HAVING" part of the query, we can only have one.. + * god this is ugly + */ + public function set_having($condition) + { + $this->_state['having'] = $condition; + + } // set_having + + /** + * set_start + * This sets the start point for our show functions + * We need to store this in the session so that it can be pulled + * back, if they hit the back button + */ + public function set_start($start) + { + $start = intval($start); + + if (!$this->is_static_content()) { + $this->_state['start'] = $start; + } + + } // set_start + + /** + * set_is_simple + * This sets the current browse object to a 'simple' browse method + * which means use the base query provided and expand from there + */ + public function set_is_simple($value) + { + $value = make_bool($value); + $this->_state['simple'] = $value; + + } // set_is_simple + + /** + * set_static_content + * This sets true/false if the content of this browse + * should be static, if they are then content filtering/altering + * methods will be skipped + */ + public function set_static_content($value) + { + $value = make_bool($value); + + // We want to start at 0 if it's static + if ($value) { + $this->set_start('0'); + } + + $this->_state['static'] = $value; + + } // set_static_content + + public function is_static_content() + { + return $this->_state['static']; + } + + /** + * is_simple + * This returns whether or not the current browse type is set to static. + */ + public function is_simple() + { + return $this->_state['simple']; + + } // is_simple + + /** + * get_savedget_saved + * This looks in the session for the saved stuff and returns what it + * finds. + */ + public function get_saved() + { + // See if we have it in the local cache first + if (is_array($this->_cache)) { + return $this->_cache; + } + + if (!$this->is_simple()) { + $sql = 'SELECT `object_data` FROM `tmp_browse` ' . + 'WHERE `sid` = ? AND `id` = ?'; + $db_results = Dba::read($sql, array(session_id(), $this->id)); + + $row = Dba::fetch_assoc($db_results); + + $this->_cache = self::_unserialize($row['object_data']); + return $this->_cache; + } else { + $objects = $this->get_objects(); + } + + return $objects; + + } // get_saved + + /** + * get_objects + * This gets an array of the ids of the objects that we are + * currently browsing by it applies the sql and logic based + * filters + */ + public function get_objects() + { + // First we need to get the SQL statement we are going to run + // This has to run against any possible filters (dependent on type) + $sql = $this->get_sql(true); + $db_results = Dba::read($sql); + + $results = array(); + while ($data = Dba::fetch_assoc($db_results)) { + $results[] = $data; + } + + $results = $this->post_process($results); + $filtered = array(); + foreach ($results as $data) { + // Make sure that this object passes the logic filter + if ($this->logic_filter($data['id'])) { + $filtered[] = $data['id']; + } + } // end while + + // Save what we've found and then return it + $this->save_objects($filtered); + + return $filtered; + + } // get_objects + + /** + * set_base_sql + * This saves the base sql statement we are going to use. + */ + private function set_base_sql($force = false, $custom_base = '') + { + // Only allow it to be set once + if (strlen($this->_state['base']) && !$force) { return true; } + + // Custom sql base + if ($force && !empty($custom_base)) { + $this->_state['custom'] = true; + $sql = $custom_base; + } else { + switch ($this->get_type()) { + case 'album': + $this->set_select("DISTINCT(`album`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `album` "; + break; + case 'artist': + $this->set_select("`artist`.`id`"); + $sql = "SELECT %%SELECT%% FROM `artist` "; + break; + case 'catalog': + $this->set_select("`artist`.`name`"); + $sql = "SELECT %%SELECT%% FROM `artist` "; + break; + case 'user': + $this->set_select("`user`.`id`"); + $sql = "SELECT %%SELECT%% FROM `user` "; + break; + case 'live_stream': + $this->set_select("`live_stream`.`id`"); + $sql = "SELECT %%SELECT%% FROM `live_stream` "; + break; + case 'playlist': + $this->set_select("`playlist`.`id`"); + $sql = "SELECT %%SELECT%% FROM `playlist` "; + break; + case 'smartplaylist': + self::set_select('`search`.`id`'); + $sql = "SELECT %%SELECT%% FROM `search` "; + break; + case 'shoutbox': + $this->set_select("`user_shout`.`id`"); + $sql = "SELECT %%SELECT%% FROM `user_shout` "; + break; + case 'video': + $this->set_select("`video`.`id`"); + $sql = "SELECT %%SELECT%% FROM `video` "; + break; + case 'tag': + $this->set_select("DISTINCT(`tag`.`id`)"); + $this->set_join('left', 'tag_map', '`tag_map`.`tag_id`', '`tag`.`id`', 1); + $sql = "SELECT %%SELECT%% FROM `tag` "; + break; + case 'wanted': + $this->set_select("DISTINCT(`wanted`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `wanted` "; + break; + case 'share': + $this->set_select("DISTINCT(`share`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `share` "; + break; + case 'channel': + $this->set_select("DISTINCT(`channel`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `channel` "; + break; + case 'broadcast': + $this->set_select("DISTINCT(`broadcast`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `broadcast` "; + break; + case 'playlist_song': + case 'song': + default: + $this->set_select("DISTINCT(`song`.`id`)"); + $sql = "SELECT %%SELECT%% FROM `song` "; + break; + } // end base sql + } + + $this->_state['base'] = $sql; + } // set_base_sql + + /** + * get_select + * This returns the selects in a format that is friendly for a sql + * statement. + */ + private function get_select() + { + $select_string = implode($this->_state['select'], ", "); + return $select_string; + + } // get_select + + /** + * get_base_sql + * This returns the base sql statement all parsed up, this should be + * called after all set operations. + */ + private function get_base_sql() + { + $sql = str_replace("%%SELECT%%", $this->get_select(), $this->_state['base']); + return $sql; + + } // get_base_sql + + /** + * get_filter_sql + * This returns the filter part of the sql statement + */ + private function get_filter_sql() + { + if (!is_array($this->_state['filter'])) { + return ''; + } + + $sql = "WHERE 1=1 AND "; + + foreach ($this->_state['filter'] + as $key => $value) { + + $sql .= $this->sql_filter($key, $value); + } + + if (AmpConfig::get('catalog_disable')) { + // Add catalog enabled filter + switch ($this->get_type()) { + case "video": + case "song": + $dis = Catalog::get_enable_filter($this->get_type(), '`' . $this->get_type() . '`.`id`'); + break; + + case "tag": + $dis = Catalog::get_enable_filter($this->get_type(), '`' . $this->get_type() . '`.`object_id`'); + break; + } + } + if (!empty($dis)) { + $sql .= $dis . " AND "; + } + + $sql = rtrim($sql,'AND ') . ' '; + + return $sql; + + } // get_filter_sql + + /** + * get_sort_sql + * Returns the sort sql part + */ + private function get_sort_sql() + { + if (!is_array($this->_state['sort'])) { + return ''; + } + + $sql = 'ORDER BY '; + + foreach ($this->_state['sort'] + as $key => $value) { + $sql .= $this->sql_sort($key, $value); + } + + $sql = rtrim($sql,'ORDER BY '); + $sql = rtrim($sql,','); + + return $sql; + + } // get_sort_sql + + /** + * get_limit_sql + * This returns the limit part of the sql statement + */ + private function get_limit_sql() + { + if (!$this->is_simple() || $this->get_start() < 0) { return ''; } + + $sql = ' LIMIT ' . intval($this->get_start()) . ',' . intval($this->get_offset()); + + return $sql; + + } // get_limit_sql + + /** + * get_join_sql + * This returns the joins that this browse may need to work correctly + */ + private function get_join_sql() + { + if (!isset($this->_state['join']) || !is_array($this->_state['join'])) { + return ''; + } + + $sql = ''; + + foreach ($this->_state['join'] as $joins) { + foreach ($joins as $join) { + $sql .= $join . ' '; + } // end foreach joins at this level + } // end foreach of this level of joins + + return $sql; + + } // get_join_sql + + /** + * get_having_sql + * this returns the having sql stuff, if we've got anything + */ + public function get_having_sql() + { + $sql = isset($this->_state['having']) ? $this->_state['having'] : ''; + + return $sql; + + } // get_having_sql + + /** + * get_sql + * This returns the sql statement we are going to use this has to be run + * every time we get the objects because it depends on the filters and + * the type of object we are currently browsing. + */ + public function get_sql($limit = true) + { + $sql = $this->get_base_sql(); + + $filter_sql = ""; + $join_sql = ""; + $having_sql = ""; + $order_sql = ""; + if (!isset($this->_state['custom']) || !$this->_state['custom']) { + $filter_sql = $this->get_filter_sql(); + $join_sql = $this->get_join_sql(); + $having_sql = $this->get_having_sql(); + $order_sql = $this->get_sort_sql(); + } + $limit_sql = $limit ? $this->get_limit_sql() : ''; + $final_sql = $sql . $join_sql . $filter_sql . $having_sql; + + if ( $this->get_type() == 'artist' && !$this->_state['custom'] ) { + $final_sql .= " GROUP BY `" . $this->get_type() . "`.`name` "; + } + $final_sql .= $order_sql . $limit_sql; + + return $final_sql; + + } // get_sql + + /** + * post_process + * This does some additional work on the results that we've received + * before returning them. + */ + private function post_process($data) + { + $tags = isset($this->_state['filter']['tag']) ? $this->_state['filter']['tag'] : ''; + + if (!is_array($tags) || sizeof($tags) < 2) { + return $data; + } + + $tag_count = sizeof($tags); + $count = array(); + + foreach ($data as $row) { + $count[$row['id']]++; + } + + $results = array(); + + foreach ($count as $key => $value) { + if ($value >= $tag_count) { + $results[] = array('id' => $key); + } + } // end foreach + + return $results; + + } // post_process + + /** + * sql_filter + * This takes a filter name and value and if it is possible + * to filter by this name on this type returns the appropriate sql + * if not returns nothing + */ + private function sql_filter($filter, $value) + { + $filter_sql = ''; + switch ($this->get_type()) { + + case 'song': + switch ($filter) { + case 'tag': + $this->set_join('left', '`tag_map`', '`tag_map`.`object_id`', '`song`.`id`', 100); + $filter_sql = " `tag_map`.`object_type`='song' AND ("; + + foreach ($value as $tag_id) { + $filter_sql .= " `tag_map`.`tag_id`='" . Dba::escape($tag_id) . "' AND"; + } + $filter_sql = rtrim($filter_sql,'AND') . ') AND '; + break; + case 'exact_match': + $filter_sql = " `song`.`title` = '" . Dba::escape($value) . "' AND "; + break; + case 'alpha_match': + $filter_sql = " `song`.`title` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `song`.`title` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `song`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $filter_sql = " `song`.`title` LIKE '" . Dba::escape($value) . "%' AND "; + if ($this->catalog != 0) { + $filter_sql .= " `song`.`catalog` = '" . $this->catalog . "' AND "; + } + break; + case 'unplayed': + $filter_sql = " `song`.`played`='0' AND "; + break; + case 'album': + $filter_sql = " `song`.`album` = '". Dba::escape($value) . "' AND "; + break; + case 'artist': + $filter_sql = " `song`.`artist` = '". Dba::escape($value) . "' AND "; + break; + case 'add_gt': + $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'add_lt': + $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'update_gt': + $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'update_lt': + $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'catalog': + if ($value != 0) { + $filter_sql = " `song`.`catalog` = '$value' AND "; + } + break; + case 'catalog_enabled': + $this->set_join('left', '`catalog`', '`catalog`.`id`', '`song`.`catalog`', 100); + $filter_sql = " `catalog`.`enabled` = '1' AND "; + break; + default: + // Rien a faire + break; + } // end list of sqlable filters + break; + case 'album': + switch ($filter) { + case 'exact_match': + $filter_sql = " `album`.`name` = '" . Dba::escape($value) . "' AND "; + break; + case 'alpha_match': + $filter_sql = " `album`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `album`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `album`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $this->set_join('left', '`song`', '`album`.`id`', '`song`.`album`', 100); + $filter_sql = " `album`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + if ($this->catalog != 0) { + $filter_sql .= "`song`.`catalog` = '" . $this->catalog . "' AND "; + } + break; + case 'artist': + $filter_sql = " `artist`.`id` = '". Dba::escape($value) . "' AND "; + break; + case 'add_lt': + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'add_gt': + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'catalog': + if ($value != 0) { + $this->set_join('left','`song`','`album`.`id`','`song`.`album`', 100); + $this->set_join('left','`catalog`','`song`.`catalog`','`catalog`.`id`', 100); + $filter_sql = " (`song`.`catalog` = '$value') AND "; + } + break; + case 'update_lt': + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'update_gt': + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'catalog_enabled': + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $this->set_join('left', '`catalog`', '`catalog`.`id`', '`song`.`catalog`', 100); + $filter_sql = " `catalog`.`enabled` = '1' AND "; + break; + default: + // Rien a faire + break; + } + break; + case 'artist': + switch ($filter) { + case 'catalog': + if ($value != 0) { + $this->set_join('left','`song`','`artist`.`id`','`song`.`artist`', 100); + $this->set_join('left','`catalog`','`song`.`catalog`','`catalog`.`id`', 100); + $filter_sql = " (`catalog`.`id` = '$value') AND "; + } + break; + case 'exact_match': + $filter_sql = " `artist`.`name` = '" . Dba::escape($value) . "' AND "; + break; + case 'alpha_match': + $filter_sql = " `artist`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `artist`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `artist`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $this->set_join('left', '`song`', '`artist`.`id`', '`song`.`artist`', 100); + $filter_sql = " `artist`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + if ($this->catalog != 0) { + $filter_sql .= "`song`.`catalog` = '" . $this->catalog . "' AND "; + } + break; + case 'add_lt': + $this->set_join('left', '`song`', '`song`.`artist`', '`artist`.`id`', 100); + $filter_sql = " `song`.`addition_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'add_gt': + $this->set_join('left', '`song`', '`song`.`artist`', '`artist`.`id`', 100); + $filter_sql = " `song`.`addition_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'update_lt': + $this->set_join('left', '`song`', '`song`.`artist`', '`artist`.`id`', 100); + $filter_sql = " `song`.`update_time` <= '" . Dba::escape($value) . "' AND "; + break; + case 'update_gt': + $this->set_join('left', '`song`', '`song`.`artist`', '`artist`.`id`', 100); + $filter_sql = " `song`.`update_time` >= '" . Dba::escape($value) . "' AND "; + break; + case 'catalog_enabled': + $this->set_join('left', '`song`', '`song`.`artist`', '`artist`.`id`', 100); + $this->set_join('left', '`catalog`', '`catalog`.`id`', '`song`.`catalog`', 100); + $filter_sql = " `catalog`.`enabled` = '1' AND "; + break; + default: + // Rien a faire + break; + } // end filter + break; + case 'live_stream': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `live_stream`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `live_stream`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `live_stream`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $filter_sql = " `live_stream`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + break; + case 'catalog_enabled': + $this->set_join('left', '`catalog`', '`catalog`.`id`', '`live_stream`.`catalog`', 100); + $filter_sql = " `catalog`.`enabled` = '1' AND "; + break; + default: + // Rien a faire + break; + } // end filter + break; + case 'playlist': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `playlist`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `playlist`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `playlist`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $filter_sql = " `playlist`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + break; + case 'playlist_type': + $user_id = intval($GLOBALS['user']->id); + $filter_sql = " (`playlist`.`type` = 'public' OR `playlist`.`user`='$user_id') AND "; + break; + default; + // Rien a faire + break; + } // end filter + break; + case 'smartplaylist': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `search`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `search`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `search`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $filter_sql = " `search`.`name` LIKE '" . Dba::escape($value) . "%' AND "; + break; + case 'playlist_type': + $user_id = intval($GLOBALS['user']->id); + $filter_sql = " (`search`.`type` = 'public' OR `search`.`user`='$user_id') AND "; + break; + } // end switch on $filter + break; + case 'tag': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `tag`.`name` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `tag`.`name` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `tag`.`name` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'exact_match': + $filter_sql = " `tag`.`name` = '" . Dba::escape($value) . "' AND "; + break; + case 'tag': + $filter_sql = " `tag`.`id` = '" . Dba::escape($value) . "' AND "; + break; + default: + // Rien a faire + break; + } // end filter + break; + case 'video': + switch ($filter) { + case 'alpha_match': + $filter_sql = " `video`.`title` LIKE '%" . Dba::escape($value) . "%' AND "; + break; + case 'regex_match': + if (!empty($value)) $filter_sql = " `video`.`title` REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'regex_not_match': + if (!empty($value)) $filter_sql = " `video`.`title` NOT REGEXP '" . Dba::escape($value) . "' AND "; + break; + case 'starts_with': + $filter_sql = " `video`.`title` LIKE '" . Dba::escape($value) . "%' AND "; + break; + default: + // Rien a faire + break; + } // end filter + break; + } // end switch on type + + return $filter_sql; + + } // sql_filter + + /** + * logic_filter + * This runs the filters that we can't easily apply + * to the sql so they have to be done after the fact + * these should be limited as they are often intensive and + * require additional queries per object... :( + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function logic_filter($object_id) + { + return true; + + } // logic_filter + + /** + * sql_sort + * This builds any order bys we need to do + * to sort the results as best we can, there is also + * a logic based sort that will come later as that's + * a lot more complicated + */ + private function sql_sort($field, $order) + { + if ($order != 'DESC') { $order == 'ASC'; } + + // Depending on the type of browsing we are doing we can apply + // different filters that apply to different fields + switch ($this->get_type()) { + case 'song': + switch ($field) { + case 'title'; + $sql = "`song`.`title`"; + break; + case 'year': + $sql = "`song`.`year`"; + break; + case 'time': + $sql = "`song`.`time`"; + break; + case 'track': + $sql = "`song`.`track`"; + break; + case 'album': + $sql = '`album`.`name`'; + $this->set_join('left', '`album`', '`album`.`id`', '`song`.`album`', 100); + break; + case 'artist': + $sql = '`artist`.`name`'; + $this->set_join('left', '`artist`', '`artist`.`id`', '`song`.`artist`', 100); + break; + default: + // Rien a faire + break; + } // end switch + break; + case 'album': + switch ($field) { + case 'name': + $sql = "`album`.`name` $order, `album`.`disk`"; + break; + case 'artist': + $sql = "`artist`.`name`"; + $this->set_join('left', '`song`', '`song`.`album`', '`album`.`id`', 100); + $this->set_join('left', '`artist`', '`song`.`artist`', '`artist`.`id`', 100); + break; + case 'year': + $sql = "`album`.`year`"; + break; + } // end switch + break; + case 'artist': + switch ($field) { + case 'name': + $sql = "`artist`.`name`"; + break; + } // end switch + break; + case 'playlist': + switch ($field) { + case 'type': + $sql = "`playlist`.`type`"; + break; + case 'name': + $sql = "`playlist`.`name`"; + break; + case 'user': + $sql = "`playlist`.`user`"; + break; + } // end switch + break; + case 'smartplaylist': + switch ($field) { + case 'type': + $sql = "`search`.`type`"; + break; + case 'name': + $sql = "`search`.`name`"; + break; + case 'user': + $sql = "`search`.`user`"; + break; + } // end switch on $field + break; + case 'live_stream': + switch ($field) { + case 'name': + $sql = "`live_stream`.`name`"; + break; + case 'codec': + $sql = "`live_stream`.`codec`"; + break; + } // end switch + break; + case 'genre': + switch ($field) { + case 'name': + $sql = "`genre`.`name`"; + break; + } // end switch + break; + case 'user': + switch ($field) { + case 'username': + $sql = "`user`.`username`"; + break; + case 'fullname': + $sql = "`user`.`fullname`"; + break; + case 'last_seen': + $sql = "`user`.`last_seen`"; + break; + case 'create_date': + $sql = "`user`.`create_date`"; + break; + } // end switch + break; + case 'video': + switch ($field) { + case 'title': + $sql = "`video`.`title`"; + break; + case 'resolution': + $sql = "`video`.`resolution_x`"; + break; + case 'length': + $sql = "`video`.`time`"; + break; + case 'codec': + $sql = "`video`.`video_codec`"; + break; + } // end switch + break; + case 'wanted': + switch ($field) { + case 'name': + $sql = "`wanted`.`name`"; + break; + case 'artist': + $sql = "`wanted`.`artist`"; + break; + case 'year': + $sql = "`wanted`.`year`"; + break; + case 'user': + $sql = "`wanted`.`user`"; + break; + case 'accepted': + $sql = "`wanted`.`accepted`"; + break; + } // end switch on field + break; + case 'share': + switch ($field) { + case 'object': + $sql = "`share`.`object_type`, `share`.`object.id`"; + break; + case 'object_type': + $sql = "`share`.`object_type`"; + break; + case 'user': + $sql = "`share`.`user`"; + break; + case 'creation_date': + $sql = "`share`.`creation_date`"; + break; + case 'lastvisit_date': + $sql = "`share`.`lastvisit_date`"; + break; + case 'counter': + $sql = "`share`.`counter`"; + break; + case 'max_counter': + $sql = "`share`.`max_counter`"; + break; + case 'allow_stream': + $sql = "`share`.`allow_stream`"; + break; + case 'allow_download': + $sql = "`share`.`allow_download`"; + break; + case 'expire': + $sql = "`share`.`expire`"; + break; + } // end switch on field + break; + case 'channel': + switch ($field) { + case 'name': + $sql = "`channel`.`name`"; + break; + case 'interface': + $sql = "`channel`.`interface`"; + break; + case 'port': + $sql = "`channel`.`port`"; + break; + case 'max_listeners': + $sql = "`channel`.`max_listeners`"; + break; + case 'listeners': + $sql = "`channel`.`listeners`"; + break; + } // end switch on field + break; + case 'broadcast': + switch ($field) { + case 'name': + $sql = "`broadcast`.`name`"; + break; + case 'user': + $sql = "`broadcast`.`user`"; + break; + case 'started': + $sql = "`broadcast`.`started`"; + break; + case 'listeners': + $sql = "`broadcast`.`listeners`"; + break; + } // end switch on field + break; + default: + // Rien a faire + break; + } // end switch + + if (isset($sql)) { return "$sql $order,"; } + + return ""; + + } // sql_sort + + /** + * resort_objects + * This takes the existing objects, looks at the current + * sort method and then re-sorts them This is internally + * called by the set_sort() function + */ + private function resort_objects() + { + // There are two ways to do this.. the easy way... + // and the vollmer way, hopefully we don't have to + // do it the vollmer way + if ($this->is_simple()) { + $sql = $this->get_sql(true); + } else { + // FIXME: this is fragile for large browses + // First pull the objects + $objects = $this->get_saved(); + + // If there's nothing there don't do anything + if (!count($objects) or !is_array($objects)) { + return false; + } + $type = $this->get_type(); + $where_sql = "WHERE `$type`.`id` IN ("; + + foreach ($objects as $object_id) { + $object_id = Dba::escape($object_id); + $where_sql .= "'$object_id',"; + } + $where_sql = rtrim($where_sql,','); + + $where_sql .= ")"; + + $sql = $this->get_base_sql(); + + $order_sql = " ORDER BY "; + + foreach ($this->_state['sort'] as $key => $value) { + $order_sql .= $this->sql_sort($key, $value); + } + // Clean her up + $order_sql = rtrim($order_sql,"ORDER BY "); + $order_sql = rtrim($order_sql,","); + + $sql = $sql . $this->get_join_sql() . $where_sql . $order_sql; + } // if not simple + + $db_results = Dba::read($sql); + + $results = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + $this->save_objects($results); + + return true; + + } // resort_objects + + /** + * store + * This saves the current state to the database + */ + public function store() + { + $id = $this->id; + if ($id != 'nocache') { + $data = serialize($this->_state); + + $sql = 'UPDATE `tmp_browse` SET `data` = ? ' . + 'WHERE `sid` = ? AND `id` = ?'; + Dba::write($sql, array($data, session_id(), $id)); + } + } + + /** + * save_objects + * This takes the full array of object ids, often passed into show and + * if necessary it saves them + */ + public function save_objects($object_ids) + { + // Saving these objects has two operations, one holds it in + // a local variable and then second holds it in a row in the + // tmp_browse table + + // Only do this if it's not a simple browse + if (!$this->is_simple()) { + $this->_cache = $object_ids; + $this->set_total(count($object_ids)); + $id = $this->id; + if ($id != 'nocache') { + $data = self::_serialize($this->_cache); + + $sql = 'UPDATE `tmp_browse` SET `object_data` = ? ' . + 'WHERE `sid` = ? AND `id` = ?'; + Dba::write($sql, array($data, session_id(), $id)); + } + } + + return true; + + } // save_objects + + /** + * get_state + * This is a debug only function + */ + public function get_state() + { + return $this->_state; + + } // get_state + +} // query diff --git a/sources/lib/class/radio.class.php b/sources/lib/class/radio.class.php new file mode 100644 index 0000000..df77f9d --- /dev/null +++ b/sources/lib/class/radio.class.php @@ -0,0 +1,220 @@ +get_info($id, 'live_stream'); + + // Set the vars + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + } // constructor + + /** + * format + * This takes the normal data from the database and makes it pretty + * for the users, the new variables are put in f_??? and f_???_link + */ + public function format() + { + // Default link used on the rightbar + $this->f_link = "url\">$this->name"; + $this->f_name_link = "site_url\">$this->name"; + $this->f_url_link = "url\">$this->url"; + + return true; + + } // format + + /** + * update + * This is a static function that takes a key'd array for input + * it depends on a ID element to determine which radio element it + * should be updating + */ + public static function update($data) + { + // Verify the incoming data + if (!$data['id']) { + Error::add('general', T_('Missing ID')); + } + + if (!$data['name']) { + Error::add('general', T_('Name Required')); + } + + $allowed_array = array('https','http','mms','mmsh','mmsu','mmst','rtsp','rtmp'); + + $elements = explode(":",$data['url']); + + if (!in_array($elements['0'],$allowed_array)) { + Error::add('general', T_('Invalid URL must be mms:// , https:// or http://')); + } + + if (Error::occurred()) { + return false; + } + + $sql = "UPDATE `live_stream` SET `name` = ?,`site_url` = ?,`url` = ?, codec = ? WHERE `id` = ?"; + $db_results = Dba::write($sql, array($data['name'], $data['site_url'], $data['url'], $data['codec'], $data['id'])); + + return $db_results; + + } // update + + /** + * create + * This is a static function that takes a key'd array for input + * and if everything is good creates the object. + */ + public static function create($data) + { + // Make sure we've got a name + if (!strlen($data['name'])) { + Error::add('name', T_('Name Required')); + } + + $allowed_array = array('https','http','mms','mmsh','mmsu','mmst','rtsp','rtmp'); + + $elements = explode(":", $data['url']); + + if (!in_array($elements['0'],$allowed_array)) { + Error::add('url', T_('Invalid URL must be http:// or https://')); + } + + // Make sure it's a real catalog + $catalog = Catalog::create_from_id($data['catalog']); + if (!$catalog->name) { + Error::add('catalog', T_('Invalid Catalog')); + } + + if (Error::occurred()) { return false; } + + // If we've made it this far everything must be ok... I hope + $sql = "INSERT INTO `live_stream` (`name`,`site_url`,`url`,`catalog`,`codec`) " . + "VALUES (?, ?, ?, ?, ?)"; + $db_results = Dba::write($sql, array($data['name'], $data['site_url'], $data['url'], $catalog->id, $data['codec'])); + + return $db_results; + + } // create + + /** + * delete + * This deletes the current object from the database + */ + public function delete() + { + $sql = "DELETE FROM `live_stream` WHERE `id` = ?"; + Dba::write($sql, array($this->id)); + + return true; + + } // delete + + /** + * get_stream_types + * This is needed by the media interface + */ + public function get_stream_types() + { + return array('foreign'); + } // native_stream + + /** + * play_url + * This is needed by the media interface + */ + public static function play_url($oid, $additional_params='',$sid='',$force_http='') + { + $radio = new Radio($oid); + + return $radio->url . $additional_params; + + } // play_url + + /** + * get_transcode_settings + * + * This will probably never be implemented + */ + public function get_transcode_settings($target = null) + { + return false; + } + + public static function get_all_radios($catalog = null) + { + $sql = "SELECT `live_stream`.`id` FROM `live_stream` JOIN `catalog` ON `catalog`.`id` = `live_stream`.`catalog` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "WHERE `catalog`.`enabled` = '1' "; + } + $params = array(); + if ($catalog) { + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND "; + } + $sql .= "`catalog`.`id` = ?"; + $params[] = $catalog; + } + $db_results = Dba::read($sql, $params); + $radios = array(); + + while ($results = Dba::fetch_assoc($db_results)) { + $radios[] = $results['id']; + } + + return $radios; + } + +} //end of radio class diff --git a/sources/lib/class/random.class.php b/sources/lib/class/random.class.php new file mode 100644 index 0000000..5d38326 --- /dev/null +++ b/sources/lib/class/random.class.php @@ -0,0 +1,362 @@ +get_recently_played('1', 'album'); + $where_sql = ""; + if ($data[0]) { + $where_sql = " AND `song`.`album`='" . $data[0] . "' "; + } + + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` " . + "WHERE `catalog`.`enabled` = '1' "; + } else { + $sql .= "WHERE '1' = '1' "; + } + $sql .= "$where_sql ORDER BY RAND() LIMIT $limit"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_album + + /** + * get_artist + * This looks at the last artist played and then randomly picks a song from the + * same artist + */ + public static function get_artist($limit) + { + $results = array(); + + $data = $GLOBALS['user']->get_recently_played('1','artist'); + $where_sql = ""; + if ($data[0]) { + $where_sql = " AND `song`.`artist`='" . $data[0] . "' "; + } + + $sql = "SELECT `song`.`id` FROM `song` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` " . + "WHERE `catalog`.`enabled` = '1' "; + } else { + $sql .= "WHERE '1' = '1' "; + } + $sql .= "$where_sql ORDER BY RAND() LIMIT $limit"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_artist + + /** + * advanced + * This processes the results of a post from a form and returns an + * array of song items that were returned from said randomness + */ + public static function advanced($type, $data) + { + /* Figure out our object limit */ + $limit = intval($data['random']); + + // Generate our matchlist + + /* If they've passed -1 as limit then get everything */ + $limit_sql = ""; + if ($data['random'] == "-1") { unset($data['random']); } else { $limit_sql = "LIMIT " . Dba::escape($limit); } + + $search_data = Search::clean_request($data); + + $search_info = false; + + if (count($search_data) > 1) { + $search = new Search($type); + $search->parse_rules($search_data); + $search_info = $search->to_sql(); + } + + $sql = ""; + switch ($type) { + case 'song': + $sql = "SELECT `song`.`id`, `size`, `time` " . + "FROM `song` "; + if ($search_info) { + $sql .= $search_info['table_sql']; + } + if (AmpConfig::get('catalog_disable')) { + $sql .= " LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog`"; + $sql .= " WHERE `catalog`.`enabled` = '1'"; + } + if ($search_info) { + if (AmpConfig::get('catalog_disable')) { + $sql .= ' AND ' . $search_info['where_sql']; + } else { + $sql .= ' WHERE ' . $search_info['where_sql']; + } + } + break; + case 'album': + $sql = "SELECT `album`.`id`, SUM(`song`.`size`) AS `size`, SUM(`song`.`time`) AS `time` FROM `album` "; + if (! $search_info || ! $search_info['join']['song']) { + $sql .= "LEFT JOIN `song` ON `song`.`album`=`album`.`id` "; + } + if ($search_info) { + $sql .= $search_info['table_sql']; + } + if (AmpConfig::get('catalog_disable')) { + $sql .= " LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog`"; + $sql .= " WHERE `catalog`.`enabled` = '1'"; + } + if ($search_info) { + if (AmpConfig::get('catalog_disable')) { + $sql .= ' AND ' . $search_info['where_sql']; + } else { + $sql .= ' WHERE ' . $search_info['where_sql']; + } + } + $sql .= ' GROUP BY `album`.`id`'; + break; + case 'artist': + $sql = "SELECT `artist`.`id`, SUM(`song`.`size`) AS `size`, SUM(`song`.`time`) AS `time` FROM `artist` "; + if (! $search_info || ! $search_info['join']['song']) { + $sql .= "LEFT JOIN `song` ON `song`.`artist`=`artist`.`id` "; + } + if ($search_info) { + $sql .= $search_info['table_sql']; + } + if (AmpConfig::get('catalog_disable')) { + $sql .= " LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog`"; + $sql .= " WHERE `catalog`.`enabled` = '1'"; + } + if ($search_info) { + if (AmpConfig::get('catalog_disable')) { + $sql .= ' AND ' . $search_info['where_sql']; + } else { + $sql .= ' WHERE ' . $search_info['where_sql']; + } + } + $sql .= ' GROUP BY `artist`.`id`'; + break; + } + $sql .= " ORDER BY RAND() $limit_sql"; + + // Run the query generated above so we can while it + $db_results = Dba::read($sql); + $results = array(); + + $size_total = 0; + $fuzzy_size = 0; + $time_total = 0; + $fuzzy_time = 0; + while ($row = Dba::fetch_assoc($db_results)) { + + // If size limit is specified + if ($data['size_limit']) { + // Convert + $new_size = ($row['size'] / 1024) / 1024; + + // Only fuzzy 100 times + if ($fuzzy_size > 100) { + break; + } + + // Add and check, skip if over size + if (($size_total + $new_size) > $data['size_limit']) { + $fuzzy_size++; + continue; + } + + $size_total = $size_total + $new_size; + $results[] = $row['id']; + + // If we are within 4mb of target then jump ship + if (($data['size_limit'] - floor($size_total)) < 4) { + break; } + } // if size_limit + + // If length really does matter + if ($data['length']) { + // base on min, seconds are for chumps and chumpettes + $new_time = floor($row['time'] / 60); + + if ($fuzzy_time > 100) { + break;; + } + + // If the new one would go over skip! + if (($time_total + $new_time) > $data['length']) { + $fuzzy_time++; + continue; + } + + $time_total = $time_total + $new_time; + $results[] = $row['id']; + + // If there are less then 2 min of free space return + if (($data['length'] - $time_total) < 2) { + return $results; + } + } // if length does matter + + if (!$data['size_limit'] && !$data['length']) { + $results[] = $row['id']; + } + + } // end while results + + switch ($type) { + case 'song': + return $results; + case 'album': + $songs = array(); + foreach ($results as $result) { + $album = new Album($result); + $songs = array_merge($songs, $album->get_songs()); + } + return $songs; + case 'artist': + $songs = array(); + foreach ($results as $result) { + $artist = new Artist($result); + $songs = array_merge($songs, $artist->get_songs()); + } + return $songs; + default: + return false; + } + } // advanced + +} //end of random class diff --git a/sources/lib/class/rating.class.php b/sources/lib/class/rating.class.php new file mode 100644 index 0000000..fbe345e --- /dev/null +++ b/sources/lib/class/rating.class.php @@ -0,0 +1,276 @@ +id = intval($id); + $this->type = $type; + + return true; + + } // Constructor + + /** + * gc + * + * Remove ratings for items that no longer exist. + */ + public static function gc() + { + foreach (array('song', 'album', 'artist', 'video') as $object_type) { + Dba::write("DELETE FROM `rating` USING `rating` LEFT JOIN `$object_type` ON `$object_type`.`id` = `rating`.`object_id` WHERE `object_type` = '$object_type' AND `$object_type`.`id` IS NULL"); + } + } + + /** + * build_cache + * This attempts to get everything we'll need for this page load in a + * single query, saving on connection overhead + */ + public static function build_cache($type, $ids) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + $ratings = array(); + $user_ratings = array(); + + $idlist = '(' . implode(',', $ids) . ')'; + $sql = "SELECT `rating`, `object_id` FROM `rating` " . + "WHERE `user` = ? AND `object_id` IN $idlist " . + "AND `object_type` = ?"; + $db_results = Dba::read($sql, array($GLOBALS['user']->id, $type)); + + while ($row = Dba::fetch_assoc($db_results)) { + $user_ratings[$row['object_id']] = $row['rating']; + } + + $sql = "SELECT AVG(`rating`) as `rating`, `object_id` FROM " . + "`rating` WHERE `object_id` IN $idlist AND " . + "`object_type` = ? GROUP BY `object_id`"; + $db_results = Dba::read($sql, array($type)); + + while ($row = Dba::fetch_assoc($db_results)) { + $ratings[$row['object_id']] = $row['rating']; + } + + foreach ($ids as $id) { + // First store the user-specific rating + if (!isset($user_ratings[$id])) { + $rating = 0; + } else { + $rating = intval($user_ratings[$id]); + } + parent::add_to_cache('rating_' . $type . '_user' . $GLOBALS['user']->id, $id, $rating); + + // Then store the average + if (!isset($ratings[$id])) { + $rating = 0; + } else { + $rating = round($ratings[$id], 1); + } + parent::add_to_cache('rating_' . $type . '_all', $id, $rating); + } + + return true; + + } // build_cache + + /** + * get_user_rating + * Get a user's rating. If no userid is passed in, we use the currently + * logged in user. + */ + public function get_user_rating($user_id = null) + { + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + + $key = 'rating_' . $this->type . '_user' . $user_id; + if (parent::is_cached($key, $this->id)) { + return parent::get_from_cache($key, $this->id); + } + + $sql = "SELECT `rating` FROM `rating` WHERE `user` = ? ". + "AND `object_id` = ? AND `object_type` = ?"; + $db_results = Dba::read($sql, array($user_id, $this->id, $this->type)); + + $rating = 0; + + if ($results = Dba::fetch_assoc($db_results)) { + $rating = $results['rating']; + } + + parent::add_to_cache($key, $this->id, $rating); + return $rating; + + } // get_user_rating + + /** + * get_average_rating + * Get the floored average rating of what everyone has rated this object + * as. This is shown if there is no personal rating. + */ + public function get_average_rating() + { + if (parent::is_cached('rating_' . $this->type . '_all', $this->id)) { + return parent::get_from_cache('rating_' . $this->type . '_user', $this->id); + } + + $sql = "SELECT AVG(`rating`) as `rating` FROM `rating` WHERE " . + "`object_id` = ? AND `object_type` = ?"; + $db_results = Dba::read($sql, array($this->id, $this->type)); + + $results = Dba::fetch_assoc($db_results); + + parent::add_to_cache('rating_' . $this->type . '_all', $this->id, $results['rating']); + return $results['rating']; + + } // get_average_rating + + /** + * get_highest_sql + * Get highest sql + */ + public static function get_highest_sql($type) + { + $type = Stats::validate_type($type); + $sql = "SELECT `object_id` as `id`, AVG(`rating`) AS `rating` FROM rating" . + " WHERE object_type = '" . $type . "'"; + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND " . Catalog::get_enable_filter($type, '`object_id`'); + } + $sql .= " GROUP BY object_id ORDER BY `rating` DESC "; + return $sql; + } + + /** + * get_highest + * Get objects with the highest average rating. + */ + public static function get_highest($type, $count='', $offset='') + { + if (!$count) { + $count = AmpConfig::get('popular_threshold'); + } + $count = intval($count); + if (!$offset) { + $limit = $count; + } else { + $limit = intval($offset) . "," . $count; + } + + /* Select Top objects counting by # of rows */ + $sql = self::get_highest_sql($type); + $sql .= "LIMIT $limit"; + $db_results = Dba::read($sql, array($type)); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } + + /** + * set_rating + * This function sets the rating for the current object. + * If no userid is passed in, we use the currently logged in user. + */ + public function set_rating($rating, $user_id = null) + { + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + $user_id = intval($user_id); + + debug_event('Rating', "Setting rating for $this->type $this->id to $rating", 5); + + // If score is -1, then remove rating + if ($rating == '-1') { + $sql = "DELETE FROM `rating` WHERE " . + "`object_id` = ? AND " . + "`object_type` = ? AND " . + "`user` = ?"; + $params = array($this->id, $this->type, $user_id); + } else { + $sql = "REPLACE INTO `rating` " . + "(`object_id`, `object_type`, `rating`, `user`) " . + "VALUES (?, ?, ?, ?)"; + $params = array($this->id, $this->type, $rating, $user_id); + } + Dba::write($sql, $params); + + parent::add_to_cache('rating_' . $this->type . '_user' . $user_id, $this->id, $rating); + + foreach (Plugin::get_plugins('save_rating') as $plugin_name) { + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $plugin->_plugin->save_rating($this, $rating); + } + } + + return true; + + } // set_rating + + /** + * show + * This takes an id and a type and displays the rating if ratings are + * enabled. If $static is true, the rating won't be editable. + */ + public static function show($object_id, $type, $static=false) + { + // If ratings aren't enabled don't do anything + if (!AmpConfig::get('ratings')) { return false; } + + $rating = new Rating($object_id, $type); + + if ($static) { + require AmpConfig::get('prefix') . '/templates/show_static_object_rating.inc.php'; + } else { + require AmpConfig::get('prefix') . '/templates/show_object_rating.inc.php'; + } + + } // show + +} //end rating class diff --git a/sources/lib/class/recommendation.class.php b/sources/lib/class/recommendation.class.php new file mode 100644 index 0000000..abd237a --- /dev/null +++ b/sources/lib/class/recommendation.class.php @@ -0,0 +1,353 @@ +body; + + return simplexml_load_string($content); + } // get_lastfm_results + + /** + * gc + * + * This cleans out old recommendations cache + */ + public static function gc() + { + Dba::write('DELETE FROM `recommendation` WHERE `last_update` < ?', array((time() - 604800))); + } + + protected static function get_recommendation_cache($type, $id, $get_items = false) + { + self::gc(); + + $sql = "SELECT `id`, `last_update` FROM `recommendation` WHERE `object_type` = ? AND `object_id` = ?"; + $db_results = Dba::read($sql, array($type, $id)); + + if ($cache = Dba::fetch_assoc($db_results)) { + if ($get_items) { + $cache['items'] = array(); + $sql = "SELECT `recommendation_id`, `name`, `rel`, `mbid` FROM `recommendation_item` WHERE `recommendation` = ?"; + $db_results = Dba::read($sql, array($cache['id'])); + while ($results = Dba::fetch_assoc($db_results)) { + $cache['items'][] = array( + 'id' => $results['recommendation_id'], + 'name' => $results['name'], + 'rel' => $results['rel'], + 'mbid' => $results['mbid'], + ); + } + } + } + + return $cache; + } + + protected static function delete_recommendation_cache($type, $id) + { + $cache = self::get_recommendation_cache($type, $id); + if ($cache['id']) { + Dba::write('DELETE FROM `recommendation_item` WHERE `recommendation` = ?', array($cache['id'])); + Dba::write('DELETE FROM `recommendation` WHERE `id` = ?', array($cache['id'])); + } + } + + protected static function update_recommendation_cache($type, $id, $recommendations) + { + self::delete_recommendation_cache($type, $id); + $sql = "INSERT INTO `recommendation` (`object_type`, `object_id`, `last_update`) VALUES (?, ?, ?)"; + Dba::write($sql, array($type, $id, time())); + $insertid = Dba::insert_id(); + foreach ($recommendations as $recommendation) { + $sql = "INSERT INTO `recommendation_item` (`recommendation`, `recommendation_id`, `name`, `rel`, `mbid`) VALUES (?, ?, ?, ?, ?)"; + Dba::write($sql, array($insertid, $recommendation['id'], $recommendation['name'], $recommendation['rel'], $recommendation['mbid'])); + } + } + + /** + * get_songs_like + * Returns a list of similar songs + */ + public static function get_songs_like($song_id, $limit = 5, $local_only = true) + { + $song = new Song($song_id); + + if (isset($song->mbid)) { + $query = 'mbid=' . rawurlencode($song->mbid); + } else { + $query = 'track=' . rawurlencode($song->title); + } + + $cache = self::get_recommendation_cache('song', $song_id, true); + if (!$cache['id']) { + $similars = array(); + $xml = self::get_lastfm_results('track.getsimilar', $query); + + if ($xml->similartracks) { + foreach ($xml->similartracks->children() as $child) { + $name = $child->name; + $local_id = null; + + $artist_name = $child->artist->name; + $s_artist_name = Catalog::trim_prefix($artist_name); + + $sql = "SELECT `song`.`id` FROM `song` " . + "LEFT JOIN `artist` ON " . + "`song`.`artist`=`artist`.`id` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `song`.`catalog` = `catalog`.`id` "; + } + $sql .= "WHERE `song`.`title` = ? " . + "AND `artist`.`name` = ? "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1'"; + } + + $db_result = Dba::read($sql, array($name, $s_artist_name['string'])); + + if ($result = Dba::fetch_assoc($db_result)) { + $local_id = $result['id']; + } + + if (is_null($local_id)) { + debug_event('Recommendation', "$name did not match any local song", 5); + $similars[] = array( + 'id' => null, + 'name' => $name, + 'rel' => $artist_name + ); + } else { + debug_event('Recommendation', "$name matched local song $local_id", 5); + $similars[] = array( + 'id' => $local_id, + 'name' => $name + ); + } + } + + if (count($similars) > 0) { + self::update_recommendation_cache('song', $song_id, $similars); + } + } + } + + if (!isset($similars) || count($similars) == 0) { + $similars = $cache['items']; + } + if ($similars) { + $results = array(); + foreach ($similars as $similar) { + if (!$local_only || !is_null($similar['id'])) { + $results[] = $similar; + } + + if ($limit && count($results) >= $limit) { + break; + } + } + } + + if (isset($results)) { + return $results; + } + + return false; + } + + /** + * get_artists_like + * Returns a list of similar artists + */ + public static function get_artists_like($artist_id, $limit = 10, $local_only = true) + { + $artist = new Artist($artist_id); + + $cache = self::get_recommendation_cache('artist', $artist_id, true); + if (!$cache['id']) { + $similars = array(); + $query = 'artist=' . rawurlencode($artist->name); + + $xml = self::get_lastfm_results('artist.getsimilar', $query); + + foreach ($xml->similarartists->children() as $child) { + $name = $child->name; + $mbid = (string) $child->mbid; + $local_id = null; + + // First we check by MBID + if ($mbid) { + $sql = "SELECT `artist`.`id` FROM `artist` WHERE `mbid` = ?"; + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND " . Catalog::get_enable_filter('artist', '`artist`.`id`'); + } + $db_result = Dba::read($sql, array($mbid)); + if ($result = Dba::fetch_assoc($db_result)) { + $local_id = $result['id']; + } + } + + // Then we fall back to the less likely to work exact + // name match + if (is_null($local_id)) { + $searchname = Catalog::trim_prefix($name); + $searchname = Dba::escape($searchname['string']); + $sql = "SELECT `artist`.`id` FROM `artist` WHERE `name` = ?"; + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND " . Catalog::get_enable_filter('artist', '`artist`.`id`'); + } + $db_result = Dba::read($sql, array($searchname)); + if ($result = Dba::fetch_assoc($db_result)) { + $local_id = $result['id']; + } + } + + // Then we give up + if (is_null($local_id)) { + debug_event('Recommendation', "$name did not match any local artist", 5); + $similars[] = array( + 'id' => null, + 'name' => $name, + 'mbid' => $mbid + ); + } else { + debug_event('Recommendation', "$name matched local artist " . $local_id, 5); + $similars[] = array( + 'id' => $local_id, + 'name' => $name + ); + } + } + + if (count($similars) > 0) { + self::update_recommendation_cache('artist', $artist_id, $similars); + } + } + + if (!isset($similars) || count($similars) == 0) { + $similars = $cache['items']; + } + if ($similars) { + $results = array(); + foreach ($similars as $similar) { + if (!$local_only || !is_null($similar['id'])) { + $results[] = $similar; + } + + if ($limit && count($results) >= $limit) { + break; + } + } + } + + if (isset($results)) { + return $results; + } + + return false; + } // get_artists_like + + /** + * get_artist_info + * Returns artist information + */ + public static function get_artist_info($artist_id, $fullname='') + { + $artist = null; + if ($artist_id) { + $artist = new Artist($artist_id); + $artist->format(); + $fullname = $artist->f_full_name; + + // Data newer than 6 months, use it + if (($artist->last_update + 15768000) > time()) { + $results = array(); + $results['summary'] = $artist->summary; + $results['placeformed'] = $artist->placeformed; + $results['yearformed'] = $artist->yearformed; + $results['largephoto'] = Art::url($artist->id, 'artist'); + $results['megaphoto'] = $results['largephoto']; + return $results; + } + } + + $query = 'artist=' . rawurlencode($fullname); + + $xml = self::get_lastfm_results('artist.getinfo', $query); + + $results = array(); + $results['summary'] = strip_tags(preg_replace("#.#", "", (string) $xml->artist->bio->summary)); + $results['placeformed'] = (string) $xml->artist->bio->placeformed; + $results['yearformed'] = (string) $xml->artist->bio->yearformed; + $results['largephoto'] = $xml->artist->image[2]; + $results['megaphoto'] = $xml->artist->image[4]; + + if ($artist) { + if (!empty($results['summary']) || !empty($results['megaphoto'])) { + $artist->update_artist_info($results['summary'], $results['placeformed'], $results['yearformed']); + + $image = Art::get_from_source(array('url' => $results['megaphoto']), 'artist'); + $rurl = pathinfo($results['megaphoto']); + $mime = 'image/' . $rurl['extension']; + $art = new Art($artist->id, 'artist'); + $art->reset(); + $art->insert($image, $mime); + $results['largephoto'] = Art::url($artist->id, 'artist'); + $results['megaphoto'] = $results['largephoto']; + } + } + + return $results; + } // get_artist_info + +} // end of recommendation class diff --git a/sources/lib/class/registration.class.php b/sources/lib/class/registration.class.php new file mode 100644 index 0000000..13ee6d6 --- /dev/null +++ b/sources/lib/class/registration.class.php @@ -0,0 +1,115 @@ +set_default_sender(); + + $mailer->subject = sprintf(T_("New User Registration at %s"), AmpConfig::get('site_title')); + + $mailer->message = sprintf(T_("Thank you for registering\n\n +Please keep this e-mail for your records. Your account information is as follows: +---------------------- +Username: %s +---------------------- + +Your account is currently inactive. You cannot use it until you've visited the following link: + +%s + +Thank you for registering +"), $username, AmpConfig::get('web_path') . "/register.php?action=validate&username=$username&auth=$validation"); + + $mailer->recipient = $email; + $mailer->recipient_name = $fullname; + + $mailer->send(); + + // Check to see if the admin should be notified + if (AmpConfig::get('admin_notify_reg')) { + $mailer->message = sprintf(T_("A new user has registered +The following values were entered. + +Username: %s +Fullname: %s +E-mail: %s +Website: %s + +"), $username, $fullname, $email, $website); + + $mailer->send_to_group('admins'); + } + + return true; + + } // send_confirmation + + /** + * show_agreement + * This shows the registration agreement, /config/registration_agreement.php + */ + public static function show_agreement() + { + $filename = AmpConfig::get('prefix') . '/config/registration_agreement.php'; + + if (!file_exists($filename)) { return false; } + + /* Check for existance */ + $fp = fopen($filename,'r'); + + if (!$fp) { return false; } + + $data = fread($fp,filesize($filename)); + + /* Scrub and show */ + echo $data; + + } // show_agreement + +} // end registration class diff --git a/sources/lib/class/scrobbler.class.php b/sources/lib/class/scrobbler.class.php new file mode 100644 index 0000000..24a9fc6 --- /dev/null +++ b/sources/lib/class/scrobbler.class.php @@ -0,0 +1,270 @@ +error_msg = ''; + $this->username = trim($username); + $this->password = trim($password); + $this->challenge = $challenge; + $this->submit_host = $host; + $this->submit_port = $port; + $this->submit_url = $url; + $this->queued_tracks = array(); + if ($scrobble_host) { $this->scrobble_host = $scrobble_host; } + + } // scrobbler + + /** + * get_error_msg + */ + public function get_error_msg() + { + return $this->error_msg; + + } // get_error_msg + + /** + * get_queue_count + */ + public function get_queue_count() + { + return count($this->queued_tracks); + + } // get_queue_count + + /** + * handshake + * This does a handshake with the audioscrobber server it doesn't pass the password, but + * it does pass the username and has a 10 second timeout + */ + public function handshake() + { + $data = array(); + $as_socket = fsockopen($this->scrobble_host, 80, $errno, $errstr, 2); + if (!$as_socket) { + $this->error_msg = $errstr; + return false; + } + + $username = rawurlencode($this->username); + $timestamp = time(); + $auth_token = rawurlencode(md5($this->password . $timestamp)); + + $get_string = "GET /?hs=true&p=1.2&c=apa&v=0.1&u=$username&t=$timestamp&a=$auth_token HTTP/1.1\r\n"; + + fwrite($as_socket, $get_string); + fwrite($as_socket, "Host: $this->scrobble_host\r\n"); + fwrite($as_socket, "Accept: */*\r\n\r\n"); + + $buffer = ''; + while (!feof($as_socket)) { + $buffer .= fread($as_socket, 4096); + } + fclose($as_socket); + $split_response = preg_split("/\r\n\r\n/", $buffer); + if (!isset($split_response[1])) { + $this->error_msg = 'Did not receive a valid response'; + return false; + } + $response = explode("\n", $split_response[1]); + + // Handle the fact Libre.FM has extranious values at the start of it's handshake response + if (is_numeric(trim($response['0']))) { + array_shift($response); + debug_event('SCROBBLER','Junk in handshake, removing first line',1); + } + if (substr($response[0], 0, 6) == 'FAILED') { + $this->error_msg = substr($response[0], 7); + return false; + } + if (substr($response[0], 0, 7) == 'BADUSER') { + $this->error_msg = 'Invalid Username'; + return false; + } + if (substr($response[0],0,7) == 'BADTIME') { + $this->error_msg = 'Your time is too far off from the server, or your PHP timezone is incorrect'; + return false; + } + if (substr($response[0], 0, 6) == 'UPDATE') { + $this->error_msg = 'You need to update your client: '.substr($response[0], 7); + return false; + } + + if (preg_match('/http:\/\/([^\/]+)\/(.*)$/', $response[3], $matches)) { + $host_parts = explode(":",$matches[1]); + $data['submit_host'] = $host_parts[0]; + $data['submit_port'] = $host_parts[1] ? $host_parts[1] : '80'; + $data['submit_url'] = '/' . $matches[2]; + } else { + $this->error_msg = "Invalid POST URL returned, unable to continue. Sent:\n$get_string\n----\nReceived:\n" . $buffer . + "\n---------\nExpected:" . print_r($response, true); + return false; + } + + // Remove any extra junk around the challenge + $data['challenge'] = trim($response[1]); + return $data; + + } // handshake + + /** + * queue_track + * This queues the LastFM track by storing it in this object, it doesn't actually + * submit the track or talk to LastFM in anyway, kind of useless for our uses but its + * here, and that's how it is. + */ + public function queue_track($artist, $album, $title, $timestamp, $length,$track) + { + if ($length < 30) { + debug_event('Scrobbler',"Not queuing track, too short",'5'); + return false; + } + + $newtrack = array(); + $newtrack['artist'] = $artist; + $newtrack['album'] = $album; + $newtrack['title'] = $title; + $newtrack['track'] = $track; + $newtrack['length'] = $length; + $newtrack['time'] = $timestamp; + + $this->queued_tracks[$timestamp] = $newtrack; + return true; + + } // queue_track + + /** + * submit_tracks + * This actually talks to LastFM submiting the tracks that are queued up. It + * passed the md5'd password combinted with the challenge, which is then md5'd + */ + public function submit_tracks() + { + // Check and make sure that we've got some queued tracks + if (!count($this->queued_tracks)) { + $this->error_msg = "No tracks to submit"; + return false; + } + + //sort array by timestamp + ksort($this->queued_tracks); + + // build the query string + $query_str = 's='.rawurlencode($this->challenge).'&'; + + $i = 0; + + foreach ($this->queued_tracks as $track) { + $query_str .= "a[$i]=".rawurlencode($track['artist'])."&t[$i]=".rawurlencode($track['title'])."&b[$i]=".rawurlencode($track['album'])."&"; + $query_str .= "m[$i]=&l[$i]=".rawurlencode($track['length'])."&i[$i]=".rawurlencode($track['time'])."&"; + $query_str .= "n[$i]=" . rawurlencode($track['track']) . "&o[$i]=P&r[$i]=&"; + $i++; + } + + if (!trim($this->submit_host) || !$this->submit_port) { + $this->reset_handshake = true; + return false; + } + + $as_socket = fsockopen($this->submit_host, intval($this->submit_port), $errno, $errstr, 2); + + if (!$as_socket) { + $this->error_msg = $errstr; + $this->reset_handshake = true; + return false; + } + + $action = "POST ".$this->submit_url." HTTP/1.0\r\n"; + fwrite($as_socket, $action); + fwrite($as_socket, "Host: ".$this->submit_host."\r\n"); + fwrite($as_socket, "Accept: */*\r\n"); + fwrite($as_socket, "User-Agent: Ampache/3.6\r\n"); + fwrite($as_socket, "Content-type: application/x-www-form-urlencoded\r\n"); + fwrite($as_socket, "Content-length: ".strlen($query_str)."\r\n\r\n"); + + fwrite($as_socket, $query_str."\r\n\r\n"); + // Allow us to debug this + debug_event('SCROBBLER','Query String:' . $query_str,6); + + $buffer = ''; + while (!feof($as_socket)) { + $buffer .= fread($as_socket, 8192); + } + fclose($as_socket); + + $split_response = preg_split("/\r\n\r\n/", $buffer); + if (!isset($split_response[1])) { + $this->error_msg = 'Did not receive a valid response'; + $this->reset_handshake = true; + return false; + } + $response = explode("\n", $split_response[1]); + if (!isset($response[0])) { + $this->error_msg = 'Unknown error submitting tracks'. + "\nDebug output:\n".$buffer; + $this->reset_handshake = true; + return false; + } + if (substr($response[0], 0, 6) == 'FAILED') { + $this->error_msg = $response[0]; + $this->reset_handshake = true; + return false; + } + if (substr($response[0], 0, 7) == 'BADAUTH') { + $this->error_msg = 'Invalid username/password (' . trim($response[0]) . ')'; + return false; + } + if (substr($response[0],0,10) == 'BADSESSION') { + $this->error_msg = 'Invalid Session passed (' . trim($response[0]) . ')'; + $this->reset_handshake = true; + return false; + } + if (substr($response[0], 0, 2) != 'OK') { + $this->error_msg = 'Response Not ok, unknown error'. + "\nDebug output:\n".$buffer; + $this->reset_handshake = true; + return false; + } + + return true; + + } // submit_tracks + +} // end audioscrobbler class diff --git a/sources/lib/class/scrobbler_async.class.php b/sources/lib/class/scrobbler_async.class.php new file mode 100644 index 0000000..15e7d5f --- /dev/null +++ b/sources/lib/class/scrobbler_async.class.php @@ -0,0 +1,39 @@ +user = $user; + $this->song_info = $song_info; + } + + public function run() + { + spl_autoload_register(array('Core', 'autoload'), true, true); + Requests::register_autoloader(); + if ($this->song_info) { + User::save_songplay($this->user, $this->song_info); + } + } +} diff --git a/sources/lib/class/search.class.php b/sources/lib/class/search.class.php new file mode 100644 index 0000000..a29048d --- /dev/null +++ b/sources/lib/class/search.class.php @@ -0,0 +1,1265 @@ +searchtype = $searchtype; + if ($id) { + $info = $this->get_info($id); + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + $this->rules = unserialize($this->rules); + } + + // Define our basetypes + + $this->basetypes['numeric'][] = array( + 'name' => 'gte', + 'description' => T_('is greater than or equal to'), + 'sql' => '>=' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'lte', + 'description' => T_('is less than or equal to'), + 'sql' => '<=' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'equal', + 'description' => T_('is'), + 'sql' => '<=>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'ne', + 'description' => T_('is not'), + 'sql' => '<>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'gt', + 'description' => T_('is greater than'), + 'sql' => '>' + ); + + $this->basetypes['numeric'][] = array( + 'name' => 'lt', + 'description' => T_('is less than'), + 'sql' => '<' + ); + + + $this->basetypes['boolean'][] = array( + 'name' => 'true', + 'description' => T_('is true') + ); + + $this->basetypes['boolean'][] = array( + 'name' => 'false', + 'description' => T_('is false') + ); + + + $this->basetypes['text'][] = array( + 'name' => 'contain', + 'description' => T_('contains'), + 'sql' => 'LIKE', + 'preg_match' => array('/^/','/$/'), + 'preg_replace' => array('%', '%') + ); + + $this->basetypes['text'][] = array( + 'name' => 'notcontain', + 'description' => T_('does not contain'), + 'sql' => 'NOT LIKE', + 'preg_match' => array('/^/','/$/'), + 'preg_replace' => array('%', '%') + ); + + $this->basetypes['text'][] = array( + 'name' => 'start', + 'description' => T_('starts with'), + 'sql' => 'LIKE', + 'preg_match' => '/$/', + 'preg_replace' => '%' + ); + + $this->basetypes['text'][] = array( + 'name' => 'end', + 'description' => T_('ends with'), + 'sql' => 'LIKE', + 'preg_match' => '/^/', + 'preg_replace' => '%' + ); + + $this->basetypes['text'][] = array( + 'name' => 'equal', + 'description' => T_('is'), + 'sql' => '=' + ); + + $this->basetypes['text'][] = array( + 'name' => 'sounds', + 'description' => T_('sounds like'), + 'sql' => 'SOUNDS LIKE' + ); + + $this->basetypes['text'][] = array( + 'name' => 'notsounds', + 'description' => T_('does not sound like'), + 'sql' => 'NOT SOUNDS LIKE' + ); + + + $this->basetypes['boolean_numeric'][] = array( + 'name' => 'equal', + 'description' => T_('is'), + 'sql' => '<=>' + ); + + $this->basetypes['boolean_numeric'][] = array( + 'name' => 'ne', + 'description' => T_('is not'), + 'sql' => '<>' + ); + + + $this->basetypes['boolean_subsearch'][] = array( + 'name' => 'equal', + 'description' => T_('is'), + 'sql' => '' + ); + + $this->basetypes['boolean_subsearch'][] = array( + 'name' => 'ne', + 'description' => T_('is not'), + 'sql' => 'NOT' + ); + + + $this->basetypes['date'][] = array( + 'name' => 'lt', + 'description' => T_('before'), + 'sql' => '<' + ); + + $this->basetypes['date'][] = array( + 'name' => 'gt', + 'description' => T_('after'), + 'sql' => '>' + ); + + switch ($searchtype) { + case 'song': + $this->types[] = array( + 'name' => 'anywhere', + 'label' => T_('Any searchable text'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'title', + 'label' => T_('Title'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'album', + 'label' => T_('Album'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'artist', + 'label' => T_('Artist'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'comment', + 'label' => T_('Comment'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + + $this->types[] = array( + 'name' => 'tag', + 'label' => T_('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'file', + 'label' => T_('Filename'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'year', + 'label' => T_('Year'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'time', + 'label' => T_('Length (in minutes)'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + if (AmpConfig::get('ratings')) { + $this->types[] = array( + 'name' => 'rating', + 'label' => T_('Rating'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '1 Star', + '2 Stars', + '3 Stars', + '4 Stars', + '5 Stars' + ) + ) + ); + } + + if (AmpConfig::get('show_played_times')) { + $this->types[] = array( + 'name' => 'played_times', + 'label' => T_('# Played'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + } + + $this->types[] = array( + 'name' => 'bitrate', + 'label' => T_('Bitrate'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '32', + '40', + '48', + '56', + '64', + '80', + '96', + '112', + '128', + '160', + '192', + '224', + '256', + '320' + ) + ) + ); + + $this->types[] = array( + 'name' => 'played', + 'label' => T_('Played'), + 'type' => 'boolean', + 'widget' => array('input', 'hidden') + ); + + $this->types[] = array( + 'name' => 'added', + 'label' => T_('Added'), + 'type' => 'date', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'updated', + 'label' => T_('Updated'), + 'type' => 'date', + 'widget' => array('input', 'text') + ); + + $catalogs = array(); + foreach (Catalog::get_catalogs() as $catid) { + $catalog = Catalog::create_from_id($catid); + $catalog->format(); + $catalogs[$catid] = $catalog->f_name; + } + $this->types[] = array( + 'name' => 'catalog', + 'label' => T_('Catalog'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $catalogs) + ); + + $playlists = array(); + foreach (Playlist::get_playlists() as $playlistid) { + $playlist = new Playlist($playlistid); + $playlist->format(); + $playlists[$playlistid] = $playlist->f_name; + } + $this->types[] = array( + 'name' => 'playlist', + 'label' => T_('Playlist'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $playlists) + ); + + $this->types[] = array( + 'name' => 'playlist_name', + 'label' => T_('Playlist Name'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $playlists = array(); + foreach (Search::get_searches() as $playlistid) { + // Slightly different from the above so we don't instigate + // a vicious loop. + $playlists[$playlistid] = Search::get_name_byid($playlistid); + } + $this->types[] = array( + 'name' => 'smartplaylist', + 'label' => T_('Smart Playlist'), + 'type' => 'boolean_subsearch', + 'widget' => array('select', $playlists) + ); + break; + case 'album': + $this->types[] = array( + 'name' => 'title', + 'label' => T_('Title'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + + $this->types[] = array( + 'name' => 'year', + 'label' => T_('Year'), + 'type' => 'numeric', + 'widget' => array('input', 'text') + ); + + if (AmpConfig::get('ratings')) { + $this->types[] = array( + 'name' => 'rating', + 'label' => T_('Rating'), + 'type' => 'numeric', + 'widget' => array( + 'select', + array( + '1 Star', + '2 Stars', + '3 Stars', + '4 Stars', + '5 Stars' + ) + ) + ); + } + + $catalogs = array(); + foreach (Catalog::get_catalogs() as $catid) { + $catalog = Catalog::create_from_id($catid); + $catalog->format(); + $catalogs[$catid] = $catalog->f_name; + } + $this->types[] = array( + 'name' => 'catalog', + 'label' => T_('Catalog'), + 'type' => 'boolean_numeric', + 'widget' => array('select', $catalogs) + ); + + + $this->types[] = array( + 'name' => 'tag', + 'label' => T_('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + case 'video': + $this->types[] = array( + 'name' => 'filename', + 'label' => T_('Filename'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + case 'artist': + $this->types[] = array( + 'name' => 'name', + 'label' => T_('Name'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + $this->types[] = array( + 'name' => 'tag', + 'label' => T_('Tag'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + case 'playlist': + $this->types[] = array( + 'name' => 'name', + 'label' => T_('Name'), + 'type' => 'text', + 'widget' => array('input', 'text') + ); + break; + } // end switch on searchtype + + } // end constructor + + /** + * clean_request + * + * Sanitizes raw search data + */ + public static function clean_request($data) + { + $request = array(); + foreach ($data as $key => $value) { + $prefix = substr($key, 0, 4); + $value = trim($value); + + if ($prefix == 'rule' && strlen($value)) { + $request[$key] = Dba::escape($value); + } + } + + // Figure out if they want an AND based search or an OR based search + switch ($data['operator']) { + case 'or': + $request['operator'] = 'OR'; + break; + default: + $request['operator'] = 'AND'; + break; + } + + // Verify the type + switch ($data['type']) { + case 'album': + case 'artist': + case 'video': + case 'song': + case 'playlist': + $request['type'] = $data['type']; + break; + default: + $request['type'] = 'song'; + break; + } + + return $request; + } // end clean_request + + /** + * get_name_byid + * + * Returns the name of the saved search corresponding to the given ID + */ + public static function get_name_byid($id) + { + $sql = "SELECT `name` FROM `search` WHERE `id` = '$id'"; + $db_results = Dba::read($sql); + $r = Dba::fetch_assoc($db_results); + return $r['name']; + } + + /** + * get_searches + * + * Return the IDs of all saved searches accessible by the current user. + */ + public static function get_searches() + { + $sql = "SELECT `id` from `search` WHERE `type`='public' OR " . + "`user`='" . $GLOBALS['user']->id . "' ORDER BY `name`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + /** + * run + * + * This function actually runs the search and returns an array of the + * results. + */ + public static function run($data) + { + $limit = intval($data['limit']); + $data = Search::clean_request($data); + + $search = new Search($data['type']); + $search->parse_rules($data); + + // Generate BASE SQL + + $limit_sql = ""; + if ($limit > 0) { + $offset = intval($data['offset']); + $limit_sql = ' LIMIT '; + if ($offset) $limit_sql .= $offset . ","; + $limit_sql .= $limit; + } + + $search_info = $search->to_sql(); + $sql = $search_info['base'] . ' ' . $search_info['table_sql'] . + ' WHERE ' . $search_info['where_sql'] . " $limit_sql"; + + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + /** + * delete + * + * Does what it says on the tin. + */ + public function delete() + { + $id = Dba::escape($this->id); + $sql = "DELETE FROM `search` WHERE `id` = ?"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * format + * Gussy up the data + */ + public function format() + { + parent::format(); + $this->f_link = AmpConfig::get('web_path') . '/smartplaylist.php?action=show_playlist&playlist_id=' . $this->id; + $this->f_name_link = '' . $this->f_name . ''; + } + + /** + * get_items + * + * Return an array of the items output by our search (part of the + * playlist interface). + */ + public function get_items() + { + $results = array(); + + $sql = $this->to_sql(); + $sql = $sql['base'] . ' ' . $sql['table_sql'] . ' WHERE ' . + $sql['where_sql']; + + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array( + 'object_id' => $row['id'], + 'object_type' => $this->searchtype + ); + } + + return $results; + } + + /** + * get_random_items + * + * Returns a randomly sorted array (with an optional limit) of the items + * output by our search (part of the playlist interface) + */ + public function get_random_items($limit = null) + { + $results = array(); + + $sql = $this->to_sql(); + $sql = $sql['base'] . ' ' . $sql['table_sql'] . + ' WHERE ' . $sql['where_sql']; + + $sql .= ' ORDER BY RAND()'; + $sql .= $limit ? ' LIMIT ' . intval($limit) : ''; + + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = array( + 'object_id' => $row['id'], + 'object_type' => $this->searchtype + ); + } + + return $results; + } + + /** + * name_to_basetype + * + * Iterates over our array of types to find out the basetype for + * the passed string. + */ + public function name_to_basetype($name) + { + foreach ($this->types as $type) { + if ($type['name'] == $name) { + return $type['type']; + } + } + return false; + } + + /** + * parse_rules + * + * Takes an array of sanitized search data from the form and generates + * our real array from it. + */ + public function parse_rules($data) + { + $this->rules = array(); + foreach ($data as $rule => $value) { + if (preg_match('/^rule_(\d+)$/', $rule, $ruleID)) { + $ruleID = $ruleID[1]; + foreach (explode('|', $data['rule_' . $ruleID . '_input']) as $input) { + $this->rules[] = array( + $value, + $this->basetypes[$this->name_to_basetype($value)][$data['rule_' . $ruleID . '_operator']]['name'], + $input + ); + } + } + } + $this->logic_operator = $data['operator']; + } + + /** + * save + * + * Save this search to the database for use as a smart playlist + */ + public function save() + { + // Make sure we have a unique name + if (! $this->name) { + $this->name = $GLOBALS['user']->username . ' - ' . date('Y-m-d H:i:s', time()); + } + $sql = "SELECT `id` FROM `search` WHERE `name`='$this->name'"; + $db_results = Dba::read($sql); + if (Dba::num_rows($db_results)) { + $this->name .= uniqid('', true); + } + + $sql = "INSERT INTO `search` (`name`, `type`, `user`, `rules`, `logic_operator`) VALUES (?, ?, ?, ?, ?)"; + Dba::write($sql, array($this->name, $this->type, $GLOBALS['user']->id, serialize($this->rules), $this->logic_operator)); + $insert_id = Dba::insert_id(); + $this->id = $insert_id; + return $insert_id; + } + + + /** + * to_js + * + * Outputs the javascript necessary to re-show the current set of rules. + */ + public function to_js() + { + $js = ""; + foreach ($this->rules as $rule) { + $js .= ''; + } + return $js; + } + + /** + * to_sql + * + * Call the appropriate real function. + */ + public function to_sql() + { + return call_user_func(array($this, $this->searchtype . "_to_sql")); + } + + /** + * update + * + * This function updates the saved version with the current settings. + */ + public function update() + { + if (!$this->id) { + return false; + } + + $sql = "UPDATE `search` SET `name` = ?, `type` = ?, `rules` = ?, `logic_operator` = ? WHERE `id` = ?"; + $db_results = Dba::write($sql, array($this->name, $this->type, serialize($this->rules), $this->logic_operator, $this->id)); + return $db_results; + } + + /** + * _mangle_data + * + * Private convenience function. Mangles the input according to a set + * of predefined rules so that we don't have to include this logic in + * foo_to_sql. + */ + private function _mangle_data($data, $type, $operator) + { + if ($operator['preg_match']) { + $data = preg_replace( + $operator['preg_match'], + $operator['preg_replace'], + $data + ); + } + + if ($type == 'numeric') { + return intval($data); + } + + if ($type == 'boolean') { + return make_bool($data); + } + + return $data; + } + + /** + * album_to_sql + * + * Handles the generation of the SQL for album searches. + */ + private function album_to_sql() + { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + $table = array(); + $join = array(); + $join['tag'] = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + $operator = array(); + foreach ($this->basetypes[$type] as $op) { + if ($op['name'] == $rule[1]) { + $operator = $op; + break; + } + } + $input = $this->_mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'title': + $where[] = "`album`.`name` $sql_match_operator '$input'"; + break; + case 'year': + $where[] = "`album`.`year` $sql_match_operator '$input'"; + break; + case 'rating': + $where[] = "COALESCE(`rating`.`rating`,0) $sql_match_operator '$input'"; + $join['rating'] = true; + break; + case 'catalog': + $where[] = "`song`.`catalog` $sql_match_operator '$input'"; + $join['song'] = true; + break; + case 'tag': + $key = md5($input . $sql_match_operator); + $where[] = "`realtag_$key`.`match` > 0"; + $join['tag'][$key] = "$sql_match_operator '$input'"; + break; + default: + // Nae laird! + break; + } // switch on ruletype + } // foreach rule + + $join['song'] = $join['song'] || AmpConfig::get('catalog_disable'); + $join['catalog'] = AmpConfig::get('catalog_disable'); + + $where_sql = implode(" $sql_logic_operator ", $where); + + foreach ($join['tag'] as $key => $value) { + $table['tag_' . $key] = + "LEFT JOIN (" . + "SELECT `object_id`, COUNT(`name`) AS `match` ". + "FROM `tag` LEFT JOIN `tag_map` " . + "ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='album' " . + "AND `tag`.`name` $value GROUP BY `object_id`" . + ") AS realtag_$key " . + "ON `album`.`id`=`realtag_$key`.`object_id`"; + } + if ($join['song']) { + $table['song'] = "LEFT JOIN `song` ON `song`.`album`=`album`.`id`"; + + if ($join['catalog']) { + $table['catalog'] = "LEFT JOIN `catalog` AS `catalog_se` ON `catalog_se`.`id`=`song`.`catalog`"; + $where_sql .= " AND `catalog_se`.`enabled` = '1'"; + } + } + if ($join['rating']) { + $userid = intval($GLOBALS['user']->id); + $table['rating'] = "LEFT JOIN `rating` ON " . + "`rating`.`object_type`='album' " . + "AND `rating`.`user`='$userid' " . + "AND `rating`.`object_id`=`album`.`id`"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`album`.`id`) FROM `album`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } + + /** + * artist_to_sql + * + * Handles the generation of the SQL for artist searches. + */ + private function artist_to_sql() + { + $sql_logic_operator = $this->logic_operator; + $where = array(); + $table = array(); + $join = array(); + $join['tag'] = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + $operator = array(); + foreach ($this->basetypes[$type] as $op) { + if ($op['name'] == $rule[1]) { + $operator = $op; + break; + } + } + $input = $this->_mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'name': + $where[] = "`artist`.`name` $sql_match_operator '$input'"; + break; + case 'tag': + $key = md5($input . $sql_match_operator); + $where[] = "`realtag_$key`.`match` > 0"; + $join['tag'][$key] = "$sql_match_operator '$input'"; + break; + default: + // Nihil + break; + } // switch on ruletype + } // foreach rule + + $join['song'] = $join['song'] || AmpConfig::get('catalog_disable'); + $join['catalog'] = AmpConfig::get('catalog_disable'); + + $where_sql = implode(" $sql_logic_operator ", $where); + + foreach ($join['tag'] as $key => $value) { + $table['tag_' . $key] = + "LEFT JOIN (" . + "SELECT `object_id`, COUNT(`name`) AS `match` ". + "FROM `tag` LEFT JOIN `tag_map` " . + "ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='artist' " . + "AND `tag`.`name` $value GROUP BY `object_id`" . + ") AS realtag_$key " . + "ON `artist`.`id`=`realtag_$key`.`object_id`"; + } + + if ($join['song']) { + $table['song'] = "LEFT JOIN `song` ON `song`.`artist`=`artist`.`id`"; + + if ($join['catalog']) { + $table['catalog'] = "LEFT JOIN `catalog` AS `catalog_se` ON `catalog_se`.`id`=`song`.`catalog`"; + $where_sql .= " AND `catalog_se`.`enabled` = '1'"; + } + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`artist`.`id`) FROM `artist`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } + + /** + * song_to_sql + * Handles the generation of the SQL for song searches. + */ + private function song_to_sql() + { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + $table = array(); + $join = array(); + $join['tag'] = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + $operator = array(); + foreach ($this->basetypes[$type] as $op) { + if ($op['name'] == $rule[1]) { + $operator = $op; + break; + } + } + $input = $this->_mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'anywhere': + $where[] = "(`artist`.`name` $sql_match_operator '$input' OR `album`.`name` $sql_match_operator '$input' OR `song_data`.`comment` $sql_match_operator '$input' OR `song`.`file` $sql_match_operator '$input' OR `song`.`title` $sql_match_operator '$input')"; + $join['album'] = true; + $join['artist'] = true; + $join['song_data'] = true; + break; + case 'tag': + $key = md5($input . $sql_match_operator); + $where[] = "`realtag_$key`.`match` > 0"; + $join['tag'][$key] = "$sql_match_operator '$input'"; + break; + case 'title': + $where[] = "`song`.`title` $sql_match_operator '$input'"; + break; + case 'album': + $where[] = "`album`.`name` $sql_match_operator '$input'"; + $join['album'] = true; + break; + case 'artist': + $where[] = "`artist`.`name` $sql_match_operator '$input'"; + $join['artist'] = true; + break; + case 'time': + $input = $input * 60; + $where[] = "`song`.`time` $sql_match_operator '$input'"; + break; + case 'file': + $where[] = "`song`.`file` $sql_match_operator '$input'"; + break; + case 'year': + $where[] = "`song`.`year` $sql_match_operator '$input'"; + break; + case 'comment': + $where[] = "`song_data`.`comment` $sql_match_operator '$input'"; + $join['song_data'] = true; + break; + case 'played': + $where[] = " `song`.`played` = '$input'"; + break; + case 'bitrate': + $input = $input * 1000; + $where[] = "`song`.`bitrate` $sql_match_operator '$input'"; + break; + case 'rating': + $where[] = "COALESCE(`rating`.`rating`,0) $sql_match_operator '$input'"; + $join['rating'] = true; + break; + case 'played_times': + $where[] = "`song`.`id` IN (SELECT `object_count`.`object_id` FROM `object_count` " . + "WHERE `object_count`.`object_type` = 'song'" . + "GROUP BY `object_count`.`object_id` HAVING COUNT(*) $sql_match_operator '$input')"; + break; + case 'catalog': + $where[] = "`song`.`catalog` $sql_match_operator '$input'"; + break; + case 'playlist_name': + $join['playlist'] = true; + $join['playlist_data'] = true; + $where[] = "`playlist`.`name` $sql_match_operator '$input'"; + break; + case 'playlist': + $join['playlist_data'] = true; + $where[] = "`playlist_data`.`playlist` $sql_match_operator '$input'"; + break; + case 'smartplaylist': + $subsearch = new Search('song', $input); + $subsql = $subsearch->to_sql(); + $where[] = "$sql_match_operator (" . $subsql['where_sql'] . ")"; + // HACK: array_merge would potentially lose tags, since it + // overwrites. Save our merged tag joins in a temp variable, + // even though that's ugly. + $tagjoin = array_merge($subsql['join']['tag'], $join['tag']); + $join = array_merge($subsql['join'], $join); + $join['tag'] = $tagjoin; + break; + case 'added': + $input = strtotime($input); + $where[] = "`song`.`addition_time` $sql_match_operator $input"; + break; + case 'updated': + $input = strtotime($input); + $where[] = "`song`.`update_time` $sql_match_operator $input"; + default: + // NOSSINK! + break; + } // switch on type + } // foreach over rules + + $join['catalog'] = AmpConfig::get('catalog_disable'); + + $where_sql = implode(" $sql_logic_operator ", $where); + + // now that we know which things we want to JOIN... + if ($join['artist']) { + $table['artist'] = "LEFT JOIN `artist` ON `song`.`artist`=`artist`.`id`"; + } + if ($join['album']) { + $table['album'] = "LEFT JOIN `album` ON `song`.`album`=`album`.`id`"; + } + if ($join['song_data']) { + $table['song_data'] = "LEFT JOIN `song_data` ON `song`.`id`=`song_data`.`song_id`"; + } + foreach ($join['tag'] as $key => $value) { + $table['tag_' . $key] = + "LEFT JOIN (" . + "SELECT `object_id`, COUNT(`name`) AS `match` ". + "FROM `tag` LEFT JOIN `tag_map` " . + "ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`object_type`='song' " . + "AND `tag`.`name` $value GROUP BY `object_id`" . + ") AS realtag_$key " . + "ON `song`.`id`=`realtag_$key`.`object_id`"; + } + if ($join['rating']) { + $userid = $GLOBALS['user']->id; + $table['rating'] = "LEFT JOIN `rating` ON " . + "`rating`.`object_type`='song' AND " . + "`rating`.`user`='$userid' AND " . + "`rating`.`object_id`=`song`.`id`"; + } + if ($join['playlist_data']) { + $table['playlist_data'] = "LEFT JOIN `playlist_data` ON `song`.`id`=`playlist_data`.`object_id` AND `playlist_data`.`object_type`='song'"; + if ($join['playlist']) { + $table['playlist'] = "LEFT JOIN `playlist` ON `playlist_data`.`playlist`=`playlist`.`id`"; + } + } + + if ($join['catalog']) { + $table['catalog'] = "LEFT JOIN `catalog` AS `catalog_se` ON `catalog_se`.`id`=`song`.`catalog`"; + $where_sql .= " AND `catalog_se`.`enabled` = '1'"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`song`.`id`) FROM `song`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } + + /** + * video_to_sql + * + * Handles the generation of the SQL for video searches. + */ + private function video_to_sql() + { + $sql_logic_operator = $this->logic_operator; + + $where = array(); + $table = array(); + $join = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + $operator = array(); + foreach ($this->basetypes[$type] as $op) { + if ($op['name'] == $rule[1]) { + $operator = $op; + break; + } + } + $input = $this->_mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + switch ($rule[0]) { + case 'filename': + $where[] = "`video`.`file` $sql_match_operator '$input'"; + break; + default: + // WE WILLNA BE FOOLED AGAIN! + } // switch on ruletype + } // foreach rule + + $join['catalog'] = AmpConfig::get('catalog_disable'); + + $where_sql = implode(" $sql_logic_operator ", $where); + + if ($join['catalog']) { + $table['catalog'] = "LEFT JOIN `catalog` AS `catalog_se` ON `catalog_se`.`id`=`video`.`catalog`"; + $where_sql .= " AND `catalog_se`.`enabled` = '1'"; + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`video`.`id`) FROM `video`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } + + /** + * playlist_to_sql + * + * Handles the generation of the SQL for playlist searches. + */ + private function playlist_to_sql() + { + $sql_logic_operator = $this->logic_operator; + $where = array(); + $table = array(); + $join = array(); + + foreach ($this->rules as $rule) { + $type = $this->name_to_basetype($rule[0]); + $operator = array(); + foreach ($this->basetypes[$type] as $op) { + if ($op['name'] == $rule[1]) { + $operator = $op; + break; + } + } + $input = $this->_mangle_data($rule[2], $type, $operator); + $sql_match_operator = $operator['sql']; + + $where[] = "`playlist`.`type` = 'public'"; + + switch ($rule[0]) { + case 'name': + $where[] = "`playlist`.`name` $sql_match_operator '$input'"; + break; + default: + // Nihil + break; + } // switch on ruletype + } // foreach rule + + $join['playlist_data'] = true; + $join['song'] = $join['song'] || AmpConfig::get('catalog_disable'); + $join['catalog'] = AmpConfig::get('catalog_disable'); + + $where_sql = implode(" $sql_logic_operator ", $where); + + if ($join['playlist_data']) { + $table['playlist_data'] = "LEFT JOIN `playlist_data` ON `playlist_data`.`playlist` = `playlist`.`id`"; + } + + if ($join['song']) { + $table['song'] = "LEFT JOIN `song` ON `song`.`id`=`playlist_data`.`object_id`"; + $where_sql .= " AND `playlist_data`.`object_type` = 'song'"; + + if ($join['catalog']) { + $table['catalog'] = "LEFT JOIN `catalog` AS `catalog_se` ON `catalog_se`.`id`=`song`.`catalog`"; + $where_sql .= " AND `catalog_se`.`enabled` = '1'"; + } + } + + $table_sql = implode(' ', $table); + + return array( + 'base' => 'SELECT DISTINCT(`playlist`.`id`) FROM `playlist`', + 'join' => $join, + 'where' => $where, + 'where_sql' => $where_sql, + 'table' => $table, + 'table_sql' => $table_sql + ); + } +} diff --git a/sources/lib/class/session.class.php b/sources/lib/class/session.class.php new file mode 100644 index 0000000..d7ea2e7 --- /dev/null +++ b/sources/lib/class/session.class.php @@ -0,0 +1,413 @@ + ?'; + $db_results = Dba::read($sql, array($key, time())); + + if ($results = Dba::fetch_assoc($db_results)) { + return $results[$column]; + } + + debug_event('session', 'Unable to read session from key ' . $key . ' no data found', 5); + + return ''; + } + + /** + * username + * + * This returns the username associated with a session ID, if any + */ + public static function username($key) + { + return self::_read($key, 'username'); + } + + /** + * username + * + * This returns the agent associated with a session ID, if any + */ + public static function agent($key) + { + return self::_read($key, 'agent'); + } + + /** + * create + * This is called when you want to create a new session + * it takes care of setting the initial cookie, and inserting the first + * chunk of data, nifty ain't it! + */ + public static function create($data) + { + // Regenerate the session ID to prevent fixation + switch ($data['type']) { + case 'api': + case 'stream': + $key = isset($data['sid']) + ? $data['sid'] + : md5(uniqid(rand(), true)); + break; + case 'mysql': + default: + session_regenerate_id(); + + // Before refresh we don't have the cookie so we + // have to use session ID + $key = session_id(); + break; + } // end switch on data type + + $username = ''; + if (isset($data['username'])) { + $username = $data['username']; + } + $ip = $_SERVER['REMOTE_ADDR'] ? inet_pton($_SERVER['REMOTE_ADDR']) : '0'; + $type = $data['type']; + $value = ''; + if (isset($data['value'])) { + $value = $data['value']; + } + $agent = (!empty($data['agent'])) ? $data['agent'] : substr($_SERVER['HTTP_USER_AGENT'], 0, 254); + + if ($type == 'stream') { + $expire = time() + AmpConfig::get('stream_length'); + } else { + $expire = time() + AmpConfig::get('session_length'); + } + + if (!strlen($value)) { $value = ' '; } + + /* Insert the row */ + $sql = 'INSERT INTO `session` (`id`,`username`,`ip`,`type`,`agent`,`value`,`expire`) ' . + 'VALUES (?, ?, ?, ?, ?, ?, ?)'; + $db_results = Dba::write($sql, array($key, $username, $ip, $type, $agent, $value, $expire)); + + if (!$db_results) { + debug_event('session', 'Session creation failed', '1'); + return false; + } + + debug_event('session', 'Session created: ' . $key, '5'); + + return $key; + } + + /** + * check + * + * This checks for an existing session. If it's still valid we go ahead + * and start it and return true. + */ + public static function check() + { + $session_name = AmpConfig::get('session_name'); + + // No cookie no go! + if (!isset($_COOKIE[$session_name])) { return false; } + + // Check for a remember me + if (isset($_COOKIE[$session_name . '_remember'])) { + self::create_remember_cookie(); + } + + // Set up the cookie params before we start the session. + // This is vital + session_set_cookie_params( + AmpConfig::get('cookie_life'), + AmpConfig::get('cookie_path'), + AmpConfig::get('cookie_domain'), + AmpConfig::get('cookie_secure')); + + // Set name + session_name($session_name); + + // Ungimp IE and go + self::ungimp_ie(); + session_start(); + + return true; + } + + /** + * exists + * + * This checks to see if the specified session of the specified type + * exists + * based on the type. + */ + public static function exists($type, $key) + { + // Switch on the type they pass + switch ($type) { + case 'api': + case 'stream': + $sql = 'SELECT * FROM `session` WHERE `id` = ? AND `expire` > ? ' . + "AND `type` IN ('api', 'stream')"; + $db_results = Dba::read($sql, array($key, time())); + + if (Dba::num_rows($db_results)) { + return true; + } + break; + case 'interface': + $sql = 'SELECT * FROM `session` WHERE `id` = ? AND `expire` > ?'; + if (AmpConfig::get('use_auth')) { + // Build a list of enabled authentication types + $types = AmpConfig::get('auth_methods'); + $enabled_types = implode("','", $types); + $sql .= " AND `type` IN('$enabled_types')"; + } + $db_results = Dba::read($sql, array($key, time())); + + if (Dba::num_rows($db_results)) { + return true; + } + break; + default: + return false; + } + + // Default to false + return false; + } + + /** + * extend + * + * This takes a SID and extends its expiration. + */ + public static function extend($sid, $type = null) + { + $time = time(); + $expire = isset($_COOKIE[AmpConfig::get('session_name') . '_remember']) + ? $time + AmpConfig::get('remember_length') + : $time + AmpConfig::get('session_length'); + + if ($type == 'stream') { + $expire = $time + AmpConfig::get('stream_length'); + } + + $sql = 'UPDATE `session` SET `expire` = ? WHERE `id`= ?'; + if ($db_results = Dba::write($sql, array($expire, $sid))) { + debug_event('session', $sid . ' has been extended to ' . @date('r', $expire) . ' extension length ' . ($expire - $time), 5); + } + + return $db_results; + } + + /** + * _auto_init + * + * This function is called when the object is included, this sets up the + * session_save_handler + */ + public static function _auto_init() + { + if (!function_exists('session_start')) { + header("Location:" . AmpConfig::get('web_path') . "/test.php"); + exit; + } + + session_set_save_handler( + array('Session', 'open'), + array('Session', 'close'), + array('Session', 'read'), + array('Session', 'write'), + array('Session', 'destroy'), + array('Session', 'gc')); + + // Make sure session_write_close is called during the early part of + // shutdown, to avoid issues with object destruction. + register_shutdown_function('session_write_close'); + } + + /** + * create_cookie + * + * This is separated into its own function because of some flaws in + * specific webservers *cough* IIS *cough* which prevent us from setting + * a cookie at the same time as a header redirect. As such on view of a + * login a cookie is set with the proper name. + */ + public static function create_cookie() + { + // Set up the cookie prefs before we throw down, this is very important + $cookie_life = AmpConfig::get('cookie_life'); + $cookie_path = AmpConfig::get('cookie_path'); + $cookie_domain = false; + $cookie_secure = AmpConfig::get('cookie_secure'); + + session_set_cookie_params($cookie_life, $cookie_path, $cookie_domain, $cookie_secure); + + session_name(AmpConfig::get('session_name')); + + /* Start the session */ + self::ungimp_ie(); + session_start(); + } + + /** + * create_remember_cookie + * + * This function just creates the remember me cookie, nothing special. + */ + public static function create_remember_cookie() + { + $remember_length = AmpConfig::get('remember_length'); + $session_name = AmpConfig::get('session_name'); + + AmpConfig::set('cookie_life', $remember_length, true); + setcookie($session_name . '_remember', "Rappelez-vous, rappelez-vous le 27 mars", time() + $remember_length, '/'); + } + + /** + * ungimp_ie + * + * This function sets the cache limiting to public if you are running + * some flavor of IE and not using HTTPS. + */ + public static function ungimp_ie() + { + // If no https, no ungimpage required + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'on') { + return true; + } + + $browser = new Horde_Browser(); + if ($browser->isBrowser('msie')) { + session_cache_limiter('public'); + } + + return true; + } + +} diff --git a/sources/lib/class/share.class.php b/sources/lib/class/share.class.php new file mode 100644 index 0000000..a7296e6 --- /dev/null +++ b/sources/lib/class/share.class.php @@ -0,0 +1,305 @@ +get_info($id); + + // Foreach what we've got + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + } //constructor + + public static function delete_share($id) + { + $sql = "DELETE FROM `share` WHERE `id` = ?"; + $params = array( $id ); + if (!$GLOBALS['user']->has_access('75')) { + $sql .= " AND `user` = ?"; + $params[] = $GLOBALS['user']->id; + } + + return Dba::write($sql, $params); + } + + public static function delete_shares($object_type, $object_id) + { + $sql = "DELETE FROM `share` WHERE `object_type` = ? AND `object_id` = ?"; + + Dba::write($sql, array($object_type, $object_id)); + } + + public static function generate_secret($length = 8) + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $secret = ''; + for ($i = 0; $i < $length; $i++) { + $secret .= $characters[rand(0, strlen($characters) - 1)]; + } + + return $secret; + } + + public static function format_type($type) + { + switch ($type) { + case 'album': + case 'song': + case 'playlist': + return $type; + default: + return ''; + } + } + + public static function create_share($object_type, $object_id, $allow_stream=true, $allow_download=true, $expire=0, $secret='', $max_counter=0, $description='') + { + $object_type = self::format_type($object_type); + if (empty($object_type)) return ''; + + if (!$allow_stream && !$allow_download) return ''; + + $sql = "INSERT INTO `share` (`user`, `object_type`, `object_id`, `creation_date`, `allow_stream`, `allow_download`, `expire_days`, `secret`, `counter`, `max_counter`, `description`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + $params = array($GLOBALS['user']->id, $object_type, $object_id, time(), $allow_stream ?: 0, $allow_download ?: 0, $expire, $secret, 0, $max_counter, $description); + Dba::write($sql, $params); + + $id = Dba::insert_id(); + + $url = self::get_url($id, $secret); + // Get a shortener url if any available + foreach (Plugin::get_plugins('shortener') as $plugin_name) { + try { + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $short_url = $plugin->_plugin->shortener($url); + if (!empty($short_url)) { + $url = $short_url; + break; + } + } + } catch (Exception $e) { + debug_event('share', 'Share plugin error: ' . $e->getMessage(), '1'); + } + } + $sql = "UPDATE `share` SET `public_url` = ? WHERE `id` = ?"; + Dba::write($sql, array($url, $id)); + + return $id; + } + + public static function get_url($id, $secret) + { + $url = AmpConfig::get('web_path') . '/share.php?id=' . $id; + if (!empty($secret)) { + $url .= '&secret=' . $secret; + } + + return $url; + } + + public static function get_share_list_sql() + { + $sql = "SELECT `id` FROM `share` "; + + if (!$GLOBALS['user']->has_access('75')) { + $sql .= "WHERE `user` = '" . scrub_in($GLOBALS['user']->id) . "'"; + } + + return $sql; + } + + public static function get_share_list() + { + $sql = self::get_share_list_sql(); + $db_results = Dba::read($sql); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + public static function get_shares($object_type, $object_id) + { + $sql = "SELECT `id` FROM `share` WHERE `object_type` = ? AND `object_id` = ?"; + $db_results = Dba::read($sql, array($object_type, $object_id)); + $results = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + + public function show_action_buttons() + { + if ($this->id) { + if ($GLOBALS['user']->has_access('75') || $this->user == $GLOBALS['user']->id) { + echo "id ."\">" . UI::get_icon('delete', T_('Delete')) . ""; + } + } + } + + public function format() + { + $object = new $this->object_type($this->object_id); + $object->format(); + $this->f_object_link = $object->f_link; + $user = new User($this->user); + $this->f_user = $user->fullname; + $this->f_allow_stream = $this->allow_stream; + $this->f_allow_download = $this->allow_download; + $this->f_creation_date = date("Y-m-d H:i:s", $this->creation_date); + $this->f_lastvisit_date = ($this->lastvisit_date > 0) ? date("Y-m-d H:i:s", $this->creation_date) : ''; + } + + public function save_access() + { + $sql = "UPDATE `share` SET `counter` = (`counter` + 1), lastvisit_date = ? WHERE `id` = ?"; + return Dba::write($sql, array(time(), $this->id)); + } + + public function is_valid($secret, $action) + { + if (!$this->id) { + debug_event('share', 'Access Denied: Invalid share.', '3'); + return false; + } + + if (!AmpConfig::get('share')) { + debug_event('share', 'Access Denied: share feature disabled.', '3'); + return false; + } + + if ($this->expire_days > 0 && ($this->creation_date + ($this->expire_days * 86400)) < time()) { + debug_event('share', 'Access Denied: share expired.', '3'); + return false; + } + + if ($this->max_counter > 0 && $this->counter >= $this->max_counter) { + debug_event('share', 'Access Denied: max counter reached.', '3'); + return false; + } + + if (!empty($this->secret) && $secret != $this->secret) { + debug_event('share', 'Access Denied: secret requires to access share ' . $this->id . '.', '3'); + return false; + } + + if ($action == 'download' && (!AmpConfig::get('download') || !$this->allow_download)) { + debug_event('share', 'Access Denied: download unauthorized.', '3'); + return false; + } + + if ($action == 'stream' && !$this->allow_stream) { + debug_event('share', 'Access Denied: stream unauthorized.', '3'); + return false; + } + + return true; + } + + public function create_fake_playlist() + { + $playlist = new Stream_Playlist(-1); + $medias = array(); + + switch ($this->object_type) { + case 'album': + case 'playlist': + $object = new $this->object_type($this->object_id); + $songs = $object->get_songs(); + foreach ($songs as $id) { + $medias[] = array( + 'object_type' => 'song', + 'object_id' => $id, + ); + } + break; + default: + $medias[] = array( + 'object_type' => $this->object_type, + 'object_id' => $this->object_id, + ); + break; + } + + $playlist->add($medias, '&share_id=' . $this->id . '&share_secret=' . $this->secret); + return $playlist; + } + + public function is_shared_song($song_id) + { + $is_shared = false; + switch ($this->object_type) { + case 'album': + case 'playlist': + $object = new $this->object_type($this->object_id); + $songs = $object->get_songs(); + foreach ($songs as $id) { + $is_shared = ($song_id == $id); + if ($is_shared) { break; } + } + break; + default: + $is_shared = ($this->object_type == 'song' && $this->object_id == $song_id); + break; + } + + return $is_shared; + } + +} // end of recommendation class diff --git a/sources/lib/class/shoutbox.class.php b/sources/lib/class/shoutbox.class.php new file mode 100644 index 0000000..33e2ed5 --- /dev/null +++ b/sources/lib/class/shoutbox.class.php @@ -0,0 +1,302 @@ +_get_info($shout_id); + + return true; + + } // Constructor + + /** + * _get_info + * does the db call, reads from the user_shout table + */ + private function _get_info($shout_id) + { + $sql = "SELECT * FROM `user_shout` WHERE `id` = ?"; + $db_results = Dba::read($sql, array($shout_id)); + + $data = Dba::fetch_assoc($db_results); + + foreach ($data as $key=>$value) { + $this->$key = $value; + } + + return true; + + } // _get_info + + /** + * gc + * + * Cleans out orphaned shoutbox items + */ + public static function gc() + { + foreach (array('song', 'album', 'artist') as $object_type) { + Dba::write("DELETE FROM `user_shout` USING `user_shout` LEFT JOIN `$object_type` ON `$object_type`.`id` = `user_shout`.`object_id` WHERE `$object_type`.`id` IS NULL AND `user_shout`.`object_type` = '$object_type'"); + } + } + + /** + * get_top + * This returns the top user_shouts, shoutbox objects are always shown regardless and count against the total + * number of objects shown + */ + public static function get_top($limit) + { + $shouts = self::get_sticky(); + + // If we've already got too many stop here + if (count($shouts) > $limit) { + $shouts = array_slice($shouts,0,$limit); + return $shouts; + } + + // Only get as many as we need + $limit = intval($limit) - count($shouts); + $sql = "SELECT * FROM `user_shout` WHERE `sticky`='0' ORDER BY `date` DESC LIMIT $limit"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $shouts[] = $row['id']; + } + + return $shouts; + + } // get_top + + public static function get_shouts_since($time) + { + $sql = "SELECT * FROM `user_shout` WHERE `date` > ? ORDER BY `date` DESC"; + $db_results = Dba::read($sql, array($time)); + + $shouts = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $shouts[] = $row['id']; + } + + return $shouts; + + } + + /** + * get_sticky + * This returns all current sticky shoutbox items + */ + public static function get_sticky() + { + $sql = "SELECT * FROM `user_shout` WHERE `sticky`='1' ORDER BY `date` DESC"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_sticky + + /** + * get_object + * This takes a type and an ID and returns a created object + */ + public static function get_object($type,$object_id) + { + $allowed_objects = array('song','genre','album','artist','radio'); + + if (!in_array($type,$allowed_objects)) { + return false; + } + + $object = new $type($object_id); + + return $object; + + } // get_object + + /** + * get_image + * This returns an image tag if the type of object we're currently rolling with + * has an image associated with it + */ + public function get_image() + { + switch ($this->object_type) { + case 'album': + $image_string = ""; + break; + case 'song': + $song = new Song($this->object_id); + $image_string = ""; + break; + case 'artist': + default: + $image_string = ""; + break; + } // end switch + + return $image_string; + + } // get_image + + /** + * create + * This takes a key'd array of data as input and inserts a new shoutbox entry, it returns the auto_inc id + */ + public static function create($data) + { + $sticky = isset($data['sticky']) ? 1 : 0; + $sql = "INSERT INTO `user_shout` (`user`,`date`,`text`,`sticky`,`object_id`,`object_type`, `data`) " . + "VALUES (? , ?, ?, ?, ?, ?, ?)"; + Dba::write($sql, array($GLOBALS['user']->id, time(), strip_tags($data['comment']), $sticky, $data['object_id'], $data['object_type'], $data['data'])); + + $insert_id = Dba::insert_id(); + + return $insert_id; + + } // create + + /** + * update + * This takes a key'd array of data as input and updates a shoutbox entry + */ + public static function update($data) + { + $id = Dba::escape($data['shout_id']); + $text = Dba::escape(strip_tags($data['comment'])); + $sticky = make_bool($data['sticky']); + + $sql = "UPDATE `user_shout` SET `text`='$text', `sticky`='$sticky' WHERE `id`='$id'"; + Dba::write($sql); + + return true; + + } // create + + /** + * format + * this function takes the object and reformats some values + */ + + public function format() + { + $this->sticky = ($this->sticky == "0") ? 'No' : 'Yes'; + $this->date = date("m\/d\/Y - H:i", $this->date); + return true; + + } //format + + /** + * delete + * this function deletes a specific shoutbox entry + */ + + public function delete($shout_id) + { + // Delete the shoutbox post + $shout_id = Dba::escape($shout_id); + $sql = "DELETE FROM `user_shout` WHERE `id`='$shout_id'"; + Dba::write($sql); + + } // delete + + public function get_display($details = true, $jsbuttons = false) + { + $object = Shoutbox::get_object($this->object_type, $this->object_id); + $object->format(); + $user = new User($this->user); + $user->format(); + $img = $this->get_image(); + $html = "
"; + $html .= "
"; + if ($details && $img) { + $html .= "
" . $img . "
"; + } + $html .= "
"; + if ($details) { + $html .= "
" . $object->f_link . "
"; + $html .= "
".date("Y/m/d H:i:s", $this->date) . "
"; + } + $html .= "
" . preg_replace('/(\r\n|\n|\r)/', '
', $this->text) . "
"; + $html .= "
"; + $html .= "
"; + $html .= ""; + $html .= "
"; + + return $html; + } + + public static function get_shouts($object_type, $object_id) + { + $sql = "SELECT `id` FROM `user_shout` WHERE `object_type` = ? AND `object_id` = ? ORDER BY `sticky`, `date` DESC"; + $db_results = Dba::read($sql, array($object_type, $object_id)); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + +} // Shoutbox class diff --git a/sources/lib/class/slideshow.class.php b/sources/lib/class/slideshow.class.php new file mode 100644 index 0000000..6f896fc --- /dev/null +++ b/sources/lib/class/slideshow.class.php @@ -0,0 +1,62 @@ +id); + $images = array(); + if (count($songs) > 0) { + $last_song = new Song($songs[0]['object_id']); + $last_song->format(); + $images = self::get_images($last_song->f_artist); + } + + return $images; + } + + protected static function get_images($artist_name) + { + $images = array(); + if (AmpConfig::get('echonest_api_key')) { + $echonest = new EchoNest_Client(new EchoNest_HttpClient_Requests()); + $echonest->authenticate(AmpConfig::get('echonest_api_key')); + + try { + $images = $echonest->getArtistApi()->setName($artist_name)->getImages(); + } catch (Exception $e) { + debug_event('echonest', 'EchoNest artist images error: ' . $e->getMessage(), '1'); + } + } + + foreach (Plugin::get_plugins('get_photos') as $plugin_name) { + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $images += $plugin->_plugin->get_photos($artist_name); + } + } + + return $images; + } + +} // end of Slideshow class diff --git a/sources/lib/class/song.class.php b/sources/lib/class/song.class.php new file mode 100644 index 0000000..eeea3ee --- /dev/null +++ b/sources/lib/class/song.class.php @@ -0,0 +1,1197 @@ +id = intval($id); + + if ($info = $this->_get_info()) { + foreach ($info as $key => $value) { + $this->$key = $value; + } + $data = pathinfo($this->file); + $this->type = strtolower($data['extension']); + $this->mime = self::type_to_mime($this->type); + } else { + $this->id = null; + return false; + } + + return true; + + } // constructor + + /** + * insert + * + * This inserts the song described by the passed array + */ + public static function insert($results) + { + $catalog = $results['catalog']; + $file = $results['file']; + $title = trim($results['title']) ?: $file; + $artist = $results['artist']; + $album = $results['album']; + $bitrate = $results['bitrate'] ?: 0; + $rate = $results['rate'] ?: 0; + $mode = $results['mode']; + $size = $results['size'] ?: 0; + $time = $results['time'] ?: 0; + $track = $results['track']; + $track_mbid = $results['mb_trackid'] ?: $results['mbid']; + if ($track_mbid == '') $track_mbid = null; + $album_mbid = $results['mb_albumid']; + $artist_mbid = $results['mb_artistid']; + $disk = $results['disk'] ?: 0; + $year = $results['year'] ?: 0; + $comment = $results['comment']; + $tags = $results['genre']; // multiple genre support makes this an array + $lyrics = $results['lyrics']; + + $artist_id = Artist::check($artist, $artist_mbid); + $album_id = Album::check($album, $year, $disk, $album_mbid); + + $sql = 'INSERT INTO `song` (`file`, `catalog`, `album`, `artist`, ' . + '`title`, `bitrate`, `rate`, `mode`, `size`, `time`, `track`, ' . + '`addition_time`, `year`, `mbid`) ' . + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + + $db_results = Dba::write($sql, array( + $file, $catalog, $album_id, $artist_id, + $title, $bitrate, $rate, $mode, $size, $time, $track, + time(), $year, $track_mbid)); + + if (!$db_results) { + debug_event('song', 'Unable to insert ' . $file, 2); + return false; + } + + $song_id = Dba::insert_id(); + + if (is_array($tags)) { + foreach ($tags as $tag) { + $tag = trim($tag); + if (!empty($tag)) { + Tag::add('song', $song_id, $tag, false); + Tag::add('album', $album_id, $tag, false); + Tag::add('artist', $artist_id, $tag, false); + } + } + } + + $sql = 'INSERT INTO `song_data` (`song_id`, `comment`, `lyrics`) ' . + 'VALUES(?, ?, ?)'; + Dba::write($sql, array($song_id, $comment, $lyrics)); + + return true; + } + + /** + * gc + * + * Cleans up the song_data table + */ + public static function gc() + { + Dba::write('DELETE FROM `song_data` USING `song_data` LEFT JOIN `song` ON `song`.`id` = `song_data`.`song_id` WHERE `song`.`id` IS NULL'); + } + + /** + * build_cache + * + * This attempts to reduce queries by asking for everything in the + * browse all at once and storing it in the cache, this can help if the + * db connection is the slow point. + */ + public static function build_cache($song_ids) + { + if (!is_array($song_ids) || !count($song_ids)) { return false; } + + $idlist = '(' . implode(',', $song_ids) . ')'; + + // Callers might have passed array(false) because they are dumb + if ($idlist == '()') { return false; } + + // Song data cache + $sql = 'SELECT `song`.`id`, `file`, `catalog`, `album`, ' . + '`year`, `artist`, `title`, `bitrate`, `rate`, ' . + '`mode`, `size`, `time`, `track`, `played`, ' . + '`song`.`enabled`, `update_time`, `tag_map`.`tag_id`, '. + '`mbid`, `addition_time` ' . + 'FROM `song` LEFT JOIN `tag_map` ' . + 'ON `tag_map`.`object_id`=`song`.`id` ' . + "AND `tag_map`.`object_type`='song' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `song`.`id` IN $idlist "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $db_results = Dba::read($sql); + + $artists = array(); + $albums = array(); + $tags = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + if (AmpConfig::get('show_played_times')) { + $row['object_cnt'] = Stats::get_object_count('song', $row['id']); + } + parent::add_to_cache('song', $row['id'], $row); + $artists[$row['artist']] = $row['artist']; + $albums[$row['album']] = $row['album']; + if ($row['tag_id']) { + $tags[$row['tag_id']] = $row['tag_id']; + } + } + + Artist::build_cache($artists); + Album::build_cache($albums); + Tag::build_cache($tags); + Tag::build_map_cache('song', $song_ids); + Art::build_cache($albums); + + // If we're rating this then cache them as well + if (AmpConfig::get('ratings')) { + Rating::build_cache('song', $song_ids); + } + if (AmpConfig::get('userflags')) { + Userflag::build_cache('song', $song_ids); + } + + // Build a cache for the song's extended table + $sql = "SELECT * FROM `song_data` WHERE `song_id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('song_data', $row['song_id'], $row); + } + + return true; + + } // build_cache + + /** + * _get_info + */ + private function _get_info() + { + $id = $this->id; + + if (parent::is_cached('song', $id)) { + return parent::get_from_cache('song', $id); + } + + $sql = 'SELECT `song`.`id`, `song`.`file`, `song`.`catalog`, `song`.`album`, `song`.`year`, `song`.`artist`,' . + '`song`.`title`, `song`.`bitrate`, `song`.`rate`, `song`.`mode`, `song`.`size`, `song`.`time`, `song`.`track`, ' . + '`song`.`played`, `song`.`enabled`, `song`.`update_time`, `song`.`mbid`, `song`.`addition_time`, ' . + '`album`.`mbid` AS `album_mbid`, `artist`.`mbid` AS `artist_mbid` ' . + 'FROM `song` LEFT JOIN `album` ON `album`.`id` = `song`.`album` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` ' . + 'WHERE `song`.`id` = ?'; + $db_results = Dba::read($sql, array($id)); + + $results = Dba::fetch_assoc($db_results); + if (isset($results['id'])) { + if (AmpConfig::get('show_played_times')) { + $results['object_cnt'] = Stats::get_object_count('song', $results['id']); + } + + parent::add_to_cache('song', $id, $results); + return $results; + } + + return false; + } + + /** + * _get_ext_info + * This function gathers information from the song_ext_info table and adds it to the + * current object + */ + public function _get_ext_info() + { + $id = intval($this->id); + + if (parent::is_cached('song_data',$id)) { + return parent::get_from_cache('song_data',$id); + } + + $sql = "SELECT * FROM song_data WHERE `song_id`='$id'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + parent::add_to_cache('song_data',$id,$results); + + return $results; + + } // _get_ext_info + + /** + * fill_ext_info + * This calls the _get_ext_info and then sets the correct vars + */ + public function fill_ext_info() + { + $info = $this->_get_ext_info(); + + foreach ($info as $key=>$value) { + if ($key != 'song_id') { + $this->$key = $value; + } + } // end foreach + + } // fill_ext_info + + /** + * type_to_mime + * + * Returns the mime type for the specified file extension/type + */ + public static function type_to_mime($type) + { + // FIXME: This should really be done the other way around. + // Store the mime type in the database, and provide a function + // to make it a human-friendly type. + switch ($type) { + case 'spx': + case 'ogg': + return 'application/ogg'; + case 'wma': + case 'asf': + return 'audio/x-ms-wma'; + case 'mp3': + case 'mpeg3': + return 'audio/mpeg'; + case 'rm': + case 'ra': + return 'audio/x-realaudio'; + case 'flac'; + return 'audio/x-flac'; + case 'wv': + return 'audio/x-wavpack'; + case 'aac': + case 'mp4': + case 'm4a': + return 'audio/mp4'; + case 'aacp': + return 'audio/aacp'; + case 'mpc': + return 'audio/x-musepack'; + default: + return 'audio/mpeg'; + } + + } + + /** + * get_disabled + * + * Gets a list of the disabled songs for and returns an array of Songs + */ + public static function get_disabled($count = 0) + { + $results = array(); + + $sql = "SELECT `id` FROM `song` WHERE `enabled`='0'"; + if ($count) { $sql .= " LIMIT $count"; } + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = new Song($r['id']); + } + + return $results; + } + + /** + * find_duplicates + * + * This function takes a search type and returns a list of probable + * duplicates + */ + public static function find_duplicates($search_type) + { + $where_sql = $_REQUEST['search_disabled'] ? '' : "WHERE `enabled` != '0'"; + $sql = 'SELECT `id`, `artist`, `album`, `title`, ' . + 'COUNT(`title`) FROM `song` ' . $where_sql . + ' GROUP BY `title`'; + + if ($search_type == 'artist_title' || + $search_type == 'artist_album_title') { + $sql .= ',`artist`'; + } + if ($search_type == 'artist_album_title') { + $sql .= ',`album`'; + } + + $sql .= ' HAVING COUNT(`title`) > 1 ORDER BY `title`'; + + $db_results = Dba::read($sql); + + $results = array(); + + while ($item = Dba::fetch_assoc($db_results)) { + $results[] = $item; + } // end while + + return $results; + } + + public static function get_duplicate_info($dupe, $search_type) + { + $sql = 'SELECT `id` FROM `song` ' . + "WHERE `title`='" . Dba::escape($dupe['title']) . "' "; + + if ($search_type == 'artist_title' || + $search_type == 'artist_album_title') { + $sql .= "AND `artist`='" . Dba::escape($dupe['artist']) . "' "; + } + if ($search_type == 'artist_album_title') { + $sql .= "AND `album` = '" . Dba::escape($dupe['album']) . "' "; + } + + $sql .= 'ORDER BY `time`,`bitrate`,`size`'; + $db_results = Dba::read($sql); + + $results = array(); + + while ($item = Dba::fetch_assoc($db_results)) { + $results[] = $item['id']; + } // end while + + return $results; + } + + /** + * get_album_name + * gets the name of $this->album, allows passing of id + */ + public function get_album_name($album_id=0) + { + if (!$album_id) { $album_id = $this->album; } + $album = new Album($album_id); + if ($album->prefix) + return $album->prefix . " " . $album->name; + else + return $album->name; + } // get_album_name + + /** + * get_artist_name + * gets the name of $this->artist, allows passing of id + */ + public function get_artist_name($artist_id=0) + { + if (!$artist_id) { $artist_id = $this->artist; } + $artist = new Artist($artist_id); + if ($artist->prefix) + return $artist->prefix . " " . $artist->name; + else + return $artist->name; + + } // get_album_name + + /** + * set_played + * this checks to see if the current object has been played + * if not then it sets it to played + */ + public function set_played() + { + if ($this->played) { + return true; + } + + /* If it hasn't been played, set it! */ + self::update_played('1',$this->id); + + return true; + + } // set_played + + /** + * compare_song_information + * this compares the new ID3 tags of a file against + * the ones in the database to see if they have changed + * it returns false if nothing has changes, or the true + * if they have. Static because it doesn't need this + */ + public static function compare_song_information($song,$new_song) + { + // Remove some stuff we don't care about + unset($song->catalog,$song->played,$song->enabled,$song->addition_time,$song->update_time,$song->type); + + $array = array(); + $string_array = array('title','comment','lyrics'); + $skip_array = array('id','tag_id','mime','mb_artistid','mbid'); + + // Pull out all the currently set vars + $fields = get_object_vars($song); + + // Foreach them + foreach ($fields as $key=>$value) { + if (in_array($key,$skip_array)) { continue; } + // If it's a stringie thing + if (in_array($key,$string_array)) { + if (trim(stripslashes($song->$key)) != trim(stripslashes($new_song->$key))) { + $array['change'] = true; + $array['element'][$key] = 'OLD: ' . $song->$key . ' --> ' . $new_song->$key; + } + } // in array of stringies + else { + if ($song->$key != $new_song->$key) { + $array['change'] = true; + $array['element'][$key] = 'OLD:' . $song->$key . ' --> ' . $new_song->$key; + } + } // end else + + } // end foreach + + if ($array['change']) { + debug_event('song-diff', json_encode($array['element']), 5); + } + + return $array; + + } // compare_song_information + + + /** + * update + * This takes a key'd array of data does any cleaning it needs to + * do and then calls the helper functions as needed. + */ + public function update($data) + { + foreach ($data as $key=>$value) { + debug_event('song.class.php', $key.'='.$value, '5'); + + switch ($key) { + case 'artist_name': + // Need to create new artist according the name + $new_artist_id = Artist::check($value); + self::update_artist($new_artist_id, $this->id); + break; + case 'album_name': + // Need to create new album according the name + $new_album_id = Album::check($value); + self::update_album($new_album_id, $this->id); + break; + case 'title': + case 'track': + case 'artist': + case 'album': + case 'mbid': + // Check to see if it needs to be updated + if ($value != $this->$key) { + $function = 'update_' . $key; + self::$function($value, $this->id); + $this->$key = $value; + } + break; + case 'edit_tags': + Tag::update_tag_list($value, 'song', $this->id); + break; + default: + break; + } // end whitelist + } // end foreach + + return true; + } // update + + /** + * update_song + * this is the main updater for a song it actually + * calls a whole bunch of mini functions to update + * each little part of the song... lastly it updates + * the "update_time" of the song + */ + public static function update_song($song_id, $new_song) + { + $title = Dba::escape($new_song->title); + $bitrate = Dba::escape($new_song->bitrate); + $rate = Dba::escape($new_song->rate); + $mode = Dba::escape($new_song->mode); + $size = Dba::escape($new_song->size); + $time = Dba::escape($new_song->time); + $track = Dba::escape($new_song->track); + $mbid = Dba::escape($new_song->mbid); + $artist = Dba::escape($new_song->artist); + $album = Dba::escape($new_song->album); + $year = Dba::escape($new_song->year); + $song_id = Dba::escape($song_id); + $update_time = time(); + + $sql = "UPDATE `song` SET `album`='$album', `year`='$year', `artist`='$artist', " . + "`title`='$title', `bitrate`='$bitrate', `rate`='$rate', `mode`='$mode', " . + "`size`='$size', `time`='$time', `track`='$track', " . + "`mbid`='$mbid', " . + "`update_time`='$update_time' WHERE `id`='$song_id'"; + + Dba::write($sql); + + $comment = Dba::escape($new_song->comment); + $language = Dba::escape($new_song->language); + $lyrics = Dba::escape($new_song->lyrics); + + $sql = "UPDATE `song_data` SET `lyrics`='$lyrics', `language`='$language', `comment`='$comment' " . + "WHERE `song_id`='$song_id'"; + Dba::write($sql); + + } // update_song + + /** + * update_year + * update the year tag + */ + public static function update_year($new_year,$song_id) + { + self::_update_item('year',$new_year,$song_id,'50'); + + } // update_year + + /** + * update_language + * This updates the language tag of the song + */ + public static function update_language($new_lang,$song_id) + { + self::_update_ext_item('language',$new_lang,$song_id,'50'); + + } // update_language + + /** + * update_comment + * updates the comment field + */ + public static function update_comment($new_comment,$song_id) + { + self::_update_ext_item('comment',$new_comment,$song_id,'50'); + + } // update_comment + + /** + * update_lyrics + * updates the lyrics field + */ + public static function update_lyrics($new_lyrics,$song_id) + { + self::_update_ext_item('lyrics',$new_lyrics,$song_id,'50'); + + } // update_lyrics + + /** + * update_title + * updates the title field + */ + public static function update_title($new_title,$song_id) + { + self::_update_item('title',$new_title,$song_id,'50'); + + } // update_title + + /** + * update_bitrate + * updates the bitrate field + */ + public static function update_bitrate($new_bitrate,$song_id) + { + self::_update_item('bitrate',$new_bitrate,$song_id,'50'); + + } // update_bitrate + + /** + * update_rate + * updates the rate field + */ + public static function update_rate($new_rate,$song_id) + { + self::_update_item('rate',$new_rate,$song_id,'50'); + + } // update_rate + + /** + * update_mode + * updates the mode field + */ + public static function update_mode($new_mode,$song_id) + { + self::_update_item('mode',$new_mode,$song_id,'50'); + + } // update_mode + + /** + * update_size + * updates the size field + */ + public static function update_size($new_size,$song_id) + { + self::_update_item('size',$new_size,$song_id,'50'); + + } // update_size + + /** + * update_time + * updates the time field + */ + public static function update_time($new_time,$song_id) + { + self::_update_item('time',$new_time,$song_id,'50'); + + } // update_time + + /** + * update_track + * this updates the track field + */ + public static function update_track($new_track,$song_id) + { + self::_update_item('track',$new_track,$song_id,'50'); + + } // update_track + + public static function update_mbid($new_mbid, $song_id) + { + self::_update_item('mbid', $new_mbid, $song_id, '50'); + + } // update_mbid + + /** + * update_artist + * updates the artist field + */ + public static function update_artist($new_artist,$song_id) + { + self::_update_item('artist',$new_artist,$song_id,'50'); + + } // update_artist + + /** + * update_album + * updates the album field + */ + public static function update_album($new_album,$song_id) + { + self::_update_item('album',$new_album,$song_id,'50'); + + } // update_album + + /** + * update_utime + * sets a new update time + */ + public static function update_utime($song_id,$time=0) + { + if (!$time) { $time = time(); } + + self::_update_item('update_time',$time,$song_id,'75'); + + } // update_utime + + /** + * update_played + * sets the played flag + */ + public static function update_played($new_played,$song_id) + { + self::_update_item('played',$new_played,$song_id,'25'); + + } // update_played + + /** + * update_enabled + * sets the enabled flag + */ + public static function update_enabled($new_enabled, $song_id) + { + self::_update_item('enabled',$new_enabled,$song_id,'75'); + + } // update_enabled + + /** + * _update_item + * This is a private function that should only be called from within the song class. + * It takes a field, value song id and level. first and foremost it checks the level + * against $GLOBALS['user'] to make sure they are allowed to update this record + * it then updates it and sets $this->{$field} to the new value + */ + private static function _update_item($field, $value, $song_id, $level) + { + /* Check them Rights! */ + if (!Access::check('interface',$level)) { return false; } + + /* Can't update to blank */ + if (!strlen(trim($value)) && $field != 'comment') { return false; } + + $sql = "UPDATE `song` SET `$field` = ? WHERE `id` = ?"; + Dba::write($sql, array($value, $song_id)); + + return true; + + } // _update_item + + /** + * _update_ext_item + * This updates a song record that is housed in the song_ext_info table + * These are items that aren't used normally, and often large/informational only + */ + private static function _update_ext_item($field, $value, $song_id, $level) + { + /* Check them rights boy! */ + if (!Access::check('interface',$level)) { return false; } + + $sql = "UPDATE `song_data` SET `$field` = ? WHERE `song_id` = ?"; + Dba::write($sql, array($value, $song_id)); + + return true; + + } // _update_ext_item + + /** + * format + * This takes the current song object + * and does a ton of formating on it creating f_??? variables on the current + * object + */ + public function format() + { + $this->fill_ext_info(); + + // Format the filename + preg_match("/^.*\/(.*?)$/", $this->file, $short); + if (is_array($short) && isset($short[1])) { + $this->f_file = htmlspecialchars($short[1]); + } + + // Format the album name + $this->f_album_full = $this->get_album_name(); + $this->f_album = $this->f_album_full; + + // Format the artist name + $this->f_artist_full = $this->get_artist_name(); + $this->f_artist = $this->f_artist_full; + + // Format the title + $this->f_title_full = $this->title; + $this->f_title = $this->title; + + // Create Links for the different objects + $this->link = AmpConfig::get('web_path') . "/song.php?action=show_song&song_id=" . $this->id; + $this->f_link = "link) . "\" title=\"" . scrub_out($this->f_artist) . " - " . scrub_out($this->title) . "\"> " . scrub_out($this->f_title) . ""; + $this->f_album_link = "album . "\" title=\"" . scrub_out($this->f_album_full) . "\"> " . scrub_out($this->f_album) . ""; + $this->f_artist_link = "artist . "\" title=\"" . scrub_out($this->f_artist_full) . "\"> " . scrub_out($this->f_artist) . ""; + + // Format the Bitrate + $this->f_bitrate = intval($this->bitrate/1000) . "-" . strtoupper($this->mode); + + // Format the Time + $min = floor($this->time/60); + $sec = sprintf("%02d", ($this->time%60) ); + $this->f_time = $min . ":" . $sec; + + // Format the track (there isn't really anything to do here) + $this->f_track = $this->track; + + // Get the top tags + $this->tags = Tag::get_top_tags('song', $this->id); + $this->f_tags = Tag::get_display($this->tags); + + // Format the size + $this->f_size = UI::format_bytes($this->size); + + $this->f_lyrics = "title) . "\" href=\"" . AmpConfig::get('web_path') . "/song.php?action=show_lyrics&song_id=" . $this->id . "\">" . T_('Show Lyrics') . ""; + + return true; + + } // format + + /** + * format_pattern + * This reformats the song information based on the catalog + * rename patterns + */ + public function format_pattern() + { + $extension = ltrim(substr($this->file,strlen($this->file)-4,4),"."); + + $catalog = Catalog::create_from_id($this->catalog); + + // If we don't have a rename pattern then just return it + if (!trim($catalog->rename_pattern)) { + $this->f_pattern = $this->title; + $this->f_file = $this->title . '.' . $extension; + return; + } + + /* Create the filename that this file should have */ + $album = $this->f_album_full; + $artist = $this->f_artist_full; + $track = sprintf('%02d', $this->track); + $title = $this->title; + $year = $this->year; + + /* Start replacing stuff */ + $replace_array = array('%a','%A','%t','%T','%y','/','\\'); + $content_array = array($artist,$album,$title,$track,$year,'-','-'); + + $rename_pattern = str_replace($replace_array,$content_array,$catalog->rename_pattern); + + $rename_pattern = preg_replace("[\-\:\!]","_",$rename_pattern); + + $this->f_pattern = $rename_pattern; + $this->f_file = $rename_pattern . "." . $extension; + + } // format_pattern + + /** + * get_fields + * This returns all of the 'data' fields for this object, we need to filter out some that we don't + * want to present to a user, and add some that don't exist directly on the object but are related + */ + public static function get_fields() + { + $fields = get_class_vars('Song'); + + unset($fields['id'],$fields['_transcoded'],$fields['_fake'],$fields['cache_hit'],$fields['mime'],$fields['type']); + + // Some additional fields + $fields['tag'] = true; + $fields['catalog'] = true; +//FIXME: These are here to keep the ideas, don't want to have to worry about them for now +// $fields['rating'] = true; +// $fields['recently Played'] = true; + + return $fields; + + } // get_fields + + /** + * get_from_path + * This returns all of the songs that exist under the specified path + */ + public static function get_from_path($path) + { + $path = Dba::escape($path); + + $sql = "SELECT * FROM `song` WHERE `file` LIKE '$path%'"; + $db_results = Dba::read($sql); + + $songs = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $songs[] = $row['id']; + } + + return $songs; + + } // get_from_path + + /** + * @function get_rel_path + * @discussion returns the path of the song file stripped of the catalog path + * used for mpd playback + */ + public function get_rel_path($file_path=0,$catalog_id=0) + { + $info = null; + if (!$file_path) { + $info = $this->_get_info(); + $file_path = $info['file']; + } + if (!$catalog_id) { + if (!is_array($info)) { + $info = $this->_get_info(); + } + $catalog_id = $info['catalog']; + } + $catalog = Catalog::create_from_id( $catalog_id ); + return $catalog->get_rel_path($file_path); + + } // get_rel_path + + /** + * play_url + * This function takes all the song information and correctly formats a + * a stream URL taking into account the downsmapling mojo and everything + * else, this is the true function + */ + public static function play_url($oid, $additional_params='') + { + $song = new Song($oid); + $user_id = $GLOBALS['user']->id ? scrub_out($GLOBALS['user']->id) : '-1'; + $type = $song->type; + + // Checking if the song is gonna be transcoded into another type + // Some players doesn't allow a type streamed into another without giving the right extension + $transcode_cfg = AmpConfig::get('transcode'); + $transcode_mode = AmpConfig::get('transcode_' . $type); + if ($transcode_cfg == 'always' || ($transcode_cfg != 'never' && $transcode_mode == 'required')) { + $transcode_settings = $song->get_transcode_settings(null); + if ($transcode_settings) { + debug_event("song.class.php", "Changing play url type from {".$type."} to {".$transcode_settings['format']."} due to encoding settings...", 5); + $type = $transcode_settings['format']; + } + } + + $song_name = $song->get_artist_name() . " - " . $song->title . "." . $type; + $song_name = str_replace("/", "-", $song_name); + $song_name = str_replace("?", "", $song_name); + $song_name = str_replace("#", "", $song_name); + $song_name = rawurlencode($song_name); + + $url = Stream::get_base_url() . "type=song&oid=" . $song->id . "&uid=" . $user_id . $additional_params . "&name=" . $song_name; + + return Stream_URL::format($url); + + } // play_url + + /** + * get_recently_played + * This function returns the last X songs that have been played + * it uses the popular threshold to figure out how many to pull + * it will only return unique object + */ + public static function get_recently_played($user_id='') + { + $user_id = Dba::escape($user_id); + + $sql = "SELECT `object_id`, `user`, `object_type`, `date`, `agent` " . + "FROM `object_count` WHERE `object_type`='song' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND " . Catalog::get_enable_filter('song', '`object_id`') . " "; + } + if ($user_id) { + // If user is not empty, we're looking directly to user personal info (admin view) + $sql .= "AND `user`='$user_id' "; + } else if (!Access::check('interface','100')) { + // If user identifier is empty, we need to retrieve only users which have allowed view of personnal info + $personal_info_id = Preference::id_from_name('allow_personal_info_recent'); + if ($personal_info_id) { + $current_user = $GLOBALS['user']->id; + $sql .= "AND `user` IN (SELECT `user` FROM `user_preference` WHERE (`preference`='$personal_info_id' AND `value`='1') OR `user`='$current_user') "; + } + } + $sql .= "ORDER BY `date` DESC "; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row; + if (count($results) >= AmpConfig::get('popular_threshold')) { break; } + } + + return $results; + + } // get_recently_played + + public function get_stream_types() + { + return Song::get_stream_types_for_type($this->type); + } // end stream_types + + public static function get_stream_types_for_type($type) + { + $types = array(); + $transcode = AmpConfig::get('transcode_' . $type); + + if ($transcode != 'required') { + $types[] = 'native'; + } + if (make_bool($transcode)) { + $types[] = 'transcode'; + } + + return $types; + } // end stream_types + + public function get_transcode_settings($target = null) + { + $source = $this->type; + + if ($target) { + debug_event('song.class.php', 'Explicit format request {'.$target.'}', 5); + } else if ($target = AmpConfig::get('encode_target_' . $source)) { + debug_event('song.class.php', 'Defaulting to configured target format for ' . $source, 5); + } else if ($target = AmpConfig::get('encode_target')) { + debug_event('song.class.php', 'Using default target format', 5); + } else { + $target = $source; + debug_event('song.class.php', 'No default target for ' . $source . ', choosing to resample', 5); + } + + debug_event('song.class.php', 'Transcode settings: from ' . $source . ' to ' . $target, 5); + + $cmd = AmpConfig::get('transcode_cmd_' . $source) ?: AmpConfig::get('transcode_cmd'); + $args = AmpConfig::get('encode_args_' . $target); + + if (!$args) { + debug_event('song.class.php', 'Target format ' . $target . ' is not properly configured', 2); + return false; + } + + debug_event('song.class.php', 'Command: ' . $cmd . ' Arguments: ' . $args, 5); + return array('format' => $target, 'command' => $cmd . ' ' . $args); + } + + public function get_lyrics() + { + if ($this->lyrics) { + return array('text' => $this->lyrics); + } + + foreach (Plugin::get_plugins('get_lyrics') as $plugin_name) { + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $lyrics = $plugin->_plugin->get_lyrics($this); + if ($lyrics != false) { + return $lyrics; + } + } + } + } + + public function run_custom_play_action($action_index, $codec='') + { + $transcoder = array(); + $actions = Song::get_custom_play_actions(); + if ($action_index <= count($actions)) { + $action = $actions[$action_index - 1]; + if (!$codec) { + $codec = $this->type; + } + + $run = str_replace("%f", $this->file, $action['run']); + $run = str_replace("%c", $codec, $run); + $run = str_replace("%a", $this->f_artist, $run); + $run = str_replace("%A", $this->f_album, $run); + $run = str_replace("%t", $this->f_title, $run); + + debug_event('song', "Running custom play action: " . $run, 3); + + $descriptors = array(1 => array('pipe', 'w')); + if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + // Windows doesn't like to provide stderr as a pipe + $descriptors[2] = array('pipe', 'w'); + } + $process = proc_open($run, $descriptors, $pipes); + + $transcoder['process'] = $process; + $transcoder['handle'] = $pipes[1]; + $transcoder['stderr'] = $pipes[2]; + $transcoder['format'] = $codec; + } + + return $transcoder; + } + + public function show_custom_play_actions() + { + $actions = Song::get_custom_play_actions(); + foreach ($actions as $action) { + echo Ajax::button('?page=stream&action=directplay&playtype=song&song_id=' . $this->id . '&custom_play_action=' . $action['index'], $action['icon'], T_($action['title']), $action['icon'] . '_song_' . $this->id); + } + } + + public static function get_custom_play_actions() + { + $actions = array(); + $i = 0; + while (AmpConfig::get('custom_play_action_title_' . $i)) { + $actions[] = array( + 'index' => ($i + 1), + 'title' => AmpConfig::get('custom_play_action_title_' . $i), + 'icon' => AmpConfig::get('custom_play_action_icon_' . $i), + 'run' => AmpConfig::get('custom_play_action_run_' . $i), + ); + ++$i; + } + + return $actions; + } + +} // end of song class diff --git a/sources/lib/class/song_preview.class.php b/sources/lib/class/song_preview.class.php new file mode 100644 index 0000000..5048e98 --- /dev/null +++ b/sources/lib/class/song_preview.class.php @@ -0,0 +1,285 @@ +id = intval($id); + + if ($info = $this->_get_info()) { + foreach ($info as $key => $value) { + $this->$key = $value; + } + $data = pathinfo($this->file); + $this->type = strtolower($data['extension']); + $this->mime = Song::type_to_mime($this->type); + } else { + $this->id = null; + return false; + } + + return true; + + } // constructor + + /** + * insert + * + * This inserts the song preview described by the passed array + */ + public static function insert($results) + { + $sql = 'INSERT INTO `song_preview` (`file`, `album_mbid`, `artist`, `artist_mbid`, `title`, `disk`, `track`, `mbid`, `session`) ' . + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + + $db_results = Dba::write($sql, array( + $results['file'], + $results['album_mbid'], + $results['artist'], + $results['artist_mbid'], + $results['title'], + $results['disk'], + $results['track'], + $results['mbid'], + $results['session'], + )); + + if (!$db_results) { + debug_event('song_preview', 'Unable to insert ' . $results[''], 2); + return false; + } + + return Dba::insert_id(); + } + + /** + * build_cache + * + * This attempts to reduce queries by asking for everything in the + * browse all at once and storing it in the cache, this can help if the + * db connection is the slow point. + */ + public static function build_cache($song_ids) + { + if (!is_array($song_ids) || !count($song_ids)) { return false; } + + $idlist = '(' . implode(',', $song_ids) . ')'; + + // Callers might have passed array(false) because they are dumb + if ($idlist == '()') { return false; } + + // Song data cache + $sql = 'SELECT `id`, `file`, `album_mbid`, `artist`, `artist_mbid`, `title`, `disk`, `track`, `mbid` ' . + 'FROM `song_preview` ' . + "WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + $artists = array(); + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('song_preview', $row['id'], $row); + if ($row['artist']) { + $artists[$row['artist']] = $row['artist']; + } + } + + Artist::build_cache($artists); + + return true; + + } // build_cache + + /** + * _get_info + */ + private function _get_info() + { + $id = $this->id; + + if (parent::is_cached('song_preview', $id)) { + return parent::get_from_cache('song_preview', $id); + } + + $sql = 'SELECT `id`, `file`, `album_mbid`, `artist`, `artist_mbid`, `title`, `disk`, `track`, `mbid` ' . + 'FROM `song_preview` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($id)); + + $results = Dba::fetch_assoc($db_results); + if (!empty($results['id'])) { + if (empty($results['artist_mbid'])) { + $sql = 'SELECT `mbid` FROM `artist` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($results['artist'])); + if ($artist_res = Dba::fetch_assoc($db_results)) { + $results['artist_mbid'] = $artist_res['mbid']; + } + } + parent::add_to_cache('song_preview', $id, $results); + return $results; + } + + return false; + } + + /** + * get_artist_name + * gets the name of $this->artist, allows passing of id + */ + public function get_artist_name($artist_id=0) + { + if (!$artist_id) { $artist_id = $this->artist; } + $artist = new Artist($artist_id); + if ($artist->prefix) + return $artist->prefix . " " . $artist->name; + else + return $artist->name; + + } // get_album_name + + /** + * format + * This takes the current song object + * and does a ton of formating on it creating f_??? variables on the current + * object + */ + public function format() + { + // Format the filename + preg_match("/^.*\/(.*?)$/",$this->file, $short); + $this->f_file = htmlspecialchars($short[1]); + + // Format the artist name + if ($this->artist) { + $this->f_artist_full = $this->get_artist_name(); + $this->f_artist_link = "artist . "\" title=\"" . scrub_out($this->f_artist_full) . "\"> " . scrub_out($this->f_artist) . ""; + } else { + $wartist = Wanted::get_missing_artist($this->artist_mbid); + $this->f_artist_link = $wartist['link']; + $this->f_artist_full = $wartist['name']; + } + $this->f_artist = $this->f_artist_full; + + // Format the title + $this->f_title_full = $this->title; + $this->f_title = $this->title; + + $this->link = "#"; + $this->f_link = "link) . "\" title=\"" . scrub_out($this->f_artist) . " - " . scrub_out($this->title) . "\"> " . scrub_out($this->f_title) . ""; + $this->f_album_link = "album_mbid . "&artist=" . $this->artist . "\" title=\"" . $this->f_album . "\">" . $this->f_album . ""; + + // Format the track (there isn't really anything to do here) + $this->f_track = $this->track; + + return true; + + } // format + + /** + * play_url + * This function takes all the song information and correctly formats a + * a stream URL taking into account the downsmapling mojo and everything + * else, this is the true function + */ + public static function play_url($oid, $additional_params='') + { + $song = new Song_Preview($oid); + $user_id = $GLOBALS['user']->id ? scrub_out($GLOBALS['user']->id) : '-1'; + $type = $song->type; + + $song_name = rawurlencode($song->get_artist_name() . " - " . $song->title . "." . $type); + + $url = Stream::get_base_url() . "type=song_preview&oid=$song->id&uid=$user_id&name=$song_name"; + + return Stream_URL::format($url . $additional_params); + + } // play_url + + public function get_stream_types() + { + return array('native'); + } + + /** + * get_transcode_settings + * + * FIXME: Song Preview transcoding is not implemented + */ + public function get_transcode_settings($target = null) + { + return false; + } + + public static function get_song_previews($album_mbid) + { + $songs = array(); + + $sql = "SELECT `id` FROM `song_preview` " . + "WHERE `session` = ? AND `album_mbid` = ?"; + $db_results = Dba::read($sql, array(session_id(), $album_mbid)); + + while ($results = Dba::fetch_assoc($db_results)) { + $songs[] = new Song_Preview($results['id']); + } + + return $songs; + } + + public static function gc() + { + $sql = 'DELETE FROM `song_preview` USING `song_preview` ' . + 'LEFT JOIN `session` ON `session`.`id`=`song_preview`.`session` ' . + 'WHERE `session`.`id` IS NULL'; + return Dba::write($sql); + } + +} // end of song_preview class diff --git a/sources/lib/class/stats.class.php b/sources/lib/class/stats.class.php new file mode 100644 index 0000000..64ddcd9 --- /dev/null +++ b/sources/lib/class/stats.class.php @@ -0,0 +1,378 @@ +id; + + $sql = "SELECT * FROM `object_count` " . + "LEFT JOIN `song` ON `song`.`id` = `object_count`.`object_id` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `object_count`.`user` = ? AND `object_count`.`object_type`='song' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY `object_count`.`date` DESC LIMIT 1"; + $db_results = Dba::read($sql, array($user_id)); + + $results = Dba::fetch_assoc($db_results); + + return $results; + + } // get_last_song + + /** + * get_object_history + * This returns the objects that have happened for $user_id sometime after $time + * used primarly by the democratic cooldown code + */ + public static function get_object_history($user_id='',$time) + { + $user_id = $user_id ? $user_id : $GLOBALS['user']->id; + + $sql = "SELECT * FROM `object_count` " . + "LEFT JOIN `song` ON `song`.`id` = `object_count`.`object_id` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + } + $sql .= "WHERE `object_count`.`user` = ? AND `object_count`.`object_type`='song' AND `object_count`.`date` >= ? "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND `catalog`.`enabled` = '1' "; + } + $sql .= "ORDER BY `object_count`.`date` DESC"; + $db_results = Dba::read($sql, array($user_id, $time)); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['object_id']; + } + + return $results; + + } // get_object_history + + /** + * get_top_sql + * This returns the get_top sql + */ + public static function get_top_sql($type, $threshold = '') + { + $type = self::validate_type($type); + /* If they don't pass one, then use the preference */ + if (!$threshold) { + $threshold = AmpConfig::get('stats_threshold'); + } + $date = time() - (86400*$threshold); + + /* Select Top objects counting by # of rows */ + $sql = "SELECT object_id as `id`, COUNT(*) AS `count` FROM object_count" . + " WHERE object_type = '" . $type ."' AND date >= '" . $date . "' "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND " . Catalog::get_enable_filter($type, '`object_id`'); + } + $sql .= " GROUP BY object_id ORDER BY `count` DESC "; + return $sql; + } + + /** + * get_top + * This returns the top X for type Y from the + * last stats_threshold days + */ + public static function get_top($type,$count='',$threshold = '',$offset='') + { + if (!$count) { + $count = AmpConfig::get('popular_threshold'); + } + + $count = intval($count); + if (!$offset) { + $limit = $count; + } else { + $limit = intval($offset) . "," . $count; + } + + $sql = self::get_top_sql($type, $threshold); + $sql .= "LIMIT $limit"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + return $results; + + } // get_top + + /** + * get_recent_sql + * This returns the get_recent sql + */ + public static function get_recent_sql($type, $user_id='') + { + $type = self::validate_type($type); + + $user_sql = ''; + if (!empty($user_id)) { + $user_sql = " AND `user` = '" . $user_id . "'"; + } + + $sql = "SELECT DISTINCT(`object_id`) as `id`, MAX(`date`) FROM object_count" . + " WHERE `object_type` = '" . $type ."'" . $user_sql; + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND " . Catalog::get_enable_filter($type, '`object_id`'); + } + $sql .= " GROUP BY `object_id` ORDER BY MAX(`date`) DESC, `id` "; + + return $sql; + } + + /** + * get_recent + * This returns the recent X for type Y + */ + public static function get_recent($type, $count='',$offset='') + { + if (!$count) { + $count = AmpConfig::get('popular_threshold'); + } + + $count = intval($count); + $type = self::validate_type($type); + if (!$offset) { + $limit = $count; + } else { + $limit = intval($offset) . "," . $count; + } + + $sql = self::get_recent_sql($type); + $sql .= "LIMIT $limit"; + $db_results = Dba::read($sql); + + $results = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_recent + + /** + * get_user + * This gets all stats for atype based on user with thresholds and all + * If full is passed, doesn't limit based on date + */ + public static function get_user($count,$type,$user,$full='') + { + $count = intval($count); + $type = self::validate_type($type); + + /* If full then don't limit on date */ + if ($full) { + $date = '0'; + } else { + $date = time() - (86400*AmpConfig::get('stats_threshold')); + } + + /* Select Objects based on user */ + //FIXME:: Requires table scan, look at improving + $sql = "SELECT object_id,COUNT(id) AS `count` FROM object_count" . + " WHERE object_type = ? AND date >= ? AND user = ?" . + " GROUP BY object_id ORDER BY `count` DESC LIMIT $count"; + $db_results = Dba::read($sql, array($type, $date, $user)); + + $results = array(); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r; + } + + return $results; + + } // get_user + + /** + * validate_type + * This function takes a type and returns only those + * which are allowed, ensures good data gets put into the db + */ + public static function validate_type($type) + { + switch ($type) { + case 'artist': + case 'album': + case 'genre': + case 'song': + case 'video': + return $type; + default: + return 'song'; + } // end switch + + } // validate_type + + /** + * get_newest_sql + * This returns the get_newest sql + */ + public static function get_newest_sql($type, $catalog=0) + { + $type = self::validate_type($type); + + $sql = "SELECT DISTINCT(`$type`) as `id`, MIN(`addition_time`) AS `real_atime` FROM `song` "; + $sql .= "LEFT JOIN `catalog` ON `catalog`.`id` = `song`.`catalog` "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "WHERE `catalog`.`enabled` = '1' "; + } + if ($catalog > 0) { + $sql .= "AND `catalog` = '" . scrub_in($catalog) ."' "; + } + $sql .= "GROUP BY `$type` ORDER BY `real_atime` DESC "; + return $sql; + } + + /** + * get_newest + * This returns an array of the newest artists/albums/whatever + * in this ampache instance + */ + public static function get_newest($type, $count='', $offset='', $catalog=0) + { + if (!$count) { $count = AmpConfig::get('popular_threshold'); } + if (!$offset) { + $limit = $count; + } else { + $limit = $offset . ',' . $count; + } + + $sql = self::get_newest_sql($type, $catalog); + $sql .= "LIMIT $limit"; + $db_results = Dba::read($sql); + + $items = array(); + + while ($row = Dba::fetch_row($db_results)) { + $items[] = $row[0]; + } // end while results + + return $items; + + } // get_newest + +} // Stats class diff --git a/sources/lib/class/stream.class.php b/sources/lib/class/stream.class.php new file mode 100644 index 0000000..4176f1c --- /dev/null +++ b/sources/lib/class/stream.class.php @@ -0,0 +1,376 @@ + 1) { + $sql = 'SELECT COUNT(*) FROM `now_playing` ' . + 'WHERE `user` IN ' . + '(SELECT DISTINCT `user_preference`.`user` ' . + 'FROM `preference` JOIN `user_preference` ' . + 'ON `preference`.`id` = ' . + '`user_preference`.`preference` ' . + "WHERE `preference`.`name` = 'play_type' " . + "AND `user_preference`.`value` = 'downsample')"; + + $db_results = Dba::read($sql); + $results = Dba::fetch_row($db_results); + + $active_streams = intval($results[0]) ?: 0; + debug_event('stream', 'Active transcoding streams: ' . $active_streams, 5); + + // We count as one for the algorithm + // FIXME: Should this reflect the actual bit rates? + $active_streams++; + $sample_rate = floor($max_bitrate / $active_streams); + + // Exit if this would be insane + if ($sample_rate < ($min_bitrate ?: 8)) { + debug_event('stream', 'Max transcode bandwidth already allocated. Active streams: ' . $active_streams, 2); + header('HTTP/1.1 503 Service Temporarily Unavailable'); + exit(); + } + + // Never go over the user's sample rate + if ($sample_rate > $user_sample_rate) { + $sample_rate = $user_sample_rate; + } + } // end if we've got bitrates + else { + $sample_rate = $user_sample_rate; + } + + return $sample_rate; + } + + /** + * start_transcode + * + * This is a rather complex function that starts the transcoding or + * resampling of a song and returns the opened file handle. + */ + public static function start_transcode($song, $type = null, $bitrate=0) + { + debug_event('stream.class.php', 'Starting transcode for {'.$song->file.'}. Type {'.$type.'}. Bitrate {'.$bitrate.'}...', 5); + + $transcode_settings = $song->get_transcode_settings($type); + // Bail out early if we're unutterably broken + if ($transcode_settings == false) { + debug_event('stream', 'Transcode requested, but get_transcode_settings failed', 2); + return false; + } + + if ($bitrate == 0) { + $sample_rate = self::get_allowed_bitrate($song); + debug_event('stream', 'Configured bitrate is ' . $sample_rate, 5); + // Validate the bitrate + $sample_rate = self::validate_bitrate($sample_rate); + } else { + $sample_rate = $bitrate; + } + + // Never upsample a song + if ($song->type == $transcode_settings['format'] && ($sample_rate * 1000) > $song->bitrate) { + debug_event('stream', 'Clamping bitrate to avoid upsampling to ' . $sample_rate, 5); + $sample_rate = self::validate_bitrate($song->bitrate / 1000); + } + + debug_event('stream', 'Final transcode bitrate is ' . $sample_rate, 5); + + $song_file = scrub_arg($song->file); + + // Finalise the command line + $command = $transcode_settings['command']; + + $string_map = array( + '%FILE%' => $song_file, + '%SAMPLE%' => $sample_rate + ); + + foreach ($string_map as $search => $replace) { + $command = str_replace($search, $replace, $command, $ret); + if (!$ret) { + debug_event('downsample', "$search not in downsample command", 5); + } + } + + debug_event('downsample', "Downsample command: $command", 3); + + $descriptors = array(1 => array('pipe', 'w')); + if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + // Windows doesn't like to provide stderr as a pipe + $descriptors[2] = array('pipe', 'w'); + } + + $process = proc_open($command, $descriptors, $pipes); + return array( + 'process' => $process, + 'handle' => $pipes[1], + 'stderr' => $pipes[2], + 'format' => $transcode_settings['format'] + ); + } + + /** + * validate_bitrate + * this function takes a bitrate and returns a valid one + */ + public static function validate_bitrate($bitrate) + { + /* Round to standard bitrates */ + $sample_rate = 16*(floor($bitrate/16)); + + return $sample_rate; + } + + /** + * gc_now_playing + * + * This will garbage collect the now playing data, + * this is done on every play start. + */ + public static function gc_now_playing() + { + // Remove any now playing entries for sessions that have been GC'd + $sql = "DELETE FROM `now_playing` USING `now_playing` " . + "LEFT JOIN `session` ON `session`.`id` = `now_playing`.`id` " . + "WHERE `session`.`id` IS NULL OR `now_playing`.`expire` < '" . time() . "'"; + Dba::write($sql); + } + + /** + * insert_now_playing + * + * This will insert the now playing data. + */ + public static function insert_now_playing($oid, $uid, $length, $sid, $type) + { + $time = intval(time() + $length); + $type = strtolower($type); + + // Ensure that this client only has a single row + $sql = 'REPLACE INTO `now_playing` ' . + '(`id`,`object_id`,`object_type`, `user`, `expire`, `insertion`) ' . + 'VALUES (?, ?, ?, ?, ?, ?)'; + Dba::write($sql, array($sid, $oid, $type, $uid, $time, time())); + } + + /** + * clear_now_playing + * + * There really isn't anywhere else for this function, shouldn't have + * deleted it in the first place. + */ + public static function clear_now_playing() + { + $sql = 'TRUNCATE `now_playing`'; + Dba::write($sql); + + return true; + } + + /** + * get_now_playing + * + * This returns the now playing information + */ + public static function get_now_playing() + { + $sql = 'SELECT `session`.`agent`, `np`.* FROM `now_playing` AS `np` '; + $sql .= 'LEFT JOIN `session` ON `session`.`id` = `np`.`id` '; + + if (AmpConfig::get('now_playing_per_user')) { + $sql .= 'INNER JOIN ( ' . + 'SELECT MAX(`insertion`) AS `max_insertion`, `user`, `id` ' . + 'FROM `now_playing` ' . + 'GROUP BY `user`' . + ') `np2` ' . + 'ON `np`.`user` = `np2`.`user` ' . + 'AND `np`.`insertion` = `np2`.`max_insertion` '; + } + + if (!Access::check('interface','100')) { + // We need to check only for users which have allowed view of personnal info + $personal_info_id = Preference::id_from_name('allow_personal_info_now'); + if ($personal_info_id) { + $current_user = $GLOBALS['user']->id; + $sql .= "WHERE (`np`.`user` IN (SELECT `user` FROM `user_preference` WHERE ((`preference`='$personal_info_id' AND `value`='1') OR `user`='$current_user'))) "; + } + } + + $sql .= 'ORDER BY `np`.`expire` DESC'; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $type = $row['object_type']; + $media = new $type($row['object_id']); + $media->format(); + $client = new User($row['user']); + $results[] = array( + 'media' => $media, + 'client' => $client, + 'agent' => $row['agent'], + 'expire' => $row['expire'] + ); + } // end while + + return $results; + + } // get_now_playing + + /** + * check_lock_media + * + * This checks to see if the media is already being played. + */ + public static function check_lock_media($media_id, $type) + { + $sql = 'SELECT `object_id` FROM `now_playing` WHERE ' . + '`object_id` = ? AND `object_type` = ?'; + $db_results = Dba::read($sql, array($media_id, $type)); + + if (Dba::num_rows($db_results)) { + debug_event('Stream', 'Unable to play media currently locked by another user', 3); + return false; + } + + return true; + } + + /** + * auto_init + * This is called on class load it sets the session + */ + public static function _auto_init() + { + // Generate the session ID. This is slightly wasteful. + $data = array(); + $data['type'] = 'stream'; + if (isset($_REQUEST['client'])) { + $data['agent'] = $_REQUEST['client']; + } + self::$session = Session::create($data); + } + + /** + * run_playlist_method + * + * This takes care of the different types of 'playlist methods'. The + * reason this is here is because it deals with streaming rather than + * playlist mojo. If something needs to happen this will echo the + * javascript required to cause a reload of the iframe. + */ + public static function run_playlist_method() + { + // If this wasn't ajax included run away + if (!defined('AJAX_INCLUDE')) { return false; } + + switch (AmpConfig::get('playlist_method')) { + case 'send': + $_SESSION['iframe']['target'] = AmpConfig::get('web_path') . '/stream.php?action=basket'; + break; + case 'send_clear': + $_SESSION['iframe']['target'] = AmpConfig::get('web_path') . '/stream.php?action=basket&playlist_method=clear'; + break; + case 'clear': + case 'default': + default: + return true; + + } // end switch on method + + // Load our javascript + echo ""; + + } // run_playlist_method + + /** + * get_base_url + * This returns the base requirements for a stream URL this does not include anything after the index.php?sid=???? + */ + public static function get_base_url() + { + $session_string = ''; + if (AmpConfig::get('require_session')) { + $session_string = 'ssid=' . self::$session . '&'; + } + + $web_path = AmpConfig::get('web_path'); + + if (AmpConfig::get('force_http_play') OR !empty(self::$force_http)) { + $web_path = str_replace("https://", "http://",$web_path); + } + if (AmpConfig::get('http_port') != '80') { + if (preg_match("/:(\d+)/",$web_path,$matches)) { + $web_path = str_replace(':' . $matches['1'],':' . AmpConfig::get('http_port'),$web_path); + } else { + $web_path = str_replace(AmpConfig::get('http_host'), AmpConfig::get('http_host') . ':' . AmpConfig::get('http_port'), $web_path); + } + } + + $url = $web_path . "/play/index.php?$session_string"; + + return $url; + + } // get_base_url + +} //end of stream class diff --git a/sources/lib/class/stream_playlist.class.php b/sources/lib/class/stream_playlist.class.php new file mode 100644 index 0000000..e2990ae --- /dev/null +++ b/sources/lib/class/stream_playlist.class.php @@ -0,0 +1,477 @@ +id = Stream::$session; + + if (!Session::exists('stream', $this->id)) { + debug_event('stream_playlist', 'Session::exists failed', 2); + return false; + } + + $this->user = intval($GLOBALS['user']->id); + + $sql = 'SELECT * FROM `stream_playlist` WHERE `sid` = ? ORDER BY `id`'; + $db_results = Dba::read($sql, array($this->id)); + + while ($row = Dba::fetch_assoc($db_results)) { + $this->urls[] = new Stream_URL($row); + } + } + + return true; + } + + private function _add_url($url) + { + debug_event("stream_playlist.class.php", "Adding url {".json_encode($url)."}...", 5); + + $this->urls[] = $url; + + $sql = 'INSERT INTO `stream_playlist` '; + + $fields = array(); + $fields[] = '`sid`'; + $values = array(); + $values[] = $this->id; + $holders = array(); + $holders[] = '?'; + + foreach ($url->properties as $field) { + if ($url->$field) { + $fields[] = '`' . $field . '`'; + $holders[] = '?'; + $values[] = $url->$field; + } + } + $sql .= '(' . implode(', ', $fields) . ') '; + $sql .= 'VALUES(' . implode(', ', $holders) . ')'; + + return Dba::write($sql, $values); + } + + public static function gc() + { + $sql = 'DELETE FROM `stream_playlist` USING `stream_playlist` ' . + 'LEFT JOIN `session` ON `session`.`id`=`stream_playlist`.`sid` ' . + 'WHERE `session`.`id` IS NULL'; + return Dba::write($sql); + } + + /** + * media_to_urlarray + * Formats the URL and media information and adds it to the object + */ + public static function media_to_urlarray($media, $additional_params='') + { + $urls = array(); + foreach ($media as $medium) { + $url = array(); + + if ($medium['custom_play_action']) { + $additional_params .= "&custom_play_action=" . $medium['custom_play_action']; + } + + $type = $medium['object_type']; + //$url['object_id'] = $medium['object_id']; + $url['type'] = $type; + + $object = new $type($medium['object_id']); + $object->format(); + // Don't add disabled media objects to the stream playlist + // Playing a disabled media return a 404 error that could make failed the player (mpd ...) + if (make_bool($object->enabled)) { + //FIXME: play_url shouldn't be static + $url['url'] = $type::play_url($object->id, $additional_params); + + $api_session = (AmpConfig::get('require_session')) ? Stream::$session : false; + + // Set a default which can be overridden + $url['author'] = 'Ampache'; + $url['time'] = $object->time; + switch ($type) { + case 'song': + $url['title'] = $object->title; + $url['author'] = $object->f_artist_full; + $url['info_url'] = $object->f_link; + $url['image_url'] = Art::url($object->album, 'album', $api_session); + $url['album'] = $object->f_album_full; + break; + case 'video': + $url['title'] = 'Video - ' . $object->title; + $url['author'] = $object->f_artist_full; + break; + case 'radio': + $url['title'] = 'Radio - ' . $object->name; + if (!empty($object->site_url)) { + $url['title'] .= ' (' . $object->site_url . ')'; + } + $url['codec'] = $object->codec; + break; + case 'song_preview': + $url['title'] = $object->title; + $url['author'] = $object->f_artist_full; + break; + case 'channel': + $url['title'] = $object->name; + break; + case 'random': + $url['title'] = 'Random URL'; + break; + default: + $url['title'] = 'URL-Add'; + $url['time'] = -1; + break; + } + + $urls[] = new Stream_URL($url); + } + } + + return $urls; + } + + public static function check_autoplay_append() + { + // For now, only iframed web player support media append in the currently played playlist + return ((AmpConfig::get('iframes') && AmpConfig::get('play_type') == 'web_player') || + AmpConfig::get('play_type') == 'localplay' + ); + } + + public function generate_playlist($type, $redirect = false) + { + if (!count($this->urls)) { + debug_event('stream_playlist', 'Error: Empty URL array for ' . $this->id, 2); + return false; + } + + debug_event('stream_playlist', 'Generating a {'.$type.'} object...', 5); + + $ext = $type; + switch ($type) { + case 'download': + case 'democratic': + case 'localplay': + case 'web_player': + // These are valid, but witchy + $ct = ""; + $redirect = false; + unset($ext); + break; + case 'asx': + $ct = 'video/x-ms-wmv'; + break; + case 'pls': + $ct = 'audio/x-scpls'; + break; + case 'ram': + $ct = 'audio/x-pn-realaudio ram'; + break; + case 'simple_m3u': + $ext = 'm3u'; + $ct = 'audio/x-mpegurl'; + break; + case 'xspf': + $ct = 'application/xspf+xml'; + break; + case 'm3u': + default: + // Assume M3U if the pooch is screwed + $ext = $type = 'm3u'; + $ct = 'audio/x-mpegurl'; + break; + } + + if ($redirect) { + // Our ID is the SID, so we always want to include it + AmpConfig::set('require_session', true, true); + header('Location: ' . Stream::get_base_url() . 'uid=' . scrub_out($this->user) . '&type=playlist&playlist_type=' . scrub_out($type)); + exit; + } + + if (isset($ext)) { + header('Cache-control: public'); + header('Content-Disposition: filename=ampache_playlist.' . $ext); + header('Content-Type: ' . $ct . ';'); + } + + $this->{'create_' . $type}(); + } + + /** + * add + * Adds an array of media + */ + public function add($media = array(), $additional_params = '') + { + $urls = $this->media_to_urlarray($media, $additional_params); + foreach ($urls as $url) { + $this->_add_url($url); + } + } + + /** + * add_urls + * Add an array of urls. This is used for things that aren't coming + * from media objects + */ + public function add_urls($urls = array()) + { + if (!is_array($urls)) { return false; } + + foreach ($urls as $url) { + $this->_add_url(new Stream_URL(array( + 'url' => $url, + 'title' => 'URL-Add', + 'author' => 'Ampache', + 'time' => '-1' + ))); + } + } + + /** + * create_simplem3u + * this creates a simple m3u without any of the extended information + */ + public function create_simple_m3u() + { + foreach ($this->urls as $url) { + echo $url->url . "\n"; + } + + } // simple_m3u + + /** + * create_m3u + * creates an m3u file, this includes the EXTINFO and as such can be + * large with very long playlists + */ + public function create_m3u() + { + echo "#EXTM3U\n"; + + foreach ($this->urls as $url) { + echo '#EXTINF:' . $url->time, ',' . $url->author . ' - ' . $url->title . "\n"; + echo $url->url . "\n"; + } + + } // create_m3u + + /** + * create_pls + */ + public function create_pls() + { + echo "[playlist]\n"; + echo 'NumberOfEntries=' . count($this->urls) . "\n"; + $i = 0; + foreach ($this->urls as $url) { + $i++; + echo 'File' . $i . '='. $url->url . "\n"; + echo 'Title' . $i . '=' . $url->author . ' - ' . + $url->title . "\n"; + echo 'Length' . $i . '=' . $url->time . "\n"; + } + + echo "Version=2\n"; + } // create_pls + + /** + * create_asx + * This should really only be used if all of the content is ASF files. + */ + public function create_asx() + { + echo '' . "\n"; + echo "Ampache ASX Playlist\n"; + echo '' . "\n"; + + foreach ($this->urls as $url) { + echo "\n"; + echo '' . scrub_out($url->title) . "\n"; + echo '' . scrub_out($url->author) . "\n"; + //FIXME: duration looks hacky and wrong + echo "\t\t" . '' . "\n"; + echo "\t\t" . '' . "\n"; + echo "\t\t" . '' . "\n"; + echo "\t\t" . '' . "\n"; + echo '' . "\n"; + echo "\n"; + } + + echo "\n"; + + } // create_asx + + /** + * create_xspf + */ + public function create_xspf() + { + $result = ""; + foreach ($this->urls as $url) { + $xml = array(); + + $xml['track'] = array( + 'title' => $url->title, + 'creator' => $url->author, + 'duration' => $url->time * 1000, + 'location' => $url->url, + 'identifier' => $url->url + ); + if ($url->type == 'video') { + $xml['track']['meta'] = + array( + 'attribute' => 'rel="provider"', + 'value' => 'video' + ); + } + if ($url->info_url) { + $xml['track']['info'] = $url->info_url; + } + if ($url->image_url) { + $xml['track']['image'] = $url->image_url; + } + if ($url->album) { + $xml['track']['album'] = $url->album; + } + + $result .= XML_Data::keyed_array($xml, true); + + } // end foreach + + XML_Data::set_type('xspf'); + echo XML_Data::header(); + echo $result; + echo XML_Data::footer(); + + } // create_xspf + + /** + * create_web_player + * + * Creates an web player. + */ + public function create_web_player() + { + if (AmpConfig::get("iframes")) { + require AmpConfig::get('prefix') . '/templates/create_web_player_embedded.inc.php'; + } else { + require AmpConfig::get('prefix') . '/templates/create_web_player.inc.php'; + } + + } // create_web_player + + /** + * create_localplay + * This calls the Localplay API to add the URLs and then start playback + */ + public function create_localplay() + { + $localplay = new Localplay(AmpConfig::get('localplay_controller')); + $localplay->connect(); + $append = $_REQUEST['append']; + if (!$append) { + $localplay->delete_all(); + } + foreach ($this->urls as $url) { + $localplay->add_url($url); + } + if (!$append) { + $localplay->play(); + } + + } // create_localplay + + /** + * create_democratic + * + * This 'votes' on the songs; it inserts them into a tmp_playlist with user + * set to -1. + */ + public function create_democratic() + { + $democratic = Democratic::get_current_playlist(); + $democratic->set_parent(); + $items = array(); + + foreach ($this->urls as $url) { + $data = Stream_URL::parse($url->url); + $items[] = array($data['type'], $data['id']); + } + + $democratic->add_vote($items); + } + + /** + * create_download + * This prompts for a download of the song + */ + private function create_download() + { + // There should only be one here... + if (count($this->urls) != 1) { + debug_event('stream_playlist', 'Download called, but $urls contains ' . json_encode($this->urls), 2); + } + + // Header redirect baby! + $url = current($this->urls); + header('Location: ' . $url->url . '&action=download'); + exit; + } //create_download + + /** + * create_ram + *this functions creates a RAM file for use by Real Player + */ + public function create_ram() + { + foreach ($this->urls as $url) { + echo $url->url . "\n"; + } + } // create_ram +} diff --git a/sources/lib/class/stream_url.class.php b/sources/lib/class/stream_url.class.php new file mode 100644 index 0000000..dddf16f --- /dev/null +++ b/sources/lib/class/stream_url.class.php @@ -0,0 +1,91 @@ + 0) + $url .= '&'; + $url .= $args[$i] . '=' . $args[$i + 1]; + } + } + } + + $query = parse_url($url, PHP_URL_QUERY); + $elements = explode('&', $query); + $results = array(); + + $results['base_url'] = $url; + + foreach ($elements as $element) { + list($key, $value) = explode('=', $element); + switch ($key) { + case 'oid': + $key = 'id'; + break; + case 'video': + if (make_bool($value)) { + $results['type'] = 'video'; + } + default: + // Nothing + break; + } + $results[$key] = $value; + } + return $results; + } + + /** + * format + * This format the string url according to settings. + */ + public static function format($url) + { + if (AmpConfig::get('stream_beautiful_url')) { + $url = str_replace('index.php?&', '', $url); + $url = str_replace('index.php?', '', $url); + $url = str_replace('&', '/', $url); + $url = str_replace('=', '/', $url); + } + + return $url; + } +} diff --git a/sources/lib/class/subsonic_api.class.php b/sources/lib/class/subsonic_api.class.php new file mode 100644 index 0000000..22c972a --- /dev/null +++ b/sources/lib/class/subsonic_api.class.php @@ -0,0 +1,1443 @@ + 1) { + if ($rhpart[0] != "Transfer-Encoding") { + header($rheader); + } + } + return strlen($header); + } + + public static function follow_stream($url) + { + set_time_limit(0); + + if (function_exists('curl_version')) { + // Curl support, we stream transparently to avoid redirect. Redirect can fail on few clients + $ch = curl_init($url); + curl_setopt_array($ch, array( + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_WRITEFUNCTION => array('Subsonic_Api', 'output_body'), + CURLOPT_HEADERFUNCTION => array('Subsonic_Api', 'output_header'), + // Ignore invalid certificate + // Default trusted chain is crap anyway and currently no custom CA option + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_TIMEOUT => 0 + )); + curl_exec($ch); + curl_close($ch); + } else { + // Stream media using http redirect if no curl support + + // Bug fix for android clients looking for /rest/ in destination url + // Warning: external catalogs will not work! + $url = str_replace('/play/', '/rest/fake/', $url); + header("Location: " . $url); + } + } + + public static function setHeader($f) + { + if (strtolower($f) == "json") { + header("Content-type: application/json; charset=" . AmpConfig::get('site_charset')); + } else if (strtolower($f) == "jsonp") { + header("Content-type: text/javascript; charset=" . AmpConfig::get('site_charset')); + } else { + header("Content-type: text/xml; charset=" . AmpConfig::get('site_charset')); + } + } + + public static function apiOutput($input, $xml) + { + $f = $input['f']; + $callback = $input['callback']; + self::apiOutput2(strtolower($f), $xml, $callback); + } + + public static function apiOutput2($f, $xml, $callback='') + { + if ($f == "json") { + echo json_encode(self::xml2json($xml), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); + } else if ($f == "jsonp") { + echo $callback . '(' . json_encode(self::xml2json($xml), JSON_PRETTY_PRINT) . ')'; + } else { + $xmlstr = $xml->asXml(); + // Format xml output + $dom = new DOMDocument(); + $dom->loadXML($xmlstr); + $dom->formatOutput = true; + echo $dom->saveXML(); + } + + } + + /** + * xml2json based from http://outlandish.com/blog/xml-to-json/ + * Because we cannot use only json_encode to respect JSON Subsonic API + */ + private static function xml2json($xml, $options = array()) + { + $defaults = array( + 'namespaceSeparator' => ':',//you may want this to be something other than a colon + 'attributePrefix' => '', //to distinguish between attributes and nodes with the same name + 'alwaysArray' => array(), //array of xml tag names which should always become arrays + 'autoArray' => true, //only create arrays for tags which appear more than once + 'textContent' => '$', //key used for the text content of elements + 'autoText' => true, //skip textContent key if node has no attributes or child nodes + 'keySearch' => false, //optional search and replace on tag and attribute names + 'keyReplace' => false, //replace values for above search values (as passed to str_replace()) + 'boolean' => true //replace true and false string with boolean values + ); + $options = array_merge($defaults, $options); + $namespaces = $xml->getDocNamespaces(); + $namespaces[''] = null; //add base (empty) namespace + + //get attributes from all namespaces + $attributesArray = array(); + foreach ($namespaces as $prefix => $namespace) { + foreach ($xml->attributes($namespace) as $attributeName => $attribute) { + //replace characters in attribute name + if ($options['keySearch']) $attributeName = + str_replace($options['keySearch'], $options['keyReplace'], $attributeName); + $attributeKey = $options['attributePrefix'] + . ($prefix ? $prefix . $options['namespaceSeparator'] : '') + . $attributeName; + $strattr = (string) $attribute; + if ($options['boolean'] && ($strattr == "true" || $strattr == "false")) { + $vattr = ($strattr == "true"); + } else { + $vattr = $strattr; + } + $attributesArray[$attributeKey] = $vattr; + } + } + + //get child nodes from all namespaces + $tagsArray = array(); + foreach ($namespaces as $prefix => $namespace) { + foreach ($xml->children($namespace) as $childXml) { + //recurse into child nodes + $childArray = self::xml2json($childXml, $options); + list($childTagName, $childProperties) = each($childArray); + + //replace characters in tag name + if ($options['keySearch']) $childTagName = + str_replace($options['keySearch'], $options['keyReplace'], $childTagName); + //add namespace prefix, if any + if ($prefix) $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName; + + if (!isset($tagsArray[$childTagName])) { + //only entry with this key + //test if tags of this type should always be arrays, no matter the element count + $tagsArray[$childTagName] = + in_array($childTagName, $options['alwaysArray']) || !$options['autoArray'] + ? array($childProperties) : $childProperties; + } elseif ( + is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName]) + === range(0, count($tagsArray[$childTagName]) - 1) + ) { + //key already exists and is integer indexed array + $tagsArray[$childTagName][] = $childProperties; + } else { + //key exists so convert to integer indexed array with previous value in position 0 + $tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties); + } + } + } + + //get text content of node + $textContentArray = array(); + $plainText = trim((string) $xml); + if ($plainText !== '') $textContentArray[$options['textContent']] = $plainText; + + //stick it all together + $propertiesArray = !$options['autoText'] || $attributesArray || $tagsArray || ($plainText === '') + ? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText; + + //return node as array + return array( + $xml->getName() => $propertiesArray + ); + } + + + /** + * ping + * Simple server ping to test connectivity with the server. + * Takes no parameter. + */ + public static function ping($input) + { + // Don't check client API version here. Some client give version 0.0.0 for ping command + + self::apiOutput($input, Subsonic_XML_Data::createSuccessResponse()); + } + + /** + * getLicense + * Get details about the software license. Always return a valid default license. + * Takes no parameter. + */ + public static function getlicense($input) + { + self::check_version($input); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addLicense($r); + self::apiOutput($input, $r); + } + + /** + * getMusicFolders + * Get all configured top-level music folders (= ampache catalogs). + * Takes no parameter. + */ + public static function getmusicfolders($input) + { + self::check_version($input); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addMusicFolders($r, Catalog::get_catalogs()); + self::apiOutput($input, $r); + } + + /** + * getIndexes + * Get an indexed structure of all artists. + * Takes optional musicFolderId and optional ifModifiedSince in parameters. + */ + public static function getindexes($input) + { + self::check_version($input); + + $musicFolderId = $input['musicFolderId']; + $ifModifiedSince = $input['ifModifiedSince']; + + $catalogs = array(); + if (!empty($musicFolderId)) { + $catalogs[] = $musicFolderId; + } else { + $catalogs = Catalog::get_catalogs(); + } + + $lastmodified = 0; + $fcatalogs = array(); + + foreach ($catalogs as $id) { + $clastmodified = 0; + $catalog = Catalog::create_from_id($id); + + if ($catalog->last_update > $clastmodified) $clastmodified = $catalog->last_update; + if ($catalog->last_add > $clastmodified) $clastmodified = $catalog->last_add; + if ($catalog->last_clean > $clastmodified) $clastmodified = $catalog->last_clean; + + if ($clastmodified > $lastmodified) $lastmodified = $clastmodified; + if (!empty($ifModifiedSince) && $clastmodified > $ifModifiedSince) $fcatalogs[] = $id; + } + if (empty($ifModifiedSince)) $fcatalogs = $catalogs; + + $r = Subsonic_XML_Data::createSuccessResponse(); + if (count($fcatalogs) > 0) { + $artists = Catalog::get_artists($fcatalogs); + Subsonic_XML_Data::addArtistsIndexes($r, $artists, $lastmodified); + } + self::apiOutput($input, $r); + } + + /** + * getMusicDirectory + * Get a list of all files in a music directory. + * Takes the directory id in parameters. + */ + public static function getmusicdirectory($input) + { + self::check_version($input); + + $id = self::check_parameter($input, 'id'); + + $r = Subsonic_XML_Data::createSuccessResponse(); + if (Subsonic_XML_Data::isArtist($id)) { + $artist = new Artist(Subsonic_XML_Data::getAmpacheId($id)); + Subsonic_XML_Data::addArtistDirectory($r, $artist); + } else if (Subsonic_XML_Data::isAlbum($id)) { + $album = new Album(Subsonic_XML_Data::getAmpacheId($id)); + Subsonic_XML_Data::addAlbumDirectory($r, $album); + } + self::apiOutput($input, $r); + } + + /** + * getGenres + * Get all genres. + * Takes no parameter. + */ + public static function getgenres($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addGenres($r, Tag::get_tags()); + self::apiOutput($input, $r); + } + + /** + * getArtists + * Get all artists. + * Takes no parameter. + */ + public static function getartists($input) + { + self::check_version($input, "1.7.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + $artists = Catalog::get_artists(Catalog::get_catalogs()); + Subsonic_XML_Data::addArtistsRoot($r, $artists); + self::apiOutput($input, $r); + } + + /** + * getArtist + * Get details fro an artist, including a list of albums. + * Takes the artist id in parameter. + */ + public static function getartist($input) + { + self::check_version($input, "1.7.0"); + + $artistid = self::check_parameter($input, 'id'); + + $artist = new Artist(Subsonic_XML_Data::getAmpacheId($artistid)); + if (empty($artist->name)) { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND, "Artist not found."); + } else { + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addArtist($r, $artist, true, true); + } + self::apiOutput($input, $r); + } + + /** + * getAlbum + * Get details for an album, including a list of songs. + * Takes the album id in parameter. + */ + public static function getalbum($input) + { + self::check_version($input, "1.7.0"); + + $albumid = self::check_parameter($input, 'id'); + + $album = new Album(Subsonic_XML_Data::getAmpacheId($albumid)); + if (empty($album->name)) { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND, "Album not found."); + } else { + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addAlbum($r, $album, true); + } + + self::apiOutput($input, $r); + } + + /** + * getVideos + * Get all videos. + * Takes no parameter. + * Not supported yet. + */ + public static function getvideos($input) + { + self::check_version($input, "1.7.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addVideos($r); + self::apiOutput($input, $r); + } + + /** + * getAlbumList + * Get a list of random, newest, highest rated etc. albums. + * Takes the list type with optional size and offset in parameters. + */ + public static function getalbumlist($input, $elementName="albumList") + { + self::check_version($input, "1.2.0"); + + $type = self::check_parameter($input, 'type'); + + $size = $input['size']; + $offset = $input['offset']; + + $albums = array(); + if ($type == "random") { + $albums = Album::get_random($size); + } else if ($type == "newest") { + $albums = Stats::get_newest("album", $size, $offset); + } else if ($type == "highest") { + $albums = Rating::get_highest("album", $size, $offset); + } else if ($type == "frequent") { + $albums = Stats::get_top("album", $size, '', $offset); + } else if ($type == "recent") { + $albums = Stats::get_recent("album", $size, $offset); + } else if ($type == "starred") { + $albums = Userflag::get_latest('album'); + } else if ($type == "alphabeticalByName") { + $albums = Catalog::get_albums($size, $offset); + } else if ($type == "alphabeticalByArtist") { + $albums = Catalog::get_albums_by_artist($size, $offset); + } + + if (count($albums)) { + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addAlbumList($r, $albums, $elementName); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + } + + self::apiOutput($input, $r); + } + + /** + * getAlbumList2 + * See getAlbumList. + */ + public static function getalbumlist2($input) + { + self::check_version($input, "1.7.0"); + self::getAlbumList($input, "albumList2"); + } + + /** + * getRandomSongs + * Get random songs matching the given criteria. + * Takes the optional size, genre, fromYear, toYear and music folder id in parameters. + */ + public static function getrandomsongs($input) + { + self::check_version($input, "1.2.0"); + + $size = $input['size']; + if (!$size) $size = 10; + $genre = $input['genre']; + $fromYear = $input['fromYear']; + $toYear = $input['toYear']; + $musicFolderId = $input['musicFolderId']; + + $search = array(); + $search['limit'] = $size; + $search['random'] = $size; + $search['type'] = "song"; + $i = 0; + if ($genre) { + $search['rule_'.$i.'_input'] = $genre; + $search['rule_'.$i.'_operator'] = 0; + $search['rule_'.$i.''] = "tag"; + ++$i; + } + if ($fromYear) { + $search['rule_'.$i.'_input'] = $fromYear; + $search['rule_'.$i.'_operator'] = 0; + $search['rule_'.$i.''] = "year"; + ++$i; + } + if ($toYear) { + $search['rule_'.$i.'_input'] = $toYear; + $search['rule_'.$i.'_operator'] = 1; + $search['rule_'.$i.''] = "year"; + ++$i; + } + if ($musicFolderId) { + if (Subsonic_XML_Data::isArtist($musicFolderId)) { + $artist = new Artist(Subsonic_XML_Data::getAmpacheId($musicFolderId)); + $finput = $artist->name; + $ftype = "artist"; + } else if (Subsonic_XML_Data::isAlbum($musicFolderId)) { + $album = new Album(Subsonic_XML_Data::getAmpacheId($musicFolderId)); + $finput = $album->name; + $ftype = "artist"; + } else { + $finput = ""; + $ftype = ""; + } + $search['rule_'.$i.'_input'] = $finput; + $search['rule_'.$i.'_operator'] = 4; + $search['rule_'.$i.''] = $ftype; + ++$i; + } + if ($i > 0) { + $songs = Random::advanced("song", $search); + } else { + $songs = Random::get_default($size); + } + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addRandomSongs($r, $songs); + self::apiOutput($input, $r); + } + + /** + * getSong + * Get details for a song + * Takes the song id in parameter. + */ + public static function getsong($input) + { + self::check_version($input, "1.7.0"); + + $songid = self::check_parameter($input, 'id'); + $r = Subsonic_XML_Data::createSuccessResponse(); + $song = new Song(Subsonic_XML_Data::getAmpacheId($songid)); + Subsonic_XML_Data::addSong($r, $song); + self::apiOutput($input, $r); + } + + /** + * getSongsByGenre + * Get songs in a given genre. + * Takes the genre with optional count and offset in parameters. + */ + public static function getsongsbygenre($input) + { + self::check_version($input, "1.9.0"); + + $genre = self::check_parameter($input, 'genre'); + $count = $input['count']; + $offset = $input['offset']; + + $tag = Tag::construct_from_name($genre); + if ($tag->id) { + $songs = Tag::get_tag_objects("song", $tag->id, $count, $offset); + } else { + $songs = array(); + } + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addSongsByGenre($r, $songs); + self::apiOutput($input, $r); + } + + /** + * getNowPlaying + * Get what is currently being played by all users. + * Takes no parameter. + */ + public static function getnowplaying($input) + { + self::check_version($input); + + $data = Stream::get_now_playing(); + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addNowPlaying($r, $data); + self::apiOutput($input, $r); + } + + /** + * search2 + * Get albums, artists and songs matching the given criteria. + * Takes query with optional artist count, artist offset, album count, album offset, song count and song offset in parameters. + */ + public static function search2($input, $elementName="searchResult2") + { + self::check_version($input, "1.2.0"); + + $query = self::check_parameter($input, 'query'); + + $artistCount = $input['artistCount']; + $artistOffset = $input['artistOffset']; + $albumCount = $input['albumCount']; + $albumOffset = $input['albumOffset']; + $songCount = $input['songCount']; + $songOffset = $input['songOffset']; + + $sartist = array(); + $sartist['limit'] = $artistCount; + if ($artistOffset) $sartist['offset'] = $artistOffset; + $sartist['rule_1_input'] = $query; + $sartist['rule_1_operator'] = 0; + $sartist['rule_1'] = "name"; + $sartist['type'] = "artist"; + $artists = Search::run($sartist); + + $salbum = array(); + $salbum['limit'] = $albumCount; + if ($albumOffset) $salbum['offset'] = $albumOffset; + $salbum['rule_1_input'] = $query; + $salbum['rule_1_operator'] = 0; + $salbum['rule_1'] = "title"; + $salbum['type'] = "album"; + $albums = Search::run($salbum); + + $ssong = array(); + $ssong['limit'] = $songCount; + if ($songOffset) $ssong['offset'] = $songOffset; + $ssong['rule_1_input'] = $query; + $ssong['rule_1_operator'] = 0; + $ssong['rule_1'] = "anywhere"; + $ssong['type'] = "song"; + $songs = Search::run($ssong); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addSearchResult($r, $artists, $albums, $songs, $elementName); + self::apiOutput($input, $r); + } + + /** + * search3 + * See search2. + */ + public static function search3($input) + { + self::check_version($input, "1.7.0"); + self::search2($input, "searchResult3"); + } + + /** + * getPlaylists + * Get all playlists a user is allowed to play. + * Takes optional user in parameter. + */ + public static function getplaylists($input) + { + self::check_version($input); + + $r = Subsonic_XML_Data::createSuccessResponse(); + $username = $input['username']; + + // Don't allow playlist listing for another user + if (empty($username) || $username == $GLOBALS['user']->username) { + Subsonic_XML_Data::addPlaylists($r, Playlist::get_playlists()); + } else { + $user = User::get_from_username($username); + if ($user->id) { + Subsonic_XML_Data::addPlaylists($r, Playlist::get_users($user->id)); + } else { + Subsonic_XML_Data::addPlaylists($r, array()); + } + } + self::apiOutput($input, $r); + } + + /** + * getPlaylist + * Get the list of files in a saved playlist. + * Takes the playlist id in parameters. + */ + public static function getplaylist($input) + { + self::check_version($input); + + $playlistid = self::check_parameter($input, 'id'); + + $playlist = new Playlist($playlistid); + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addPlaylist($r, $playlist, true); + self::apiOutput($input, $r); + } + + /** + * createPlaylist + * Create (or updates) a playlist. + * Takes playlist id in parameter if updating, name in parameter if creating and a list of song id for the playlist. + */ + public static function createplaylist($input) + { + self::check_version($input, "1.2.0"); + + $playlistId = $input['playlistId']; + $name = $input['name']; + $songId = $input['songId']; + + if ($playlistId) { + self::_updatePlaylist($playlistId, $name, $songId); + $r = Subsonic_XML_Data::createSuccessResponse(); + } else if (!empty($name)) { + $playlistId = Playlist::create($name, 'public'); + if (count($songId) > 0) { + self::_updatePlaylist($playlistId, "", $songId); + } + $r = Subsonic_XML_Data::createSuccessResponse(); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_MISSINGPARAM); + } + self::apiOutput($input, $r); + } + + private static function _updatePlaylist($id, $name, $songsIdToAdd = array(), $songIndexToRemove = array(), $public = true) + { + $playlist = new Playlist($id); + + $newdata = array(); + $newdata['name'] = (!empty($name)) ? $name : $playlist->name; + $newdata['pl_type'] = ($public) ? "public" : "private"; + $playlist->update($newdata); + + if (!is_array($songsIdToAdd)) { + $songsIdToAdd = array($songsIdToAdd); + } + if (count($songsIdToAdd) > 0) { + $playlist->add_songs(Subsonic_XML_Data::getAmpacheIds($songsIdToAdd)); + } + + if (!is_array($songIndexToRemove)) { + $songIndexToRemove = array($songIndexToRemove); + } + if (is_array($songIndexToRemove) && count($songIndexToRemove) > 0) { + $tracks = Subsonic_XML_Data::getAmpacheIds($songIndexToRemove); + foreach ($tracks as $track) { + $playlist->delete_track_number($track); + } + } + } + + /** + * updatePlaylist + * Update a playlist. + * Takes playlist id in parameter with optional name, comment, public level and a list of song id to add/remove. + */ + public static function updateplaylist($input) + { + self::check_version($input, "1.7.0"); + + $playlistId = self::check_parameter($input, 'playlistId'); + + $name = $input['name']; + $comment = $input['comment']; // Not supported. + $public = boolean($input['public']); + echo $public; + $songIdToAdd = $input['songIdToAdd']; + $songIndexToRemove = $input['songIndexToRemove']; + + $r = Subsonic_XML_Data::createSuccessResponse(); + self::apiOutput($input, $r); + } + + /** + * deletePlaylist + * Delete a saved playlist. + * Takes playlist id in parameter. + */ + public static function deleteplaylist($input) + { + self::check_version($input, "1.2.0"); + + $playlistId = self::check_parameter($input, 'playlistId'); + + $playlist = new Playlist($playlistId); + $playlist->delete(); + + $r = Subsonic_XML_Data::createSuccessResponse(); + self::apiOutput($input, $r); + } + + /** + * stream + * Streams a given media file. + * Takes the file id in parameter with optional max bit rate, file format, time offset, size and estimate content length option. + */ + public static function stream($input) + { + self::check_version($input, "1.0.0", true); + + $fileid = self::check_parameter($input, 'id', true); + + $maxBitRate = $input['maxBitRate']; // Not supported. + $format = $input['format']; // mp3, flv or raw. Not supported. + $timeOffset = $input['timeOffset']; // For video streaming. Not supported. + $size = $input['size']; // For video streaming. Not supported. + $maxBitRate = $input['maxBitRate']; // For video streaming. Not supported. + $estimateContentLength = $input['estimateContentLength']; // Force content-length guessing if transcode + + $params = '&client=' . $input['c']; + if ($estimateContentLength == 'true') { + $params .= '&content_length=required'; + } + $url = Song::play_url(Subsonic_XML_Data::getAmpacheId($fileid), $params); + self::follow_stream($url); + } + + /** + * download + * Downloads a given media file. + * Takes the file id in parameter. + */ + public static function download($input) + { + self::check_version($input, "1.0.0", true); + + $fileid = self::check_parameter($input, 'id', true); + + $url = Song::play_url(Subsonic_XML_Data::getAmpacheId($fileid), '&action=download' . '&client=' . $input['c']); + self::follow_stream($url); + } + + /** + * hls + * Create an HLS playlist. + * Takes the file id in parameter with optional max bit rate. + */ + public static function hls($input) + { + self::check_version($input, "1.7.0", true); + + $fileid = self::check_parameter($input, 'id', true); + + $bitRate = $input['bitRate']; // Not supported. + + $media = array(); + $media['object_type'] = 'song'; + $media['object_id'] = Subsonic_XML_Data::getAmpacheId($fileid); + + $medias = array(); + $medias[] = $media; + $stream = new Stream_Playlist(); + $stream->add($medias); + + header('Content-Type: application/vnd.apple.mpegurl;'); + $stream->create_m3u(); + } + + /** + * getCoverArt + * Get a cover art image. + * Takes the cover art id in parameter. + */ + public static function getcoverart($input) + { + self::check_version($input, "1.0.0", true); + + $id = self::check_parameter($input, 'id', true); + $size = $input['size']; + + $art = null; + if (Subsonic_XML_Data::isArtist($id)) { + $art = new Art(Subsonic_XML_Data::getAmpacheId($id), "artist"); + } else if (Subsonic_XML_Data::isAlbum($id)) { + $art = new Art(Subsonic_XML_Data::getAmpacheId($id), "album"); + } else if (Subsonic_XML_Data::isSong($id)) { + $art = new Art(Subsonic_XML_Data::getAmpacheId($id), "song"); + } + + if ($art != null) { + $art->get_db(); + if (!$size) { + header('Content-type: ' . $art->raw_mime); + header('Content-Length: ' . strlen($art->raw)); + echo $art->raw; + } else { + $dim = array(); + $dim['width'] = $size; + $dim['height'] = $size; + $thumb = $art->get_thumb($dim); + header('Content-type: ' . $thumb['thumb_mime']); + header('Content-Length: ' . strlen($thumb['thumb'])); + echo $thumb['thumb']; + } + } + } + + /** + * setRating + * Sets the rating for a music file. + * Takes the file id and rating in parameters. + */ + public static function setrating($input) + { + self::check_version($input, "1.6.0"); + + $id = self::check_parameter($input, 'id'); + $rating = $input['rating']; + + $robj = null; + if (Subsonic_XML_Data::isArtist($id)) { + $robj = new Rating(Subsonic_XML_Data::getAmpacheId($id), "artist"); + } else if (Subsonic_XML_Data::isAlbum($id)) { + $robj = new Rating(Subsonic_XML_Data::getAmpacheId($id), "album"); + } else if (Subsonic_XML_Data::isSong($id)) { + $robj = new Rating(Subsonic_XML_Data::getAmpacheId($id), "song"); + } + + if ($robj != null) { + $robj->set_rating($rating); + + $r = Subsonic_XML_Data::createSuccessResponse(); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND, "Media not found."); + } + + self::apiOutput($input, $r); + } + + /** + * getStarred + * Get starred songs, albums and artists. + * Takes no parameter. + * Not supported. + */ + public static function getstarred($input, $elementName="starred") + { + self::check_version($input, "1.7.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + Subsonic_XML_Data::addStarred($r, Userflag::get_latest('artist'), Userflag::get_latest('album'), Userflag::get_latest('song'), $elementName); + self::apiOutput($input, $r); + } + + + /** + * getStarred2 + * See getStarred. + */ + public static function getstarred2($input) + { + self::getStarred($input, "starred2"); + } + + /** + * star + * Attaches a star to a song, album or artist. + * Takes the optional file id, album id or artist id in parameters. + * Not supported. + */ + public static function star($input) + { + self::check_version($input, "1.7.0"); + + self::_setStar($input, true); + } + + /** + * unstar + * Removes the star from a song, album or artist. + * Takes the optional file id, album id or artist id in parameters. + * Not supported. + */ + public static function unstar($input) + { + self::check_version($input, "1.7.0"); + + self::_setStar($input, false); + } + + private static function _setStar($input, $star) + { + $id = $input['id']; + $albumId = $input['albumId']; + $artistId = $input['artistId']; + + // Normalize all in one array + $ids = array(); + + $r = Subsonic_XML_Data::createSuccessResponse(); + if ($id) { + if (!is_array($id)) { + $id = array($id); + } + foreach ($id as $i) { + $aid = Subsonic_XML_Data::getAmpacheId($i); + if (Subsonic_XML_Data::isArtist($i)) { + $type = 'artist'; + } else if (Subsonic_XML_Data::isAlbum($i)) { + $type = 'album'; + } else if (Subsonic_XML_Data::isSong($i)) { + $type = 'song'; + } else { + $type = ""; + } + $ids[] = array('id' => $aid, 'type' => $type); + } + } else if ($albumId) { + if (!is_array($albumId)) { + $albumId = array($albumId); + } + foreach ($albumId as $i) { + $aid = Subsonic_XML_Data::getAmpacheId($i); + $ids[] = array('id' => $aid, 'album'); + } + } else if ($artistId) { + if (!is_array($artistId)) { + $artistId = array($artistId); + } + foreach ($artistId as $i) { + $aid = Subsonic_XML_Data::getAmpacheId($i); + $ids[] = array('id' => $aid, 'artist'); + } + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_MISSINGPARAM); + } + + foreach ($ids as $i) { + $flag = new Userflag($i['id'], $i['type']); + $flag->set_flag($star); + } + self::apiOutput($input, $r); + } + + /** + * getUser + * Get details about a given user. + * Takes the username in parameter. + * Not supported. + */ + public static function getuser($input) + { + self::check_version($input, "1.3.0"); + + $username = self::check_parameter($input, 'username'); + + if ($GLOBALS['user']->access >= 100 || $GLOBALS['user']->username == $username) { + $r = Subsonic_XML_Data::createSuccessResponse(); + if ($GLOBALS['user']->username == $username) { + $user = $GLOBALS['user']; + } else { + $user = User::get_from_username($username); + } + Subsonic_XML_Data::addUser($r, $user); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_UNAUTHORIZED, $GLOBALS['user']->username . ' is not authorized to get details for other users.'); + } + self::apiOutput($input, $r); + } + + /** + * getUsers + * Get details about a given user. + * Takes no parameter. + * Not supported. + */ + public static function getusers($input) + { + self::check_version($input, "1.7.0"); + + if ($GLOBALS['user']->access >= 100) { + $r = Subsonic_XML_Data::createSuccessResponse(); + $users = User::get_valid_users(); + Subsonic_XML_Data::addUsers($r, $users); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_UNAUTHORIZED, $GLOBALS['user']->username . ' is not authorized to get details for other users.'); + } + self::apiOutput($input, $r); + } + + /** + * getInternetRadioStations + * Get all internet radio stations + * Takes no parameter. + */ + public static function getinternetradiostations($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + $radios = Radio::get_all_radios(); + Subsonic_XML_Data::addRadios($r, $radios); + self::apiOutput($input, $r); + } + + /** + * getShares + * Get information about shared media this user is allowed to manage. + * Takes no parameter. + */ + public static function getshares($input) + { + self::check_version($input, "1.6.0"); + + $r = Subsonic_XML_Data::createSuccessResponse(); + $shares = Share::get_share_list(); + Subsonic_XML_Data::addShares($r, $shares); + self::apiOutput($input, $r); + } + + /** + * createShare + * Create a public url that can be used by anyone to stream media. + * Takes the file id with optional description and expires parameters. + */ + public static function createshare($input) + { + self::check_version($input, "1.6.0"); + + $id = self::check_parameter($input, 'id'); + $description = $input['description']; + $expires = $input['expires']; + + if (AmpConfig::get('share')) { + if ($expires) { + $expire_days = round((($expires / 1000) - time()) / 86400, 0, PHP_ROUND_HALF_EVEN); + } else { + $expire_days = AmpConfig::get('share_expire'); + } + + $object_id = Subsonic_XML_Data::getAmpacheId($id); + if (Subsonic_XML_Data::isAlbum($id)) { + $object_type = 'album'; + } else if (Subsonic_XML_Data::isSong($id)) { + $object_type = 'song'; + } + + if (!empty($object_type)) { + $r = Subsonic_XML_Data::createSuccessResponse(); + $shares = array(); + $shares[] = Share::create_share($object_type, $object_id, true, Access::check_function('download'), $expire_days, Share::generate_secret(), 0, $description); + Subsonic_XML_Data::addShares($r, $shares); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + } + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_UNAUTHORIZED); + } + self::apiOutput($input, $r); + } + + /** + * deleteShare + * Delete an existing share. + * Takes the share id to delete in parameters. + */ + public static function deleteshare($input) + { + self::check_version($input, "1.6.0"); + + $id = self::check_parameter($input, 'id'); + + if (AmpConfig::get('share')) { + if (Share::delete_share($id)) { + $r = Subsonic_XML_Data::createSuccessResponse(); + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + } + } else { + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_UNAUTHORIZED); + } + self::apiOutput($input, $r); + } + + /**** CURRENT UNSUPPORTED FUNCTIONS ****/ + + /** + * getLyrics + * Searches and returns lyrics for a given song. + * Takes the optional artist and title in parameters. + */ + public static function getlyrics($input) + { + self::check_version($input, "1.2.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * updateShare + * Update the description and/or expiration date for an existing share. + * Takes the share id to update with optional description and expires parameters. + * Not supported. + */ + public static function updateshare($input) + { + self::check_version($input, "1.6.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * scrobble + * Scrobbles a given music file on last.fm. + * Takes the file id with optional time and submission parameters. + * Not supported. Already done by Ampache if plugin enabled. + */ + public static function scrobble($input) + { + self::check_version($input, "1.5.0"); + + // Ignore error to not break clients + $r = Subsonic_XML_Data::createSuccessResponse(); + self::apiOutput($input, $r); + } + + /** + * createUser + * Create a new user. + * Takes the username, password and email with optional roles in parameters. + * Not supported. + */ + public static function createuser($input) + { + self::check_version($input, "1.1.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * deleteUser + * Delete an existing user. + * Takes the username in parameter. + * Not supported. + */ + public static function deleteuser($input) + { + self::check_version($input, "1.3.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * changePassword + * Change the password of an existing user. + * Takes the username with new password in parameters. + * Not supported. + */ + public static function changepassword($input) + { + self::check_version($input, "1.1.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * getPodcasts + * Get all podcast channels. + * Takes the optional includeEpisodes and channel id in parameters + * Not supported. + */ + public static function getpodcasts($input) + { + self::check_version($input, "1.6.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * refreshPodcasts + * Request the server to check for new podcast episodes. + * Takes no parameters. + * Not supported. + */ + public static function refreshpodcasts($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * createPodcastChannel + * Add a new podcast channel. + * Takes the podcast url in parameter. + * Not supported. + */ + public static function createpodcastchannel($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * deletePodcastChannel + * Delete an existing podcast channel + * Takes the podcast id in parameter. + * Not supported. + */ + public static function deletepodcastchannel($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * deletePodcastEpisode + * Delete a podcast episode + * Takes the podcast episode id in parameter. + * Not supported. + */ + public static function deletepodcastepisode($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * downloadPodcastEpisode + * Request the server to download a podcast episode + * Takes the podcast episode id in parameter. + * Not supported. + */ + public static function downloadpodcastepisode($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * jukeboxControl + * Control the jukebox. + * Takes the action with optional index, offset, song id and volume gain in parameters. + * Not supported. + */ + public static function jukeboxcontrol($input) + { + self::check_version($input, "1.2.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * getChatMessages + * Get the current chat messages. + * Takes no parameter. + * Not supported. + */ + public static function getchatmessages($input) + { + self::check_version($input, "1.2.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * addChatMessages + * Add a message to the chat. + * Takes the message in parameter. + * Not supported. + */ + public static function addchatmessages($input) + { + self::check_version($input, "1.2.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * getBookmarks + * Get all user bookmarks. + * Takes no parameter. + * Not supported. + */ + public static function getbookmarks($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * createBookmark + * Creates or updates a bookmark. + * Takes the file id and position with optional comment in parameters. + * Not supported. + */ + public static function createbookmark($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } + + /** + * deleteBookmark + * Delete an existing bookmark. + * Takes the file id in parameter. + * Not supported. + */ + public static function deletebookmark($input) + { + self::check_version($input, "1.9.0"); + + $r = Subsonic_XML_Data::createError(Subsonic_XML_Data::SSERROR_DATA_NOTFOUND); + self::apiOutput($input, $r); + } +} diff --git a/sources/lib/class/subsonic_xml_data.class.php b/sources/lib/class/subsonic_xml_data.class.php new file mode 100644 index 0000000..dddc885 --- /dev/null +++ b/sources/lib/class/subsonic_xml_data.class.php @@ -0,0 +1,591 @@ += Subsonic_XML_Data::AMPACHEID_ARTIST && $id < Subsonic_XML_Data::AMPACHEID_ALBUM); + } + + public static function isAlbum($id) + { + return ($id >= Subsonic_XML_Data::AMPACHEID_ALBUM && $id < Subsonic_XML_Data::AMPACHEID_SONG); + } + + public static function isSong($id) + { + return ($id >= Subsonic_XML_Data::AMPACHEID_SONG); + } + + public static function createFailedResponse($version = "") + { + $response = self::createResponse($version); + $response->addAttribute('status', 'failed'); + return $response; + } + + public static function createSuccessResponse($version = "") + { + $response = self::createResponse($version); + $response->addAttribute('status', 'ok'); + return $response; + } + + public static function createResponse($version = "") + { + if (empty($version)) $version = Subsonic_XML_Data::API_VERSION; + $response = new SimpleXMLElement(''); + $response->addAttribute('xmlns', 'http://subsonic.org/restapi'); + $response->addAttribute('version', $version); + return $response; + } + + public static function createError($code, $message = "", $version = "") + { + if (empty($version)) $version = Subsonic_XML_Data::API_VERSION; + $response = self::createFailedResponse($version); + self::setError($response, $code, $message); + return $response; + } + + /** + * Set error information. + * + * @param SimpleXMLElement $xml Parent node + * @param integer $code Error code + * @param string $string Error message + */ + public static function setError($xml, $code, $message = "") + { + $xerr = $xml->addChild('error'); + $xerr->addAttribute('code', $code); + + if (empty($message)) { + switch ($code) { + case Subsonic_XML_Data::SSERROR_GENERIC: $message = "A generic error."; break; + case Subsonic_XML_Data::SSERROR_MISSINGPARAM: $message = "Required parameter is missing."; break; + case Subsonic_XML_Data::SSERROR_APIVERSION_CLIENT: $message = "Incompatible Subsonic REST protocol version. Client must upgrade."; break; + case Subsonic_XML_Data::SSERROR_APIVERSION_SERVER: $message = "Incompatible Subsonic REST protocol version. Server must upgrade."; break; + case Subsonic_XML_Data::SSERROR_BADAUTH: $message = "Wrong username or password."; break; + case Subsonic_XML_Data::SSERROR_UNAUTHORIZED: $message = "User is not authorized for the given operation."; break; + case Subsonic_XML_Data::SSERROR_TRIAL: $message = "The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium. Visit subsonic.org for details."; break; + case Subsonic_XML_Data::SSERROR_DATA_NOTFOUND: $message = "The requested data was not found."; break; + } + } + + $xerr->addAttribute("message", $message); + } + + public static function addLicense($xml) + { + $xlic = $xml->addChild('license'); + $xlic->addAttribute('valid', 'true'); + $xlic->addAttribute('email', 'webmaster@ampache.org'); + $xlic->addAttribute('key', 'ABC123DEF'); + $xlic->addAttribute('date', '2009-09-03T14:46:43'); + } + + public static function addMusicFolders($xml, $catalogs) + { + $xfolders = $xml->addChild('musicFolders'); + foreach ($catalogs as $id) { + $catalog = Catalog::create_from_id($id); + $xfolder = $xfolders->addChild('musicFolder'); + $xfolder->addAttribute('id', $id); + $xfolder->addAttribute('name', $catalog->name); + } + } + + public static function addArtistsIndexes($xml, $artists, $lastModified) + { + $xindexes = $xml->addChild('indexes'); + $xindexes->addAttribute('lastModified', $lastModified * 1000); + self::addArtists($xindexes, $artists); + } + + public static function addArtistsRoot($xml, $artists) + { + $xartists = $xml->addChild('artists'); + self::addArtists($xartists, $artists, true); + } + + public static function addArtists($xml, $artists, $extra=false) + { + $xlastcat = null; + $xsharpcat = null; + $xlastletter = ''; + foreach ($artists as $artist) { + if (strlen($artist->name) > 0) { + $letter = strtoupper($artist->name[0]); + if ($letter == "X" || $letter == "Y" || $letter == "Z") $letter = "X-Z"; + else if (!preg_match("/^[A-W]$/", $letter)) $letter = "#"; + + if ($letter != $xlastletter) { + $xlastletter = $letter; + if ($letter == '#' && $xsharpcat != null) { + $xlastcat = $xsharpcat; + } else { + $xlastcat = $xml->addChild('index'); + $xlastcat->addAttribute('name', $xlastletter); + + if ($letter == '#') { + $xsharpcat = $xlastcat; + } + } + } + } + + self::addArtist($xlastcat, $artist, $extra); + } + } + + public static function addArtist($xml, $artist, $extra=false, $albums=false) + { + $xartist = $xml->addChild('artist'); + $xartist->addAttribute('id', self::getArtistId($artist->id)); + $xartist->addAttribute('name', $artist->name); + + $allalbums = array(); + if ($extra || $albums) { + $allalbums = $artist->get_albums(null, true); + } + + if ($extra) { + //$xartist->addAttribute('coverArt'); + $xartist->addAttribute('albumCount', count($allalbums)); + } + if ($albums) { + foreach ($allalbums as $id) { + $album = new Album($id); + self::addAlbum($xartist, $album); + } + } + } + + public static function addAlbumList($xml, $albums, $elementName="albumList") + { + $xlist = $xml->addChild($elementName); + foreach ($albums as $id) { + $album = new Album($id); + self::addAlbum($xlist, $album); + } + } + + public static function addAlbum($xml, $album, $songs=false, $elementName="album") + { + $xalbum = $xml->addChild($elementName); + $xalbum->addAttribute('id', self::getAlbumId($album->id)); + $xalbum->addAttribute('album', $album->name); + $xalbum->addAttribute('title', self::formatAlbum($album)); + $xalbum->addAttribute('name', $album->name); + $xalbum->addAttribute('isDir', 'true'); + $album->format(); + if ($album->has_art) { + $xalbum->addAttribute('coverArt', self::getAlbumId($album->id)); + } + $xalbum->addAttribute('songCount', $album->song_count); + $xalbum->addAttribute('duration', $album->total_duration); + $xalbum->addAttribute('artistId', self::getArtistId($album->artist_id)); + $xalbum->addAttribute('parent', self::getArtistId($album->artist_id)); + $xalbum->addAttribute('artist', $album->artist_name); + + $rating = new Rating($album->id, "album"); + $rating_value = $rating->get_average_rating(); + $xalbum->addAttribute('averageRating', ($rating_value) ? $rating_value : 0); + + if ($songs) { + $allsongs = $album->get_songs(); + foreach ($allsongs as $id) { + $song = new Song($id); + self::addSong($xalbum, $song); + } + } + } + + public static function addSong($xml, $song, $elementName='song') + { + self::createSong($xml, $song, $elementName); + } + + public static function createSong($xml, $song, $elementName='song') + { + $xsong = $xml->addChild($elementName); + $xsong->addAttribute('id', self::getSongId($song->id)); + $xsong->addAttribute('parent', self::getAlbumId($song->album)); + //$xsong->addAttribute('created', ); + $xsong->addAttribute('title', $song->title); + $xsong->addAttribute('isDir', 'false'); + $xsong->addAttribute('isVideo', 'false'); + $xsong->addAttribute('type', 'music'); + $album = new Album($song->album); + $xsong->addAttribute('albumId', self::getAlbumId($album->id)); + $xsong->addAttribute('album', $album->name); + $artist = new Artist($song->artist); + $xsong->addAttribute('artistId', self::getArtistId($album->id)); + $xsong->addAttribute('artist', $artist->name); + $xsong->addAttribute('coverArt', self::getAlbumId($album->id)); + $xsong->addAttribute('duration', $song->time); + $xsong->addAttribute('bitRate', intval($song->bitrate / 1000)); + if ($song->track > 0) { + $xsong->addAttribute('track', $song->track); + } + if ($song->year > 0) { + $xsong->addAttribute('year', $song->year); + } + $tags = Tag::get_object_tags('song', $song->id); + if (count($tags) > 0) $xsong->addAttribute('genre', $tags[0]['name']); + $xsong->addAttribute('size', $song->size); + if ($album->disk > 0) $xsong->addAttribute('discNumber', $album->disk); + $xsong->addAttribute('suffix', $song->type); + $xsong->addAttribute('contentType', $song->mime); + // Create a clean fake path instead of song real file path to have better offline mode storage on Subsonic clients + $path = $artist->name . '/' . $album->name . '/' . basename($song->file); + $xsong->addAttribute('path', $path); + + // Set transcoding information if required + $transcode_cfg = AmpConfig::get('transcode'); + $transcode_mode = AmpConfig::get('transcode_' . $song->type); + if ($transcode_cfg == 'always' || ($transcode_cfg != 'never' && $transcode_mode == 'required')) { + $transcode_settings = $song->get_transcode_settings(null); + if ($transcode_settings) { + $transcode_type = $transcode_settings['format']; + $xsong->addAttribute('transcodedSuffix', $transcode_type); + $xsong->addAttribute('transcodedContentType', Song::type_to_mime($transcode_type)); + } + } + + return $xsong; + } + + private static function formatAlbum($album) + { + $name = $album->name; + if ($album->year > 0) { + $name .= " [" . $album->year . "]"; + } + + if ($album->disk) { + $name .= " [" . T_('Disk') . " " . $album->disk . "]"; + } + + return $name; + } + + public static function addArtistDirectory($xml, $artist) + { + $xdir = $xml->addChild('directory'); + $xdir->addAttribute('id', self::getArtistId($artist->id)); + $xdir->addAttribute('name', $artist->name); + + $allalbums = $artist->get_albums(null, true); + foreach ($allalbums as $id) { + $album = new Album($id); + self::addAlbum($xdir, $album, false, "child"); + } + } + + public static function addAlbumDirectory($xml, $album) + { + $xdir = $xml->addChild('directory'); + $xdir->addAttribute('id', self::getAlbumId($album->id)); + $xdir->addAttribute('name', self::formatAlbum($album)); + $album->format(); + //$xdir->addAttribute('parent', self::getArtistId($album->artist_id)); + + $allsongs = $album->get_songs(); + foreach ($allsongs as $id) { + $song = new Song($id); + self::addSong($xdir, $song, "child"); + } + } + + public static function addGenres($xml, $tags) + { + $xgenres = $xml->addChild('genres'); + + foreach ($tags as $tag) { + $otag = new Tag($tag['id']); + $xgenres->addChild('genre', $otag->name); + } + } + + public static function addVideos($xml) + { + // Not supported yet + $xml->addChild('videos'); + } + + public static function addPlaylists($xml, $playlists) + { + $xplaylists = $xml->addChild('playlists'); + foreach ($playlists as $id) { + $playlist = new Playlist($id); + self::addPlaylist($xplaylists, $playlist); + } + } + + public static function addPlaylist($xml, $playlist, $songs=false) + { + $xplaylist = $xml->addChild('playlist'); + $xplaylist->addAttribute('id', $playlist->id); + $xplaylist->addAttribute('name', $playlist->name); + $user = new User($playlist->user); + $xplaylist->addAttribute('owner', $user->username); + $xplaylist->addAttribute('public', ($playlist->type != "private") ? "true" : "false"); + $xplaylist->addAttribute('created', date("c", $playlist->date)); + $xplaylist->addAttribute('songCount', $playlist->get_song_count()); + $xplaylist->addAttribute('duration', $playlist->get_total_duration()); + + if ($songs) { + $allsongs = $playlist->get_songs(); + foreach ($allsongs as $id) { + $song = new Song($id); + self::addSong($xplaylist, $song, "entry"); + } + } + } + + public static function addRandomSongs($xml, $songs) + { + $xsongs = $xml->addChild('randomSongs'); + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xsongs, $song); + } + } + + public static function addSongsByGenre($xml, $songs) + { + $xsongs = $xml->addChild('songsByGenre'); + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xsongs, $song); + } + } + + public static function addNowPlaying($xml, $data) + { + $xplaynow = $xml->addChild('nowPlaying'); + foreach ($data as $d) { + $track = self::createSong($xplaynow, $d['media'], "entry"); + $track->addAttribute('username', $d['client']->username); + $track->addAttribute('minutesAgo', intval(time() - ($d['expire'] - AmpConfig::get('stream_length')) / 1000)); + $track->addAttribute('playerId', $d['agent']); + } + } + + public static function addSearchResult($xml, $artists, $albums, $songs, $elementName = "searchResult2") + { + $xresult = $xml->addChild($elementName); + foreach ($artists as $id) { + $artist = new Artist($id); + self::addArtist($xresult, $artist); + } + foreach ($albums as $id) { + $album = new Album($id); + self::addAlbum($xresult, $album); + } + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xresult, $song); + } + } + + public static function addStarred($xml, $artists, $albums, $songs, $elementName="starred") + { + $xstarred = $xml->addChild($elementName); + + foreach ($artists as $id) { + $artist = new Artist($id); + self::addArtist($xstarred, $artist); + } + + foreach ($albums as $id) { + $album = new Album($id); + self::addAlbum($xstarred, $album); + } + + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xstarred, $song); + } + } + + public static function addUser($xml, $user) + { + $xuser = $xml->addChild('user'); + $xuser->addAttribute('username', $user->username); + $xuser->addAttribute('email', $user->email); + $xuser->addAttribute('scrobblingEnabled', 'false'); + $isManager = ($user->access >= 75); + $isAdmin = ($user->access >= 100); + $xuser->addAttribute('adminRole', $isAdmin ? 'true' : 'false'); + $xuser->addAttribute('settingsRole', $isAdmin ? 'true' : 'false'); + $xuser->addAttribute('downloadRole', Preference::get_by_user($user->id, 'download') ? 'true' : 'false'); + $xuser->addAttribute('playlistRole', 'true'); + $xuser->addAttribute('coverArtRole', $isManager ? 'true' : 'false'); + $xuser->addAttribute('commentRole', 'false'); + $xuser->addAttribute('podcastRole', 'false'); + $xuser->addAttribute('streamRole', 'true'); + $xuser->addAttribute('jukeboxRole', 'false'); + $xuser->addAttribute('shareRole', 'false'); + } + + public static function addUsers($xml, $users) + { + $xusers = $xml->addChild('users'); + foreach ($users as $id) { + $user = new User($id); + self::addUser($xusers, $user); + } + } + + public static function addRadio($xml, $radio) + { + $xradio = $xml->addChild('internetRadioStation '); + $xradio->addAttribute('id', $radio->id); + $xradio->addAttribute('name', $radio->name); + $xradio->addAttribute('streamUrl', $radio->url); + $xradio->addAttribute('homePageUrl', $radio->site_url); + } + + public static function addRadios($xml, $radios) + { + $xradios = $xml->addChild('internetRadioStations'); + foreach ($radios as $id) { + $radio = new Radio($id); + self::addRadio($xradios, $radio); + } + } + + public static function addShare($xml, $share) + { + $xshare = $xml->addChild('share '); + $xshare->addAttribute('id', $share->id); + $xshare->addAttribute('url', $share->public_url); + $xshare->addAttribute('description', $share->description); + $user = new User($share->user); + $xshare->addAttribute('username', $user->username); + $xshare->addAttribute('created', date("c", $share->creation_date)); + if ($share->lastvisit_date > 0) { + $xshare->addAttribute('lastVisited', date("c", $share->lastvisit_date)); + } + if ($share->expire_days > 0) { + $xshare->addAttribute('expires', date("c", $share->creation_date + ($share->expire_days * 86400))); + } + $xshare->addAttribute('visitCount', $share->counter); + + if ($share->object_type == 'song') { + $song = new Song($share->object_id); + self::addSong($xshare, $song, "entry"); + } elseif ($share->object_type == 'playlist') { + $playlist = new Playlist($share->object_id); + $songs = $playlist->get_songs(); + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xshare, $song, "entry"); + } + } elseif ($share->object_type == 'album') { + $album = new Album($share->object_id); + $songs = $album->get_songs(); + foreach ($songs as $id) { + $song = new Song($id); + self::addSong($xshare, $song, "entry"); + } + } + } + + public static function addShares($xml, $shares) + { + $xshares = $xml->addChild('shares'); + foreach ($shares as $id) { + $share = new Share($id); + // Don't add share with max counter already reached + if ($share->max_counter == 0 || $share->counter < $share->max_counter) { + self::addShare($xshares, $share); + } + } + } +} diff --git a/sources/lib/class/tag.class.php b/sources/lib/class/tag.class.php new file mode 100644 index 0000000..a318432 --- /dev/null +++ b/sources/lib/class/tag.class.php @@ -0,0 +1,563 @@ +get_info($id); + + foreach ($info as $key=>$value) { + $this->$key = $value; + } // end foreach + + } // constructor + + /** + * construct_from_name + * This attempts to construct the tag from a name, rather then the ID + */ + public static function construct_from_name($name) + { + $tag_id = self::tag_exists($name); + + $tag = new Tag($tag_id); + + return $tag; + + } // construct_from_name + + /** + * build_cache + * This takes an array of object ids and caches all of their information + * in a single query, cuts down on the connections + */ + public static function build_cache($ids) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + $idlist = '(' . implode(',', $ids) . ')'; + + $sql = "SELECT * FROM `tag` WHERE `id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('tag', $row['id'], $row); + } + + return true; + } // build_cache + + /** + * build_map_cache + * This builds a cache of the mappings for the specified object, no limit is given + */ + public static function build_map_cache($type, $ids) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + $type = self::validate_type($type); + $idlist = '(' . implode(',',$ids) . ')'; + + $sql = "SELECT `tag_map`.`id`,`tag_map`.`tag_id`, `tag`.`name`,`tag_map`.`object_id`,`tag_map`.`user` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag_map`.`tag_id`=`tag`.`id` " . + "WHERE `tag_map`.`object_type`='$type' AND `tag_map`.`object_id` IN $idlist"; + + $db_results = Dba::read($sql); + + $tags = array(); + $tag_map = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $tags[$row['object_id']][$row['tag_id']] = array('user'=>$row['user'], 'id'=>$row['tag_id'], 'name'=>$row['name']); + $tag_map[$row['object_id']] = array('id'=>$row['id'],'tag_id'=>$row['tag_id'],'user'=>$row['user'],'object_type'=>$type,'object_id'=>$row['object_id']); + } + + // Run through our original ids as we also want to cache NULL + // results + foreach ($ids as $id) { + if (!isset($tags[$id])) { + $tags[$id] = null; + $tag_map[$id] = null; + } + parent::add_to_cache('tag_top_' . $type, $id, $tags[$id]); + parent::add_to_cache('tag_map_' . $type, $id, $tag_map[$id]); + } + + return true; + + } // build_map_cache + + /** + * add + * This is a wrapper function, it figures out what we need to add, be it a tag + * and map, or just the mapping + */ + public static function add($type, $id, $value, $user=false) + { + // Validate the tag type + if (!self::validate_type($type)) { return false; } + + if (!is_numeric($id)) { return false; } + + $cleaned_value = $value; + + if (!strlen($cleaned_value)) { return false; } + + $uid = ($user === false) ? intval($user) : intval($GLOBALS['user']->id); + + // Check and see if the tag exists, if not create it, we need the tag id from this + if (!$tag_id = self::tag_exists($cleaned_value)) { + $tag_id = self::add_tag($cleaned_value); + } + + if (!$tag_id) { + debug_event('Error','Error unable to create tag value:' . $cleaned_value . ' unknown error','1'); + return false; + } + + // We've got the tag id, let's see if it's already got a map, if not then create the map and return the value + if (!$map_id = self::tag_map_exists($type,$id,$tag_id,$uid)) { + $map_id = self::add_tag_map($type,$id,$tag_id,$uid); + } + + return $map_id; + + } // add + + /** + * add_tag + * This function adds a new tag, for now we're going to limit the tagging a bit + */ + public static function add_tag($value) + { + if (!strlen($value)) { return false; } + + $value = Dba::escape($value); + + $sql = "REPLACE INTO `tag` SET `name`='$value'"; + Dba::write($sql); + $insert_id = Dba::insert_id(); + + parent::add_to_cache('tag_name', $value, $insert_id); + + return $insert_id; + + } // add_tag + + /** + * update + * Update the name of the tag + */ + public function update($name) + { + //debug_event('tag.class', 'Updating tag {'.$this->id.'} with name {'.$name.'}...', '5'); + if (!strlen($name)) { return false; } + + $name = Dba::escape($name); + + $sql = 'UPDATE `tag` SET `name` = ? WHERE `id` = ?'; + Dba::write($sql, array($name, $this->id)); + + } // add_tag + + /** + * add_tag_map + * This adds a specific tag to the map for specified object + */ + public static function add_tag_map($type,$object_id,$tag_id,$user='') + { + $uid = ($user == '') ? intval($GLOBALS['user']->id) : intval($user); + $tag_id = intval($tag_id); + if (!self::validate_type($type)) { return false; } + $id = intval($object_id); + + if (!$tag_id || !$id) { return false; } + + $sql = "INSERT INTO `tag_map` (`tag_id`,`user`,`object_type`,`object_id`) " . + "VALUES ('$tag_id','$uid','$type','$id')"; + Dba::write($sql); + $insert_id = Dba::insert_id(); + + parent::add_to_cache('tag_map_' . $type,$insert_id,array('tag_id'=>$tag_id,'user'=>$uid,'object_type'=>$type,'object_id'=>$id)); + + return $insert_id; + + } // add_tag_map + + /** + * gc + * + * This cleans out tag_maps that are obsolete and then removes tags that + * have no maps. + */ + public static function gc() + { + $sql = "DELETE FROM `tag_map` USING `tag_map` LEFT JOIN `song` ON `song`.`id`=`tag_map`.`object_id` " . + "WHERE `tag_map`.`object_type`='song' AND `song`.`id` IS NULL"; + Dba::write($sql); + + $sql = "DELETE FROM `tag_map` USING `tag_map` LEFT JOIN `album` ON `album`.`id`=`tag_map`.`object_id` " . + "WHERE `tag_map`.`object_type`='album' AND `album`.`id` IS NULL"; + Dba::write($sql); + + $sql = "DELETE FROM `tag_map` USING `tag_map` LEFT JOIN `artist` ON `artist`.`id`=`tag_map`.`object_id` " . + "WHERE `tag_map`.`object_type`='artist' AND `artist`.`id` IS NULL"; + Dba::write($sql); + + $sql = "DELETE FROM `tag_map` USING `tag_map` LEFT JOIN `video` ON `video`.`id`=`tag_map`.`object_id` " . + "WHERE `tag_map`.`object_type`='video' AND `video`.`id` IS NULL"; + Dba::write($sql); + + // Now nuke the tags themselves + $sql = "DELETE FROM `tag` USING `tag` LEFT JOIN `tag_map` ON `tag`.`id`=`tag_map`.`tag_id` " . + "WHERE `tag_map`.`id` IS NULL"; + Dba::write($sql); + } + + /** + * delete + * + * Delete the tag and all maps + */ + public function delete() + { + $sql = "DELETE FROM `tag_map` WHERE `tag_map`.`tag_id`='".$this->id."'"; + Dba::write($sql); + + $sql = "DELETE FROM `tag` WHERE `tag`.`id`='".$this->id."'"; + Dba::write($sql); + + // Call the garbage collector to clean everything + Tag::gc(); + + parent::clear_cache(); + } + + /** + * tag_exists + * This checks to see if a tag exists, this has nothing to do with objects or maps + */ + public static function tag_exists($value) + { + if (parent::is_cached('tag_name',$value)) { + return parent::get_from_cache('tag_name',$value); + } + + $value = Dba::escape($value); + $sql = "SELECT * FROM `tag` WHERE `name`='$value'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + parent::add_to_cache('tag_name',$results['name'],$results['id']); + + return $results['id']; + + } // tag_exists + + /** + * tag_map_exists + * This looks to see if the current mapping of the current object of the current tag of the current + * user exists, lots of currents... taste good in scones. + */ + public static function tag_map_exists($type,$object_id,$tag_id,$user) + { + if (!self::validate_type($type)) { return false; } + + $object_id = Dba::escape($object_id); + $tag_id = Dba::escape($tag_id); + $user = Dba::escape($user); + $type = Dba::escape($type); + + $sql = "SELECT * FROM `tag_map` WHERE `tag_id`='$tag_id' AND `user`='$user' AND `object_id`='$object_id' AND `object_type`='$type'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + return $results['id']; + + } // tag_map_exists + + /** + * get_top_tags + * This gets the top tags for the specified object using limit + */ + public static function get_top_tags($type, $object_id, $limit = 10) + { + //debug_event('tag.class', 'Getting tags for type {'.$type.'} object_id {'.$object_id.'}...', '5'); + if (!self::validate_type($type)) { return false; } + + $object_id = intval($object_id); + + $limit = intval($limit); + $sql = "SELECT `tag_map`.`id`, `tag_map`.`tag_id`, `tag`.`name`, `tag_map`.`user` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag_map`.`tag_id`=`tag`.`id` " . + "WHERE `tag_map`.`object_type`='$type' AND `tag_map`.`object_id`='$object_id' ". + "GROUP BY `tag`.`name` LIMIT $limit"; + + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[$row['id']] = array('user'=>$row['user'], 'id'=>$row['tag_id'], 'name'=>$row['name']); + } + + return $results; + + } // get_top_tags + + /** + * get_object_tags + * Display all tags that apply to maching target type of the specified id + * + */ + public static function get_object_tags($type, $id) + { + if (!self::validate_type($type)) { return array(); } + + $id = Dba::escape($id); + + $sql = "SELECT `tag_map`.`id`, `tag`.`name`, `tag_map`.`user` FROM `tag` " . + "LEFT JOIN `tag_map` ON `tag_map`.`tag_id`=`tag`.`id` " . + "WHERE `tag_map`.`object_type`='$type' AND `tag_map`.`object_id`='$id'"; + + $results = array(); + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row; + } + + return $results; + } // get_object_tags + + /** + * get_tag_objects + * This gets the objects from a specified tag and returns an array of object ids, nothing more + */ + public static function get_tag_objects($type,$tag_id,$count='',$offset='') + { + if (!self::validate_type($type)) { return array(); } + + $limit_sql = ""; + if ($count) { + $limit_sql = "LIMIT "; + if ($offset) $limit_sql .= intval($offset) . ','; + $limit_sql .= intval($count); + } + + $sql = "SELECT DISTINCT `tag_map`.`object_id` FROM `tag_map` " . + "WHERE `tag_map`.`tag_id` = ? AND `tag_map`.`object_type` = ? $limit_sql "; + if (AmpConfig::get('catalog_disable')) { + $sql .= "AND " . Catalog::get_enable_filter($type, '`tag_map`.`object_id`'); + } + $db_results = Dba::read($sql, array($tag_id, $type)); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['object_id']; + } + + return $results; + } // get_tag_objects + + /** + * get_tags + * This is a non-object non type dependent function that just returns tags + * we've got, it can take filters (this is used by the tag cloud) + */ + public static function get_tags($limit = 0) + { + //debug_event('tag.class.php', 'Get tags list called...', '5'); + if (parent::is_cached('tags_list', 'no_name')) { + //debug_event('tag.class.php', 'Tags list found into cache memory!', '5'); + return parent::get_from_cache('tags_list', 'no_name'); + } + + $results = array(); + + $sql = "SELECT `tag_map`.`tag_id`, `tag`.`name`, COUNT(`tag_map`.`object_id`) AS `count` " . + "FROM `tag_map` " . + "LEFT JOIN `tag` ON `tag`.`id`=`tag_map`.`tag_id` " . + "GROUP BY `tag`.`name` ORDER BY `count` DESC "; + + if ($limit > 0) { + $sql .= " LIMIT $limit"; + } + + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[$row['tag_id']] = array('id'=>$row['tag_id'], 'name'=>$row['name'], 'count'=>$row['count']); + } + + parent::add_to_cache('tags_list', 'no_name', $results); + return $results; + + } // get_tags + + /** + * get_display + * This returns a human formated version of the tags that we are given + * it also takes a type so that it knows how to return it, this is used + * by the formating functions of the different objects + */ + public static function get_display($tags) + { + //debug_event('tag.class.php', 'Get display tags called...', '5'); + if (!is_array($tags)) { return ''; } + + $results = ''; + + // Iterate through the tags, format them according to type and element id + foreach ($tags as $tag_id=>$value) { + /*debug_event('tag.class.php', $tag_id, '5'); + foreach ($value as $vid=>$v) { + debug_event('tag.class.php', $vid.' = {'.$v.'}', '5'); + }*/ + $results .= $value['name'] . ', '; + } + + $results = rtrim($results, ', '); + + return $results; + + } // get_display + + /** + * update_tag_list + * Update the tags list based on commated list (ex. tag1,tag2,tag3,..) + */ + public static function update_tag_list($tags_comma, $type, $object_id) + { + debug_event('tag.class', 'Updating tags for values {'.$tags_comma.'} type {'.$type.'} object_id {'.$object_id.'}', '5'); + + $ctags = Tag::get_top_tags($type, $object_id); + $editedTags = explode(",", $tags_comma); + + if (is_array($ctags)) { + foreach ($ctags as $ctid => $ctv) { + if ($ctv['id'] != '') { + $ctag = new Tag($ctv['id']); + debug_event('tag.class', 'Processing tag {'.$ctag->name.'}...', '5'); + $found = false; + + foreach ($editedTags as $tk => $tv) { + if ($ctag->name == $tv) { + $found = true; + break; + } + } + + if ($found) { + debug_event('tag.class', 'Already found. Do nothing.', '5'); + unset($editedTags[$tk]); + } else { + debug_event('tag.class', 'Not found in the new list. Delete it.', '5'); + $ctag->remove_map($type, $object_id); + } + } + } + } + + // Look if we need to add some new tags + foreach ($editedTags as $tk => $tv) { + debug_event('tag.class', 'Adding new tag {'.$tv.'}', '5'); + if ($tv != '') { + Tag::add($type, $object_id, $tv, false); + } + } + } // update_tag_list + + /** + * count + * This returns the count for the all objects associated with this tag + * If a type is specific only counts for said type are returned + */ + public function count($type='') + { + $filter_sql = ""; + if ($type) { + $filter_sql = " AND `object_type`='" . Dba::escape($type) . "'"; + } + + $results = array(); + + $sql = "SELECT COUNT(`id`) AS `count`,`object_type` FROM `tag_map` WHERE `tag_id`='" . Dba::escape($this->id) . "'" . $filter_sql . " GROUP BY `object_type`"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[$row['object_type']] = $row['count']; + } + + return $results; + + } // count + + /** + * remove_map + * This will only remove tag maps for the current user + */ + public function remove_map($type,$object_id) + { + if (!self::validate_type($type)) { return false; } + + $sql = "DELETE FROM `tag_map` WHERE `tag_id` = ? AND `object_type` = ? AND `object_id` = ? AND `user` = ?"; + Dba::write($sql, array($this->id, $type, $object_id, $GLOBALS['user']->id)); + + return true; + + } // remove_map + + /** + * validate_type + * This validates the type of the object the user wants to tag, we limit this to types + * we currently support + */ + public static function validate_type($type) + { + $valid_array = array('song','artist','album','video','playlist','live_stream','channel','broadcast'); + + if (in_array($type,$valid_array)) { return $type; } + + return false; + + } // validate_type + +} // end of Tag class diff --git a/sources/lib/class/tmp_playlist.class.php b/sources/lib/class/tmp_playlist.class.php new file mode 100644 index 0000000..d0127fa --- /dev/null +++ b/sources/lib/class/tmp_playlist.class.php @@ -0,0 +1,355 @@ +id = intval($playlist_id); + $info = $this->_get_info(); + + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + + } // __construct + + /** + * _get_info + * This is an internal (private) function that gathers the information + * for this object from the playlist_id that was passed in. + */ + private function _get_info() + { + $sql = "SELECT * FROM `tmp_playlist` WHERE `id`='" . Dba::escape($this->id) . "'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + return $results; + + } // _get_info + + /** + * get_from_session + * This returns a playlist object based on the session that is passed to + * us. This is used by the load_playlist on user for the most part. + */ + public static function get_from_session($session_id) + { + $session_id = Dba::escape($session_id); + + $sql = "SELECT `id` FROM `tmp_playlist` WHERE `session`='$session_id'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_row($db_results); + + if (!$results['0']) { + $results['0'] = Tmp_Playlist::create(array( + 'session_id' => $session_id, + 'type' => 'user', + 'object_type' => 'song' + )); + } + + $playlist = new Tmp_Playlist($results['0']); + + return $playlist; + + } // get_from_session + + /** + * get_from_userid + * This returns a tmp playlist object based on a userid passed + * this is used for the user profiles page + */ + public static function get_from_userid($user_id) + { + // This is a little stupid, but because we don't have the + // user_id in the session or in the tmp_playlist table we have + // to do it this way. + $client = new User($user_id); + $username = Dba::escape($client->username); + + $sql = "SELECT `tmp_playlist`.`id` FROM `tmp_playlist` " . + "LEFT JOIN `session` ON " . + "`session`.`id`=`tmp_playlist`.`session` " . + "WHERE `session`.`username`='$username' " . + "ORDER BY `session`.`expire` DESC"; + $db_results = Dba::read($sql); + + $data = Dba::fetch_assoc($db_results); + + return $data['id']; + + } // get_from_userid + + /** + * get_items + * Returns an array of all object_ids currently in this Tmp_Playlist. + */ + public function get_items() + { + $id = Dba::escape($this->id); + + /* Select all objects from this playlist */ + $sql = "SELECT `object_type`, `id`, `object_id` " . + "FROM `tmp_playlist_data` " . + "WHERE `tmp_playlist`='$id' ORDER BY `id` ASC"; + $db_results = Dba::read($sql); + + /* Define the array */ + $items = array(); + + while ($results = Dba::fetch_assoc($db_results)) { + $key = $results['id']; + $items[$key] = array( + 'object_type' => $results['object_type'], + 'object_id' => $results['object_id'] + ); + } + + return $items; + + } // get_items + + /** + * get_next_object + * This returns the next object in the tmp_playlist. + */ + public function get_next_object() + { + $id = Dba::escape($this->id); + + $sql = "SELECT `object_id` FROM `tmp_playlist_data` " . + "WHERE `tmp_playlist`='$id' ORDER BY `id` LIMIT 1"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_assoc($db_results); + + return $results['object_id']; + + } // get_next_object + + /** + * count_items + * This returns a count of the total number of tracks that are in this + * tmp playlist + */ + public function count_items() + { + $id = Dba::escape($this->id); + + $sql = "SELECT COUNT(`id`) FROM `tmp_playlist_data` WHERE " . + "`tmp_playlist`='$id'"; + $db_results = Dba::read($sql); + + $results = Dba::fetch_row($db_results); + + return $results['0']; + + } // count_items + + /** + * clear + * This clears all the objects out of a single playlist + */ + public function clear() + { + $sql = "DELETE FROM `tmp_playlist_data` WHERE `tmp_playlist` = ?"; + Dba::write($sql, array($this->id)); + + return true; + + } // clear + + /** + * create + * This function initializes a new Tmp_Playlist. It is associated with + * the current session rather than a user, as you could have the same + * user logged in from multiple locations. + */ + public static function create($data) + { + $sql = "INSERT INTO `tmp_playlist` " . + "(`session`,`type`,`object_type`) " . + " VALUES (?, ?, ?)"; + Dba::write($sql, array($data['session_id'], $data['type'], $data['object_type'])); + + $id = Dba::insert_id(); + + /* Clean any other playlists associated with this session */ + self::session_clean($data['session_id'], $id); + + return $id; + + } // create + + /** + * update_playlist + * This updates the base_playlist on this tmp_playlist + */ + public function update_playlist($playlist_id) + { + $sql = "UPDATE `tmp_playlist` SET " . + "`base_playlist`= ? WHERE `id`= ?"; + Dba::write($sql, array($playlist_id, $this->id)); + + return true; + + } // update_playlist + + /** + * session_clean + * This deletes any other tmp_playlists associated with this + * session + */ + public static function session_clean($sessid, $id) + { + $sql = "DELETE FROM `tmp_playlist` WHERE `session`= ? AND `id` != ?"; + Dba::write($sql, array($sessid, $id)); + + /* Remove associated tracks */ + self::prune_tracks(); + + return true; + + } // session_clean + + /** + * gc + * This cleans up old data + */ + public static function gc() + { + self::prune_playlists(); + self::prune_tracks(); + Dba::write("DELETE FROM `tmp_playlist_data` USING `tmp_playlist_data` LEFT JOIN `song` ON `tmp_playlist_data`.`object_id` = `song`.`id` WHERE `song`.`id` IS NULL"); + } + + /** + * prune_playlists + * This deletes any playlists that don't have an associated session + */ + public static function prune_playlists() + { + /* Just delete if no matching session row */ + $sql = "DELETE FROM `tmp_playlist` USING `tmp_playlist` " . + "LEFT JOIN `session` " . + "ON `session`.`id`=`tmp_playlist`.`session` " . + "WHERE `session`.`id` IS NULL " . + "AND `tmp_playlist`.`type` != 'vote'"; + Dba::write($sql); + + return true; + + } // prune_playlists + + /** + * prune_tracks + * This prunes tracks that don't have playlists or don't have votes + */ + public static function prune_tracks() + { + // This prune is always run and clears data for playlists that + // don't exist anymore + $sql = "DELETE FROM `tmp_playlist_data` USING " . + "`tmp_playlist_data` LEFT JOIN `tmp_playlist` ON " . + "`tmp_playlist_data`.`tmp_playlist`=`tmp_playlist`.`id` " . + "WHERE `tmp_playlist`.`id` IS NULL"; + Dba::write($sql); + + } // prune_tracks + + /** + * add_object + * This adds the object of $this->object_type to this tmp playlist + * it takes an optional type, default is song + */ + public function add_object($object_id,$object_type) + { + $sql = "INSERT INTO `tmp_playlist_data` " . + "(`object_id`,`tmp_playlist`,`object_type`) " . + " VALUES (?, ?, ?)"; + Dba::write($sql, array($object_id, $this->id, $object_type)); + + return true; + + } // add_object + + /** + * vote_active + * This checks to see if this playlist is a voting playlist + * and if it is active + */ + public function vote_active() + { + /* Going to do a little more here later */ + if ($this->type == 'vote') { return true; } + + return false; + + } // vote_active + + /** + * delete_track + * This deletes a track from the tmpplaylist + */ + public function delete_track($id) + { + /* delete the track its self */ + $sql = "DELETE FROM `tmp_playlist_data` WHERE `id` = ?"; + Dba::write($sql, array($id)); + + return true; + + } // delete_track + +} // class Tmp_Playlist diff --git a/sources/lib/class/ui.class.php b/sources/lib/class/ui.class.php new file mode 100644 index 0000000..a58acec --- /dev/null +++ b/sources/lib/class/ui.class.php @@ -0,0 +1,342 @@ + self::$_ticker + 1)) { + self::$_ticker = time(); + return true; + } + + return false; + } + + /** + * clean_utf8 + * + * Removes characters that aren't valid in XML (which is a subset of valid + * UTF-8, but close enough for our purposes.) + * See http://www.w3.org/TR/2006/REC-xml-20060816/#charsets + */ + public static function clean_utf8($string) + { + if ($string) { + $clean = preg_replace('/[^\x{9}\x{a}\x{d}\x{20}-\x{d7ff}\x{e000}-\x{fffd}\x{10000}-\x{10ffff}]|[\x{7f}-\x{84}\x{86}-\x{9f}\x{fdd0}-\x{fddf}\x{1fffe}-\x{1ffff}\x{2fffe}-\x{2ffff}\x{3fffe}-\x{3ffff}\x{4fffe}-\x{4ffff}\x{5fffe}-\x{5ffff}\x{6fffe}-\x{6ffff}\x{7fffe}-\x{7ffff}\x{8fffe}-\x{8ffff}\x{9fffe}-\x{9ffff}\x{afffe}-\x{affff}\x{bfffe}-\x{bffff}\x{cfffe}-\x{cffff}\x{dfffe}-\x{dffff}\x{efffe}-\x{effff}\x{ffffe}-\x{fffff}\x{10fffe}-\x{10ffff}]/u', '', $string); + + // Other cleanup regex. Takes too long to process. + /*$regex = <<<'END' +/ + ( + (?: [\x00-\x7F] # single-byte sequences 0xxxxxxx + | [\xC0-\xDF][\x80-\xBF] # double-byte sequences 110xxxxx 10xxxxxx + | [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences 1110xxxx 10xxxxxx * 2 + | [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3 + ){1,100} # ...one or more times + ) +| . # anything else +/x +END; + $clean = preg_replace($regex, '$1', $string);*/ + + if ($clean) { + return $clean; + } + + debug_event('UI', 'Charset cleanup failed, something might break', 1); + } + } + + /** + * flip_class + * + * First initialised with an array of two class names. Subsequent calls + * reverse the array then return the first element. + */ + public static function flip_class($classes = null) + { + if (is_array($classes)) { + self::$_classes = $classes; + } else { + self::$_classes = array_reverse(self::$_classes); + } + return self::$_classes[0]; + } + + /** + * format_bytes + * + * Turns a size in bytes into the best human-readable value + */ + public static function format_bytes($value, $precision = 2) + { + $pass = 0; + while (strlen(floor($value)) > 3) { + $value /= 1024; + $pass++; + } + + switch ($pass) { + case 1: $unit = 'kB'; break; + case 2: $unit = 'MB'; break; + case 3: $unit = 'GB'; break; + case 4: $unit = 'TB'; break; + case 5: $unit = 'PB'; break; + default: $unit = 'B'; break; + } + + return round($value, $precision) . ' ' . $unit; + } + + /** + * unformat_bytes + * + * Parses a human-readable size + */ + public static function unformat_bytes($value) + { + if (preg_match('/^([0-9]+) *([[:alpha:]]+)$/', $value, $matches)) { + $value = $matches[1]; + $unit = strtolower(substr($matches[2], 0, 1)); + } else { + return $value; + } + + switch ($unit) { + case 'p': + $value *= 1024; + case 't': + $value *= 1024; + case 'g': + $value *= 1024; + case 'm': + $value *= 1024; + case 'k': + $value *= 1024; + } + + return $value; + } + + /** + * get_icon + * + * Returns an tag for the specified icon + */ + public static function get_icon($name, $title = null, $id = null) + { + $bUseSprite = file_exists(AmpConfig::get('prefix') . AmpConfig::get('theme_path') . '/images/icons.sprite.png'); + + if (is_array($name)) { + $hover_name = $name[1]; + $name = $name[0]; + } + + $title = $title ?: T_(ucfirst($name)); + + $icon_url = self::_find_icon($name); + if (isset($hover_name)) { + $hover_url = self::_find_icon($hover_name); + } + if ($bUseSprite) { + $tag = ''; + echo "updateText('$field', '$value');"; + echo "\n"; + ob_flush(); + flush(); + } +} diff --git a/sources/lib/class/update.class.php b/sources/lib/class/update.class.php new file mode 100644 index 0000000..b84e311 --- /dev/null +++ b/sources/lib/class/update.class.php @@ -0,0 +1,2458 @@ + $current_version) { + return true; + } + } + + return false; + } + + /** + * populate_version + * just sets an array the current differences + * that require an update + */ + public static function populate_version() + { + /* Define the array */ + $version = array(); + + $update_string = '- Moved back to ID for user tracking internally.
' . + '- Added date to user_vote to allow sorting by vote time.
' . + '- Added Random Method and Object Count Preferences.
' . + '- Removed some unused tables/fields.
' . + '- Added Label, Catalog # and Language to Extended Song Data Table.'; + $version[] = array('version' => '340001','description' => $update_string); + + $update_string = '- Added Offset Limit to Preferences and removed from user table.'; + $version[] = array('version' => '340002','description' => $update_string); + + $update_string = '- Moved Art from the Album table into album_data to improve performance.
' . + '- Made some minor changes to song table to reduce size of each row.
' . + '- Moved song_ext_data to song_data to match album_data pattern.
' . + '- Added Playlist Method and Rate Limit Preferences.
' . + '- Renamed preferences and ratings to preference and rating to fit table pattern.
' . + '- Fixed rating table, renamed user_rating to rating and switched 00 for -1.
'; + $version[] = array('version' => '340003','description' => $update_string); + + $update_string = '- Alter the Session.id to be VARCHAR(64) to account for all potential configs.
' . + '- Added new user_shout table for Sticky objects / shoutbox.
' . + '- Added new playlist preferences, and new preference catagory of playlist.
' . + '- Tweaked Now Playing Table.
'; + $version[] = array('version' => '340004','description' => $update_string); + + $update_string = '- Altered Ratings table so the fields make more sense.
' . + '- Moved Random Method to Playlist catagory.
' . + '- Added Transcode Method to Streaming.
'; + $version[] = array('version' => '340005','description' => $update_string); + + $update_string = '- Remove Random Method config option, ended up being useless.
' . + '- Check and change album_data.art to a MEDIUMBLOB if needed.
'; + $version[] = array('version' => '340006','description' => $update_string); + + $update_string = '- Altered the session table, making value a LONGTEXT.
'; + $version[] = array('version' => '340007','description' => $update_string); + + $update_string = '- Modified Playlist_Data table to account for multiple object types.
' . + '- Verified previous updates, adjusting as needed.
' . + '- Dropped Allow Downsampling pref, configured in cfg file.
' . + '- Renamed Downsample Rate --> Transcode Rate to reflect new terminiology.
'; + $version[] = array('version' => '340008','description' => $update_string); + + $update_string = '- Added disk to Album table.
' . + '- Added artist_data for artist images and bios.
' . + '- Added DNS to access list to allow for dns based ACLs.
'; + $version[] = array('version' => '340009','description' => $update_string); + + $update_string = '- Removed Playlist Add preference.
' . + '- Moved Localplay* preferences to options.
' . + '- Tweaked Default Playlist Method.
' . + '- Change wording on Localplay preferences.
'; + $version[] = array('version' => '340010','description'=>$update_string); + + $update_string = '- Added Democratic Table for new democratic play features.
' . + '- Added Add Path to Catalog to improve add speeds on large catalogs.
'; + $version[] = array('version' => '340012','description'=>$update_string); + + $update_string = '- Removed Unused Preferences.
' . + '- Changed Localplay Config to Localplay Access.
' . + '- Changed all XML-RPC acls to RPC to reflect inclusion of new API.
'; + $version[] = array('version' => '340013','description'=>$update_string); + + $update_string = '- Removed API Session table, been a nice run....
' . + '- Alterted Session table to handle API sessions correctly.
'; + $version[] = array('version' => '340014','description'=>$update_string); + + $update_string = '- Alter Playlist Date Field to fix issues with some MySQL configurations.
' . + '- Alter Rating type to correct AVG issue on searching.
'; + $version[] = array('version' => '340015','description'=>$update_string); + + $update_string = '- Alter the Democratic Playlist table, adding base_playlist.
' . + '- Alter tmp_playlist to account for Democratic changes.
' . + '- Cleared Existing Democratic playlists due to changes.
'; + $version[] = array('version' => '340016','description'=>$update_string); + + $update_string = '- Fix Tables for new Democratic Play methodology.
'; + $version[] = array('version' => '340017','description'=>$update_string); + + $update_string = '- Modify the Tag tables so that they actually work.
' . + '- Alter the Prefix fields to allow for more prefixs.
'; + $version[] = array('version' => '350001','description'=>$update_string); + + $update_string = '- Remove Genre Field from song table.
' . + '- Add user_catalog table for tracking user<-->catalog mappings.
' . + '- Add tmp_browse to handle caching rather then session table.
'; + $version[] = array('version' => '350002','description'=>$update_string); + + $update_string = '- Modify Tag tables.
' . + '- Remove useless config preferences.
'; + $version[] = array('version'=> '350003','description'=>$update_string); + + $update_string = '- Modify ACL table to enable IPv6 ACL support
' . + '- Modify Session Tables to store IPv6 addresses if provided
' . + '- Modify IP History table to store IPv6 addresses and User Agent
'; + $version[] = array('version'=>'350004','description'=>$update_string); + + $update_string = "- Add table for Video files
"; + $version[] = array('version'=>'350005','description'=>$update_string); + + $update_string = "- Add data for Lyrics
"; + $version[] = array('version'=>'350006','description'=>$update_string); + + $update_string = '- Remove unused fields from catalog, playlist, playlist_data
' . + '- Add tables for dynamic playlists
' . + '- Add last_clean to catalog table
' . + '- Add track to tmp_playlist_data
' . + '- Increase Thumbnail blob size
'; + $version[] = array('version'=>'350007','description'=>$update_string); + + $update_string = '- Modify Now Playing table to handle Videos
' . + '- Modify tmp_browse to make it easier to prune
' . + '- Add missing indexes to the _data tables
' . + '- Drop unused song.hash
' . + '- Add addition_time and update_time to video table
'; + $version[] = array('version'=>'350008','description'=>$update_string); + + $update_string = '- Add MBID (MusicBrainz ID) fields
' . + '- Remove useless preferences
'; + $version[] = array('version'=>'360001','description'=>$update_string); + + $update_string = '- Add Bandwidth and Feature preferences to simplify how interface is presented
' . + '- Change Tables to FULLTEXT() for improved searching
' . + '- Increase Filename lengths to 4096
' . + '- Remove useless "KEY" reference from ACL and Catalog tables
' . + '- Add new Remote User / Remote Password fields to Catalog
'; + $version[] = array('version'=>'360002','description'=>$update_string); + + $update_string = '- Add image table to store images.
' . + '- Drop album_data and artist_data.
'; + $version[] = array('version'=>'360003','description'=>$update_string); + + $update_string = '- Add uniqueness constraint to ratings.
'; + $version[] = array('version' => '360004','description' => $update_string); + + $update_string = '- Modify tmp_browse to allow caching of multiple browses per session.
'; + $version[] = array('version' => '360005','description' => $update_string); + + $update_string = '- Add table for dynamic playlists.
'; + $version[] = array('version' => '360006','description' => $update_string); + + $update_string = '- Verify remote_username and remote_password were added correctly to catalog table.
'; + $version[] = array('version' => '360008','description' => $update_string); + + $update_string = '- Allow long sessionids in tmp_playlist table.
'; + $version[] = array('version' => '360009', 'description' => $update_string); + + $update_string = '- Allow compound MBIDs in the artist table.
'; + $version[] = array('version' => '360010', 'description' => $update_string); + + $update_string = '- Add table to store stream session playlist.
'; + $version[] = array('version' => '360011', 'description' => $update_string); + + $update_string = '- Drop enum for the type field in session.
'; + $version[] = array('version' => '360012', 'description' => $update_string); + + $update_string = '- Update stream_playlist table to address performance issues.
'; + $version[] = array('version' => '360013', 'description' => $update_string); + + $update_string = '- Increase the length of sessionids again.
'; + $version[] = array('version' => '360014', 'description' => $update_string); + + $update_string = '- Add iframes parameter to preferences.
'; + $version[] = array('version' => '360015', 'description' => $update_string); + + $update_string = '- Optionally filter Now Playing to return only the last song per user.
'; + $version[] = array('version' => '360016', 'description' => $update_string); + + $update_string = '- Add user flags on objects.
'; + $version[] = array('version' => '360017', 'description' => $update_string); + + $update_string = '- Add album default sort value to preferences.
'; + $version[] = array('version' => '360018', 'description' => $update_string); + + $update_string = '- Add option to show number of times a song was played.
'; + $version[] = array('version' => '360019', 'description' => $update_string); + + $update_string = '- Catalog types are plugins now.
'; + $version[] = array('version' => '360020', 'description' => $update_string); + + $update_string = '- Add insertion date on Now Playing and option to show the current song in page title for Web player.
'; + $version[] = array('version' => '360021', 'description' => $update_string); + + $update_string = '- Remove unused live_stream fields and add codec field.
'; + $version[] = array('version' => '360022', 'description' => $update_string); + + $update_string = '- Enable/Disable SubSonic and Plex backend.
'; + $version[] = array('version' => '360023', 'description' => $update_string); + + $update_string = '- Drop flagged table.
'; + $version[] = array('version' => '360024', 'description' => $update_string); + + $update_string = '- Add options to enable HTML5 / Flash on web players.
'; + $version[] = array('version' => '360025', 'description' => $update_string); + + $update_string = '- Added agent to `object_count` table.
'; + $version[] = array('version' => '360026','description' => $update_string); + + $update_string = '- Add option to allow/disallow to show personnal information to other users (now playing and recently played).
'; + $version[] = array('version' => '360027','description' => $update_string); + + $update_string = '- Personnal information: allow/disallow to show in now playing.
'. + '- Personnal information: allow/disallow to show in recently played.
'. + '- Personnal information: allow/disallow to show time and/or agent in recently played.
'; + $version[] = array('version' => '360028','description' => $update_string); + + $update_string = '- Add new table to store wanted releases.
'; + $version[] = array('version' => '360029','description' => $update_string); + + $update_string = '- New table to store song previews.
'; + $version[] = array('version' => '360030','description' => $update_string); + + $update_string = '- Add option to fix header/sidebars position on compatible themes.
'; + $version[] = array('version' => '360031','description' => $update_string); + + $update_string = '- Add check update automatically option.
'; + $version[] = array('version' => '360032','description' => $update_string); + + $update_string = '- Add song waveform as song data.
'; + $version[] = array('version' => '360033','description' => $update_string); + + $update_string = '- Add settings for confirmation when closing window and auto-pause between tabs.
'; + $version[] = array('version' => '360034','description' => $update_string); + + $update_string = '- Add beautiful stream url setting.
'; + $version[] = array('version' => '360035','description' => $update_string); + + $update_string = '- Remove unused parameters.
'; + $version[] = array('version' => '360036','description' => $update_string); + + $update_string = '- Add sharing features.
'; + $version[] = array('version' => '360037','description' => $update_string); + + $update_string = '- Add missing albums browse on missing artists.
'; + $version[] = array('version' => '360038','description' => $update_string); + + $update_string = '- Add website field on users.
'; + $version[] = array('version' => '360039','description' => $update_string); + + $update_string = '- Add channels.
'; + $version[] = array('version' => '360041','description' => $update_string); + + $update_string = '- Add broadcasts and player control.
'; + $version[] = array('version' => '360042','description' => $update_string); + + $update_string = '- Add slideshow on currently played artist preference.
'; + $version[] = array('version' => '360043','description' => $update_string); + + $update_string = '- Add artist description/recommendation external service data cache.
'; + $version[] = array('version' => '360044','description' => $update_string); + + $update_string = '- Set user field on playlists as optional.
'; + $version[] = array('version' => '360045','description' => $update_string); + + $update_string = '- Add broadcast web player by default preference.
'; + $version[] = array('version' => '360046','description' => $update_string); + + $update_string = '- Add apikey field on users.
'; + $version[] = array('version' => '360047','description' => $update_string); + + $update_string = '- Add concerts options.
'; + $version[] = array('version' => '360048','description' => $update_string); + + $update_string = '- Add album group multiple disks setting.
'; + $version[] = array('version' => '360049','description' => $update_string); + + $update_string = '- Add top menu setting.
'; + $version[] = array('version' => '360050','description' => $update_string); + + $update_string = '- Copy default .htaccess configurations.
'; + $version[] = array('version' => '360051','description' => $update_string); + + return $version; + } + + /** + * display_update + * This displays a list of the needed + * updates to the database. This will actually + * echo out the list... + */ + public static function display_update() + { + $current_version = self::get_version(); + if (!is_array(self::$versions)) { + self::$versions = self::populate_version(); + } + $update_needed = false; + + if (!defined('CLI')) { echo "
    \n"; } + + foreach (self::$versions as $update) { + + if ($update['version'] > $current_version) { + $update_needed = true; + if (!defined('CLI')) { echo '
  • '; } + echo 'Version: ', self::format_version($update['version']); + if (defined('CLI')) { + echo "\n", str_replace('
    ', "\n", $update['description']), "\n"; + } else { + echo '

    ', $update['description'], "
  • \n"; + } + } // if newer + + } // foreach versions + + if (!defined('CLI')) { echo "
\n"; } + + if (!$update_needed) { + if (!defined('CLI')) { echo '

'; } + echo T_('No updates needed.'); + if (!defined('CLI')) { + echo '[Return]

'; + } else { + echo "\n"; + } + } + } // display_update + + /** + * run_update + * This function actually updates the db. + * it goes through versions and finds the ones + * that need to be run. Checking to make sure + * the function exists first. + */ + public static function run_update() + { + /* Nuke All Active session before we start the mojo */ + $sql = "TRUNCATE session"; + Dba::write($sql); + + // Prevent the script from timing out, which could be bad + set_time_limit(0); + + $current_version = self::get_version(); + + // Run a check to make sure that they don't try to upgrade from a version that + // won't work. + if ($current_version < '340002') { + echo "

Database version too old, please upgrade to Ampache-3.3.3.5 first

"; + return false; + } + + + $methods = get_class_methods('Update'); + + if (!is_array((self::$versions))) { + self::$versions = self::populate_version(); + } + + foreach (self::$versions as $version) { + + // If it's newer than our current version let's see if a function + // exists and run the bugger. + if ($version['version'] > $current_version) { + $update_function = "update_" . $version['version']; + if (in_array($update_function,$methods)) { + $success = call_user_func(array('Update',$update_function)); + + // If the update fails drop out + if ($success) { + self::set_version('db_version', $version['version']); + } else { + Error::display('update'); + return false; + } + } + + } + + } // end foreach version + + // Once we've run all of the updates let's re-sync the character set as + // the user can change this between updates and cause mis-matches on any + // new tables. + Dba::reset_db_charset(); + + // Let's also clean up the preferences unconditionally + User::rebuild_all_preferences(); + } // run_update + + /** + * set_version + * + * This updates the 'update_info' which is used by the updater + * and plugins + */ + private static function set_version($key, $value) + { + $sql = "UPDATE update_info SET value='$value' WHERE `key`='$key'"; + Dba::write($sql); + } + + /** + * update_340003 + * This update moves the album art out of the album table + * and puts it in an album_data table. It also makes some + * minor changes to the song table in an attempt to reduce + * the size of each row + */ + public static function update_340003() + { + $retval = true; + $sql = "ALTER TABLE `song` CHANGE `mode` `mode` ENUM( 'abr', 'vbr', 'cbr' ) NULL DEFAULT 'cbr'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` CHANGE `time` `time` SMALLINT( 5 ) UNSIGNED NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` CHANGE `rate` `rate` MEDIUMINT( 8 ) UNSIGNED NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` CHANGE `bitrate` `bitrate` MEDIUMINT( 8 ) UNSIGNED NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` CHANGE `track` `track` SMALLINT( 5 ) UNSIGNED NULL DEFAULT NULL "; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `user` CHANGE `disabled` `disabled` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `album_data` (" . + "`album_id` INT( 11 ) UNSIGNED NOT NULL , " . + "`art` MEDIUMBLOB NULL , " . + "`art_mime` VARCHAR( 64 ) NULL , " . + "`thumb` BLOB NULL , " . + "`thumb_mime` VARCHAR( 64 ) NULL , " . + "UNIQUE ( `album_id` )" . + ") ENGINE = MYISAM"; + $retval = Dba::write($sql) ? $retval : false; + + /* Foreach the Albums and move the data into the new album_data table */ + $sql = "SELECT * FROM album"; + $db_results = Dba::write($sql); + + while ($data = Dba::fetch_assoc($db_results)) { + $id = $data['id']; + $art = Dba::escape($data['art']); + $art_mime = Dba::escape($data['art_mime']); + $sql = "INSERT INTO `album_data` (`album_id`,`art`,`art_mime`)" . + " VALUES ('$id','$art','$art_mime')"; + $retval = Dba::write($sql) ? $retval : false; + } // end while + + $sql = "RENAME TABLE `song_ext_data` TO `song_data`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "RENAME TABLE `preferences` TO `preference`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "RENAME TABLE `ratings` TO `rating`"; + $retval = Dba::write($sql) ? $retval : false; + + // Go ahead and drop the art/thumb stuff + $sql = "ALTER TABLE `album` DROP `art`, DROP `art_mime`, DROP `thumb`, DROP `thumb_mime`"; + $retval = Dba::write($sql) ? $retval : false; + + // We need to fix the user_vote table + $sql = "ALTER TABLE `user_vote` CHANGE `user` `user` INT( 11 ) UNSIGNED NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + // Remove offset limit from the user + $sql = "ALTER TABLE `user` DROP `offset_limit`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `rating` CHANGE `user_rating` `rating` ENUM( '-1', '0', '1', '2', '3', '4', '5' ) NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + /* Add the rate_limit preference */ + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('rate_limit','8192','Rate Limit','100','integer','streaming')"; + $retval = Dba::write($sql) ? $retval : false; + + /* Add the playlist_method preference and remove it from the user table */ + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('playlist_method','normal','Playlist Method','5','string','streaming')"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `update_info` ADD UNIQUE (`key`)"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } // update_340003 + + /** + * update_340004 + * Update the session.id to varchar(64) to handle + * newer configs + */ + public static function update_340004() + { + $retval = true; + /* Alter the session.id so that it's 64 */ + $sql = "ALTER TABLE `session` CHANGE `id` `id` VARCHAR( 64 ) NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + /* Add Playlist Related Preferences */ + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('playlist_add','append','Add Behavior','5','string','playlist')"; + $retval = Dba::write($sql) ? $retval : false; + + // Switch the existing preferences over to this new catagory + $sql = "UPDATE `preference` SET `catagory`='playlist' WHERE `name`='playlist_method' " . + " OR `name`='playlist_type'"; + $retval = Dba::write($sql) ? $retval : false; + + // Change the default value for playlist_method + $sql = "UPDATE `preference` SET `value`='normal' WHERE `name`='playlist_method'"; + $retval = Dba::write($sql) ? $retval : false; + + // Add in the shoutbox + $sql = "CREATE TABLE `user_shout` (`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , " . + "`user` INT( 11 ) NOT NULL , " . + "`text` TEXT NOT NULL , " . + "`date` INT( 11 ) UNSIGNED NOT NULL , " . + "`sticky` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0', " . + "`object_id` INT( 11 ) UNSIGNED NOT NULL , " . + "`object_type` VARCHAR( 32 ) NOT NULL " . + ") ENGINE = MYISAM"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `user_shout` ADD INDEX ( `sticky` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `user_shout` ADD INDEX ( `date` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `user_shout` ADD INDEX ( `user` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `now_playing` CHANGE `start_time` `expire` INT( 11 ) UNSIGNED NOT NULL DEFAULT '0'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "OPTIMIZE TABLE `album`"; + Dba::write($sql); + + return $retval; + } // update_340004 + + /** + * update_340005 + * This update fixes the preferences types + */ + public static function update_340005() + { + $retval = true; + + $sql = "UPDATE `preference` SET `catagory`='playlist' WHERE `name`='random_method'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('transcode','default','Transcoding','25','string','streaming')"; + $retval = Dba::write($sql) ? $retval : false; + + /* We need to check for playlist_method here because I fubar'd an earlier update */ + $sql = "SELECT * FROM `preference` WHERE `name`='playlist_method'"; + $db_results = Dba::read($sql); + if (!Dba::num_rows($db_results)) { + /* Add the playlist_method preference and remove it from the user table */ + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('playlist_method','default','Playlist Method','5','string','playlist')"; + $retval = Dba::write($sql) ? $retval : false; + } + + // Add in the object_type to the tmpplaylist data table so that we can have non-songs in there + $sql = "ALTER TABLE `tmp_playlist_data` ADD `object_type` VARCHAR( 32 ) NULL AFTER `tmp_playlist`"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } // update_340005 + + /** + * update_340006 + * This just updates the size of the album_data table + * and removes the random_method config option + */ + public static function update_340006() + { + // No matter what remove that random method preference + Dba::write("DELETE FROM `preference` WHERE `name`='random_method'"); + return true; + } + + /** + * update_340007 + * This update converts the session.value to a longtext + * and adds a session_stream table + */ + public static function update_340007() + { + $retval = true; + // Tweak the session table to handle larger session vars for my page-a-nation hotness + $sql = "ALTER TABLE `session` CHANGE `value` `value` LONGTEXT CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `now_playing` CHANGE `id` `id` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + // Now longer needed because of the new hotness + $sql = "ALTER TABLE `now_playing` DROP `session`"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } // update_340007 + + /** + * update_340008 + * This modifies the playlist table to handle the different types of objects that it needs to be able to + * store, and tweaks how dynamic playlist stuff works + */ + public static function update_340008() + { + $retval = true; + $sql = "ALTER TABLE `playlist_data` CHANGE `song` `object_id` INT( 11 ) UNSIGNED NULL DEFAULT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `playlist_data` CHANGE `dyn_song` `dynamic_song` TEXT CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `playlist_data` ADD `object_type` VARCHAR( 32 ) NOT NULL DEFAULT 'song' AFTER `object_id`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `playlist` ADD `genre` INT( 11 ) UNSIGNED NOT NULL AFTER `type`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DELETE FROM `preference` WHERE `name`='allow_downsample_playback'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "UPDATE `preference` SET `description`='Transcode Bitrate' WHERE `name`='sample_rate'"; + $retval = Dba::write($sql) ? $retval : false; + + // Check for old tables and drop if found, seems like there was a glitch + // that caused them not to get dropped.. *shrug* + $sql = "DROP TABLE IF EXISTS `preferences`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DROP TABLE IF EXISTS `song_ext_data`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DROP TABLE IF EXISTS `ratings`"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_340009 + * This modifies the song table to handle pos fields + */ + public static function update_340009() + { + $retval = true; + $sql = "ALTER TABLE `album` ADD `disk` smallint(5) UNSIGNED DEFAULT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `album` ADD INDEX (`disk`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` ADD `dns` VARCHAR( 255 ) NOT NULL AFTER `end`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `artist_data` (" . + "`artist_id` INT( 11 ) UNSIGNED NOT NULL ," . + "`art` MEDIUMBLOB NOT NULL ," . + "`art_mime` VARCHAR( 32 ) NOT NULL ," . + "`thumb` BLOB NOT NULL ," . + "`thumb_mime` VARCHAR( 32 ) NOT NULL ," . + "`bio` TEXT NOT NULL , " . + "UNIQUE (`artist_id`) ) ENGINE = MYISAM"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_340010 + * Bunch of minor tweaks to the preference table + */ + public static function update_340010() + { + $retval = true; + $sql = "UPDATE `preference` SET `catagory`='options' WHERE `name` LIKE 'localplay_%'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DELETE FROM `preference` WHERE `name`='playlist_add'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "UPDATE `preference` SET `catagory`='plugins' WHERE (`name` LIKE 'mystrands_%' OR `name` LIKE 'lastfm_%') AND `catagory`='options'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "UPDATE `preference` SET `value`='default' WHERE `name`='playlist_method'"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "UPDATE `preference` SET `description`='Localplay Config' WHERE `name`='localplay_level'"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_340012 + * This update adds in the democratic stuff, checks for some potentially screwed up indexes + * and removes the timestamp from the playlist, and adds the field to the catalog for the upload dir + */ + public static function update_340012() + { + $retval = true; + $sql = "ALTER TABLE `catalog` ADD `add_path` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL AFTER `path`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `democratic` (`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ," . + "`name` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL ," . + "`cooldown` TINYINT( 4 ) UNSIGNED NULL ," . + "`level` TINYINT( 4 ) UNSIGNED NOT NULL DEFAULT '25'," . + "`user` INT( 11 ) NOT NULL ," . + "`primary` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0'" . + ") ENGINE = MYISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `democratic` ADD INDEX (`primary`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `democratic` ADD INDEX (`level`)"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_340013 + * + * This update removes a whole bunch of preferences that are no longer + * being used in any way, and changes the ACL XML-RPC to just RPC + */ + public static function update_340013() + { + $sql = "DELETE FROM `preference` WHERE `name`='localplay_mpd_hostname' OR `name`='localplay_mpd_port' " . + "OR `name`='direct_link' OR `name`='localplay_mpd_password' OR `name`='catalog_echo_count'"; + Dba::write($sql); + + $sql = "UPDATE `preference` SET `description`='Localplay Access' WHERE `name`='localplay_level'"; + Dba::write($sql); + + $sql = "UPDATE `access_list` SET `type`='rpc' WHERE `type`='xml-rpc'"; + Dba::write($sql); + + // We're not manipulating the structure, so we'll pretend it always works + return true; + } + + /** + * update_340014 + * + * This update drops the session_api table that I added just two updates ago + * it's been nice while it lasted but it's time to pack your stuff and GTFO + * at the same time it updates the core session table to handle the + * additional stuff we're going to ask it to do. + */ + public static function update_340014() + { + $retval = true; + $sql = "DROP TABLE IF EXISTS `session_api`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `session` CHANGE `type` `type` ENUM ('mysql','ldap','http','api','xml-rpc') NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `session` ADD `agent` VARCHAR ( 255 ) NOT NULL AFTER `type`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `session` ADD INDEX (`type`)"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_340015 + * + * This update tweaks the playlist table responding to complaints from usres + * who say it doesn't work, unreproduceable. This also adds an index to the + * album art table to try to make the random album art faster + */ + public static function update_340015() + { + $retval = true; + $sql = "ALTER TABLE `playlist` DROP `date`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `playlist` ADD `date` INT ( 11 ) UNSIGNED NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + // Pull all of the rating information + $sql = "SELECT `id`,`rating` FROM `rating`"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row; + } + + $sql = "ALTER TABLE `rating` DROP `rating`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `rating` ADD `rating` TINYINT ( 4 ) NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + foreach ($results as $row) { + $rating = Dba::escape($row['rating']); + $id = Dba::escape($row['id']); + $sql = "UPDATE `rating` SET `rating`='$rating' WHERE `id`='$id'"; + Dba::write($sql); + } + + return $retval; + } + + /** + * update_340016 + * + * This adds in the base_playlist to the democratic table... should have + * done this in the previous one but I screwed up... sigh. + */ + public static function update_340016() + { + $sql = "ALTER TABLE `democratic` ADD `base_playlist` INT ( 11 ) UNSIGNED NOT NULL AFTER `name`"; + return Dba::write($sql); + } + + /** + * update_340017 + * + * This finalizes the democratic table. + * And fixes the charset crap. + */ + public static function update_340017() + { + $retval = true; + + $sql = "ALTER TABLE `tmp_playlist` DROP `base_playlist`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DELETE FROM `tmp_playlist` WHERE `session`='-1'"; + Dba::write($sql); + + $sql = "TRUNCATE `democratic`"; + Dba::write($sql); + + return $retval; + } + + /** + * update_350001 + * + * This updates modifies the tag tables per codeunde1load's specs from his + * tag patch. + * + * It also adjusts the prefix fields so that we can use more prefixes, + */ + public static function update_350001() + { + $retval = true; + $sql = "ALTER TABLE `tag_map` ADD `tag_id` INT ( 11 ) UNSIGNED NOT NULL AFTER `id`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "RENAME TABLE `tags` TO `tag`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `tag` CHANGE `map_id` `id` INT ( 11 ) UNSIGNED NOT NULL auto_increment"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `album` CHANGE `prefix` `prefix` VARCHAR ( 32 ) NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `artist` CHANGE `prefix` `prefix` VARCHAR ( 32 ) NULL"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_350002 + * + * This update adds in the browse_cache table that we use to hold people's + * cached browse results. Rather then try to store everything in the session + * we split them out into one serialized array per row, per person. A little + * slow this way when browsing, but faster and more flexible when not. + */ + public static function update_350002() + { + $retval = true; + + $sql = "ALTER TABLE `song` DROP `genre`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `user_catalog` (`user` INT( 11 ) UNSIGNED NOT NULL ,`catalog` INT( 11 ) UNSIGNED NOT NULL ,`level` SMALLINT( 4 ) UNSIGNED NOT NULL DEFAULT '5', " . + "INDEX ( `user` )) ENGINE = MYISAM"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `user_catalog` ADD INDEX ( `catalog` )"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_350003 + * + * This update tweakes the tag tables a little bit more, we're going to + * simplify things for the first little bit and then if it all works out + * we will worry about making it complex again. One thing at a time people... + */ + public static function update_350003() + { + $retval = true; + + $sql = "ALTER TABLE `tag` DROP `order`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `tag` ADD UNIQUE ( `name` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `tag` CHANGE `name` `name` VARCHAR( 255 )"; + $retval = Dba::write($sql) ? $retval : false; + + // Make sure that they don't have any of the mystrands crap left + $sql = "DELETE FROM `preference` WHERE `name`='mystrands_user' OR `name`='mystrands_pass'"; + Dba::write($sql); + + return $retval; + } // update_350003 + + /** + * update_350004 + * + * This update makes some changes to the ACL table so that it can support + * IPv6 entries as well as some other feature enhancements. + */ + public static function update_350004() + { + $retval = true; + + $sql = "ALTER TABLE `session` CHANGE `ip` `ip` VARBINARY( 255 ) NULL"; + $retval = Dba::write($sql) ? $retval : false; + + // Pull all of the IP history, this could take a while + $sql = "SELECT * FROM `ip_history`"; + $db_results = Dba::read($sql); + + $ip_history = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $row['ip'] = long2ip($row['ip']); + $ip_history[] = $row; + } + + // Clear the table before we make the changes + $sql = "TRUNCATE `ip_history`"; + Dba::write($sql); + + $sql = "ALTER TABLE `ip_history` CHANGE `ip` `ip` VARBINARY( 255 ) NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `ip_history` ADD `agent` VARCHAR ( 255 ) NULL AFTER `date`"; + $retval = Dba::write($sql) ? $retval : false; + + // Reinsert the old rows + foreach ($ip_history as $row) { + $ip = Dba::escape(inet_pton($row['ip'])); + $sql = "INSERT INTO `ip_history` (`user`,`ip`,`date`,`agent`) " . + "VALUES ('" . $row['user'] . "','" . $ip . "','" . $row['date'] . "',NULL)"; + Dba::write($sql); + } + + // First pull all of their current ACL's + $sql = "SELECT * FROM `access_list`"; + $db_results = Dba::read($sql); + + $acl_information = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $row['start'] = long2ip($row['start']); + $row['end'] = long2ip($row['end']); + $acl_information[] = $row; + } + + $sql = "TRUNCATE `access_list`"; + Dba::write($sql); + + // Make the changes to the database + $sql = "ALTER TABLE `access_list` CHANGE `start` `start` VARBINARY( 255 ) NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` CHANGE `end` `end` VARBINARY( 255 ) NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` DROP `dns`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` ADD `enabled` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '1' AFTER `key`"; + $retval = Dba::write($sql) ? $retval : false; + + // If we had nothing in there before add some base ALLOW ALL stuff as + // we're going to start defaulting Access Control on. + if (!count($acl_information)) { + $v6_start = Dba::escape(inet_pton('::')); + $v6_end = Dba::escape(inet_pton('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff')); + $v4_start = Dba::escape(inet_pton('0.0.0.0')); + $v4_end = Dba::escape(inet_pton('255.255.255.255')); + $sql = "INSERT INTO `access_list` (`name`,`level`,`start`,`end`,`key`,`user`,`type`,`enabled`) " . + "VALUES ('DEFAULTv4','75','$v4_start','$v4_end',NULL,'-1','interface','1')"; + Dba::write($sql); + $sql = "INSERT INTO `access_list` (`name`,`level`,`start`,`end`,`key`,`user`,`type`,`enabled`) " . + "VALUES ('DEFAULTv4','75','$v4_start','$v4_end',NULL,'-1','stream','1')"; + Dba::write($sql); + $sql = "INSERT INTO `access_list` (`name`,`level`,`start`,`end`,`key`,`user`,`type`,`enabled`) " . + "VALUES ('DEFAULTv6','75','$v6_start','$v6_end',NULL,'-1','interface','1')"; + Dba::write($sql); + $sql = "INSERT INTO `access_list` (`name`,`level`,`start`,`end`,`key`,`user`,`type`,`enabled`) " . + "VALUES ('DEFAULTv6','75','$v6_start','$v6_end',NULL,'-1','stream','1')"; + Dba::write($sql); + } // Adding default information + + foreach ($acl_information as $row) { + $row['start'] = Dba::escape(inet_pton($row['start'])); + $row['end'] = Dba::escape(inet_pton($row['end'])); + $row['key'] = Dba::escape($row['key']); + $sql = "INSERT INTO `access_list` (`name`,`level`,`start`,`end`,`key`,`user`,`type`,`enabled`) " . + "VALUES ('" . Dba::escape($row['name']) . "','" . intval($row['level']) . + "','" . $row['start'] . "','" . $row['end'] . "','" . $row['key'] . "','" . intval($row['user']) . "','" . + $row['type'] . "','1')"; + Dba::write($sql); + } // end foreach of existing rows + + return $retval; + } + + /** + * update_350005 + * + * This update adds the video table... *gasp* no you didn't + */ + public static function update_350005() + { + $retval = true; + + $sql = " CREATE TABLE `video` (" . + "`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ," . + "`file` VARCHAR( 255 ) NOT NULL , " . + "`catalog` INT( 11 ) UNSIGNED NOT NULL ," . + "`title` VARCHAR( 255 ) NOT NULL ," . + "`video_codec` VARCHAR( 255 ) NOT NULL ," . + "`audio_codec` VARCHAR( 255 ) NOT NULL ," . + "`resolution_x` MEDIUMINT UNSIGNED NOT NULL ," . + "`resolution_y` MEDIUMINT UNSIGNED NOT NULL ," . + "`time` INT( 11 ) UNSIGNED NOT NULL ," . + "`size` BIGINT UNSIGNED NOT NULL," . + "`mime` VARCHAR( 255 ) NOT NULL," . + "`enabled` TINYINT( 1) NOT NULL DEFAULT '1'" . + ") ENGINE = MYISAM "; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` ADD INDEX ( `enabled` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD INDEX ( `file` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD INDEX ( `enabled` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD INDEX ( `title` )"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_350006 + * + * This update inserts the Lyrics pref table... + */ + public static function update_350006() + { + $sql = "INSERT INTO `preference` VALUES (69,'show_lyrics','0','Show Lyrics',0,'boolean','interface')"; + Dba::write($sql); + + $sql = "INSERT INTO `user_preference` VALUES (1,69,'0')"; + Dba::write($sql); + + return true; + } + + /** + * update_350007 + * + * This update adds in the random rules tables. Also increase the size of the + * blobs on the album and artist data and add track to tmp_playlist_data + */ + public static function update_350007() + { + $retval = true; + + // We need to clear the thumbs as they will need to be re-generated + $sql = "UPDATE `album_data` SET `thumb`=NULL,`thumb_mime`=NULL"; + Dba::write($sql); + + $sql = "UPDATE `artist_data` SET `thumb`=NULL,`thumb_mime`=NULL"; + Dba::write($sql); + + // Remove dead column + $sql = "ALTER TABLE `playlist_data` DROP `dynamic_song`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `playlist` DROP `genre`"; + $retval = Dba::write($sql) ? $retval : false; + + // Add track item to tmp_playlist_data so we can order this stuff manually + $sql = "ALTER TABLE `tmp_playlist_data` ADD `track` INT ( 11 ) UNSIGNED NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "DROP TABLE `genre`"; + $retval = Dba::write($sql) ? $retval : false; + + // Clean up the catalog and add last_clean to it + $sql = "ALTER TABLE `catalog` ADD `last_clean` INT ( 11 ) UNSIGNED NULL AFTER `last_update`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `catalog` DROP `add_path`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `dynamic_playlist` (" . + "`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ," . + "`name` VARCHAR( 255 ) NOT NULL ," . + "`user` INT( 11 ) NOT NULL ," . + "`date` INT( 11 ) UNSIGNED NOT NULL ," . + "`type` VARCHAR( 128 ) NOT NULL" . + ") ENGINE = MYISAM "; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `dynamic_playlist_data` (" . + "`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ," . + "`dynamic_id` INT( 11 ) UNSIGNED NOT NULL ," . + "`field` VARCHAR( 255 ) NOT NULL ," . + "`internal_operator` VARCHAR( 64 ) NOT NULL ," . + "`external_operator` VARCHAR( 64 ) NOT NULL ," . + "`value` VARCHAR( 255 ) NOT NULL" . + ") ENGINE = MYISAM"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_350008 + * + * Change song_id references to be object so they are a little more general. + * Add type to the now playing table so that we can handle different playing + * information. + */ + public static function update_350008() + { + $retval = true; + $sql = "ALTER TABLE `now_playing` CHANGE `song_id` `object_id` INT( 11 ) UNSIGNED NOT NULL"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `now_playing` ADD `object_type` VARCHAR ( 255 ) NOT NULL AFTER `object_id`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `now_playing` ADD INDEX ( `expire` )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD `addition_time` INT( 11 ) UNSIGNED NOT NULL AFTER `mime`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD `update_time` INT( 11 ) UNSIGNED NULL AFTER `addition_time`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD INDEX (`addition_time`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` ADD INDEX (`update_time`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` DROP `hash`"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_360001 + * + * This adds the MB UUIDs to the different tables as well as some additional + * cleanup. + */ + public static function update_360001() + { + $retval = true; + + $sql = "ALTER TABLE `album` ADD `mbid` CHAR ( 36 ) AFTER `prefix`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `artist` ADD `mbid` CHAR ( 36 ) AFTER `prefix`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` ADD `mbid` CHAR ( 36 ) AFTER `track`"; + $retval = Dba::write($sql) ? $retval : false; + + // Remove any RIO related information from the database as the plugin has been removed + $sql = "DELETE FROM `update_info` WHERE `key` LIKE 'Plugin_Ri%'"; + Dba::write($sql); + + $sql = "DELETE FROM `preference` WHERE `name` LIKE 'rio_%'"; + Dba::write($sql); + + return $retval; + } + + /** + * update_360002 + * + * This update makes changes to the cataloging to accomodate the new method + * for syncing between Ampache instances. + */ + public static function update_360002() + { + $retval = true; + // Drop the key from catalog and ACL + $sql = "ALTER TABLE `catalog` DROP `key`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `access_list` DROP `key`"; + $retval = Dba::write($sql) ? $retval : false; + + // Add in Username / Password for catalog - to be used for remote catalogs + $sql = "ALTER TABLE `catalog` ADD `remote_username` VARCHAR ( 255 ) AFTER `catalog_type`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `catalog` ADD `remote_password` VARCHAR ( 255 ) AFTER `remote_username`"; + $retval = Dba::write($sql) ? $retval : false; + + // Adjust the Filename field in song, make it gi-normous. If someone has + // anything close to this file length, they seriously need to reconsider + // what they are doing. + $sql = "ALTER TABLE `song` CHANGE `file` `file` VARCHAR ( 4096 )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `video` CHANGE `file` `file` VARCHAR ( 4096 )"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `live_stream` CHANGE `url` `url` VARCHAR ( 4096 )"; + $retval = Dba::write($sql) ? $retval : false; + + // Index the Artist, Album, and Song tables for fulltext searches. + $sql = "ALTER TABLE `artist` ADD FULLTEXT(`name`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `album` ADD FULLTEXT(`name`)"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "ALTER TABLE `song` ADD FULLTEXT(`title`)"; + $retval = Dba::write($sql) ? $retval : false; + + // Now add in the min_object_count preference and the random_method + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('bandwidth','50','Bandwidth','5','integer','interface')"; + Dba::write($sql); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('features','50','Features','5','integer','interface')"; + Dba::write($sql); + + return $retval; + } + + /** + * update_360003 + * + * This update moves the image data to its own table. + */ + public static function update_360003() + { + $sql = "CREATE TABLE `image` (" . + "`id` int(11) unsigned NOT NULL auto_increment," . + "`image` mediumblob NOT NULL," . + "`mime` varchar(64) NOT NULL," . + "`size` varchar(64) NOT NULL," . + "`object_type` varchar(64) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "PRIMARY KEY (`id`)," . + "KEY `object_type` (`object_type`)," . + "KEY `object_id` (`object_id`)" . + ") ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $retval = Dba::write($sql); + + foreach (array('album', 'artist') as $type) { + $sql = "SELECT `" . $type . "_id` AS `object_id`, " . + "`art`, `art_mime` FROM `" . $type . + "_data` WHERE `art` IS NOT NULL"; + $db_results = Dba::read($sql); + while ($row = Dba::fetch_assoc($db_results)) { + $sql = "INSERT INTO `image` " . + "(`image`, `mime`, `size`, " . + "`object_type`, `object_id`) " . + "VALUES('" . Dba::escape($row['art']) . + "', '" . $row['art_mime'] . + "', 'original', '" . $type . "', '" . + $row['object_id'] . "')"; + Dba::write($sql); + } + $sql = "DROP TABLE `" . $type . "_data`"; + $retval = Dba::write($sql) ? $retval : false; + } + + return $retval; + } + + /** + * update_360004 + * + * This update creates an index on the rating table. + */ + public static function update_360004() + { + $sql = "CREATE UNIQUE INDEX `unique_rating` ON `rating` (`user`, `object_type`, `object_id`)"; + return Dba::write($sql); + } + + /** + * update_360005 + * + * This changes the tmp_browse table around. + */ + public static function update_360005() + { + $retval = true; + + $sql = "DROP TABLE IF EXISTS `tmp_browse`"; + $retval = Dba::write($sql) ? $retval : false; + + $sql = "CREATE TABLE `tmp_browse` (" . + "`id` int(13) NOT NULL auto_increment," . + "`sid` varchar(128) character set utf8 NOT NULL default ''," . + "`data` longtext NOT NULL," . + "`object_data` longtext," . + "PRIMARY KEY (`sid`,`id`)" . + ") ENGINE=MyISAM DEFAULT CHARSET=utf8"; + $retval = Dba::write($sql) ? $retval : false; + + return $retval; + } + + /** + * update_360006 + * + * This adds the table for newsearch/dynamic playlists + */ + public static function update_360006() + { + $sql = "CREATE TABLE `search` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user` int(11) NOT NULL, + `type` enum('private','public') CHARACTER SET utf8 DEFAULT NULL, + `rules` mediumtext NOT NULL, + `name` varchar(255) CHARACTER SET utf8 DEFAULT NULL, + `logic_operator` varchar(3) CHARACTER SET utf8 DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8"; + return Dba::write($sql); + } + + /** + * update_360008 + * + * Fix bug that caused the remote_username/password fields to not be created. + * FIXME: Huh? + */ + public static function update_360008() + { + $retval = true; + $remote_username = false; + $remote_password = false; + + $sql = "DESCRIBE `catalog`"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + if ($row['Field'] == 'remote_username') { + $remote_username = true; + } + if ($row['Field'] == 'remote_password') { + $remote_password = true; + } + } // end while + + if (!$remote_username) { + // Add in Username / Password for catalog - to be used for remote catalogs + $sql = "ALTER TABLE `catalog` ADD `remote_username` VARCHAR ( 255 ) AFTER `catalog_type`"; + $retval = Dba::write($sql) ? $retval : false; + } + if (!$remote_password) { + $sql = "ALTER TABLE `catalog` ADD `remote_password` VARCHAR ( 255 ) AFTER `remote_username`"; + $retval = Dba::write($sql) ? $retval : false; + } + + return $retval; + } + + /** + * update_360009 + * + * The main session table was already updated to use varchar(64) for the ID, + * tmp_playlist needs the same change + */ + public static function update_360009() + { + $sql = "ALTER TABLE `tmp_playlist` CHANGE `session` `session` VARCHAR(64)"; + return Dba::write($sql); + } + + /** + * update_360010 + * + * MBz NGS means collaborations have more than one MBID (the ones + * belonging to the underlying artists). We need a bigger column. + */ + public static function update_360010() + { + $sql = 'ALTER TABLE `artist` CHANGE `mbid` `mbid` VARCHAR(1369)'; + return Dba::write($sql); + } + + /** + * update_360011 + * + * We need a place to store actual playlist data for downloadable + * playlist files. + */ + public static function update_360011() + { + $sql = 'CREATE TABLE `stream_playlist` (' . + '`id` int(11) unsigned NOT NULL AUTO_INCREMENT,' . + '`sid` varchar(64) NOT NULL,' . + '`url` text NOT NULL,' . + '`info_url` text DEFAULT NULL,' . + '`image_url` text DEFAULT NULL,' . + '`title` varchar(255) DEFAULT NULL,' . + '`author` varchar(255) DEFAULT NULL,' . + '`album` varchar(255) DEFAULT NULL,' . + '`type` varchar(255) DEFAULT NULL,' . + '`time` smallint(5) DEFAULT NULL,' . + 'PRIMARY KEY (`id`), KEY `sid` (`sid`))'; + return Dba::write($sql); + } + + /** + * update_360012 + * + * Drop the enum on session.type + */ + public static function update_360012() + { + return Dba::write('ALTER TABLE `session` CHANGE `type` `type` VARCHAR(16) DEFAULT NULL'); + } + + /** + * update_360013 + * + * MyISAM works better out of the box for the stream_playlist table + */ + public static function update_360013() + { + return Dba::write('ALTER TABLE `stream_playlist` ENGINE=MyISAM'); + } + + /** + * update_360014 + * + * PHP session IDs are an ever-growing beast. + */ + public static function update_360014() + { + $retval = true; + + $retval = Dba::write('ALTER TABLE `stream_playlist` CHANGE `sid` `sid` VARCHAR(256)') ? $retval : false; + $retval = Dba::write('ALTER TABLE `tmp_playlist` CHANGE `session` `session` VARCHAR(256)') ? $retval : false; + $retval = Dba::write('ALTER TABLE `session` CHANGE `id` `id` VARCHAR(256) NOT NULL') ? $retval : false; + + return $retval; + } + + /** + * update_360015 + * + * This inserts the Iframes preference... + */ + public static function update_360015() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('iframes','1','Iframes',25,'boolean','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /* + * update_360016 + * + * Add Now Playing filtered per user preference option + */ + public static function update_360016() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('now_playing_per_user','1','Now playing filtered per user',50,'boolean','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360017 + * + * New table to store user flags. + */ + public static function update_360017() + { + $sql = "CREATE TABLE `user_flag` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`user` int(11) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "`object_type` varchar(32) CHARACTER SET utf8 DEFAULT NULL," . + "`date` int(11) unsigned NOT NULL DEFAULT '0'," . + "PRIMARY KEY (`id`)," . + "UNIQUE KEY `unique_userflag` (`user`,`object_type`,`object_id`)," . + "KEY `object_id` (`object_id`)) ENGINE = MYISAM"; + return Dba::write($sql); + } + + /** + * update_360018 + * + * Add Album default sort preference... + */ + public static function update_360018() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('album_sort','0','Album Default Sort',25,'string','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360019 + * + * Add Show number of times a song was played preference + */ + public static function update_360019() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('show_played_times','0','Show # played',25,'string','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360020 + * + * Catalog types are plugins now + */ + public static function update_360020() + { + $sql = "SELECT `id`, `catalog_type`, `path`, `remote_username`, `remote_password` FROM `catalog`"; + $db_results = Dba::read($sql); + + $c = Catalog::create_catalog_type('local'); + $c->install(); + $c = Catalog::create_catalog_type('remote'); + $c->install(); + + while ($results = Dba::fetch_assoc($db_results)) { + if ($results['catalog_type'] == 'local') { + $sql = "INSERT INTO `catalog_local` (`path`, `catalog_id`) VALUES (?, ?)"; + Dba::write($sql, array($results['path'], $results['id'])); + } elseif ($results['catalog_type'] == 'remote') { + $sql = "INSERT INTO `catalog_remote` (`uri`, `username`, `password`, `catalog_id`) VALUES (?, ?, ?, ?)"; + Dba::write($sql, array($results['path'], $results['remote_username'], $results['remote_password'], $results['id'])); + } + } + + $sql = "ALTER TABLE `catalog` DROP `path`, DROP `remote_username`, DROP `remote_password`"; + Dba::write($sql); + + $sql = "ALTER TABLE `catalog` MODIFY COLUMN `catalog_type` varchar(128)"; + Dba::write($sql); + + $sql = "UPDATE `artist` SET `mbid` = null WHERE `mbid` = ''"; + Dba::write($sql); + + $sql = "UPDATE `album` SET `mbid` = null WHERE `mbid` = ''"; + Dba::write($sql); + + $sql = "UPDATE `song` SET `mbid` = null WHERE `mbid` = ''"; + Dba::write($sql); + + return true; + } + + /** + * update_360021 + * + * Add insertion date on Now Playing and option to show the current song in page title for Web player + */ + public static function update_360021() + { + $sql = "ALTER TABLE `now_playing` ADD `insertion` INT (11) AFTER `expire`"; + Dba::write($sql); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('song_page_title','1','Show current song in Web player page title',25,'boolean','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360022 + * + * Remove unused live_stream fields and add codec field + */ + public static function update_360022() + { + $sql = "ALTER TABLE `live_stream` ADD `codec` VARCHAR(32) NULL AFTER `catalog`, DROP `frequency`, DROP `call_sign`"; + Dba::write($sql); + + $sql = "ALTER TABLE `stream_playlist` ADD `codec` VARCHAR(32) NULL AFTER `time`"; + Dba::write($sql); + + return true; + } + + /** + * update_360023 + * + * Enable/Disable SubSonic and Plex backend + */ + public static function update_360023() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('subsonic_backend','1','Use SubSonic backend',25,'boolean','system')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('plex_backend','0','Use Plex backend',25,'boolean','system')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360024 + * + * Drop unused flagged table + */ + public static function update_360024() + { + $sql = "DROP TABLE `flagged`"; + Dba::write($sql); + + return true; + } + + /** + * update_360025 + * + * Add options to enable HTML5 / Flash on web players + */ + public static function update_360025() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('webplayer_flash','1','Authorize Flash Web Player(s)',25,'boolean','streaming')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('webplayer_html5','1','Authorize HTML5 Web Player(s)',25,'boolean','streaming')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360026 + * + * Add agent field in `object_count` table + */ + public static function update_360026() + { + $sql = "ALTER TABLE `object_count` ADD `agent` VARCHAR(255) NULL AFTER `user`"; + Dba::write($sql); + + return true; + } + + /** + * update_360027 + * + * Personal information: allow/disallow to show my personal information into now playing and recently played lists. + */ + public static function update_360027() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('allow_personal_info','1','Allow to show my personal info to other users (now playing, recently played)',25,'boolean','interface')"; + Dba::write($sql); + + $id = Dba::insert_id(); + + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360028 + * + * Personal information: allow/disallow to show in now playing. + * Personal information: allow/disallow to show in recently played. + * Personal information: allow/disallow to show time and/or agent in recently played. + */ + public static function update_360028() + { + // Update previous update preference + $sql = "UPDATE `preference` SET `name`='allow_personal_info_now', `description`='Personal information visibility - Now playing' WHERE `name`='allow_personal_info'"; + Dba::write($sql); + + // Insert new recently played preference + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('allow_personal_info_recent','1','Personal information visibility - Recently played',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + // Insert streaming time preference + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('allow_personal_info_time','1','Personal information visibility - Recently played - Allow to show streaming date/time',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + // Insert streaming agent preference + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('allow_personal_info_agent','1','Personal information visibility - Recently played - Allow to show streaming agent',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360029 + * + * New table to store wanted releases + */ + public static function update_360029() + { + $sql = "CREATE TABLE `wanted` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`user` int(11) NOT NULL," . + "`artist` int(11) NOT NULL," . + "`mbid` varchar(36) CHARACTER SET utf8 NULL," . + "`name` varchar(255) CHARACTER SET utf8 NOT NULL," . + "`year` int(4) NULL," . + "`date` int(11) unsigned NOT NULL DEFAULT '0'," . + "`accepted` tinyint(1) NOT NULL DEFAULT '0'," . + "PRIMARY KEY (`id`)," . + "UNIQUE KEY `unique_wanted` (`user`, `artist`,`mbid`)) ENGINE = MYISAM"; + + return Dba::write($sql); + } + + /** + * update_360030 + * + * New table to store song previews + */ + public static function update_360030() + { + $sql = "CREATE TABLE `song_preview` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`session` varchar(256) CHARACTER SET utf8 NOT NULL," . + "`artist` int(11) NOT NULL," . + "`title` varchar(255) CHARACTER SET utf8 NOT NULL," . + "`album_mbid` varchar(36) CHARACTER SET utf8 NULL," . + "`mbid` varchar(36) CHARACTER SET utf8 NULL," . + "`disk` int(11) NULL," . + "`track` int(11) NULL," . + "`file` varchar(255) CHARACTER SET utf8 NULL," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + + return Dba::write($sql); + } + + /** + * update_360031 + * + * Add option to fix header/sidebars position on compatible themes + */ + public static function update_360031() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('ui_fixed','0','Fix header/sidebars position on compatible themes',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360032 + * + * Add check update automatically option + */ + public static function update_360032() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('autoupdate','1','Check for Ampache updates automatically',25,'boolean','system')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + Preference::insert('autoupdate_lastcheck','AutoUpdate last check time','','25','string','internal'); + Preference::insert('autoupdate_lastversion','AutoUpdate last version from last check','','25','string','internal'); + Preference::insert('autoupdate_lastversion_new','AutoUpdate last version from last check is newer','','25','boolean','internal'); + + return true; + } + + /** + * update_360033 + * + * Add song waveform as song data + */ + public static function update_360033() + { + $sql = "ALTER TABLE `song_data` ADD `waveform` MEDIUMBLOB NULL AFTER `language`"; + Dba::write($sql); + + $sql = "ALTER TABLE `user_shout` ADD `data` VARCHAR(256) NULL AFTER `object_type`"; + Dba::write($sql); + + return true; + } + + /** + * update_360034 + * + * Add settings for confirmation when closing window and auto-pause between tabs + */ + public static function update_360034() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('webplayer_confirmclose','0','Confirmation when closing current playing window',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('webplayer_pausetabs','1','Auto-pause betweens tabs',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'1')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360035 + * + * Add beautiful stream url setting + */ + public static function update_360035() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('stream_beautiful_url','0','Use beautiful stream url',25,'boolean','streaming')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360036 + * + * Remove some unused parameters + */ + public static function update_360036() + { + $sql = "DELETE FROM `preference` WHERE `name` LIKE 'ellipse_threshold_%'"; + Dba::write($sql); + + $sql = "DELETE FROM `preference` WHERE `name` = 'min_object_count'"; + Dba::write($sql); + + $sql = "DELETE FROM `preference` WHERE `name` = 'bandwidth'"; + Dba::write($sql); + + $sql = "DELETE FROM `preference` WHERE `name` = 'features'"; + Dba::write($sql); + + $sql = "DELETE FROM `preference` WHERE `name` = 'tags_userlist'"; + Dba::write($sql); + + return true; + } + + /** + * update_360037 + * + * Add sharing features + */ + public static function update_360037() + { + $sql = "CREATE TABLE `share` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`user` int(11) unsigned NOT NULL," . + "`object_type` varchar(32) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "`allow_stream` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`allow_download` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`expire_days` int(4) unsigned NOT NULL DEFAULT '0'," . + "`max_counter` int(4) unsigned NOT NULL DEFAULT '0'," . + "`secret` varchar(20) CHARACTER SET utf8 NULL," . + "`counter` int(4) unsigned NOT NULL DEFAULT '0'," . + "`creation_date` int(11) unsigned NOT NULL DEFAULT '0'," . + "`lastvisit_date` int(11) unsigned NOT NULL DEFAULT '0'," . + "`public_url` varchar(255) CHARACTER SET utf8 NULL," . + "`description` varchar(255) CHARACTER SET utf8 NULL," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + Dba::write($sql); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('share','0','Allow Share',25,'boolean','system')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('share_expire','7','Share links default expiration days (0=never)',25,'integer','system')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'7')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360038 + * + * Add missing albums browse on missing artists + */ + public static function update_360038() + { + $sql = "ALTER TABLE `wanted` ADD `artist_mbid` varchar(1369) CHARACTER SET utf8 NULL AFTER `artist`"; + Dba::write($sql); + + $sql = "ALTER TABLE `wanted` MODIFY `artist` int(11) NULL"; + Dba::write($sql); + + $sql = "ALTER TABLE `song_preview` ADD `artist_mbid` varchar(1369) CHARACTER SET utf8 NULL AFTER `artist`"; + Dba::write($sql); + + $sql = "ALTER TABLE `song_preview` MODIFY `artist` int(11) NULL"; + Dba::write($sql); + + return true; + } + + /** + * update_360039 + * + * Add website field on users + */ + public static function update_360039() + { + $sql = "ALTER TABLE `user` ADD `website` varchar(255) CHARACTER SET utf8 NULL AFTER `email`"; + Dba::write($sql); + + return true; + } + + /** + * update_360040 skipped. + */ + + /** + * update_360041 + * + * Add channels + */ + public static function update_360041() + { + $sql = "CREATE TABLE `channel` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`name` varchar(64) CHARACTER SET utf8 NULL," . + "`description` varchar(256) CHARACTER SET utf8 NULL," . + "`url` varchar(256) CHARACTER SET utf8 NULL," . + "`interface` varchar(64) CHARACTER SET utf8 NULL," . + "`port` int(11) unsigned NOT NULL DEFAULT '0'," . + "`fixed_endpoint` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`object_type` varchar(32) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "`is_private` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`random` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`loop` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`admin_password` varchar(20) CHARACTER SET utf8 NULL," . + "`start_date` int(11) unsigned NOT NULL DEFAULT '0'," . + "`max_listeners` int(11) unsigned NOT NULL DEFAULT '0'," . + "`peak_listeners` int(11) unsigned NOT NULL DEFAULT '0'," . + "`listeners` int(11) unsigned NOT NULL DEFAULT '0'," . + "`connections` int(11) unsigned NOT NULL DEFAULT '0'," . + "`stream_type` varchar(8) CHARACTER SET utf8 NOT NULL DEFAULT 'mp3'," . + "`bitrate` int(11) unsigned NOT NULL DEFAULT '128'," . + "`pid` int(11) unsigned NOT NULL DEFAULT '0'," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + return Dba::write($sql); + } + + /** + * update_360042 + * + * Add broadcasts and player control + */ + public static function update_360042() + { + $sql = "CREATE TABLE `broadcast` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`user` int(11) unsigned NOT NULL," . + "`name` varchar(64) CHARACTER SET utf8 NULL," . + "`description` varchar(256) CHARACTER SET utf8 NULL," . + "`is_private` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`song` int(11) unsigned NOT NULL DEFAULT '0'," . + "`started` tinyint(1) unsigned NOT NULL DEFAULT '0'," . + "`listeners` int(11) unsigned NOT NULL DEFAULT '0'," . + "`key` varchar(32) CHARACTER SET utf8 NULL," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + Dba::write($sql); + + $sql = "CREATE TABLE `player_control` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`user` int(11) unsigned NOT NULL," . + "`cmd` varchar(32) CHARACTER SET utf8 NOT NULL," . + "`value` varchar(256) CHARACTER SET utf8 NULL," . + "`object_type` varchar(32) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "`send_date` int(11) unsigned NOT NULL DEFAULT '0'," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + + return Dba::write($sql); + } + + /** + * update_360043 + * + * Add slideshow on currently played artist preference + */ + public static function update_360043() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('slideshow_time','0','Artist slideshow inactivity time',25,'integer','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360044 + * + * Add artist description/recommendation external service data cache + */ + public static function update_360044() + { + $sql = "ALTER TABLE `artist` ADD `summary` TEXT CHARACTER SET utf8 NULL," . + "ADD `placeformed` varchar(64) NULL," . + "ADD `yearformed` int(4) NULL," . + "ADD `last_update` int(11) unsigned NOT NULL DEFAULT '0'"; + Dba::write($sql); + + $sql = "CREATE TABLE `recommendation` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`object_type` varchar(32) NOT NULL," . + "`object_id` int(11) unsigned NOT NULL," . + "`last_update` int(11) unsigned NOT NULL DEFAULT '0'," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + Dba::write($sql); + + $sql = "CREATE TABLE `recommendation_item` (" . + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," . + "`recommendation` int(11) unsigned NOT NULL," . + "`recommendation_id` int(11) unsigned NULL," . + "`name` varchar(256) NULL," . + "`rel` varchar(256) NULL," . + "`mbid` varchar(1369) NULL," . + "PRIMARY KEY (`id`)) ENGINE = MYISAM"; + Dba::write($sql); + + return true; + } + + /** + * update_360045 + * + * Set user field on playlists as optional + */ + public static function update_360045() + { + $sql = "ALTER TABLE `playlist` MODIFY `user` int(11) NULL"; + Dba::write($sql); + + return true; + } + + /** + * update_360046 + * + * Add broadcast web player by default preference + */ + public static function update_360046() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('broadcast_by_default','0','Broadcast web player by default',25,'boolean','streaming')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360047 + * + * Add apikey field on users + */ + public static function update_360047() + { + $sql = "ALTER TABLE `user` ADD `apikey` varchar(255) CHARACTER SET utf8 NULL AFTER `website`"; + Dba::write($sql); + + return true; + } + + /** + * update_360048 + * + * Add concerts options + */ + public static function update_360048() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('concerts_limit_future','0','Limit number of future events',25,'integer','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('concerts_limit_past','0','Limit number of past events',25,'integer','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360049 + * + * Add album group multiple disks setting + */ + public static function update_360049() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('album_group','0','Album - Group multiple disks',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360050 + * + * Add top menu setting + */ + public static function update_360050() + { + $sql = "INSERT INTO `preference` (`name`,`value`,`description`,`level`,`type`,`catagory`) " . + "VALUES ('topmenu','0','Top menu',25,'boolean','interface')"; + Dba::write($sql); + $id = Dba::insert_id(); + $sql = "INSERT INTO `user_preference` VALUES (-1,?,'0')"; + Dba::write($sql, array($id)); + + return true; + } + + /** + * update_360051 + * + * Copy default .htaccess configurations + */ + public static function update_360051() + { + require_once AmpConfig::get('prefix') . '/lib/install.lib.php'; + + if (!install_check_server_apache()) { + debug_event('update', 'Not using Apache, update 360051 skipped.', '5'); + return true; + } + + $htaccess_play_file = AmpConfig::get('prefix') . '/play/.htaccess'; + $htaccess_rest_file = AmpConfig::get('prefix') . '/rest/.htaccess'; + + $ret = true; + if (!is_readable($htaccess_play_file)) { + $created = false; + if (check_htaccess_play_writable()) { + if (!install_rewrite_rules($htaccess_play_file, AmpConfig::get('raw_web_path'), false)) { + Error::add('general', T_('File copy error.')); + } else { + $created = true; + } + } + + if (!$created) { + Error::add('general', T_('Cannot copy default .htaccess file.') . ' Please copy ' . $htaccess_play_file . '.dist to ' . $htaccess_play_file . '.'); + $ret = false; + } + } + + if (!is_readable($htaccess_rest_file)) { + $created = false; + if (check_htaccess_rest_writable()) { + if (!install_rewrite_rules($htaccess_rest_file, AmpConfig::get('raw_web_path'), false)) { + Error::add('general', T_('File copy error.')); + } else { + $created = true; + } + } + + if (!$created) { + Error::add('general', T_('Cannot copy default .htaccess file.') . ' Please copy ' . $htaccess_rest_file . '.dist to ' . $htaccess_rest_file . '.'); + $ret = false; + } + } + + return $ret; + } +} diff --git a/sources/lib/class/user.class.php b/sources/lib/class/user.class.php new file mode 100644 index 0000000..33f4bfc --- /dev/null +++ b/sources/lib/class/user.class.php @@ -0,0 +1,1274 @@ +id = intval($user_id); + + $info = $this->_get_info(); + + foreach ($info as $key=>$value) { + // Let's not save the password in this object :S + if ($key == 'password') { continue; } + $this->$key = $value; + } + + // Make sure the Full name is always filled + if (strlen($this->fullname) < 1) { $this->fullname = $this->username; } + + } // Constructor + + /** + * count + * + * This returns the number of user accounts that exist. + */ + public static function count() + { + $sql = 'SELECT COUNT(`id`) FROM `user`'; + $db_results = Dba::read($sql); + $data = Dba::fetch_row($db_results); + $results = array(); + $results['users'] = $data[0]; + + $time = time(); + $last_seen = $time - 1200; + $sql = 'SELECT COUNT(DISTINCT `session`.`username`) FROM `session` ' . + 'INNER JOIN `user` ON `session`.`username` = `user`.`username` ' . + 'WHERE `session`.`expire` > ? and `user`.`last_seen` > ?'; + $db_results = Dba::read($sql, array($time, $last_seen)); + $data = Dba::fetch_row($db_results); + $results['connected'] = $data[0]; + return $results; + } + + /** + * _get_info + * This function returns the information for this object + */ + private function _get_info() + { + $id = intval($this->id); + + if (parent::is_cached('user',$id)) { + return parent::get_from_cache('user',$id); + } + + $data = array(); + // If the ID is -1 then + if ($id == '-1') { + $data['username'] = 'System'; + $data['fullname'] = 'Ampache User'; + $data['access'] = '25'; + return $data; + } + + $sql = "SELECT * FROM `user` WHERE `id`='$id'"; + $db_results = Dba::read($sql); + + $data = Dba::fetch_assoc($db_results); + + parent::add_to_cache('user',$id,$data); + + return $data; + + } // _get_info + + /** + * load_playlist + * This is called once per page load it makes sure that this session + * has a tmp_playlist, creating it if it doesn't, then sets $this->playlist + * as a tmp_playlist object that can be fiddled with later on + */ + public function load_playlist() + { + $session_id = session_id(); + + $this->playlist = Tmp_Playlist::get_from_session($session_id); + + } // load_playlist + + /** + * get_valid_users + * This returns all valid users in database. + */ + public static function get_valid_users() + { + $users = array(); + + $sql = "SELECT `id` FROM `user` WHERE `disabled` = '0'"; + $db_results = Dba::read($sql); + while ($results = Dba::fetch_assoc($db_results)) { + $users[] = $results['id']; + } + + return $users; + } // get_valid_users + + /** + * get_from_username + * This returns a built user from a username. This is a + * static function so it doesn't require an instance + */ + public static function get_from_username($username) + { + $sql = "SELECT `id` FROM `user` WHERE `username` = ?"; + $db_results = Dba::read($sql, array($username)); + $results = Dba::fetch_assoc($db_results); + + $user = new User($results['id']); + + return $user; + + } // get_from_username + + /** + * get_from_apikey + * This returns a built user from an apikey. This is a + * static function so it doesn't require an instance + */ + public static function get_from_apikey($apikey) + { + $user = null; + $apikey = trim($apikey); + if (!empty($apikey)) { + $sql = "SELECT `id` FROM `user` WHERE `apikey` = ?"; + $db_results = Dba::read($sql, array($apikey)); + $results = Dba::fetch_assoc($db_results); + + if ($results['id']) { + $user = new User($results['id']); + } + } + + return $user; + + } // get_from_apikey + + /** + * get_from_email + * This returns a built user from a email. This is a + * static function so it doesn't require an instance + */ + public static function get_from_email($email) + { + $user = null; + $sql = "SELECT `id` FROM `user` WHERE `email` = ?"; + $db_results = Dba::read($sql, array($email)); + if ($results = Dba::fetch_assoc($db_results)) { + $user = new User($results['id']); + } + + return $user; + + } // get_from_email + + /** + * get_from_website + * This returns users list related to a website. + */ + public static function get_from_website($website) + { + $website = rtrim($website, "/"); + $sql = "SELECT `id` FROM `user` WHERE `website` = ? LIMIT 1"; + $db_results = Dba::read($sql, array($website)); + $users = array(); + while ($results = Dba::fetch_assoc($db_results)) { + $users[] = $results['id']; + } + return $users; + + } // get_from_website + + /** + * get_catalogs + * This returns the catalogs as an array of ids that this user is allowed to access + */ + public function get_catalogs() + { + if (parent::is_cached('user_catalog',$this->id)) { + return parent::get_from_cache('user_catalog',$this->id); + } + + $sql = "SELECT * FROM `user_catalog` WHERE `user` = ?"; + $db_results = Dba::read($sql, array($this->id)); + + $catalogs = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $catalogs[] = $row['catalog']; + } + + parent::add_to_cache('user_catalog',$this->id,$catalogs); + + return $catalogs; + + } // get_catalogs + + /** + * get_preferences + * This is a little more complicate now that we've got many types of preferences + * This funtions pulls all of them an arranges them into a spiffy little array + * You can specify a type to limit it to a single type of preference + * []['title'] = ucased type name + * []['prefs'] = array(array('name','display','value')); + * []['admin'] = t/f value if this is an admin only section + */ + public function get_preferences($type = 0, $system = false) + { + // Fill out the user id + $user_id = $system ? Dba::escape(-1) : Dba::escape($this->id); + + $user_limit = ""; + if (!$system) { + $user_limit = "AND preference.catagory != 'system'"; + } else if ($type != '0') { + $user_limit = "AND preference.catagory = '" . Dba::escape($type) . "'"; + } + + + $sql = "SELECT preference.name, preference.description, preference.catagory, preference.level, user_preference.value " . + "FROM preference INNER JOIN user_preference ON user_preference.preference=preference.id " . + "WHERE user_preference.user='$user_id' " . $user_limit . + " ORDER BY preference.catagory, preference.description"; + + $db_results = Dba::read($sql); + $results = array(); + $type_array = array(); + /* Ok this is crapy, need to clean this up or improve the code FIXME */ + while ($r = Dba::fetch_assoc($db_results)) { + $type = $r['catagory']; + $admin = false; + if ($type == 'system') { $admin = true; } + $type_array[$type][$r['name']] = array('name'=>$r['name'],'level'=>$r['level'],'description'=>$r['description'],'value'=>$r['value']); + $results[$type] = array ('title'=>ucwords($type),'admin'=>$admin,'prefs'=>$type_array[$type]); + } // end while + + return $results; + + } // get_preferences + + /** + * set_preferences + * sets the prefs for this specific user + */ + public function set_preferences() + { + $user_id = Dba::escape($this->id); + + $sql = "SELECT preference.name,user_preference.value FROM preference,user_preference WHERE user_preference.user='$user_id' " . + "AND user_preference.preference=preference.id AND preference.type != 'system'"; + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $key = $r['name']; + $this->prefs[$key] = $r['value']; + } + } // set_preferences + + /** + * get_favorites + * returns an array of your $type favorites + */ + public function get_favorites($type) + { + $results = Stats::get_user(AmpConfig::get('popular_threshold'),$type,$this->id,1); + + $items = array(); + + foreach ($results as $r) { + /* If its a song */ + if ($type == 'song') { + $data = new Song($r['object_id']); + $data->count = $r['count']; + $data->format(); + $data->f_link; + $items[] = $data; + } + /* If its an album */ + elseif ($type == 'album') { + $data = new Album($r['object_id']); + //$data->count = $r['count']; + $data->format(); + $items[] = $data; + } + /* If its an artist */ + elseif ($type == 'artist') { + $data = new Artist($r['object_id']); + //$data->count = $r['count']; + $data->format(); + $data->f_name = $data->f_link; + $items[] = $data; + } + /* If it's a genre */ + elseif ($type == 'genre') { + $data = new Genre($r['object_id']); + //$data->count = $r['count']; + $data->format(); + $data->f_name = $data->f_link; + $items[] = $data; + } + + } // end foreach + + return $items; + + } // get_favorites + + /** + * get_recommendations + * This returns recommended objects of $type. The recommendations + * are based on voodoo economics,the phase of the moon and my current BAL. + */ + public function get_recommendations($type) + { + /* First pull all of your ratings of this type */ + $sql = "SELECT object_id,user_rating FROM ratings " . + "WHERE object_type='" . Dba::escape($type) . "' AND user='" . Dba::escape($this->id) . "'"; + $db_results = Dba::read($sql); + + // Incase they only have one user + $users = array(); + $ratings = array(); + while ($r = Dba::fetch_assoc($db_results)) { + /* Store the fact that you rated this */ + $key = $r['object_id']; + $ratings[$key] = true; + + /* Build a key'd array of users with this same rating */ + $sql = "SELECT user FROM ratings WHERE object_type='" . Dba::escape($type) . "' " . + "AND user !='" . Dba::escape($this->id) . "' AND object_id='" . Dba::escape($r['object_id']) . "' " . + "AND user_rating ='" . Dba::escape($r['user_rating']) . "'"; + $user_results = Dba::read($sql); + + while ($user_info = Dba::fetch_assoc($user_results)) { + $key = $user_info['user']; + $users[$key]++; + } + + } // end while + + /* now we've got your ratings, and all users and the # of ratings that match your ratings + * sort the users[$key] array by value and then find things they've rated high (4+) that you + * haven't rated + */ + $recommendations = array(); + asort($users); + + foreach ($users as $user_id=>$score) { + + /* Find everything they've rated at 4+ */ + $sql = "SELECT object_id,user_rating FROM ratings " . + "WHERE user='" . Dba::escape($user_id) . "' AND user_rating >='4' AND " . + "object_type = '" . Dba::escape($type) . "' ORDER BY user_rating DESC"; + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $key = $r['object_id']; + if (isset($ratings[$key])) { continue; } + + /* Let's only get 5 total for now */ + if (count($recommendations) > 5) { return $recommendations; } + + $recommendations[$key] = $r['user_rating']; + + } // end while + + + } // end foreach users + + return $recommendations; + + } // get_recommendations + + /** + * is_logged_in + * checks to see if $this user is logged in returns their current IP if they + * are logged in + */ + public function is_logged_in() + { + $username = Dba::escape($this->username); + + $sql = "SELECT `id`,`ip` FROM `session` WHERE `username`='$username'" . + " AND `expire` > ". time(); + $db_results = Dba::read($sql); + + if ($row = Dba::fetch_assoc($db_results)) { + $ip = $row['ip'] ? $row['ip'] : NULL; + return $ip; + } + + return false; + + } // is_logged_in + + /** + * has_access + * this function checkes to see if this user has access + * to the passed action (pass a level requirement) + */ + public function has_access($needed_level) + { + if (!AmpConfig::get('use_auth') || AmpConfig::get('demo_mode')) { return true; } + + if ($this->access >= $needed_level) { return true; } + + return false; + + } // has_access + + /** + * update + * This function is an all encompasing update function that + * calls the mini ones does all the error checking and all that + * good stuff + */ + public function update($data) + { + if (empty($data['username'])) { + Error::add('username', T_('Error Username Required')); + } + + if ($data['password1'] != $data['password2'] AND !empty($data['password1'])) { + Error::add('password', T_("Error Passwords don't match")); + } + + if (Error::occurred()) { + return false; + } + + foreach ($data as $name => $value) { + if ($name == 'password1') { + $name = 'password'; + } else { + $value = scrub_in($value); + } + + switch ($name) { + case 'password'; + case 'access': + case 'email': + case 'username': + case 'fullname': + case 'website': + if ($this->$name != $value) { + $function = 'update_' . $name; + $this->$function($value); + } + break; + default: + // Rien a faire + break; + } + } + + return true; + } + + /** + * update_username + * updates their username + */ + public function update_username($new_username) + { + $sql = "UPDATE `user` SET `username` = ? WHERE `id` = ?"; + $this->username = $new_username; + Dba::write($sql, array($new_username, $this->id)); + + } // update_username + + /** + * update_validation + * This is used by the registration mumbojumbo + * Use this function to update the validation key + * NOTE: crap this doesn't have update_item the humanity of it all + */ + public function update_validation($new_validation) + { + $sql = "UPDATE `user` SET `validation` = ?, `disabled`='1' WHERE `id` = ?"; + $db_results = Dba::write($sql, array($new_validation, $this->id)); + $this->validation = $new_validation; + + return $db_results; + + } // update_validation + + /** + * update_fullname + * updates their fullname + */ + public function update_fullname($new_fullname) + { + $sql = "UPDATE `user` SET `fullname` = ? WHERE `id` = ?"; + Dba::write($sql, array($new_fullname, $this->id)); + + } // update_fullname + + /** + * update_email + * updates their email address + */ + public function update_email($new_email) + { + $sql = "UPDATE `user` SET `email` = ? WHERE `id` = ?"; + Dba::write($sql, array($new_email, $this->id)); + + } // update_email + + /** + * update_website + * updates their website address + */ + public function update_website($new_website) + { + $new_website = rtrim($new_website, "/"); + $sql = "UPDATE `user` SET `website` = ? WHERE `id` = ?"; + Dba::write($sql, array($new_website, $this->id)); + + } // update_website + + /** + * update_apikey + * Updates their api key + */ + public function update_apikey($new_apikey) + { + $sql = "UPDATE `user` SET `apikey` = ? WHERE `id` = ?"; + Dba::write($sql, array($new_apikey, $this->id)); + + } // update_website + + /** + * generate_apikey + * Generate a new user API key + */ + public function generate_apikey() + { + $apikey = hash('md5', time() . $this->username . $this->get_password()); + $this->update_apikey($apikey); + } + + /** + * get_password + * Get the current hashed user password from database. + */ + public function get_password() + { + $sql = 'SELECT * FROM `user` WHERE `id` = ?'; + $db_results = Dba::read($sql, array($this->id)); + $row = Dba::fetch_assoc($db_results); + + return $row['password']; + } + + /** + * disable + * This disables the current user + */ + public function disable() + { + // Make sure we aren't disabling the last admin + $sql = "SELECT `id` FROM `user` WHERE `disabled` = '0' AND `id` != '" . $this->id . "' AND `access`='100'"; + $db_results = Dba::read($sql); + + if (!Dba::num_rows($db_results)) { return false; } + + $sql = "UPDATE `user` SET `disabled`='1' WHERE id='" . $this->id . "'"; + Dba::write($sql); + + // Delete any sessions they may have + $sql = "DELETE FROM `session` WHERE `username`='" . Dba::escape($this->username) . "'"; + Dba::write($sql); + + return true; + + } // disable + + /** + * enable + * this enables the current user + */ + public function enable() + { + $sql = "UPDATE `user` SET `disabled`='0' WHERE id='" . $this->id . "'"; + Dba::write($sql); + + return true; + + } // enable + + /** + * update_access + * updates their access level + */ + public function update_access($new_access) + { + /* Prevent Only User accounts */ + if ($new_access < '100') { + $sql = "SELECT `id` FROM user WHERE `access`='100' AND `id` != '$this->id'"; + $db_results = Dba::read($sql); + if (!Dba::num_rows($db_results)) { return false; } + } + + $new_access = Dba::escape($new_access); + $sql = "UPDATE `user` SET `access`='$new_access' WHERE `id`='$this->id'"; + Dba::write($sql); + + } // update_access + + /*! + @function update_last_seen + @discussion updates the last seen data for this user + */ + public function update_last_seen() + { + $sql = "UPDATE user SET last_seen='" . time() . "' WHERE `id`='$this->id'"; + Dba::write($sql); + + } // update_last_seen + + /** + * update_user_stats + * updates the playcount mojo for this specific user + */ + public function update_stats($song_id, $agent = '') + { + debug_event('user.class.php', 'Updating stats for {'.$song_id.'} {'.$agent.'}...', '5'); + $song_info = new Song($song_id); + $song_info->format(); + $user = $this->id; + + if (!strlen($song_info->file)) { return false; } + + $this->set_preferences(); + + // If pthreads available, we call save_songplay in a new thread to quickly return + if (class_exists("Thread", false)) { + debug_event('user.class.php', 'Calling save_songplay plugins in a new thread...', '5'); + $thread = new scrobbler_async($GLOBALS['user'], $song_info); + if ($thread->start()) { + //$thread->join(); + } else { + debug_event('user.class.php', 'Error when starting the thread.', '1'); + } + } else { + User::save_songplay($GLOBALS['user'], $song_info); + } + + // Do this last so the 'last played checks are correct' + Stats::insert('song', $song_id, $user, $agent); + Stats::insert('album', $song_info->album, $user, $agent); + Stats::insert('artist', $song_info->artist, $user, $agent); + + return true; + + } // update_stats + + public static function save_songplay($user, $song_info) + { + foreach (Plugin::get_plugins('save_songplay') as $plugin_name) { + try { + $plugin = new Plugin($plugin_name); + if ($plugin->load($user)) { + $plugin->_plugin->save_songplay($song_info); + } + } catch (Exception $e) { + debug_event('user.class.php', 'Stats plugin error: ' . $e->getMessage(), '1'); + } + } + } + + /** + * insert_ip_history + * This inserts a row into the IP History recording this user at this + * address at this time in this place, doing this thing.. you get the point + */ + public function insert_ip_history() + { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $sip = $_SERVER['HTTP_X_FORWARDED_FOR']; + debug_event('User Ip', 'Login from ip adress: ' . $sip,'3'); + } else { + $sip = $_SERVER['REMOTE_ADDR']; + debug_event('User Ip', 'Login from ip adress: ' . $sip,'3'); + } + + $ip = Dba::escape(inet_pton($sip)); + $date = time(); + $user = $this->id; + $agent = Dba::escape($_SERVER['HTTP_USER_AGENT']); + + $sql = "INSERT INTO `ip_history` (`ip`,`user`,`date`,`agent`) VALUES ('$ip','$user','$date','$agent')"; + Dba::write($sql); + + /* Clean up old records... sometimes */ + if (rand(1,100) > 60) { + $date = time() - (86400*AmpConfig::get('user_ip_cardinality')); + $sql = "DELETE FROM `ip_history` WHERE `date` < $date"; + Dba::write($sql); + } + + return true; + + } // insert_ip_history + + /** + * create + * inserts a new user into ampache + */ + public static function create($username, $fullname, $email, $website, $password, $access, $disabled = false) + { + $website = rtrim($website, "/"); + $password = hash('sha256', $password); + $disabled = $disabled ? 1 : 0; + + /* Now Insert this new user */ + $sql = "INSERT INTO `user` (`username`, `disabled`, " . + "`fullname`, `email`, `password`, `access`, `create_date`"; + $params = array($username, $disabled, $fullname, $email, $password, $access, time()); + if (!empty($website)) { + $sql .= ", `website`"; + $params[] = $website; + } + $sql .= ") VALUES(?, ?, ?, ?, ?, ?, ?"; + if (!empty($website)) { + $sql .= ", ?"; + } + $sql .= ")"; + $db_results = Dba::write($sql, $params); + + if (!$db_results) { return false; } + + // Get the insert_id + $insert_id = Dba::insert_id(); + + /* Populates any missing preferences, in this case all of them */ + self::fix_preferences($insert_id); + + return $insert_id; + + } // create + + /** + * update_password + * updates a users password + */ + public function update_password($new_password) + { + $new_password = hash('sha256',$new_password); + + $new_password = Dba::escape($new_password); + $sql = "UPDATE `user` SET `password` = ? WHERE `id` = ?"; + $db_results = Dba::write($sql, array($new_password, $this->id)); + + // Clear this (temp fix) + if ($db_results) { unset($_SESSION['userdata']['password']); } + + } // update_password + + /** + * format + * This function sets up the extra variables we need when we are displaying a + * user for an admin, these should not be normally called when creating a + * user object + */ + public function format() + { + /* If they have a last seen date */ + if (!$this->last_seen) { $this->f_last_seen = T_('Never'); } else { $this->f_last_seen = date("m\/d\/Y - H:i",$this->last_seen); } + + /* If they have a create date */ + if (!$this->create_date) { $this->f_create_date = T_('Unknown'); } else { $this->f_create_date = date("m\/d\/Y - H:i",$this->create_date); } + + // Base link + $this->f_link = '' . $this->fullname . ''; + + /* Calculate their total Bandwidth Usage */ + $sql = "SELECT `song`.`size` FROM `song` LEFT JOIN `object_count` ON `song`.`id`=`object_count`.`object_id` " . + "WHERE `object_count`.`user`='$this->id' AND `object_count`.`object_type`='song'"; + $db_results = Dba::read($sql); + + $total = 0; + while ($r = Dba::fetch_assoc($db_results)) { + $total = $total + $r['size']; + } + + $this->f_useage = UI::format_bytes($total); + + /* Get Users Last ip */ + if (count($data = $this->get_ip_history(1))) { + $this->ip_history = inet_ntop($data['0']['ip']); + } else { + $this->ip_history = T_('Not Enough Data'); + } + + $avatar = $this->get_avatar(); + if (!empty($avatar['url'])) { + $this->f_avatar = ''; + } + if (!empty($avatar['url_mini'])) { + $this->f_avatar_mini = ''; + } + if (!empty($avatar['url_medium'])) { + $this->f_avatar_medium = ''; + } + + } // format_user + + /** + * access_name_to_level + * This takes the access name for the user and returns the level + */ + public static function access_name_to_level($level) + { + switch ($level) { + case 'admin': + return '100'; + case 'user': + return '25'; + case 'manager': + return '75'; + case 'guest': + return '5'; + default: + return '0'; + } + + } // access_name_to_level + + /** + * fix_preferences + * This is the new fix_preferences function, it does the following + * Remove Duplicates from user, add in missing + * If -1 is passed it also removes duplicates from the `preferences` + * table. + */ + public static function fix_preferences($user_id) + { + $user_id = Dba::escape($user_id); + + /* Get All Preferences for the current user */ + $sql = "SELECT * FROM `user_preference` WHERE `user`='$user_id'"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($r = Dba::fetch_assoc($db_results)) { + $pref_id = $r['preference']; + /* Check for duplicates */ + if (isset($results[$pref_id])) { + $r['value'] = Dba::escape($r['value']); + $sql = "DELETE FROM `user_preference` WHERE `user`='$user_id' AND `preference`='" . $r['preference'] . "' AND" . + " `value`='" . Dba::escape($r['value']) . "'"; + Dba::write($sql); + } // if its set + else { + $results[$pref_id] = 1; + } + } // end while + + /* If we aren't the -1 user before we continue grab the -1 users values */ + if ($user_id != '-1') { + $sql = "SELECT `user_preference`.`preference`,`user_preference`.`value` FROM `user_preference`,`preference` " . + "WHERE `user_preference`.`preference` = `preference`.`id` AND `user_preference`.`user`='-1' AND `preference`.`catagory` !='system'"; + $db_results = Dba::read($sql); + /* While through our base stuff */ + $zero_results = array(); + while ($r = Dba::fetch_assoc($db_results)) { + $key = $r['preference']; + $zero_results[$key] = $r['value']; + } + } // if not user -1 + + // get me _EVERYTHING_ + $sql = "SELECT * FROM `preference`"; + + // If not system, exclude system... *gasp* + if ($user_id != '-1') { + $sql .= " WHERE catagory !='system'"; + } + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + + $key = $r['id']; + + /* Check if this preference is set */ + if (!isset($results[$key])) { + if (isset($zero_results[$key])) { + $r['value'] = $zero_results[$key]; + } + $value = Dba::escape($r['value']); + $sql = "INSERT INTO user_preference (`user`,`preference`,`value`) VALUES ('$user_id','$key','$value')"; + Dba::write($sql); + } + } // while preferences + + /* Let's also clean out any preferences garbage left over */ + $sql = "SELECT DISTINCT(user_preference.user) FROM user_preference " . + "LEFT JOIN user ON user_preference.user = user.id " . + "WHERE user_preference.user!='-1' AND user.id IS NULL"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($r = Dba::fetch_assoc($db_results)) { + $results[] = $r['user']; + } + + foreach ($results as $data) { + $sql = "DELETE FROM user_preference WHERE user='$data'"; + Dba::write($sql); + } + + } // fix_preferences + + /** + * delete + * deletes this user and everything associated with it. This will affect + * ratings and tottal stats + */ + public function delete() + { + /* + Before we do anything make sure that they aren't the last + admin + */ + if ($this->has_access(100)) { + $sql = "SELECT `id` FROM `user` WHERE `access`='100' AND id !='" . Dba::escape($this->id) . "'"; + $db_results = Dba::read($sql); + if (!Dba::num_rows($db_results)) { + return false; + } + } // if this is an admin check for others + + // Delete their playlists + $sql = "DELETE FROM `playlist` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Clean up the playlist data table + $sql = "DELETE FROM `playlist_data` USING `playlist_data` " . + "LEFT JOIN `playlist` ON `playlist`.`id`=`playlist_data`.`playlist` " . + "WHERE `playlist`.`id` IS NULL"; + Dba::write($sql); + + // Delete any stats they have + $sql = "DELETE FROM `object_count` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Clear the IP history for this user + $sql = "DELETE FROM `ip_history` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Nuke any access lists that are specific to this user + $sql = "DELETE FROM `access_list` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Delete their ratings + $sql = "DELETE FROM `rating` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Delete their tags + $sql = "DELETE FROM `tag_map` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Clean out the tags + $sql = "DELETE FROM `tags` USING `tag_map` LEFT JOIN `tag_map` ON tag_map.id=tags.map_id AND tag_map.id IS NULL"; + Dba::write($sql); + + // Delete their preferences + $sql = "DELETE FROM `user_preference` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Delete their voted stuff in democratic play + $sql = "DELETE FROM `user_vote` WHERE `user`='$this->id'"; + Dba::write($sql); + + // Delete their shoutbox posts + $sql = "DELETE FROM `user_shout` WHERE `user='$this->id'"; + Dba::write($sql); + + // Delete the user itself + $sql = "DELETE FROM `user` WHERE `id`='$this->id'"; + Dba::write($sql); + + $sql = "DELETE FROM `session` WHERE `username`='" . Dba::escape($this->username) . "'"; + Dba::write($sql); + + return true; + + } // delete + + /** + * is_online + * delay how long since last_seen in seconds default of 20 min + * calcs difference between now and last_seen + * if less than delay, we consider them still online + */ + public function is_online( $delay = 1200 ) + { + return time() - $this->last_seen <= $delay; + + } // is_online + + /** + * get_user_validation + *if user exists before activation can be done. + */ + public static function get_validation($username) + { + $sql = "SELECT `validation` FROM `user` WHERE `username` = ?"; + $db_results = Dba::read($sql, array($username)); + + $row = Dba::fetch_assoc($db_results); + + return $row['validation']; + + } // get_validation + + /** + * get_recently_played + * This gets the recently played items for this user respecting + * the limit passed + */ + public function get_recently_played($limit,$type='') + { + if (!$type) { $type = 'song'; } + + $sql = "SELECT * FROM `object_count` WHERE `object_type`='$type' AND `user`='$this->id' " . + "ORDER BY `date` DESC LIMIT $limit"; + $db_results = Dba::read($sql); + + $results = array(); + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['object_id']; + } + + return $results; + + } // get_recently_played + + /** + * get_ip_history + * This returns the ip_history from the + * last AmpConfig::get('user_ip_cardinality') days + */ + public function get_ip_history($count='',$distinct='') + { + $username = Dba::escape($this->id); + $count = $count ? intval($count) : intval(AmpConfig::get('user_ip_cardinality')); + + // Make sure it's something + if ($count < 1) { $count = '1'; } + $limit_sql = "LIMIT " . intval($count); + + $group_sql = ""; + if ($distinct) { $group_sql = "GROUP BY `ip`"; } + + /* Select ip history */ + $sql = "SELECT `ip`,`date` FROM `ip_history`" . + " WHERE `user`='$username'" . + " $group_sql ORDER BY `date` DESC $limit_sql"; + $db_results = Dba::read($sql); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row; + } + + return $results; + + } // get_ip_history + + /** + * get_avatar + * Get the user avatar + */ + public function get_avatar() + { + $avatar = array(); + + $avatar['title'] = T_('User avatar'); + $upavatar = new Art($this->id, 'user'); + if ($upavatar->get_db()) { + $avatar['url'] = AmpConfig::get('web_path') . '/image.php?object_type=user&id=' . $this->id; + $avatar['url_mini'] = $avatar['url']; + $avatar['url_medium'] = $avatar['url']; + $avatar['url'] .= '&thumb=3'; + $avatar['url_mini'] .= '&thumb=5'; + $avatar['url_medium'] .= '&thumb=3'; + } else { + foreach (Plugin::get_plugins('get_avatar_url') as $plugin_name) { + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $avatar['url'] = $plugin->_plugin->get_avatar_url($this); + if (!empty($avatar['url'])) { + $avatar['url_mini'] = $plugin->_plugin->get_avatar_url($this, 32); + $avatar['url_medium'] = $plugin->_plugin->get_avatar_url($this, 64); + $avatar['title'] .= ' (' . $plugin->_plugin->name . ')'; + break; + } + } + } + } + + return $avatar; + } // get_avatar + + public function upload_avatar() + { + $upload = array(); + if (!empty($_FILES['avatar']['tmp_name'])) { + $path_info = pathinfo($_FILES['avatar']['name']); + $upload['file'] = $_FILES['avatar']['tmp_name']; + $upload['mime'] = 'image/' . $path_info['extension']; + $image_data = Art::get_from_source($upload, 'user'); + + if ($image_data) { + $art = new Art($this->id, 'user'); + $art->insert($image_data, $upload['0']['mime']); + } + } + } + + public function delete_avatar() + { + $art = new Art($this->id, 'user'); + $art->reset(); + } + + /** + * activate_user + * the user from public_registration + */ + public function activate_user($username) + { + $username = Dba::escape($username); + + $sql = "UPDATE `user` SET `disabled`='0' WHERE `username` = ?"; + Dba::write($sql, array($username)); + + } // activate_user + + /** + * is_xmlrpc + * checks to see if this is a valid xmlrpc user + */ + public function is_xmlrpc() + { + /* If we aren't using XML-RPC return true */ + if (!AmpConfig::get('xml_rpc')) { + return false; + } + + //FIXME: Ok really what we will do is check the MD5 of the HTTP_REFERER + //FIXME: combined with the song title to make sure that the REFERER + //FIXME: is in the access list with full rights + return true; + + } // is_xmlrpc + + /** + * check_username + * This checks to make sure the username passed doesn't already + * exist in this instance of ampache + */ + public static function check_username($username) + { + $username = Dba::escape($username); + + $sql = "SELECT `id` FROM `user` WHERE `username`='$username'"; + $db_results = Dba::read($sql); + + if (Dba::num_rows($db_results)) { + return false; + } + + return true; + + } // check_username + + /** + * rebuild_all_preferences + * This rebuilds the user preferences for all installed users, called by the plugin functions + */ + public static function rebuild_all_preferences() + { + $sql = "SELECT * FROM `user`"; + $db_results = Dba::read($sql); + + User::fix_preferences('-1'); + + while ($row = Dba::fetch_assoc($db_results)) { + User::fix_preferences($row['id']); + } + + return true; + + } // rebuild_all_preferences + +} //end user class diff --git a/sources/lib/class/userflag.class.php b/sources/lib/class/userflag.class.php new file mode 100644 index 0000000..fae0869 --- /dev/null +++ b/sources/lib/class/userflag.class.php @@ -0,0 +1,224 @@ +id = intval($id); + $this->type = $type; + + return true; + + } // Constructor + + /** + * build_cache + * This attempts to get everything we'll need for this page load in a + * single query, saving on connection overhead + */ + public static function build_cache($type, $ids, $user_id = null) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + + $userflags = array(); + + $idlist = '(' . implode(',', $ids) . ')'; + $sql = "SELECT `object_id` FROM `user_flag` " . + "WHERE `user` = ? AND `object_id` IN $idlist " . + "AND `object_type` = ?"; + $db_results = Dba::read($sql, array($user_id, $type)); + + while ($row = Dba::fetch_assoc($db_results)) { + $userflags[$row['object_id']] = true; + } + + foreach ($ids as $id) { + if (!isset($userflags[$id])) { + $userflag = 0; + } else { + $userflag = intval($userflags[$id]); + } + parent::add_to_cache('userflag_' . $type . '_user' . $user_id, $id, $userflag); + } + + return true; + + } // build_cache + + /** + * gc + * + * Remove userflag for items that no longer exist. + */ + public static function gc() + { + foreach (array('song', 'album', 'artist', 'video') as $object_type) { + Dba::write("DELETE FROM `user_flag` USING `user_flag` LEFT JOIN `$object_type` ON `$object_type`.`id` = `user_flag`.`object_id` WHERE `object_type` = '$object_type' AND `$object_type`.`id` IS NULL"); + } + } + + public function get_flag($user_id = null) + { + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + + $key = 'userflag_' . $this->type . '_user' . $user_id; + if (parent::is_cached($key, $this->id)) { + return parent::get_from_cache($key, $this->id); + } + + $sql = "SELECT `id` FROM `user_flag` WHERE `user` = ? ". + "AND `object_id` = ? AND `object_type` = ?"; + $db_results = Dba::read($sql, array($user_id, $this->id, $this->type)); + + $flagged = false; + if (Dba::fetch_assoc($db_results)) { + $flagged = true; + } + + parent::add_to_cache($key, $this->id, $flagged); + return $flagged; + + } + + /** + * set_flag + * This function sets the user flag for the current object. + * If no userid is passed in, we use the currently logged in user. + */ + public function set_flag($flagged, $user_id = null) + { + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + $user_id = intval($user_id); + + debug_event('Userflag', "Setting userflag for $this->type $this->id to $flagged", 5); + + if (!$flagged) { + $sql = "DELETE FROM `user_flag` WHERE " . + "`object_id` = ? AND " . + "`object_type` = ? AND " . + "`user` = ?"; + $params = array($this->id, $this->type, $user_id); + } else { + $sql = "REPLACE INTO `user_flag` " . + "(`object_id`, `object_type`, `user`, `date`) " . + "VALUES (?, ?, ?, ?)"; + $params = array($this->id, $this->type, $user_id, time()); + } + Dba::write($sql, $params); + + parent::add_to_cache('userflag_' . $this->type . '_user' . $user_id, $this->id, $flagged); + + return true; + + } // set_flag + + /** + * get_latest_sql + * Get the latest sql + */ + public static function get_latest_sql($type, $user_id=null) + { + if (is_null($user_id)) { + $user_id = $GLOBALS['user']->id; + } + $user_id = intval($user_id); + $type = Stats::validate_type($type); + + $sql = "SELECT `object_id` as `id` FROM user_flag" . + " WHERE object_type = '" . $type . "' AND `user` = '" . $user_id . "'"; + if (AmpConfig::get('catalog_disable')) { + $sql .= " AND " . Catalog::get_enable_filter($type, '`object_id`'); + } + $sql .= " ORDER BY `date` DESC "; + return $sql; + } + /** + * get_latest + * Get the latest user flagged objects + */ + public static function get_latest($type, $user_id=null, $count='', $offset='') + { + if (!$count) { + $count = AmpConfig::get('popular_threshold'); + } + $count = intval($count); + if (!$offset) { + $limit = $count; + } else { + $limit = intval($offset) . "," . $count; + } + + /* Select Top objects counting by # of rows */ + $sql = self::get_latest_sql($type, $user_id); + $sql .= "LIMIT $limit"; + $db_results = Dba::read($sql, array($type, $user_id)); + + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + + } // get_latest + + /** + * show + * This takes an id and a type and displays the flag state + * enabled. + */ + public static function show($object_id, $type) + { + // If user flags aren't enabled don't do anything + if (!AmpConfig::get('userflags')) { return false; } + + $userflag = new Userflag($object_id, $type); + require AmpConfig::get('prefix') . '/templates/show_object_userflag.inc.php'; + + } // show + +} //end rating class diff --git a/sources/lib/class/vainfo.class.php b/sources/lib/class/vainfo.class.php new file mode 100644 index 0000000..c5c5ab3 --- /dev/null +++ b/sources/lib/class/vainfo.class.php @@ -0,0 +1,865 @@ +islocal = $islocal; + $this->filename = $file; + $this->encoding = $encoding ?: AmpConfig::get('site_charset'); + + /* These are needed for the filename mojo */ + $this->_file_pattern = $file_pattern; + $this->_dir_pattern = $dir_pattern; + + // FIXME: This looks ugly and probably wrong + if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { + $this->_pathinfo = str_replace('%3A', ':', urlencode($this->filename)); + $this->_pathinfo = pathinfo(str_replace('%5C', '\\', $this->_pathinfo)); + } else { + $this->_pathinfo = pathinfo(str_replace('%2F', '/', urlencode($this->filename))); + } + $this->_pathinfo['extension'] = strtolower($this->_pathinfo['extension']); + + if ($this->islocal) { + // Initialize getID3 engine + $this->_getID3 = new getID3(); + + $this->_getID3->option_md5_data = false; + $this->_getID3->option_md5_data_source = false; + $this->_getID3->option_tags_html = false; + $this->_getID3->option_extra_info = true; + $this->_getID3->option_tag_lyrics3 = true; + $this->_getID3->option_tags_process = true; + $this->_getID3->encoding = $this->encoding; + + // get id3tag encoding (try to work around off-spec id3v1 tags) + try { + $this->_raw = $this->_getID3->analyze(Core::conv_lc_file($file)); + } catch (Exception $error) { + debug_event('getID3', "Broken file detected: $file: " . $error->getMessage(), 1); + $this->_broken = true; + return false; + } + + if (AmpConfig::get('mb_detect_order')) { + $mb_order = AmpConfig::get('mb_detect_order'); + } elseif (function_exists('mb_detect_order')) { + $mb_order = implode(", ", mb_detect_order()); + } else { + $mb_order = "auto"; + } + + $test_tags = array('artist', 'album', 'genre', 'title'); + + if ($encoding_id3v1) { + $this->encoding_id3v1 = $encoding_id3v1; + } else { + $tags = array(); + foreach ($test_tags as $tag) { + if ($value = $this->_raw['id3v1'][$tag]) { + $tags[$tag] = $value; + } + } + + $this->encoding_id3v1 = self::_detect_encoding($tags, $mb_order); + } + + if (AmpConfig::get('getid3_detect_id3v2_encoding')) { + // The user has told us to be moronic, so let's do that thing + $tags = array(); + foreach ($test_tags as $tag) { + if ($value = $this->_raw['id3v2']['comments'][$tag]) { + $tags[$tag] = $value; + } + } + + $this->encoding_id3v2 = self::_detect_encoding($tags, $mb_order); + $this->_getID3->encoding_id3v2 = $this->encoding_id3v2; + } + + $this->_getID3->encoding_id3v1 = $this->encoding_id3v1; + } + } + + public function forceSize($size) + { + $this->_forcedSize = $size; + } + + /** + * _detect_encoding + * + * Takes an array of tags and attempts to automatically detect their + * encoding. + */ + private static function _detect_encoding($tags, $mb_order) + { + if (function_exists('mb_detect_encoding')) { + $encodings = array(); + if (is_array($tags)) { + foreach ($tags as $tag) { + $encodings[mb_detect_encoding($tag, $mb_order, true)]++; + } + } + + debug_event('vainfo', 'encoding detection: ' . json_encode($encodings), 5); + $high = 0; + $encoding = ''; + foreach ($encodings as $key => $value) { + if ($value > $high) { + $encoding = $key; + $high = $value; + } + } + + if ($encoding != 'ASCII' && $encoding != '0') { + return $encoding; + } else { + return 'ISO-8859-1'; + } + } + return 'ISO-8859-1'; + } + + + /** + * get_info + * + * This function runs the various steps to gathering the metadata + */ + public function get_info() + { + // If this is broken, don't waste time figuring it out a second + // time, just return their rotting carcass of a media file. + if ($this->_broken) { + $this->tags = $this->set_broken(); + return true; + } + + if ($this->islocal) { + try { + $this->_raw = $this->_getID3->analyze(Core::conv_lc_file($this->filename)); + } catch (Exception $error) { + debug_event('getID2', 'Unable to catalog file: ' . $error->getMessage(), 1); + } + } + + /* Figure out what type of file we are dealing with */ + $this->type = $this->_get_type(); + + $enabled_sources = (array) AmpConfig::get('metadata_order'); + + if (in_array('filename', $enabled_sources)) { + $this->tags['filename'] = $this->_parse_filename($this->filename); + } + + if (in_array('getID3', $enabled_sources) && $this->islocal) { + $this->tags['getID3'] = $this->_get_tags(); + } + + $this->_get_plugin_tags(); + + } // get_info + + /** + * get_tag_type + * + * This takes the result set and the tag_order defined in your config + * file and tries to figure out which tag type(s) it should use. If your + * tag_order doesn't match anything then it throws up its hands and uses + * everything in random order. + */ + public static function get_tag_type($results, $config_key = 'metadata_order') + { + $order = (array) AmpConfig::get($config_key); + + // Iterate through the defined key order adding them to an ordered array. + $returned_keys = array(); + foreach ($order as $key) { + if ($results[$key]) { + $returned_keys[] = $key; + } + } + + // If we didn't find anything then default to everything. + if (!isset($returned_keys)) { + $returned_keys = array_keys($results); + $returned_keys = sort($returned_keys); + } + + // Unless they explicitly set it, add bitrate/mode/mime/etc. + if (is_array($returned_keys)) { + if (!in_array('general', $returned_keys)) { + $returned_keys[] = 'general'; + } + } + + return $returned_keys; + } + + /** + * clean_tag_info + * + * This function takes the array from vainfo along with the + * key we've decided on and the filename and returns it in a + * sanitized format that ampache can actually use + */ + public static function clean_tag_info($results, $keys, $filename = null) + { + $info = array(); + + $info['file'] = $filename; + + // Iteration! + foreach ($keys as $key) { + $tags = $results[$key]; + + $info['file'] = $info['file'] ?: $tags['file']; + $info['bitrate'] = $info['bitrate'] ?: intval($tags['bitrate']); + $info['rate'] = $info['rate'] ?: intval($tags['rate']); + $info['mode'] = $info['mode'] ?: $tags['mode']; + $info['size'] = $info['size'] ?: $tags['size']; + $info['mime'] = $info['mime'] ?: $tags['mime']; + $info['encoding'] = $info['encoding'] ?: $tags['encoding']; + $info['rating'] = $info['rating'] ?: $tags['rating']; + $info['time'] = $info['time'] ?: intval($tags['time']); + $info['channels'] = $info['channels'] ?: $tags['channels']; + + $info['title'] = $info['title'] ?: stripslashes(trim($tags['title'])); + + $info['year'] = $info['year'] ?: intval($tags['year']); + + $info['disk'] = $info['disk'] ?: intval($tags['disk']); + + $info['totaldisks'] = $info['totaldisks'] ?: intval($tags['totaldisks']); + + $info['artist'] = $info['artist'] ?: trim($tags['artist']); + + $info['album'] = $info['album'] ?: trim($tags['album']); + + // multiple genre support + if ((!$info['genre']) && $tags['genre']) { + if (!is_array($tags['genre'])) { + // not all tag formats will return an array, but we need one + $info['genre'][] = trim($tags['genre']); + } else { + // if we trim the array we lose everything after 1st entry + foreach ($tags['genre'] as $genre) { + $info['genre'][] = trim($genre); + } + } + } + + $info['mb_trackid'] = $info['mb_trackid'] ?: trim($tags['mb_trackid']); + $info['mb_albumid'] = $info['mb_albumid'] ?: trim($tags['mb_albumid']); + $info['mb_artistid'] = $info['mb_artistid'] ?: trim($tags['mb_artistid']); + + $info['language'] = $info['language'] ?: trim($tags['language']); + + $info['lyrics'] = $info['lyrics'] + ?: str_replace( + array("\r\n","\r","\n"), + '
', + strip_tags($tags['lyrics'])); + + $info['track'] = $info['track'] ?: intval($tags['track']); + $info['resolution_x'] = $info['resolution_x'] ?: intval($tags['resolution_x']); + $info['resolution_y'] = $info['resolution_y'] ?: intval($tags['resolution_y']); + $info['audio_codec'] = $info['audio_codec'] ?: trim($tags['audio_codec']); + $info['video_codec'] = $info['video_codec'] ?: trim($tags['video_codec']); + } + + // Some things set the disk number even though there aren't multiple + if ($info['totaldisks'] == 1 && $info['disk'] == 1) { + unset($info['disk']); + unset($info['totaldisks']); + } + + return $info; + } + + /** + * _get_type + * + * This function takes the raw information and figures out what type of + * file we are dealing with. + */ + private function _get_type() + { + // There are a few places that the file type can come from, in the end + // we trust the encoding type. + if ($type = $this->_raw['video']['dataformat']) { + return $this->_clean_type($type); + } + if ($type = $this->_raw['audio']['streams']['0']['dataformat']) { + return $this->_clean_type($type); + } + if ($type = $this->_raw['audio']['dataformat']) { + return $this->_clean_type($type); + } + if ($type = $this->_raw['fileformat']) { + return $this->_clean_type($type); + } + + return false; + } + + + /** + * _get_tags + * + * This processes the raw getID3 output and bakes it. + */ + private function _get_tags() + { + $results = array(); + + // The tags can come in many different shapes and colors + // depending on the encoding time of day and phase of the moon. + + if (is_array($this->_raw['tags'])) { + foreach ($this->_raw['tags'] as $key => $tag_array) { + switch ($key) { + case 'ape': + case 'avi': + case 'flv': + case 'matroska': + debug_event('vainfo', 'Cleaning ' . $key, 5); + $parsed = $this->_cleanup_generic($tag_array); + break; + case 'vorbiscomment': + debug_event('vainfo', 'Cleaning vorbis', 5); + $parsed = $this->_cleanup_vorbiscomment($tag_array); + break; + case 'id3v1': + debug_event('vainfo', 'Cleaning id3v1', 5); + $parsed = $this->_cleanup_id3v1($tag_array); + break; + case 'id3v2': + debug_event('vainfo', 'Cleaning id3v2', 5); + $parsed = $this->_cleanup_id3v2($tag_array); + break; + case 'quicktime': + debug_event('vainfo', 'Cleaning quicktime', 5); + $parsed = $this->_cleanup_quicktime($tag_array); + break; + case 'riff': + debug_event('vainfo', 'Cleaning riff', 5); + $parsed = $this->_cleanup_riff($tag_array); + break; + case 'mpg': + case 'mpeg': + $key = 'mpeg'; + debug_event('vainfo', 'Cleaning MPEG', 5); + $parsed = $this->_cleanup_generic($tag_array); + break; + case 'asf': + case 'wmv': + $key = 'asf'; + debug_event('vainfo', 'Cleaning WMV/WMA/ASF', 5); + $parsed = $this->_cleanup_generic($tag_array); + break; + case 'lyrics3': + debug_event('vainfo', 'Cleaning lyrics3', 5); + $parsed = $this->_cleanup_lyrics($tag_array); + break; + default: + debug_event('vainfo', 'Cleaning unrecognised tag type ' . $key . ' for file ' . $this->filename, 5); + $parsed = $this->_cleanup_generic($tag_array); + break; + } + + $results[$key] = $parsed; + } + } + + $results['general'] = $this->_parse_general($this->_raw); + + $cleaned = self::clean_tag_info($results, self::get_tag_type($results, 'getid3_tag_order'), $this->filename); + $cleaned['raw'] = $results; + + return $cleaned; + } + + /** + * _get_plugin_tags + * + * Get additional metadata from plugins + */ + private function _get_plugin_tags() + { + $tag_order = AmpConfig::get('metadata_order'); + if (!is_array($tag_order)) { + $tag_order = array($tag_order); + } + + $plugin_names = Plugin::get_plugins('get_metadata'); + foreach ($tag_order as $tag_source) { + if (in_array($tag_source, $plugin_names)) { + $plugin = new Plugin($tag_source); + if ($plugin->load($GLOBALS['user'])) { + $this->tags[$tag_source] = $plugin->_plugin->get_metadata(self::clean_tag_info($this->tags, self::get_tag_type($this->tags), $this->filename)); + } + } + } + } + + /** + * _parse_general + * + * Gather and return the general information about a file (vbr/cbr, + * sample rate, channels, etc.) + */ + private function _parse_general($tags) + { + $parsed = array(); + + $parsed['title'] = urldecode($this->_pathinfo['filename']); + $parsed['mode'] = $tags['audio']['bitrate_mode']; + if ($parsed['mode'] == 'con') { + $parsed['mode'] = 'cbr'; + } + $parsed['bitrate'] = $tags['audio']['bitrate']; + $parsed['channels'] = intval($tags['audio']['channels']); + $parsed['rate'] = intval($tags['audio']['sample_rate']); + $parsed['size'] = $this->_forcedSize ?: intval($tags['filesize']); + $parsed['encoding'] = $tags['encoding']; + $parsed['mime'] = $tags['mime_type']; + $parsed['time'] = ($this->_forcedSize ? ((($this->_forcedSize - $tags['avdataoffset']) * 8) / $tags['bitrate']) : $tags['playtime_seconds']); + $parsed['video_codec'] = $tags['video']['fourcc']; + $parsed['audio_codec'] = $tags['audio']['dataformat']; + $parsed['resolution_x'] = $tags['video']['resolution_x']; + $parsed['resolution_y'] = $tags['video']['resolution_y']; + + return $parsed; + } + + /** + * _clean_type + * This standardizes the type that we are given into a recognized type. + */ + private function _clean_type($type) + { + switch ($type) { + case 'mp3': + case 'mp2': + case 'mpeg3': + return 'mp3'; + case 'vorbis': + return 'ogg'; + case 'flac': + case 'flv': + case 'mpg': + case 'mpeg': + case 'asf': + case 'wmv': + case 'avi': + case 'quicktime': + return $type; + default: + /* Log the fact that we couldn't figure it out */ + debug_event('vainfo','Unable to determine file type from ' . $type . ' on file ' . $this->filename,'5'); + return $type; + } + } + + /** + * _cleanup_generic + * + * This does generic cleanup. + */ + private function _cleanup_generic($tags) + { + $parsed = array(); + foreach ($tags as $tagname => $data) { + switch ($tagname) { + case 'genre': + // Pass the array through + $parsed[$tagname] = $data; + break; + default: + $parsed[$tagname] = $data[0]; + break; + } + } + + return $parsed; + } + + /** + * _cleanup_lyrics + * + * This is supposed to handle lyrics3. FIXME: does it? + */ + private function _cleanup_lyrics($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + if ($tag == 'unsynchedlyrics' || $tag == 'unsynchronised lyric') { + $tag = 'lyrics'; + } + $parsed[$tag] = $data[0]; + } + return $parsed; + } + + /** + * _cleanup_vorbiscomment + * + * Standardises tag names from vorbis. + */ + private function _cleanup_vorbiscomment($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + switch ($tag) { + case 'genre': + // Pass the array through + $parsed[$tag] = $data; + break; + case 'tracknumber': + $parsed['track'] = $data[0]; + break; + case 'discnumber': + $elements = explode('/', $data[0]); + $parsed['disk'] = $elements[0]; + $parsed['totaldisks'] = $elements[1]; + break; + case 'date': + $parsed['year'] = $data[0]; + break; + default: + $parsed[$tag] = $data[0]; + break; + } + } + + return $parsed; + } + + /** + * _cleanup_id3v1 + * + * Doesn't do much. + */ + private function _cleanup_id3v1($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + // This is our baseline for naming so everything's already right, + // we just need to shuffle off the array. + $parsed[$tag] = $data[0]; + } + + return $parsed; + } + + /** + * _cleanup_id3v2 + * + * Whee, v2! + */ + private function _cleanup_id3v2($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + + switch ($tag) { + case 'genre': + // Pass the array through + $parsed['genre'] = $data; + break; + case 'part_of_a_set': + $elements = explode('/', $data[0]); + $parsed['disk'] = $elements[0]; + $parsed['totaldisks'] = $elements[1]; + break; + case 'track_number': + $parsed['track'] = $data[0]; + break; + case 'comments': + $parsed['comment'] = $data[0]; + break; + default: + $parsed[$tag] = $data[0]; + break; + } + } + + // getID3 doesn't do all the parsing we need, so grab the raw data + $id3v2 = $this->_raw['id3v2']; + + if (!empty($id3v2['UFID'])) { + // Find the MBID for the track + foreach ($id3v2['UFID'] as $ufid) { + if ($ufid['ownerid'] == 'http://musicbrainz.org') { + $parsed['mb_trackid'] = $ufid['data']; + } + } + + if (!empty($id3v2['TXXX'])) { + // Find the MBIDs for the album and artist + foreach ($id3v2['TXXX'] as $txxx) { + switch ($txxx['description']) { + case 'MusicBrainz Album Id': + $parsed['mb_albumid'] = $txxx['data']; + break; + case 'MusicBrainz Artist Id': + $parsed['mb_artistid'] = $txxx['data']; + break; + } + } + } + } + + // Find all genre + if (!empty($id3v2['TCON'])) { + // Find the MBID for the track + foreach ($id3v2['TCON'] as $tcid) { + if ($tcid['framenameshort'] == "genre") { + // Removing unwanted UTF-8 charaters + $tcid['data'] = str_replace("\xFF", "", $tcid['data']); + $tcid['data'] = str_replace("\xFE", "", $tcid['data']); + + if (!empty($tcid['data'])) { + // Parsing string with the null character + $genres = explode("\0", $tcid['data']); + $parsed_genres = array(); + foreach ($genres as $g) { + if (strlen($g) > 2) { // Only allow tags with at least 3 characters + $parsed_genres[] = $g; + } + } + + if (count($parsed_genres)) { + $parsed['genre'] = $parsed_genres; + } + } + } + break; + } + } + + // Find the rating + if (is_array($id3v2['POPM'])) { + foreach ($id3v2['POPM'] as $popm) { + if (array_key_exists('email', $popm) && + $user = User::get_from_email($popm['email'])) { + if ($user) { + // Ratings are out of 255; scale it + $parsed['rating'][$user->id] = $popm['rating'] / 255 * 5; + } + } + } + } + + return $parsed; + } + + /** + * _cleanup_riff + */ + private function _cleanup_riff($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + switch ($tag) { + case 'product': + $parsed['album'] = $data[0]; + break; + default: + $parsed[$tag] = $data[0]; + break; + } + } + + return $parsed; + } + + /** + * _cleanup_quicktime + */ + private function _cleanup_quicktime($tags) + { + $parsed = array(); + + foreach ($tags as $tag => $data) { + switch ($tag) { + case 'creation_date': + if (strlen($data['0']) > 4) { + // Weird date format, attempt to normalize it + $data[0] = date('Y', strtotime($data[0])); + } + $parsed['year'] = $data[0]; + break; + case 'MusicBrainz Track Id': + $parsed['mb_trackid'] = $data[0]; + break; + case 'MusicBrainz Album Id': + $parsed['mb_albumid'] = $data[0]; + break; + case 'MusicBrainz Artist Id': + $parsed['mb_artistid'] = $data[0]; + break; + default: + $parsed[$tag] = $data[0]; + break; + } + } + + return $parsed; + } + + /** + * _parse_filename + * + * This function uses the file and directory patterns to pull out extra tag + * information. + */ + private function _parse_filename($filename) + { + $origin = $filename; + $results = array(); + + // Correctly detect the slash we need to use here + if (strpos($filename, '/') !== false) { + $slash_type = '/'; + $slash_type_preg = $slash_type; + } else { + $slash_type = '\\'; + $slash_type_preg = $slash_type . $slash_type; + } + + // Combine the patterns + $pattern = preg_quote($this->_dir_pattern) . $slash_type_preg . preg_quote($this->_file_pattern); + + // Remove first left directories from filename to match pattern + $cntslash = substr_count($pattern, $slash_type) + 1; + $filepart = explode($slash_type, $filename); + if (count($filepart) > $cntslash) { + $filename = implode($slash_type, array_slice($filepart, count($filepart) - $cntslash)); + } + + // Pull out the pattern codes into an array + preg_match_all('/\%\w/', $pattern, $elements); + + // Mangle the pattern by turning the codes into regex captures + $pattern = preg_replace('/\%[Ty]/', '([0-9]+?)', $pattern); + $pattern = preg_replace('/\%\w/', '(.+?)', $pattern); + $pattern = str_replace('/', '\/', $pattern); + $pattern = str_replace(' ', '\s', $pattern); + $pattern = '/' . $pattern . '\..+$/'; + + // Pull out our actual matches + preg_match($pattern, $filename, $matches); + + if ($matches != null) { + // The first element is the full match text + $matched = array_shift($matches); + debug_event('vainfo', $pattern . ' matched ' . $matched . ' on ' . $filename, 5); + + // Iterate over what we found + foreach ($matches as $key => $value) { + $new_key = translate_pattern_code($elements['0'][$key]); + if ($new_key) { + $results[$new_key] = $value; + } + } + + $results['title'] = $results['title'] ?: basename($filename); + if ($this->islocal) { + $results['size'] = filesize(Core::conv_lc_file($origin)); + } + } + + return $results; + } + + /** + * set_broken + * + * This fills all tag types with Unknown (Broken) + * + * @return array Return broken title, album, artist + */ + public function set_broken() + { + /* Pull In the config option */ + $order = AmpConfig::get('tag_order'); + + if (!is_array($order)) { + $order = array($order); + } + + $key = array_shift($order); + + $broken = array(); + $broken[$key] = array(); + $broken[$key]['title'] = '**BROKEN** ' . $this->filename; + $broken[$key]['album'] = 'Unknown (Broken)'; + $broken[$key]['artist'] = 'Unknown (Broken)'; + + return $broken; + + } // set_broken + +} // end class vainfo diff --git a/sources/lib/class/video.class.php b/sources/lib/class/video.class.php new file mode 100644 index 0000000..55f10fe --- /dev/null +++ b/sources/lib/class/video.class.php @@ -0,0 +1,132 @@ +get_info($id); + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + + } // Constructor + + /** + * build_cache + * Build a cache based on the array of ids passed, saves lots of little queries + */ + public static function build_cache($ids=array()) + { + if (!is_array($ids) OR !count($ids)) { return false; } + + $idlist = '(' . implode(',',$ids) . ')'; + + $sql = "SELECT * FROM `video` WHERE `video`.`id` IN $idlist"; + $db_results = Dba::read($sql); + + while ($row = Dba::fetch_assoc($db_results)) { + parent::add_to_cache('video',$row['id'],$row); + } + + } // build_cache + + /** + * format + * This formats a video object so that it is human readable + */ + public function format() + { + $this->f_title = scrub_out($this->title); + $this->f_link = scrub_out($this->title); + $this->f_codec = $this->video_codec . ' / ' . $this->audio_codec; + $this->f_resolution = $this->resolution_x . 'x' . $this->resolution_y; + $this->f_tags = ''; + $this->f_length = floor($this->time/60) . ' ' . T_('minutes'); + + } // format + + public function get_stream_types() + { + return array('native'); + + } // native_stream + + /** + * play_url + * This returns a "PLAY" url for the video in question here, this currently feels a little + * like a hack, might need to adjust it in the future + */ + public static function play_url($oid, $additional_params='',$sid='',$force_http='') + { + $video = new Video($oid); + + if (!$video->id) { return false; } + + $uid = intval($GLOBALS['user']->id); + $oid = intval($video->id); + + $url = Stream::get_base_url() . "type=video&uid=" . $uid . "&oid=" . $oid; + + return Stream_URL::format($url . $additional_params); + + } // play_url + + /** + * get_transcode_settings + * + * FIXME: Video transcoding is not implemented + */ + public function get_transcode_settings($target = null) + { + return false; + } + +} // end Video class diff --git a/sources/lib/class/wanted.class.php b/sources/lib/class/wanted.class.php new file mode 100644 index 0000000..d18520d --- /dev/null +++ b/sources/lib/class/wanted.class.php @@ -0,0 +1,425 @@ +get_info($id); + + // Foreach what we've got + foreach ($info as $key=>$value) { + $this->$key = $value; + } + + return true; + } //constructor + + /** + * get_missing_albums + * Get list of library's missing albums from MusicBrainz + */ + public static function get_missing_albums($artist, $mbid='') + { + $mb = new MusicBrainz(new RequestsMbClient()); + $includes = array( + 'release-groups' + ); + $types = explode(',', AmpConfig::get('wanted_types')); + + try { + $martist = $mb->lookup('artist', $artist ? $artist->mbid : $mbid, $includes); + } catch (Exception $e) { + return null; + } + + $owngroups = array(); + $wartist = array(); + if ($artist) { + $albums = $artist->get_albums(); + foreach ($albums as $id) { + $album = new Album($id); + if ($album->mbid) { + $malbum = $mb->lookup('release', $album->mbid, array('release-groups')); + if ($malbum->{'release-group'}) { + if (!in_array($malbum->{'release-group'}->id, $owngroups)) { + $owngroups[] = $malbum->{'release-group'}->id; + } + } + } + } + } else { + $wartist['mbid'] = $mbid; + $wartist['name'] = $martist->name; + parent::add_to_cache('missing_artist', $mbid, $wartist); + $wartist = self::get_missing_artist($mbid); + } + + $results = array(); + foreach ($martist->{'release-groups'} as $group) { + if (in_array(strtolower($group->{'primary-type'}), $types)) { + $add = true; + + for ($i = 0; $i < count($group->{'secondary-types'}) && $add; ++$i) { + $add = in_array(strtolower($group->{'secondary-types'}[$i]), $types); + } + + if ($add) { + if (!in_array($group->id, $owngroups)) { + $wantedid = self::get_wanted($group->id); + $wanted = new Wanted($wantedid); + if ($wanted->id) { + $wanted->format(); + } else { + $wanted->mbid = $group->id; + if ($artist) { + $wanted->artist = $artist->id; + } else { + $wanted->artist_mbid = $mbid; + } + $wanted->name = $group->title; + if (!empty($group->{'first-release-date'})) { + if (strlen($group->{'first-release-date'}) == 4) { + $wanted->year = $group->{'first-release-date'}; + } else { + $wanted->year = date("Y", strtotime($group->{'first-release-date'})); + } + } + $wanted->accepted = false; + $wanted->f_name_link = "id; + if ($artist) { + $wanted->f_name_link .= "&artist=" . $wanted->artist; + } else { + $wanted->f_name_link .= "&artist_mbid=" . $mbid; + } + $wanted->f_name_link .= "\" title=\"" . $wanted->name . "\">" . $wanted->name . ""; + $wanted->f_artist_link = $artist ? $artist->f_name_link : $wartist['link']; + $wanted->f_user = $GLOBALS['user']->fullname; + } + $results[] = $wanted; + } + } + } + } + + return $results; + } // get_missing_albums + + public static function get_missing_artist($mbid) + { + $wartist = array(); + + if (parent::is_cached('missing_artist', $mbid) ) { + $wartist = parent::get_from_cache('missing_artist', $mbid); + } else { + $mb = new MusicBrainz(new RequestsMbClient()); + $wartist['mbid'] = $mbid; + $wartist['name'] = T_('Unknown Artist'); + + try { + $martist = $mb->lookup('artist', $mbid); + } catch (Exception $e) { + return $wartist; + } + + $wartist['name'] = $martist->name; + parent::add_to_cache('missing_artist', $mbid, $wartist); + } + + $wartist['link'] = "" . $wartist['name'] . ""; + + return $wartist; + } + + public static function get_accepted_wanted_count() + { + $sql = "SELECT COUNT(`id`) AS `wanted_cnt` FROM `wanted` WHERE `accepted` = 1"; + $db_results = Dba::read($sql); + if ($row = Dba::fetch_assoc($db_results)) { + return $row['wanted_cnt']; + } + + return 0; + } + + public static function get_wanted($mbid) + { + $sql = "SELECT `id` FROM `wanted` WHERE `mbid` = ?"; + $db_results = Dba::read($sql, array($mbid)); + if ($row = Dba::fetch_assoc($db_results)) { + return $row['id']; + } + + return false; + } + + public static function delete_wanted($mbid) + { + $sql = "DELETE FROM `wanted` WHERE `mbid` = ?"; + $params = array( $mbid ); + if (!$GLOBALS['user']->has_access('75')) { + $sql .= " AND `user` = ?"; + $params[] = $GLOBALS['user']->id; + } + + Dba::write($sql, $params); + } + + public static function delete_wanted_release($mbid) + { + if (self::get_accepted_wanted_count() > 0) { + $mb = new MusicBrainz(new RequestsMbClient()); + $malbum = $mb->lookup('release', $mbid, array('release-groups')); + if ($malbum->{'release-group'}) { + self::delete_wanted($malbum->{'release-group'}->id); + } + } + } + + public static function delete_wanted_by_name($artist, $album_name, $year) + { + $sql = "DELETE FROM `wanted` WHERE `artist` = ? AND `name` = ? AND `year` = ?"; + $params = array( $artist, $album_name, $year ); + if (!$GLOBALS['user']->has_access('75')) { + $sql .= " AND `user` = ?"; + $params[] = $GLOBALS['user']->id; + } + + Dba::write($sql, $params); + } + + public function accept() + { + if ($GLOBALS['user']->has_access('75')) { + $sql = "UPDATE `wanted` SET `accepted` = '1' WHERE `mbid` = ?"; + Dba::write($sql, array( $this->mbid )); + $this->accepted = 1; + + foreach (Plugin::get_plugins('process_wanted') as $plugin_name) { + debug_event('wanted', 'Using Wanted Process plugin: ' . $plugin_name, '5'); + $plugin = new Plugin($plugin_name); + if ($plugin->load($GLOBALS['user'])) { + $plugin->_plugin->process_wanted($this); + } + } + } + } + + public static function has_wanted($mbid, $userid = 0) + { + if ($userid == 0) { + $userid = $GLOBALS['user']->id; + } + + $sql = "SELECT `id` FROM `wanted` WHERE `mbid` = ? AND `user` = ?"; + $db_results = Dba::read($sql, array($mbid, $userid)); + + if ($row = Dba::fetch_assoc($db_results)) { + return $row['id']; + } + + return false; + + } + + public static function add_wanted($mbid, $artist, $artist_mbid, $name, $year) + { + $sql = "INSERT INTO `wanted` (`user`, `artist`, `artist_mbid`, `mbid`, `name`, `year`, `date`, `accepted`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + $accept = $GLOBALS['user']->has_access('75') ? true : AmpConfig::get('wanted_auto_accept'); + $params = array($GLOBALS['user']->id, $artist, $artist_mbid, $mbid, $name, $year, time(), '0'); + Dba::write($sql, $params); + + if ($accept) { + $wantedid = Dba::insert_id(); + $wanted = new Wanted($wantedid); + $wanted->accept(); + } + } + + public function show_action_buttons() + { + if ($this->id) { + if (!$this->accepted) { + if ($GLOBALS['user']->has_access('75')) { + echo Ajax::button('?page=index&action=accept_wanted&mbid=' . $this->mbid,'enable', T_('Accept'),'wanted_accept_' . $this->mbid); + } + } + if ($GLOBALS['user']->has_access('75') || (Wanted::has_wanted($this->mbid) && $this->accepted != '1')) { + echo " " . Ajax::button('?page=index&action=remove_wanted&mbid=' . $this->mbid,'disable', T_('Remove'),'wanted_remove_' . $this->mbid); + } + } else { + echo Ajax::button('?page=index&action=add_wanted&mbid=' . $this->mbid . ($this->artist ? '&artist=' . $this->artist : '&artist_mbid=' . $this->artist_mbid) . '&name=' . urlencode($this->name) . '&year=' . $this->year,'add_wanted', T_('Add to wanted list'),'wanted_add_' . $this->mbid); + } + } + + public function load_all($track_details = true) + { + $mb = new MusicBrainz(new RequestsMbClient()); + $this->songs = array(); + + try { + $group = $mb->lookup('release-group', $this->mbid, array( 'releases' )); + // Set fresh data + $this->name = $group->title; + $this->year = date("Y", strtotime($group->{'first-release-date'})); + + // Load from database if already cached + $this->songs = Song_preview::get_song_previews($this->mbid); + + if (count($group->releases) > 0) { + $this->release_mbid = $group->releases[0]->id; + if ($track_details && count($this->songs) == 0) { + // Use the first release as reference for track content + $release = $mb->lookup('release', $this->release_mbid, array( 'recordings' )); + foreach ($release->media as $media) { + foreach ($media->tracks as $track) { + $song = array(); + $song['disk'] = $media->position; + $song['track'] = $track->number; + $song['title'] = $track->title; + $song['mbid'] = $track->id; + if ($this->artist) { + $song['artist'] = $this->artist; + } + $song['artist_mbid'] = $this->artist_mbid; + $song['session'] = session_id(); + $song['album_mbid'] = $this->mbid; + if (AmpConfig::get('echonest_api_key')) { + $echonest = new EchoNest_Client(new EchoNest_HttpClient_Requests()); + $echonest->authenticate(AmpConfig::get('echonest_api_key')); + $enSong = null; + try { + $enProfile = $echonest->getTrackApi()->profile('musicbrainz:track:' . $track->id); + $enSong = $echonest->getSongApi()->profile($enProfile['song_id'], array( 'id:7digital-US', 'audio_summary', 'tracks')); + } catch (Exception $e) { + debug_event('echonest', 'EchoNest track error on `' . $track->id . '` (' . $track->title . '): ' . $e->getMessage(), '1'); + } + + // Wans't able to get the song with MusicBrainz ID, try a search + if ($enSong == null) { + if ($this->artist) { + $artist = new Artist($this->artist); + $artist_name = $artist->name; + } else { + $wartist = Wanted::get_missing_artist($this->artist_mbid); + $artist_name = $wartist['name']; + } + try { + $enSong = $echonest->getSongApi()->search(array( + 'results' => '1', + 'artist' => $artist_name, + 'title' => $track->title, + 'bucket' => array( 'id:7digital-US', 'audio_summary', 'tracks'), + )); + + + } catch (Exception $e) { + debug_event('echonest', 'EchoNest song search error: ' . $e->getMessage(), '1'); + } + } + + if ($enSong != null) { + $song['file'] = $enSong[0]['tracks'][0]['preview_url']; + debug_event('echonest', 'EchoNest `' . $track->title . '` preview: ' . $song['file'], '1'); + } + } + $this->songs[] = new Song_Preview(Song_preview::insert($song)); + } + } + } + } + } catch (Exception $e) { + $this->songs = array(); + } + + foreach ($this->songs as $song) { + $song->f_album = $this->name; + $song->format(); + } + } + + public function format() + { + if ($this->artist) { + $artist = new Artist($this->artist); + $artist->format(); + $this->f_artist_link = $artist->f_name_link; + } else { + $wartist = Wanted::get_missing_artist($this->artist_mbid); + $this->f_artist_link = $wartist['link']; + } + $this->f_name_link = "mbid . "&artist=" . $this->artist . "&artist_mbid=" . $this->artist_mbid . "\" title=\"" . $this->name . "\">" . $this->name . ""; + $user = new User($this->user); + $this->f_user = $user->fullname; + + } + + public static function get_wanted_list_sql() + { + $sql = "SELECT `id` FROM `wanted` "; + + if (!$GLOBALS['user']->has_access('75')) { + $sql .= "WHERE `user` = '" . scrub_in($GLOBALS['user']->id) . "'"; + } + + return $sql; + } + + public static function get_wanted_list() + { + $sql = self::get_wanted_list_sql(); + $db_results = Dba::read($sql); + $results = array(); + + while ($row = Dba::fetch_assoc($db_results)) { + $results[] = $row['id']; + } + + return $results; + } + +} // end of recommendation class diff --git a/sources/lib/class/waveform.class.php b/sources/lib/class/waveform.class.php new file mode 100644 index 0000000..93d3a91 --- /dev/null +++ b/sources/lib/class/waveform.class.php @@ -0,0 +1,302 @@ +id) { + $song->format(); + $waveform = $song->waveform; + if (!$waveform) { + $catalog = Catalog::create_from_id($song->catalog); + if ($catalog->get_type() == 'local') { + $transcode_to = 'wav'; + $transcode_cfg = AmpConfig::get('transcode'); + $valid_types = $song->get_stream_types(); + + if ($song->type != $transcode_to) { + $basedir = AmpConfig::get('tmp_dir_path'); + if ($basedir) { + if ($transcode_cfg != 'never' && in_array('transcode', $valid_types)) { + $tmpfile = tempnam($basedir, $transcode_to); + + $tfp = fopen($tmpfile, 'wb'); + if (!is_resource($tfp)) { + debug_event('waveform', "Failed to open " . $tmpfile, 3); + return null; + } + + $transcoder = Stream::start_transcode($song, $transcode_to); + $fp = $transcoder['handle']; + if (!is_resource($fp)) { + debug_event('waveform', "Failed to open " . $song->file . " for waveform.", 3); + return null; + } + + do { + $buf = fread($fp, 2048); + fwrite($tfp, $buf); + } while (!feof($fp)); + + fclose($fp); + fclose($tfp); + + $waveform = self::create_waveform($tmpfile); + //$waveform = self::create_waveform("C:\\tmp\\test.wav"); + + @unlink($tmpfile); + } else { + debug_event('waveform', 'transcode setting to wav required for waveform.', '3'); + } + } else { + debug_event('waveform', 'tmp_dir_path setting required for waveform.', '3'); + } + } + // Already wav file, no transcode required + else { + $waveform = self::create_waveform($song->file); + } + } + + if ($waveform) { + self::save_to_db($song_id, $waveform); + } + } + } + + return $waveform; + } + + protected static function findValues($byte1, $byte2) + { + $byte1 = hexdec(bin2hex($byte1)); + $byte2 = hexdec(bin2hex($byte2)); + return ($byte1 + ($byte2*256)); + } + + /** + * Great function slightly modified as posted by Minux at + * http://forums.clantemplates.com/showthread.php?t=133805 + */ + protected static function html2rgb($input) + { + $input=($input[0]=="#")?substr($input, 1,6):substr($input, 0,6); + return array( + hexdec(substr($input, 0, 2)), + hexdec(substr($input, 2, 2)), + hexdec(substr($input, 4, 2)) + ); + } + + protected static function create_waveform($filename) + { + $detail = 5; + $width = 400; + $height = 32; + $foreground = AmpConfig::get('waveform_color') ?: '#FF0000'; + $background = ''; + $draw_flat = true; + + // generate foreground color + list($r, $g, $b) = self::html2rgb($foreground); + + $handle = fopen($filename, "r"); + // wav file header retrieval + $heading = array(); + $heading[] = fread($handle, 4); + $heading[] = bin2hex(fread($handle, 4)); + $heading[] = fread($handle, 4); + $heading[] = fread($handle, 4); + $heading[] = bin2hex(fread($handle, 4)); + $heading[] = bin2hex(fread($handle, 2)); + $heading[] = bin2hex(fread($handle, 2)); + $heading[] = bin2hex(fread($handle, 4)); + $heading[] = bin2hex(fread($handle, 4)); + $heading[] = bin2hex(fread($handle, 2)); + $heading[] = bin2hex(fread($handle, 2)); + $heading[] = fread($handle, 4); + $heading[] = bin2hex(fread($handle, 4)); + + // wav bitrate + $peek = hexdec(substr($heading[10], 0, 2)); + $byte = $peek / 8; + + // checking whether a mono or stereo wav + $channel = hexdec(substr($heading[6], 0, 2)); + + $ratio = ($channel == 2 ? 40 : 80); + + // start putting together the initial canvas + // $data_size = (size_of_file - header_bytes_read) / skipped_bytes + 1 + $data_size = floor((filesize($filename) - 44) / ($ratio + $byte) + 1); + $data_point = 0; + + // create original image width based on amount of detail + // each waveform to be processed with be $height high, but will be condensed + // and resized later (if specified) + $img = imagecreatetruecolor($data_size / $detail, $height); + + // fill background of image + if ($background == "") { + // transparent background specified + imagesavealpha($img, true); + $transparentColor = imagecolorallocatealpha($img, 0, 0, 0, 127); + imagefill($img, 0, 0, $transparentColor); + } else { + list($br, $bg, $bb) = self::html2rgb($background); + imagefilledrectangle($img, 0, 0, (int) ($data_size / $detail), $height, imagecolorallocate($img, $br, $bg, $bb)); + } while (!feof($handle) && $data_point < $data_size) { + if ($data_point++ % $detail == 0) { + $bytes = array(); + + // get number of bytes depending on bitrate + for ($i = 0; $i < $byte; $i++) + $bytes[$i] = fgetc($handle); + + switch ($byte) { + // get value for 8-bit wav + case 1: + $data = self::findValues($bytes[0], $bytes[1]); + break; + // get value for 16-bit wav + case 2: + if(ord($bytes[1]) & 128) + $temp = 0; + else + $temp = 128; + $temp = chr((ord($bytes[1]) & 127) + $temp); + $data = floor(self::findValues($bytes[0], $temp) / 256); + break; + default: + $data = 0; + break; + } + + // skip bytes for memory optimization + fseek($handle, $ratio, SEEK_CUR); + + // draw this data point + // relative value based on height of image being generated + // data values can range between 0 and 255 + $v = (int) ($data / 255 * $height); + + // don't print flat values on the canvas if not necessary + if (!($v / $height == 0.5 && !$draw_flat)) + // draw the line on the image using the $v value and centering it vertically on the canvas + imageline( + $img, + // x1 + (int) ($data_point / $detail), + // y1: height of the image minus $v as a percentage of the height for the wave amplitude + $height - $v, + // x2 + (int) ($data_point / $detail), + // y2: same as y1, but from the bottom of the image + $height - ($height - $v), + imagecolorallocate($img, $r, $g, $b) + ); + + } else { + // skip this one due to lack of detail + fseek($handle, $ratio + $byte, SEEK_CUR); + } + } + + // close and cleanup + fclose($handle); + + ob_start(); + // want it resized? + if ($width) { + // resample the image to the proportions defined in the form + $rimg = imagecreatetruecolor($width, $height); + // save alpha from original image + imagesavealpha($rimg, true); + imagealphablending($rimg, false); + // copy to resized + imagecopyresampled($rimg, $img, 0, 0, 0, 0, $width, $height, imagesx($img), imagesy($img)); + imagepng($rimg); + imagedestroy($rimg); + } else { + imagepng($img); + } + imagedestroy($img); + + $imgdata = ob_get_contents(); + ob_clean (); + return $imgdata; + } + + protected static function save_to_db($song_id, $waveform) + { + $sql = "UPDATE `song_data` SET `waveform` = ? WHERE `song_id` = ?"; + return Dba::write($sql, array($waveform, $song_id)); + } + +} // Waveform class diff --git a/sources/lib/class/webplayer.class.php b/sources/lib/class/webplayer.class.php new file mode 100644 index 0000000..04f2c13 --- /dev/null +++ b/sources/lib/class/webplayer.class.php @@ -0,0 +1,240 @@ +urls as $item) { + if ($item->type == "radio") { + $radios[] = $item; + } + } + + return (count($playlist->urls) == 1 && count($radios) > 0 && AmpConfig::get('webplayer_flash')); + } + + public static function is_playlist_video($playlist) + { + return (count($playlist->urls) > 0 && $playlist->urls[0]->type == "video"); + } + + public static function browser_info($agent=null) + { + // Declare known browsers to look for + $known = array('msie', 'trident', 'firefox', 'safari', 'webkit', 'opera', 'netscape', 'konqueror', 'gecko'); + + // Clean up agent and build regex that matches phrases for known browsers + // (e.g. "Firefox/2.0" or "MSIE 6.0" (This only matches the major and minor + // version numbers. E.g. "2.0.0.6" is parsed as simply "2.0" + $agent = strtolower($agent ? $agent : $_SERVER['HTTP_USER_AGENT']); + $pattern = '#(?' . join('|', $known) . ')[/ ]+(?[0-9]+(?:\.[0-9]+)?)#'; + + // Find all phrases (or return empty array if none found) + if (!preg_match_all($pattern, $agent, $matches)) return array(); + + // Since some UAs have more than one phrase (e.g Firefox has a Gecko phrase, + // Opera 7,8 have a MSIE phrase), use the last one found (the right-most one + // in the UA). That's usually the most correct. + $i = count($matches['browser'])-1; + return array($matches['browser'][$i] => $matches['version'][$i]); + + } + + protected static function get_types($item, $force_type='') + { + $types = array('real' => 'mp3', 'player' => ''); + + $browsers = array_keys(self::browser_info()); + $browser = ''; + if (count($browsers) > 0 ) { + $browser = $browsers[0]; + } + + if (!empty($force_type)) { + debug_event("webplayer.class.php", "Forcing type to {".$force_type."}", 5); + $types['real'] = $force_type; + } else { + if ($browser == "msie" || $browser == "trident" || $browser == "webkit" || $browser == "safari") { + $types['real'] = "mp3"; + } else { + $types['real'] = "ogg"; + } + } + + $song = null; + $urlinfo = Stream_URL::parse($item->url); + if ($urlinfo['id'] && $urlinfo['type'] == 'song') { + $song = new Song($urlinfo['id']); + } else if ($urlinfo['id'] && $urlinfo['type'] == 'song_preview') { + $song = new Song_Preview($urlinfo['id']); + } else if (isset($urlinfo['demo_id'])) { + $democratic = new Democratic($urlinfo['demo_id']); + if ($democratic->id) { + $song_id = $democratic->get_next_object(); + if ($song_id) { + $song = new Song($song_id); + } + } + } + + if ($song != null) { + $ftype = $song->type; + + $transcode = false; + $transcode_cfg = AmpConfig::get('transcode'); + // Check transcode is required + $ftype_transcode = AmpConfig::get('transcode_' . $ftype); + $valid_types = Song::get_stream_types_for_type($ftype); + if ($transcode_cfg == 'always' || !empty($force_type) || $ftype_transcode == 'required' || ($types['real'] != $ftype && !AmpConfig::get('webplayer_flash'))) { + if ($transcode_cfg == 'always' || ($transcode_cfg != 'never' && in_array('transcode', $valid_types))) { + // Transcode only if excepted type available + $transcode_settings = $song->get_transcode_settings($types['real']); + if ($transcode_settings && AmpConfig::get('transcode_player_customize')) { + $transcode = true; + } else { + if (!in_array('native', $valid_types)) { + $transcode_settings = $song->get_transcode_settings(null); + if ($transcode_settings) { + $types['real'] = $transcode_settings['format']; + $transcode = true; + } + } + } + } + } + + if (!$transcode) { + $types['real'] = $ftype; + } + if ($types['real'] == "flac" || $types['real'] == "ogg") $types['player'] = "oga"; + else if ($types['real'] == "mp4") $types['player'] = "m4a"; + } else if ($urlinfo['id'] && $urlinfo['type'] == 'video') { + $video = new Video($urlinfo['id']); + $types['real'] = pathinfo($video->file, PATHINFO_EXTENSION); + + if ($types['real'] == "ogg") $types['player'] = "ogv"; + else if ($types['real'] == "webm") $types['player'] = "webmv"; + else if ($types['real'] == "mp4") $types['player'] = "m4v"; + } else if ($item->type == 'radio') { + $types['real'] = $item->codec; + if ($types['real'] == "flac" || $types['real'] == "ogg") $types['player'] = "oga"; + } else { + $ext = pathinfo($item->url, PATHINFO_EXTENSION); + if (!empty($ext)) $types['real'] = $ext; + } + + if (empty($types['player'])) $types['player'] = $types['real']; + + debug_event("webplayer.class.php", "Types {".json_encode($types)."}", 5); + return $types; + } + + public static function get_supplied_types($playlist) + { + $jptypes = array(); + foreach ($playlist->urls as $item) { + $force_type = ''; + if ($item->type == 'broadcast') { + $force_type = 'mp3'; + } + $types = self::get_types($item, $force_type); + if (!in_array($types['player'], $jptypes)) { + $jptypes[] = $types['player']; + } + } + + return $jptypes; + } + + public static function add_media_js($playlist, $callback_container='') + { + $addjs = ""; + foreach ($playlist->urls as $item) { + if ($item->type == 'broadcast') { + $addjs .= $callback_container . "startBroadcastListening('" . $item->url . "');"; + break; + } else { + $addjs .= $callback_container . "addMedia(" . self::get_media_js_param($item) . ");"; + } + } + + return $addjs; + } + + public static function get_media_js_param($item, $force_type='') + { + $js = array(); + foreach (array('title', 'author') as $member) { + if ($member == "author") + $kmember = "artist"; + else + $kmember = $member; + + $js[$kmember] = $item->$member; + } + $url = $item->url; + + $types = self::get_types($item, $force_type); + + $song = null; + $urlinfo = Stream_URL::parse($url); + $url = $urlinfo['base_url']; + + if ($urlinfo['id'] && $urlinfo['type'] == 'song') { + $song = new Song($urlinfo['id']); + } else if ($urlinfo['id'] && $urlinfo['type'] == 'song_preview') { + $song = new Song_Preview($urlinfo['id']); + } else if (isset($urlinfo['demo_id'])) { + $democratic = new Democratic($urlinfo['demo_id']); + if ($democratic->id) { + $song_id = $democratic->get_next_object(); + if ($song_id) { + $song = new Song($song_id); + } + } + } + + if ($song != null) { + $js['artist_id'] = $song->artist; + $js['album_id'] = $song->album; + $js['song_id'] = $song->id; + + if ($song->type != $types['real']) { + $url .= '&transcode_to=' . $types['real']; + } + //$url .= "&content_length=required"; + } + + $js['filetype'] = $types['player']; + $js['url'] = $url; + if ($urlinfo['type'] == 'song') { + $js['poster'] = $item->image_url . (!AmpConfig::get('iframes') ? '&thumb=4' : ''); + } + + debug_event("webplayer.class.php", "Return get_media_js_param {".json_encode($js)."}", 5); + + return json_encode($js); + } +} diff --git a/sources/lib/class/xml_data.class.php b/sources/lib/class/xml_data.class.php new file mode 100644 index 0000000..ec1de77 --- /dev/null +++ b/sources/lib/class/xml_data.class.php @@ -0,0 +1,668 @@ +" . self::_footer(); + return $string; + + } // error + + /** + * single_string + * + * This takes two values, first the key second the string + * + * @param string $key (description here...) + * @param string $string xml data + * @return string return xml + */ + public static function single_string($key, $string='') + { + $final = self::_header(); + if (!empty($string)) { + $final .= "\t<$key>"; + } else { + $final .= "\t<$key />"; + } + $final .= self::_footer(); + + return $final; + + } // single_string + + /** + * header + * + * This returns the header + * + * @see _header() + * @return string return xml + */ + public static function header() + { + return self::_header(); + + } // header + + /** + * footer + * + * This returns the footer + * + * @see _footer() + * @return string return xml + */ + public static function footer() + { + return self::_footer(); + + } // footer + + /** + * tags_string + * + * This returns the formatted 'tags' string for an xml document + * + */ + private static function tags_string($tags) + { + $string = ''; + + if (is_array($tags)) { + + foreach ($tags as $tag_id => $data) { + $tag = new Tag($tag_id); + $string .= "\tid . + '" count="' . count($data['users']) . + '">name . "]]>\n"; + } + } + + return $string; + + } // tags_string + + /** + * keyed_array + * + * This will build an xml document from a key'd array, + * + * @param array $array (description here...) + * @param boolean $callback (description here...) + * @return string return xml + */ + public static function keyed_array($array,$callback='') + { + $string = ''; + + // Foreach it + foreach ($array as $key=>$value) { + $attribute = ''; + // See if the key has attributes + if (is_array($value) AND isset($value[''])) { + $attribute = ' ' . $value['']; + $key = $value['value']; + } + + // If it's an array, run again + if (is_array($value)) { + $value = self::keyed_array($value,1); + $string .= "<$key$attribute>\n$value\n\n"; + } else { + $string .= "\t<$key$attribute>\n"; + } + + } // end foreach + + if (!$callback) { + $string = self::_header() . $string . self::_footer(); + } + + return $string; + + } // keyed_array + + /** + * tags + * + * This returns tags to the user, in a pretty xml document with the information + * + * @param array $tags (description here...) + * @return string return xml + */ + public static function tags($tags) + { + if (count($tags) > self::$limit OR self::$offset > 0) { + $tags = array_splice($tags,self::$offset,self::$limit); + } + + $string = ''; + + foreach ($tags as $tag_id) { + $tag = new Tag($tag_id); + $counts = $tag->count(); + $string .= "\n" . + "\tname]]>\n" . + "\t" . intval($counts['album']) . "\n" . + "\t" . intval($counts['artist']) . "\n" . + "\t" . intval($counts['song']) . "\n" . + "\t" . intval($counts['video']) . "\n" . + "\t" . intval($counts['playlist']) . "\n" . + "\t" . intval($counts['live_stream']) . "\n" . + "\n"; + } // end foreach + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // tags + + /** + * artists + * + * This takes an array of artists and then returns a pretty xml document with the information + * we want + * + * @param array $artists (description here...) + * @return string return xml + */ + public static function artists($artists) + { + if (count($artists) > self::$limit OR self::$offset > 0) { + $artists = array_splice($artists,self::$offset,self::$limit); + } + + $string = ''; + + Rating::build_cache('artist',$artists); + + foreach ($artists as $artist_id) { + $artist = new Artist($artist_id); + $artist->format(); + + $rating = new Rating($artist_id,'artist'); + $tag_string = self::tags_string($artist->tags); + + $string .= "id . "\">\n" . + "\tf_full_name . "]]>\n" . + $tag_string . + "\t" . $artist->albums . "\n" . + "\t" . $artist->songs . "\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_average_rating() . "\n" . + "\t" . $artist->mbid . "\n" . + "\t" . $artist->summary . "\n" . + "\t" . $artist->summary . "\n" . + "\t" . $artist->summary . "\n" . + "\n"; + } // end foreach artists + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // artists + + /** + * albums + * + * This echos out a standard albums XML document, it pays attention to the limit + * + * @param array $albums (description here...) + * @return string return xml + */ + public static function albums($albums) + { + if (count($albums) > self::$limit OR self::$offset > 0) { + $albums = array_splice($albums,self::$offset,self::$limit); + } + + Rating::build_cache('album',$albums); + + $string = ""; + foreach ($albums as $album_id) { + $album = new Album($album_id); + $album->format(); + + $rating = new Rating($album_id,'album'); + + // Build the Art URL, include session + $art_url = AmpConfig::get('web_path') . '/image.php?id=' . $album->id . '&auth=' . scrub_out($_REQUEST['auth']); + + $string .= "id . "\">\n" . + "\tname . "]]>\n"; + + // Do a little check for artist stuff + if ($album->artist_count != 1) { + $string .= "\t\n"; + } else { + $string .= "\tartist_id\">artist_name]]>\n"; + } + + $string .= "\t" . $album->year . "\n" . + "\t" . $album->song_count . "\n" . + "\t" . $album->disk . "\n" . + self::tags_string($album->tags) . + "\t\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_average_rating() . "\n" . + "\t" . $album->mbid . "\n" . + "\n"; + } // end foreach + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // albums + + /** + * playlists + * + * This takes an array of playlist ids and then returns a nice pretty XML document + * + * @param array $playlists (description here...) + * @return string return xml + */ + public static function playlists($playlists) + { + if (count($playlists) > self::$limit OR self::$offset > 0) { + $playlists = array_slice($playlists,self::$offset,self::$limit); + } + + $string = ''; + + // Foreach the playlist ids + foreach ($playlists as $playlist_id) { + $playlist = new Playlist($playlist_id); + $playlist->format(); + $item_total = $playlist->get_song_count(); + + // Build this element + $string .= "id\">\n" . + "\tname]]>\n" . + "\tf_user]]>\n" . + "\t$item_total\n" . + "\t$playlist->type\n" . + "\n"; + + + } // end foreach + + // Build the final and then send her off + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // playlists + + /** + * songs + * + * This returns an xml document from an array of song ids. + * (Spiffy isn't it!) + */ + public static function songs($songs) + { + if (count($songs) > self::$limit OR self::$offset > 0) { + $songs = array_slice($songs, self::$offset, self::$limit); + } + + Song::build_cache($songs); + Stream::set_session($_REQUEST['auth']); + + $string = ""; + // Foreach the ids! + foreach ($songs as $song_id) { + $song = new Song($song_id); + + // If the song id is invalid/null + if (!$song->id) { continue; } + + $tag_string = self::tags_string(Tag::get_top_tags('song', $song_id)); + $rating = new Rating($song_id, 'song'); + $art_url = Art::url($song->album, 'album', $_REQUEST['auth']); + + $string .= "id . "\">\n" . + "\t<![CDATA[" . $song->title . "]]>\n" . + "\tartist . + '">get_artist_name() . + "]]>\n" . + "\talbum . + '">get_album_name(). + "]]>\n" . + $tag_string . + "\tfile . "]]>\n" . + "\t" . $song->track . "\n" . + "\t\n" . + "\t" . $song->year . "\n" . + "\t" . $song->bitrate . "\n". + "\t" . $song->mode . "\n". + "\t" . $song->mime . "\n" . + "\tid) . "]]>\n" . + "\t" . $song->size . "\n". + "\t" . $song->mbid . "\n". + "\t" . $song->album_mbid . "\n". + "\t" . $song->artist_mbid . "\n". + "\t\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_average_rating() . "\n" . + "\n"; + + } // end foreach + + return self::_header() . $string . self::_footer(); + + } // songs + + /** + * videos + * + * This builds the xml document for displaying video objects + * + * @param array $videos (description here...) + * @return string return xml + */ + public static function videos($videos) + { + if (count($videos) > self::$limit OR self::$offset > 0) { + $videos = array_slice($videos,self::$offset,self::$limit); + } + + $string = ''; + foreach ($videos as $video_id) { + $video = new Video($video_id); + $video->format(); + + $string .= "\n"; + + } // end foreach + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // videos + + /** + * democratic + * + * This handles creating an xml document for democratic items, this can be a little complicated + * due to the votes and all of that + * + * @param array $object_ids Object IDs + * @return string return xml + */ + public static function democratic($object_ids=array()) + { + if (!is_array($object_ids)) { $object_ids = array(); } + + $democratic = Democratic::get_current_playlist(); + + $string = ''; + + foreach ($object_ids as $row_id=>$data) { + $song = new $data['object_type']($data['object_id']); + $song->format(); + + //FIXME: This is duplicate code and so wrong, functions need to be improved + $tag = new Tag($song->tags['0']); + $song->genre = $tag->id; + $song->f_genre = $tag->name; + + $tag_string = self::tags_string($song->tags); + + $rating = new Rating($song->id,'song'); + + $art_url = Art::url($song->album, 'album', $_REQUEST['auth']); + + $string .= "id . "\">\n" . + "\t<![CDATA[" . $song->title . "]]>\n" . + "\tartist . "\">f_artist_full . "]]>\n" . + "\talbum . "\">f_album_full . "]]>\n" . + "\tgenre . "\">f_genre . "]]>\n" . + $tag_string . + "\t" . $song->track . "\n" . + "\t\n" . + "\t" . $song->mime . "\n" . + "\tid) . "]]>\n" . + "\t" . $song->size . "\n" . + "\t\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_user_rating() . "\n" . + "\t" . $rating->get_average_rating() . "\n" . + "\t" . $democratic->get_vote($row_id) . "\n" . + "\n"; + + } // end foreach + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // democratic + + /** + * rss_feed + * + * (description here...) + * + * @param array $data (descriptiong here...) + * @param string $title RSS feed title + * @param string $description (not use yet?) + * @param string $date publish date + * @return string RSS feed xml + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function rss_feed($data,$title,$description,$date) + { + $string = "\t$title\n\t" . AmpConfig::get('web_path') . "\n\t" . + "" . date("r",$date) . "\n"; + + // Pass it to the keyed array xml function + foreach ($data as $item) { + // We need to enclose it in an item tag + $string .= self::keyed_array(array('item'=>$item),1); + } + + $final = self::_header() . $string . self::_footer(); + + return $final; + + } // rss_feed + + /** + * _header + * + * this returns a standard header, there are a few types + * so we allow them to pass a type if they want to + * + * @return string Header xml tag. + */ + private static function _header() + { + switch (self::$type) { + case 'xspf': + $header = "\n" . + "\n " . + "Ampache XSPF Playlist\n" . + "" . scrub_out(AmpConfig::get('site_title')) . "\n" . + "" . scrub_out(AmpConfig::get('site_title')) . "\n" . + "". AmpConfig::get('web_path') ."\n" . + "\n"; + break; + case 'itunes': + $header = "\n" . + "\n"; + "\n" . + "\n" . + "\n" . + " Major Version1\n" . + " Minor Version1\n" . + " Application Version7.0.2\n" . + " Features1\n" . + " Show Content Ratings\n" . + " Tracks\n" . + " \n"; + break; + case 'rss': + $header = "\n " . + "\n" . + "\n\n"; + break; + default: + $header = "\n\n"; + break; + } // end switch + + return $header; + + } // _header + + /** + * _footer + * + * this returns the footer for this document, these are pretty boring + * + * @return string Footer xml tag. + */ + private static function _footer() + { + switch (self::$type) { + case 'itunes': + $footer = "\t\t\t\n\n\n"; + break; + case 'xspf': + $footer = "\n\n"; + break; + case 'rss': + $footer = "\n\n\n"; + break; + default: + $footer = "\n\n"; + break; + } // end switch on type + + + return $footer; + + } // _footer + +} // XML_Data diff --git a/sources/lib/debug.lib.php b/sources/lib/debug.lib.php new file mode 100644 index 0000000..2f39a93 --- /dev/null +++ b/sources/lib/debug.lib.php @@ -0,0 +1,247 @@ += 60 || $current == 0); + +} // check_php_timelimit + +/** + * check_safe_mode + * Checks to make sure we aren't in safe mode + */ +function check_php_safemode() +{ + if (ini_get('safe_mode')) { + return false; + } + return true; +} + +/** + * check_override_memory + * This checks to see if we can manually override the memory limit + */ +function check_override_memory() +{ + /* Check memory */ + $current_memory = ini_get('memory_limit'); + $current_memory = substr($current_memory,0,strlen($current_memory)-1); + $new_limit = ($current_memory+16) . "M"; + + /* Bump it by 16 megs (for getid3)*/ + if (!ini_set('memory_limit',$new_limit)) { + return false; + } + + // Make sure it actually worked + $new_memory = ini_get('memory_limit'); + + if ($new_limit != $new_memory) { + return false; + } + + return true; +} + +/** + * check_override_exec_time + * This checks to see if we can manually override the max execution time + */ +function check_override_exec_time() +{ + $current = ini_get('max_execution_time'); + set_time_limit($current+60); + + if ($current == ini_get('max_execution_time')) { + return false; + } + + return true; +} + +/** + * check_config_writable + * This checks whether we can write the config file + */ +function check_config_writable() +{ + // file eixsts && is writable, or dir is writable + return ((file_exists(AmpConfig::get('prefix') . '/config/ampache.cfg.php') && is_writable(AmpConfig::get('prefix') . '/config/ampache.cfg.php')) + || (!file_exists(AmpConfig::get('prefix') . '/config/ampache.cfg.php') && is_writeable(AmpConfig::get('prefix') . '/config/'))); +} + +function check_htaccess_rest_writable() +{ + return ((file_exists(AmpConfig::get('prefix') . '/rest/.htaccess') && is_writable(AmpConfig::get('prefix') . '/rest/.htaccess')) + || (!file_exists(AmpConfig::get('prefix') . '/rest/.htaccess') && is_writeable(AmpConfig::get('prefix') . '/rest/'))); +} + +function check_htaccess_play_writable() +{ + return ((file_exists(AmpConfig::get('prefix') . '/play/.htaccess') && is_writable(AmpConfig::get('prefix') . '/play/.htaccess')) + || (!file_exists(AmpConfig::get('prefix') . '/play/.htaccess') && is_writeable(AmpConfig::get('prefix') . '/play/'))); +} + +/** + * debug_result + * + * Convenience function to format the output. + */ +function debug_result($status = false, $value = null, $comment = '') +{ + $class = $status ? 'success' : 'danger'; + + if (!$value) { + $value = $status ? 'OK' : 'ERROR'; + } + + return ''; +} diff --git a/sources/lib/general.lib.php b/sources/lib/general.lib.php new file mode 100644 index 0000000..9e2642a --- /dev/null +++ b/sources/lib/general.lib.php @@ -0,0 +1,318 @@ +'album', + '%a'=>'artist', + '%c'=>'comment', + '%g'=>'genre', + '%T'=>'track', + '%t'=>'title', + '%y'=>'year', + '%o'=>'zz_other'); + + if (isset($code_array[$code])) { + return $code_array[$code]; + } + + return false; + +} // translate_pattern_code + +/** + * generate_config + * + * This takes an array of results and re-generates the config file + * this is used by the installer and by the admin/system page + */ +function generate_config($current) +{ + // Start building the new config file + $distfile = AmpConfig::get('prefix') . '/config/ampache.cfg.php.dist'; + $handle = fopen($distfile,'r'); + $dist = fread($handle,filesize($distfile)); + fclose($handle); + + $data = explode("\n",$dist); + + $final = ""; + foreach ($data as $line) { + if (preg_match("/^;?([\w\d]+)\s+=\s+[\"]{1}(.*?)[\"]{1}$/",$line,$matches) + || preg_match("/^;?([\w\d]+)\s+=\s+[\']{1}(.*?)[\']{1}$/", $line, $matches) + || preg_match("/^;?([\w\d]+)\s+=\s+[\'\"]{0}(.*)[\'\"]{0}$/",$line,$matches)) { + + $key = $matches[1]; + $value = $matches[2]; + + // Put in the current value + if ($key == 'config_version') { + $line = $key . ' = ' . escape_ini($value); + } elseif (isset($current[$key])) { + $line = $key . ' = "' . escape_ini($current[$key]) . '"'; + unset($current[$key]); + } + } + + $final .= $line . "\n"; + } + + return $final; +} + +/** + * escape_ini + * + * Escape a value used for inserting into an ini file. + * Won't quote ', like addslashes does. + */ +function escape_ini($str) +{ + return str_replace('"', '\"', $str); +} diff --git a/sources/lib/i18n.php b/sources/lib/i18n.php new file mode 100644 index 0000000..1cab9c0 --- /dev/null +++ b/sources/lib/i18n.php @@ -0,0 +1,52 @@ + $value) { + $results[$key] = $value; + } +} + +$results['raw_web_path'] = $results['web_path']; +if (empty($results['http_host'])) { + $results['http_host'] = $_SERVER['HTTP_HOST']; +} +$results['web_path'] = $http_type . $results['http_host'] . $results['web_path']; +$results['http_port'] = (!empty($results['http_port'])) ? $results['http_port'] : $http_port; +$results['site_charset'] = $results['site_charset'] ?: 'UTF-8'; +$results['raw_web_path'] = $results['raw_web_path'] ?: '/'; +$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?: ''; + +if (isset($results['user_ip_cardinality']) && !$results['user_ip_cardinality']) { + $results['user_ip_cardinality'] = 42; +} + +/* Variables needed for Auth class */ +$results['cookie_path'] = $results['raw_web_path']; +$results['cookie_domain'] = $results['http_port']; +$results['cookie_life'] = $results['session_cookielife']; +$results['cookie_secure'] = $results['session_cookiesecure']; + +// Library and module includes we can't do with the autoloader +require_once $prefix . '/modules/getid3/getid3.php'; +require_once $prefix . '/modules/phpmailer/class.phpmailer.php'; +require_once $prefix . '/modules/phpmailer/class.smtp.php'; +require_once $prefix . '/modules/infotools/AmazonSearchEngine.class.php'; +require_once $prefix . '/modules/infotools/lastfm.class.php'; +require_once $prefix . '/modules/musicbrainz/MusicBrainz.php'; +require_once $prefix . '/modules/musicbrainz/Exception.php'; +require_once $prefix . '/modules/musicbrainz/Clients/MbClient.php'; +require_once $prefix . '/modules/musicbrainz/Clients/RequestsMbClient.php'; +require_once $prefix . '/modules/ampacheapi/AmpacheApi.lib.php'; + +require_once $prefix . '/modules/EchoNest/Autoloader.php'; +EchoNest_Autoloader::register(); + +/* Temp Fixes */ +$results = Preference::fix_preferences($results); + +AmpConfig::set_by_array($results, true); + +// Modules (These are conditionally included depending upon config values) +if (AmpConfig::get('ratings')) { + require_once $prefix . '/lib/rating.lib.php'; +} + +/* Set a new Error Handler */ +$old_error_handler = set_error_handler('ampache_error_handler'); + +/* Check their PHP Vars to make sure we're cool here */ +$post_size = @ini_get('post_max_size'); +if (substr($post_size,strlen($post_size)-1,strlen($post_size)) != 'M') { + /* Sane value time */ + ini_set('post_max_size','8M'); +} + +// In case the local setting is 0 +ini_set('session.gc_probability','5'); + +if (!isset($results['memory_limit']) || + (UI::unformat_bytes($results['memory_limit']) < UI::unformat_bytes('32M')) +) { + $results['memory_limit'] = '32M'; +} + +set_memory_limit($results['memory_limit']); + +/**** END Set PHP Vars ****/ + +// If we want a session +if (!defined('NO_SESSION') && AmpConfig::get('use_auth')) { + /* Verify their session */ + if (!Session::exists('interface', $_COOKIE[AmpConfig::get('session_name')])) { + Auth::logout($_COOKIE[AmpConfig::get('session_name')]); + exit; + } + + // This actually is starting the session + Session::check(); + + /* Create the new user */ + $GLOBALS['user'] = User::get_from_username($_SESSION['userdata']['username']); + + /* If the user ID doesn't exist deny them */ + if (!$GLOBALS['user']->id && !AmpConfig::get('demo_mode')) { + Auth::logout(session_id()); + exit; + } + + /* Load preferences and theme */ + $GLOBALS['user']->update_last_seen(); +} elseif (!AmpConfig::get('use_auth')) { + $auth['success'] = 1; + $auth['username'] = '-1'; + $auth['fullname'] = "Ampache User"; + $auth['id'] = -1; + $auth['offset_limit'] = 50; + $auth['access'] = AmpConfig::get('default_auth_level') ? User::access_name_to_level(AmpConfig::get('default_auth_level')) : '100'; + if (!Session::exists('interface', $_COOKIE[AmpConfig::get('session_name')])) { + Session::create_cookie(); + Session::create($auth); + Session::check(); + $GLOBALS['user'] = new User($auth['username']); + $GLOBALS['user']->username = $auth['username']; + $GLOBALS['user']->fullname = $auth['fullname']; + $GLOBALS['user']->access = $auth['access']; + } else { + Session::check(); + if ($_SESSION['userdata']['username']) { + $GLOBALS['user'] = User::get_from_username($_SESSION['userdata']['username']); + } else { + $GLOBALS['user'] = new User($auth['username']); + $GLOBALS['user']->id = '-1'; + $GLOBALS['user']->username = $auth['username']; + $GLOBALS['user']->fullname = $auth['fullname']; + $GLOBALS['user']->access = $auth['access']; + } + if (!$GLOBALS['user']->id AND !AmpConfig::get('demo_mode')) { + Auth::logout(session_id()); exit; + } + $GLOBALS['user']->update_last_seen(); + } +} +// If Auth, but no session is set +else { + if (isset($_REQUEST['sid'])) { + session_name(AmpConfig::get('session_name')); + session_id(scrub_in($_REQUEST['sid'])); + session_start(); + $GLOBALS['user'] = new User($_SESSION['userdata']['uid']); + } else { + $GLOBALS['user'] = new User(); + } + +} // If NO_SESSION passed + +// Load the Preferences from the database +Preference::init(); + +if (session_id()) { + Session::extend(session_id()); + // We only need to create the tmp playlist if we have a session + $GLOBALS['user']->load_playlist(); +} + +/* Add in some variables for ajax done here because we need the user */ +AmpConfig::set('ajax_url', AmpConfig::get('web_path') . '/server/ajax.server.php', true); +AmpConfig::set('ajax_server', AmpConfig::get('web_path') . '/server', true); + +// Load gettext mojo +load_gettext(); + +/* Set CHARSET */ +header ("Content-Type: text/html; charset=" . AmpConfig::get('site_charset')); + +/* Clean up a bit */ +unset($array); +unset($results); + +/* Check to see if we need to perform an update */ +if (!defined('OUTDATED_DATABASE_OK')) { + if (Update::need_update()) { + header("Location: " . AmpConfig::get('web_path') . "/update.php"); + exit(); + } +} +// For the XMLRPC stuff +$GLOBALS['xmlrpc_internalencoding'] = AmpConfig::get('site_charset'); + +// If debug is on GIMMIE DA ERRORS +if (AmpConfig::get('debug')) { + error_reporting(E_ALL); +} diff --git a/sources/lib/install.lib.php b/sources/lib/install.lib.php new file mode 100644 index 0000000..9291e5d --- /dev/null +++ b/sources/lib/install.lib.php @@ -0,0 +1,411 @@ +downloadHeaders(basename($file), 'text/plain', false, strlen($final)); + echo $final; + exit(); + } + + return true; +} + +/** + * install_insert_db + * + * Inserts the database using the values from Config. + */ +function install_insert_db($db_user = null, $db_pass = null, $create_db = true, $overwrite = false, $create_tables = true) +{ + $database = AmpConfig::get('database_name'); + // Make sure that the database name is valid + preg_match('/([^\d\w\_\-])/', $database, $matches); + + if (count($matches)) { + Error::add('general', T_('Error: Invalid database name.')); + return false; + } + + if (!Dba::check_database()) { + Error::add('general', sprintf(T_('Error: Unable to make database connection: %s'), Dba::error())); + return false; + } + + $db_exists = Dba::read('SHOW TABLES'); + + if ($db_exists && $create_db) { + if ($overwrite) { + Dba::write('DROP DATABASE `' . $database . '`'); + } else { + Error::add('general', T_('Error: Database already exists and overwrite not checked')); + return false; + } + } + + if ($create_db) { + if (!Dba::write('CREATE DATABASE `' . $database . '`')) { + Error::add('general', sprintf(T_('Error: Unable to create database: %s'), Dba::error())); + return false; + } + } + + Dba::disconnect(); + + // Check to see if we should create a user here + if (strlen($db_user) && strlen($db_pass)) { + $db_host = AmpConfig::get('database_hostname'); + $sql = 'GRANT ALL PRIVILEGES ON `' . Dba::escape($database) . '`.* TO ' . + "'" . Dba::escape($db_user) . "'"; + if ($db_host == 'localhost' || strpos($db_host, '/') === 0) { + $sql .= "@'localhost'"; + } + $sql .= "IDENTIFIED BY '" . Dba::escape($db_pass) . "' WITH GRANT OPTION"; + if (!Dba::write($sql)) { + Error::add('general', sprintf(T_('Error: Unable to create user %1$s with permissions to %2$s on %3$s: %4$s'), $db_user, $database, $db_host, Dba::error())); + return false; + } + } // end if we are creating a user + + if ($create_tables) { + $sql_file = AmpConfig::get('prefix') . '/sql/ampache.sql'; + $query = fread(fopen($sql_file, 'r'), filesize($sql_file)); + $pieces = split_sql($query); + $errors = array(); + for ($i=0; $i 0, we can ignore lazy comparison problems + if (!file_put_contents($config_file, $final)) { + Error::add('general', T_('Error writing config file')); + return false; + } + } + } else { + $browser = new Horde_Browser(); + $browser->downloadHeaders('ampache.cfg.php', 'text/plain', false, strlen($final)); + echo $final; + exit(); + } + + return true; +} + +/** + * install_create_account + * this creates your initial account and sets up the preferences for the -1 user and you + */ +function install_create_account($username, $password, $password2) +{ + if (!strlen($username) OR !strlen($password)) { + Error::add('general', T_('No Username/Password specified')); + return false; + } + + if ($password !== $password2) { + Error::add('general', T_('Passwords do not match')); + return false; + } + + if (!Dba::check_database()) { + Error::add('general', sprintf(T_('Database connection failed: %s'), Dba::error())); + return false; + } + + if (!Dba::check_database_inserted()) { + Error::add('general', sprintf(T_('Database select failed: %s'), Dba::error())); + return false; + } + + $username = Dba::escape($username); + $password = Dba::escape($password); + + $insert_id = User::create($username,'Administrator','','',$password,'100'); + + if (!$insert_id) { + Error::add('general', sprintf(T_('Administrative user creation failed: %s'), Dba::error())); + return false; + } + + // Fix the system users preferences + User::fix_preferences('-1'); + + return true; + +} // install_create_account + +function command_exists($command) +{ + if (!function_exists('proc_open')) { + return false; + } + + $whereIsCommand = (PHP_OS == 'WINNT') ? 'where' : 'which'; + $process = proc_open( + "$whereIsCommand $command", + array( + 0 => array("pipe", "r"), //STDIN + 1 => array("pipe", "w"), //STDOUT + 2 => array("pipe", "w"), //STDERR + ), + $pipes + ); + + if ($process !== false) { + $stdout = stream_get_contents($pipes[1]); + stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $stdout != ''; + } + + return false; +} + +/** + * install_get_transcode_modes + * get transcode modes available on this machine. + */ +function install_get_transcode_modes() +{ + $modes = array(); + + if (command_exists('ffmpeg')) { + $modes[] = 'ffmpeg'; + } + if (command_exists('avconv')) { + $modes[] = 'avconv'; + } + + return $modes; +} // install_get_transcode_modes + +function install_config_transcode_mode($mode) +{ + $trconfig = array( + 'encode_target' => 'mp3', + 'transcode_m4a' => 'required', + 'transcode_flac' => 'required', + 'transcode_mpc' => 'required', + 'transcode_ogg' => 'allowed', + 'transcode_wav' => 'required' + ); + if ($mode == 'ffmpeg' || $mode == 'avconv') { + $trconfig['transcode_cmd'] = $mode . ' -i %FILE%'; + $trconfig['encode_args_mp3'] = '-vn -b:a %SAMPLE%K -c:a libmp3lame -f mp3 pipe:1'; + $trconfig['encode_args_ogg'] = '-vn -b:a %SAMPLE%K -c:a libvorbis -f ogg pipe:1'; + $trconfig['encode_args_wav'] = '-vn -b:a %SAMPLE%K -c:a pcm_s16le -f wav pipe:1'; + AmpConfig::set_by_array($trconfig, true); + } +} diff --git a/sources/lib/javascript/.htaccess b/sources/lib/javascript/.htaccess new file mode 100644 index 0000000..6db0ae1 --- /dev/null +++ b/sources/lib/javascript/.htaccess @@ -0,0 +1 @@ +Allow from all \ No newline at end of file diff --git a/sources/lib/javascript/ajax.js b/sources/lib/javascript/ajax.js new file mode 100644 index 0000000..8664e2c --- /dev/null +++ b/sources/lib/javascript/ajax.js @@ -0,0 +1,62 @@ +// vim:set softtabstop=4 shiftwidth=4 expandtab: +// +// Copyright 2001 - 2013 Ampache.org +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License v2 +// as published by the Free Software Foundation. +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +// Some cutesy flashing thing while we run +$(document).ajaxSend(function () { + $('#ajax-loading').show(); +}); +$(document).ajaxComplete(function () { + $('#ajax-loading').hide(); +}); + +// ajaxPost +// Post the contents of a form. +function ajaxPost(url, input, source) { + if ($(source)) { + $(source).off('click'); + } + $.ajax(url, { success: processContents, type: 'post', data: $('#'+input).serialize() }); +} // ajaxPost + +// ajaxPut +// Get response from the specified URL. +function ajaxPut(url, source) { + if ($(source)) { + $(source).off('click'); + } + $.ajax(url, { success: processContents, type: 'post', dataType: 'xml' }); +} // ajaxPut + +// ajaxState +// Post the contents of a form without doing any observe() things. +function ajaxState(url, input) { + $.ajax({ + url : url, + type : 'POST', + data : $('#' + input).serialize(true), + success : processContents + }); +} // ajaxState + +// processContents +// Iterate over a response and do any updates we received. +function processContents(data) { + $(data).find('content').each(function () { + $('#' + $(this).attr('div')).html($(this).text()); + }); +} // processContents + diff --git a/sources/lib/javascript/base.js b/sources/lib/javascript/base.js new file mode 100644 index 0000000..99a4dbb --- /dev/null +++ b/sources/lib/javascript/base.js @@ -0,0 +1,108 @@ +// vim:set softtabstop=4 shiftwidth=4 expandtab: +// +// Copyright 2001 - 2013 Ampache.org +// 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 v2 +// as published by the Free Software Foundation. +// +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +$(document).ready(function () { + $('.default_hidden').hide(); + + $("#tabs li").click(function() { + $("#tabs li").removeClass('tab_active'); + $(this).addClass("tab_active"); + $(".tab_content").hide(); + var selected_tab = $(this).find("a").attr("href"); + $(selected_tab).fadeIn(); + + return false; + }); +}); + +$(function() { + var rightmenu = $("#rightbar"); + var rightsubmenu = $("#rightbar .submenu"); + var pos = rightmenu.offset(); + if (rightmenu.hasClass('rightbar-float')) { + $(window).scroll(function() { + if ($(this).scrollTop() > (pos.top)) { + rightmenu.addClass('fixedrightbar'); + rightsubmenu.addClass('fixedrightbarsubmenu'); + } + else if ($(this).scrollTop() <= pos.top && rightmenu.hasClass('fixedrightbar')) { + rightmenu.removeClass('fixedrightbar'); + rightsubmenu.removeClass('fixedrightbarsubmenu'); + } + else { + rightmenu.offset({ left: pos.left, top: pos.top }); + } + }) + } +}); + +// flipField +// Toggles the disabled property on the specifed field +function flipField(field) { + if ($(field).disabled == false) { + $(field).disabled = true; + } + else { + $(field).disabled = false; + } +} + +// updateText +// Changes the specified elements innards. Used for the catalog mojo fluff. +function updateText(field, value) { + $('#'+field).html(value); +} + +// toggleVisible +// Toggles display type between block and none. Used for ajax loading div. +function toggleVisible(element) { + var target = $('#' + element); + if (target.is(':visible')) { + target.hide(); + } else { + target.show(); + } +} + +// delayRun +// This function delays the run of another function by X milliseconds +function delayRun(element, time, method, page, source) { + var function_string = method + '(\'' + page + '\',\'' + source + '\')'; + var action = function () { eval(function_string); }; + + if (element.zid) { + clearTimeout(element.zid); + } + + element.zid = setTimeout(action, time); +} + +// reloadUtil +// Reload our util frame +// IE issue fixed by Spocky, we have to use the iframe for Democratic Play & +// Localplay, which don't actually prompt for a new file +function reloadUtil(target) { + $('#util_iframe').prop('src', target); +} + +// reloadRedirect +// Send them elsewhere +function reloadRedirect(target) { + window.location = target; +} diff --git a/sources/lib/javascript/search-data.php b/sources/lib/javascript/search-data.php new file mode 100644 index 0000000..7e2ec24 --- /dev/null +++ b/sources/lib/javascript/search-data.php @@ -0,0 +1,51 @@ + $value) { + $json .= '"' . $key . '" : '; + if (is_array($value)) { + $json .= arrayToJSON($value); + } else { + // Make sure to strip backslashes and convert things to + // entities in our output + $json .= '"' . scrub_out(str_replace('\\', '', $value)) . '"'; + } + $json .= ' , '; + } + $json = rtrim($json, ', '); + return $json . ' }'; +} + +Header('content-type: application/x-javascript'); + +$search = new Search($_REQUEST['type']); + +echo 'var types = '; +echo arrayToJSON($search->types) . ";\n"; +echo 'var basetypes = '; +echo arrayToJSON($search->basetypes) . ";\n"; +echo 'removeIcon = \'' . UI::get_icon('disable', T_('Remove')) . '\';'; diff --git a/sources/lib/javascript/search.js b/sources/lib/javascript/search.js new file mode 100644 index 0000000..17679c2 --- /dev/null +++ b/sources/lib/javascript/search.js @@ -0,0 +1,172 @@ +// vim:set softtabstop=4 shiftwidth=4 expandtab: +// +// Copyright 2010 - 2013 Ampache.org +// 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 v2 +// as published by the Free Software Foundation. +// +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +var rowIter = 1; +var rowCount = 0; + +var SearchRow = { + add: function(ruleType, operator, input) { + if (typeof(ruleType) != 'string') { + ruleType = 0; + } + else { + jQuery.each(types, function(i) { + if (types[i].name == ruleType) { + ruleType = i; + return false; + } + }); + } + + if (typeof(operator) != 'string') { + operator = 0; + } + else { + if (ruleType != null) { + var opts = basetypes[types[ruleType].type]; + jQuery.each(opts, function(i) { + if (opts[i].name == operator) { + operator = i; + return false; + } + }); + } + } + + var row = document.createElement('tr'); + var cells = new Array(); + for (var i = 0 ; i < 5 ; i++) { + cells[i] = document.createElement('td'); + } + + cells[0].appendChild(SearchRow.constructOptions(ruleType, rowIter)); + cells[1].appendChild(SearchRow.constructOperators(ruleType, rowIter, operator)); + cells[2].appendChild(SearchRow.constructInput(ruleType, rowIter, input)); + cells[3].innerHTML = removeIcon; + + jQuery.each(cells, function(i) { + row.appendChild(cells[i]); + }); + + $('#searchtable').append(row); + rowCount++; + + $(cells[3]).on('click', function(){if(rowCount > 1) { this.parentNode.remove(); rowCount--; }}); + + rowIter++; + }, + constructInput: function(ruleType, ruleNumber, input) { + if (input === null || input === undefined) { + input = ''; + } + + widget = types[ruleType].widget; + + var inputNode = document.createElement(widget['0']); + inputNode.id = 'rule_' + ruleNumber + '_input'; + inputNode.name = 'rule_' + ruleNumber + '_input'; + + switch(widget['0']) { + case 'input': + inputNode.setAttribute('type', widget['1']); + inputNode.setAttribute('value', input); + break; + case 'select': + jQuery.each(widget['1'], function(i) { + var option = document.createElement('option'); + if ( isNaN(parseInt(widget['1'][i])) ) { + realvalue = i; + } + else { + realvalue = parseInt(widget['1'][i]); + } + if ( input == realvalue ) { + option.selected = true; + } + option.value = realvalue; + option.innerHTML = widget['1'][i]; + inputNode.appendChild(option); + }); + break; + } + + return inputNode; + }, + constructOptions: function(ruleType, ruleNumber) { + var optionsNode = document.createElement('select'); + optionsNode.id = 'rule_' + ruleNumber; + optionsNode.name = 'rule_' + ruleNumber; + + jQuery.each(types, function(i) { + var option = document.createElement('option'); + option.innerHTML = types[i].label; + option.value = types[i].name; + if ( i == ruleType ) { + option.selected = true; + } + optionsNode.appendChild(option); + }); + + $(optionsNode).change(SearchRow.update); + + return optionsNode; + }, + constructOperators: function(ruleType, ruleNumber, operator) { + var operatorNode = document.createElement('select'); + operatorNode.id = 'rule_' + ruleNumber + '_operator'; + operatorNode.name = 'rule_' + ruleNumber + '_operator'; + + basetype = types[ruleType].type; + operatorNode.className = 'operator' + basetype; + + var opts = basetypes[basetype]; + jQuery.each(opts, function(i) { + var option = document.createElement('option'); + option.innerHTML = opts[i].description; + option.value = i; + if (i == operator) { + option.selected = true; + } + operatorNode.appendChild(option); + }); + + return operatorNode; + }, + update: function() { + var r_findID = /rule_(\d+)/; + var targetID = r_findID.exec(this.id)[1]; + + var operator = $('#rule_' + targetID + '_operator'); + if (operator.className != 'operator' + types[this.selectedIndex].type) { + var operator_cell = operator.parent(); + operator.remove(); + operator_cell.append(SearchRow.constructOperators(this.selectedIndex, targetID)); + } + + var input = $('#rule_' + targetID + '_input'); + + if (input.type == 'text') { + var oldinput = input.value; + } + + var input_cell = input.parent(); + input.remove(); + input_cell.append(SearchRow.constructInput(this.selectedIndex, targetID, oldinput)); + } +}; diff --git a/sources/lib/javascript/tabledata.js b/sources/lib/javascript/tabledata.js new file mode 100644 index 0000000..4dae987 --- /dev/null +++ b/sources/lib/javascript/tabledata.js @@ -0,0 +1,28 @@ +// vim:set softtabstop=4 shiftwidth=4 expandtab: +// +// Copyright 2001 - 2014 Ampache.org +// 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 v2 +// as published by the Free Software Foundation. +// +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +$(document).ready(function() { + if ($('.tabledata').mediaTable()) { + ResponsiveElements.init(); + setTimeout(function() { + $('.tabledata').mediaTable('analyze'); + }, 1); + } +}); \ No newline at end of file diff --git a/sources/lib/javascript/tools.js b/sources/lib/javascript/tools.js new file mode 100644 index 0000000..6473708 --- /dev/null +++ b/sources/lib/javascript/tools.js @@ -0,0 +1,306 @@ +// vim:set softtabstop=4 shiftwidth=4 expandtab: +// +// Copyright 2001 - 2013 Ampache.org +// 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 v2 +// as published by the Free Software Foundation. +// +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +/***********/ +/* Filters */ +/***********/ + +function showFilters(element) { + var link = $('.browse-options-link'); + link.hide(); + var content = $('.browse-options-content'); + content.show(); +} + +/************************************************************/ +/* Dialog selection to add song to an existing/new playlist */ +/************************************************************/ + +var closeplaylist; +function showPlaylistDialog(e, item_type, item_ids) { + $("#playlistdialog").dialog("close"); + + var parent = this; + parent.itemType = item_type; + parent.contentUrl = jsAjaxServer + '/show_edit_playlist.server.php?action=show_edit_object&item_type=' + item_type + '&item_id=' + item_ids; + parent.editDialogId = '
'; + + $(parent.editDialogId).dialog({ + modal: false, + dialogClass: 'playlistdialogstyle', + resizable: false, + draggable: false, + width: 300, + height: 100, + autoOpen: false, + open: function () { + closeplaylist = 1; + $(document).bind('click', overlayclickclose); + $(this).load(parent.contentUrl, function() { + $('#playlistdialog').focus(); + }); + }, + focus: function() { + closeplaylist = 0; + }, + close: function (e) { + $(document).unbind('click'); + $(this).empty(); + $(this).dialog("destroy"); + } + }); + + $("#playlistdialog").dialog("option", "position", [e.clientX + 10, e.clientY]); + $("#playlistdialog").dialog("open"); + closeplaylist = 0; +} + +function overlayclickclose() { + if (closeplaylist) { + $("#playlistdialog").dialog("close"); + } + closeplaylist = 1; +} + +function handlePlaylistAction(url, id) { + ajaxPut(url, id); + $("#playlistdialog").dialog("close"); +} + +/************************************************************/ +/* Dialog selection to start a broadcast */ +/************************************************************/ + +var closebroadcasts; +function showBroadcastsDialog(e) { + $("#broadcastsdialog").dialog("close"); + + var parent = this; + parent.contentUrl = jsAjaxServer + '/ajax.server.php?page=player&action=show_broadcasts'; + parent.editDialogId = '
'; + + $(parent.editDialogId).dialog({ + modal: false, + dialogClass: 'broadcastsdialogstyle', + resizable: false, + draggable: false, + width: 150, + height: 70, + autoOpen: false, + open: function () { + closebroadcasts = 1; + $(document).bind('click', broverlayclickclose); + $(this).load(parent.contentUrl, function() { + $('#broadcastsdialog').focus(); + }); + }, + focus: function() { + closebroadcasts = 0; + }, + close: function (e) { + $(document).unbind('click'); + $(this).empty(); + $(this).dialog("destroy"); + } + }); + + $("#broadcastsdialog").dialog("option", "position", [e.clientX - 180, e.clientY]); + $("#broadcastsdialog").dialog("open"); + closebroadcasts = 0; +} + +function broverlayclickclose() { + if (closebroadcasts) { + $("#broadcastsdialog").dialog("close"); + } + closebroadcasts = 1; +} + +function handleBroadcastAction(url, id) { + ajaxPut(url, id); + $("#broadcastsdialog").dialog("close"); +} + +/***************************************************/ +/* Edit modal dialog for artists, albums and songs */ +/***************************************************/ + +var tag_choices = undefined; + +function showEditDialog(edit_type, edit_id, edit_form_id, edit_title, refresh_row_prefix, refresh_action) { + var parent = this; + parent.editFormId = 'form#' + edit_form_id; + parent.contentUrl = jsAjaxServer + '/show_edit.server.php?action=show_edit_object&id=' + edit_id + '&type=' + edit_type; + parent.saveUrl = jsAjaxUrl + '?action=edit_object&id=' + edit_id + '&type=' + edit_type; + parent.editDialogId = '
'; + parent.refreshRowPrefix = refresh_row_prefix; + parent.refreshAction = refresh_action; + parent.editId = edit_id; + + // Convert choices string ("tag0,tag1,tag2,...") to choices array + parent.editTagChoices = new Array(); + if (tag_choices == undefined && tag_choices != '') { + // Load tag map + $.ajax(jsAjaxServer + '/ajax.server.php?page=tag&action=get_tag_map', { success: function(data) { + tag_choices = $(data).find('content').text(); + if (tag_choices != '') { + showEditDialog(edit_type, edit_id, edit_form_id, edit_title, refresh_row_prefix, refresh_action); + } + }, type: 'post', dataType: 'xml' }); + return; + } + var splitted = tag_choices.split(','); + var i; + for (i = 0; i < splitted.length; ++i) { + parent.editTagChoices.push($.trim(splitted[i])); + } + + parent.dialog_buttons = {}; + this.dialog_buttons[jsSaveTitle] = function() { + $.ajax({ + url : parent.saveUrl, + type : 'POST', + data : $(parent.editFormId).serializeArray(), + success : function(resp){ + $("#editdialog").dialog("close"); + + if (parent.refreshAction != '') { + var new_id = $.trim(resp.lastChild.textContent); + + // resp should contain the new identifier, otherwise we take the same as the edited item + if (new_id == '') { + new_id = parent.editId; + } + + var url = jsAjaxServer + '/refresh_updated.server.php?action=' + parent.refreshAction + '&id=' + new_id; + // Reload only table + $('#' + parent.refreshRowPrefix + parent.editId).load(url, function() { + // Update the current row identifier with new id + $('#' + parent.refreshRowPrefix + parent.editId).attr("id", parent.refreshRowPrefix + new_id); + }); + } else { + location.reload(true); + } + }, + error : function(resp){ + $("#editdialog").dialog("close"); + } + }); + } + this.dialog_buttons[jsCancelTitle] = function() { + $("#editdialog").dialog("close"); + } + + $(parent.editDialogId).dialog({ + title: edit_title, + modal: true, + dialogClass: 'editdialogstyle', + resizable: false, + width: 666, + autoOpen: false, + show: { effect: "fade", duration: 400 }, + open: function () { + $(this).load(parent.contentUrl, function() { + $(this).dialog('option', 'position', 'center'); + + if ($('#edit_tags').length > 0) { + $("#edit_tags").tagit({ + allowSpaces: true, + singleField: true, + singleFieldDelimiter: ',', + availableTags: parent.editTagChoices + }); + } + }); + }, + close: function (e) { + $(this).empty(); + $(this).dialog("destroy"); + }, + buttons: dialog_buttons + }); + + $("#editdialog").dialog("open"); +} + +$(window).resize(function() { + $("#editdialog").dialog("option", "position", ['center', 'center']); +}); + +function check_inline_song_edit(type, song) { + var source = '#' + type + '_select_' + song; + if ($(source + ' option:selected').val() == -1) { + $(source).replaceWith(''); + } +} + +/*********************/ +/* Sortable table */ +/*********************/ + +$(document).ready(function () { + + var eles = $("tbody[id^='sortableplaylist_']"); + if (eles != null) { + var len = eles.length; + for (var i = 0; i < len; i++) { + $('#' + eles[i].id).sortable({ + axis: 'y', + delay: 200 + }); + } + } +}); + +function submitNewItemsOrder(itemId, tableid, rowPrefix, updateUrl, refreshAction) { + var parent = this; + parent.itemId = itemId; + parent.refreshAction = refreshAction; + + var table = document.getElementById(tableid); + var rowLength = table.rows.length; + var finalOrder = ''; + + for (var i = 0; i < rowLength; ++i) { + var row = table.rows[i]; + if (row.id != '') { + var songid = row.id.replace(rowPrefix, ''); + finalOrder += songid + ";" + } + } + + if (finalOrder != '') { + $.ajax({ + url : updateUrl, + type : 'GET', + data : 'order=' + finalOrder, + success : function(resp){ + var url = jsAjaxServer + '/refresh_reordered.server.php?action=' + parent.refreshAction + '&id=' + parent.itemId; + // Reload only table + $('#reordered_list_' + parent.itemId).load(url, function() { + $('#sortableplaylist_' + parent.itemId).sortable({ + axis: 'y', + delay: 200 + }); + }); + } + }); + } +} diff --git a/sources/lib/log.lib.php b/sources/lib/log.lib.php new file mode 100644 index 0000000..0f27939 --- /dev/null +++ b/sources/lib/log.lib.php @@ -0,0 +1,155 @@ + $event_description \n"; + + // Do the deed + $log_write = error_log($log_line, 3, $log_filename); + + if (!$log_write) { + echo "Warning: Unable to write to log ($log_filename) Please check your log_path variable in ampache.cfg.php"; + } + +} // log_event + +/* + * ampache_error_handler + * + * An error handler for ampache that traps as many errors as it can and logs + * them. + */ +function ampache_error_handler($errno, $errstr, $errfile, $errline) +{ + $level = 1; + + switch ($errno) { + case E_WARNING: + $error_name = 'Runtime Error'; + break; + case E_COMPILE_WARNING: + case E_NOTICE: + case E_CORE_WARNING: + $error_name = 'Warning'; + $level = 6; + break; + case E_ERROR: + $error_name = 'Fatal run-time Error'; + break; + case E_PARSE: + $error_name = 'Parse Error'; + break; + case E_CORE_ERROR: + $error_name = 'Fatal Core Error'; + break; + case E_COMPILE_ERROR: + $error_name = 'Zend run-time Error'; + break; + case E_STRICT: + $error_name = "Strict Error"; + break; + default: + $error_name = "Error"; + $level = 2; + break; + } // end switch + + // List of things that should only be displayed if they told us to turn + // on the firehose + $ignores = array( + // We know var is deprecated, shut up + 'var: Deprecated. Please use the public/private/protected modifiers', + // getid3 spews errors, yay! + 'getimagesize() [', + 'Non-static method getid3', + 'Assigning the return value of new by reference is deprecated', + // The XML-RPC lib is broken (kinda) + 'used as offset, casting to integer' + ); + + foreach ($ignores as $ignore) { + if (strpos($errstr, $ignore) !== false) { + $error_name = 'Ignored ' . $error_name; + $level = 7; + } + } + + if (error_reporting() == 0) { + // Ignored, probably via @. But not really, so use the super-sekrit level + $level = 7; + } + + if (strpos($errstr, 'date.timezone') !== false) { + $error_name = 'Warning'; + $errstr = 'You have not set a valid timezone (date.timezone) in your php.ini file. This may cause display issues with dates. This warning is non-critical and not caused by Ampache.'; + } + + $log_line = "[$error_name] $errstr in file $errfile($errline)"; + debug_event('PHP', $log_line, $level, '', 'ampache'); +} + +/** + * debug_event + * This function is called inside ampache, it's actually a wrapper for the + * log_event. It checks config for debug and debug_level and only + * calls log event if both requirements are met. + */ +function debug_event($type, $message, $level, $file = '', $username = '') +{ + if (!AmpConfig::get('debug') || $level > AmpConfig::get('debug_level')) { + return false; + } + + if (!$username && isset($GLOBALS['user'])) { + $username = $GLOBALS['user']->username; + } + + // If the message is multiple lines, make multiple log lines + foreach (explode("\n", $message) as $line) { + log_event($username, $type, $line, $file); + } + +} // debug_event diff --git a/sources/lib/login.php b/sources/lib/login.php new file mode 100644 index 0000000..f9ef14d --- /dev/null +++ b/sources/lib/login.php @@ -0,0 +1,192 @@ +disabled) { + $auth['success'] = false; + Error::add('general', T_('User Disabled please contact Admin')); + debug_event('Login', scrub_out($username) . ' is disabled and attempted to login', '1'); + } // if user disabled + elseif (AmpConfig::get('prevent_multiple_logins')) { + $session_ip = $user->is_logged_in(); + $current_ip = inet_pton($_SERVER['REMOTE_ADDR']); + if ($current_ip && ($current_ip != $session_ip)) { + $auth['success'] = false; + Error::add('general', T_('User Already Logged in')); + debug_event('Login', scrub_out($username) . ' is already logged in from ' . $session_ip . ' and attempted to login from ' . $current_ip, '1'); + } // if logged in multiple times + } // if prevent multiple logins + elseif (AmpConfig::get('auto_create') && $auth['success'] && + ! $user->username) { + /* This is run if we want to autocreate users who don't + exist (useful for non-mysql auth) */ + $access = AmpConfig::get('auto_user') + ? User::access_name_to_level(AmpConfig::get('auto_user')) + : '5'; + $name = $auth['name']; + $email = $auth['email']; + $website = $auth['website']; + + /* Attempt to create the user */ + if (User::create($username, $name, $email, $website, + hash('sha256', mt_rand()), $access)) { + $user = User::get_from_username($username); + } else { + $auth['success'] = false; + Error::add('general', T_('Unable to create local account')); + } + } // End if auto_create + + // This allows stealing passwords validated by external means + // such as LDAP + if (AmpConfig::get('auth_password_save') && $auth['success'] && isset($password)) { + $user->update_password($password); + } +} + +/* If the authentication was a success */ +if (isset($auth) && $auth['success'] && isset($user)) { + // $auth->info are the fields specified in the config file + // to retrieve for each user + Session::create($auth); + + // Not sure if it was me or php tripping out, + // but naming this 'user' didn't work at all + $_SESSION['userdata'] = $auth; + + // Record the IP of this person! + if (AmpConfig::get('track_user_ip')) { + $user->insert_ip_history(); + } + + // Update data from this auth if ours are empty + if (empty($user->fullname) && !empty($auth['name'])) { + $user->update_fullname($auth['name']); + } + if (empty($user->email) && !empty($auth['email'])) { + $user->update_email($auth['email']); + } + if (empty($user->website) && !empty($auth['website'])) { + $user->update_website($auth['website']); + } + + // If an admin, check for update + if (AmpConfig::get('autoupdate') && Access::check('interface','100')) { + AutoUpdate::is_update_available(true); + } + + /* Make sure they are actually trying to get to this site and don't try + * to redirect them back into an admin section + */ + $web_path = AmpConfig::get('web_path'); + if ((substr($_POST['referrer'], 0, strlen($web_path)) == $web_path) && + strpos($_POST['referrer'], 'install.php') === false && + strpos($_POST['referrer'], 'login.php') === false && + strpos($_POST['referrer'], 'logout.php') === false && + strpos($_POST['referrer'], 'update.php') === false && + strpos($_POST['referrer'], 'activate.php') === false && + strpos($_POST['referrer'], 'admin') === false ) { + + header('Location: ' . $_POST['referrer']); + exit(); + } // if we've got a referrer + header('Location: ' . AmpConfig::get('web_path') . '/index.php'); + exit(); +} // auth success diff --git a/sources/lib/preferences.php b/sources/lib/preferences.php new file mode 100644 index 0000000..b5801c4 --- /dev/null +++ b/sources/lib/preferences.php @@ -0,0 +1,345 @@ + $r['id'], 'name' => $r['name'],'type' => $r['type']); + } // end collecting keys + + /* Foreach through possible keys and assign them */ + foreach ($results as $data) { + /* Get the Value from POST/GET var called $data */ + $name = $data['name']; + $apply_to_all = 'check_' . $data['name']; + $new_level = 'level_' . $data['name']; + $id = $data['id']; + $value = scrub_in($_REQUEST[$name]); + + /* Some preferences require some extra checks to be performed */ + switch ($name) { + case 'sample_rate': + $value = Stream::validate_bitrate($value); + break; + default: + break; + } + + if (preg_match('/_pass$/', $name)) { + if ($value == '******') { unset($_REQUEST[$name]); } else if (preg_match('/md5_pass$/', $name)) { + $value = md5($value); + } + } + + /* Run the update for this preference only if it's set */ + if (isset($_REQUEST[$name])) { + Preference::update($id,$pref_id,$value,$_REQUEST[$apply_to_all]); + } + + if (Access::check('interface','100') AND $_REQUEST[$new_level]) { + Preference::update_level($id,$_REQUEST[$new_level]); + } + + } // end foreach preferences + + // Now that we've done that we need to invalidate the cached preverences + Preference::clear_from_session(); + +} // update_preferences + +/** + * update_preference + * This function updates a single preference and is called by the update_preferences function + */ +function update_preference($user_id,$name,$pref_id,$value) +{ + $apply_check = "check_" . $name; + $level_check = "level_" . $name; + + /* First see if they are an administrator and we are applying this to everything */ + if ($GLOBALS['user']->has_access(100) AND make_bool($_REQUEST[$apply_check])) { + Preference::update_all($pref_id,$value); + return true; + } + + /* Check and see if they are an admin and the level def is set */ + if ($GLOBALS['user']->has_access(100) AND make_bool($_REQUEST[$level_check])) { + Preference::update_level($pref_id,$_REQUEST[$level_check]); + } + + /* Else make sure that the current users has the right to do this */ + if (Preference::has_access($name)) { + $sql = "UPDATE `user_preference` SET `value` = ? WHERE `preference` = ? AND `user` = ?"; + Dba::write($sql, array($value, $pref_id, $user_id)); + return true; + } + + return false; + +} // update_preference + +/** + * create_preference_input + * takes the key and then creates the correct type of input for updating it + */ +function create_preference_input($name,$value) +{ + if (!Preference::has_access($name)) { + if ($value == '1') { + echo "Enabled"; + } elseif ($value == '0') { + echo "Disabled"; + } else { + if (preg_match('/_pass$/', $name)) { + echo "******"; + } else { + echo $value; + } + } + return; + } // if we don't have access to it + + switch ($name) { + case 'display_menu': + case 'download': + case 'quarantine': + case 'upload': + case 'access_list': + case 'lock_songs': + case 'xml_rpc': + case 'force_http_play': + case 'no_symlinks': + case 'use_auth': + case 'access_control': + case 'allow_stream_playback': + case 'allow_democratic_playback': + case 'allow_localplay_playback': + case 'demo_mode': + case 'condPL': + case 'rio_track_stats': + case 'rio_global_stats': + case 'direct_link': + case 'iframes': + case 'now_playing_per_user': + case 'show_played_times': + case 'song_page_title': + case 'subsonic_backend': + case 'plex_backend': + case 'webplayer_flash': + case 'webplayer_html5': + case 'allow_personal_info_now': + case 'allow_personal_info_recent': + case 'allow_personal_info_time': + case 'allow_personal_info_agent': + case 'ui_fixed': + case 'autoupdate': + case 'webplayer_confirmclose': + case 'webplayer_pausetabs': + case 'stream_beautiful_url': + case 'share': + case 'share_social': + case 'broadcast_by_default': + case 'album_group': + case 'topmenu': + $is_true = ''; + $is_false = ''; + if ($value == '1') { + $is_true = "selected=\"selected\""; } else { + $is_false = "selected=\"selected\""; + } + echo "\n"; + break; + case 'play_type': + $is_localplay = ''; + $is_democratic = ''; + $is_web_player = ''; + $is_stream = ''; + if ($value == 'localplay') { + $is_localplay = 'selected="selected"'; + } elseif ($value == 'democratic') { + $is_democratic = 'selected="selected"'; + } elseif ($value == 'web_player') { + $is_web_player = 'selected="selected"'; + } else { + $is_stream = "selected=\"selected\""; + } + echo "\n"; + break; + case 'playlist_type': + $var_name = $value . "_type"; + ${$var_name} = "selected=\"selected\""; + echo "\n"; + break; + case 'lang': + $languages = get_languages(); + echo '\n"; + break; + case 'localplay_controller': + $controllers = Localplay::get_controllers(); + echo "\n"; + break; + case 'localplay_level': + $is_user = ''; + $is_admin = ''; + $is_manager = ''; + if ($value == '25') { + $is_user = 'selected="selected"'; + } elseif ($value == '100') { + $is_admin = 'selected="selected"'; + } elseif ($value == '50') { + $is_manager = 'selected="selected"'; + } + echo "\n"; + break; + case 'theme_name': + $themes = get_themes(); + echo "\n"; + break; + case 'playlist_method': + ${$value} = ' selected="selected"'; + echo "\n"; + break; + case 'transcode': + ${$value} = ' selected="selected"'; + echo "\n"; + break; + case 'show_lyrics': + $is_true = ''; + $is_false = ''; + if ($value == '1') { + $is_true = "selected=\"selected\""; + } else { + $is_false = "selected=\"selected\""; + } + echo "\n"; + break; + case 'album_sort': + $is_sort_year_asc = ''; + $is_sort_year_desc = ''; + $is_sort_name_asc = ''; + $is_sort_name_desc = ''; + $is_sort_default = ''; + if ($value == 'year_asc') { + $is_sort_year_asc = 'selected="selected"'; + } elseif ($value == 'year_desc') { + $is_sort_year_desc = 'selected="selected"'; + } elseif ($value == 'name_asc') { + $is_sort_name_asc = 'selected="selected"'; + } elseif ($value == 'name_desc') { + $is_sort_name_desc = 'selected="selected"'; + } else { + $is_sort_default = 'selected="selected"'; + } + + echo "\n"; + break; + default: + if (preg_match('/_pass$/', $name)) { + echo ''; + } else { + echo ''; + } + break; + + } + +} // create_preference_input diff --git a/sources/lib/rating.lib.php b/sources/lib/rating.lib.php new file mode 100644 index 0000000..54d40f7 --- /dev/null +++ b/sources/lib/rating.lib.php @@ -0,0 +1,59 @@ +\n"; + + $sql = "SELECT `id`, `name`, `prefix`, `disk` FROM `album` ORDER BY `name`"; + $db_results = Dba::read($sql); + + while ($r = Dba::fetch_assoc($db_results)) { + $selected = ''; + $album_name = trim($r['prefix'] . " " . $r['name']); + if ($r['disk'] >= 1) { + $album_name .= ' [Disk ' . $r['disk'] . ']'; + } + if ($r['id'] == $album_id) { + $selected = "selected=\"selected\""; + } + + echo "\t\n"; + + } // end while + + if ($allow_add) { + // Append additional option to the end with value=-1 + echo "\t\n"; + } + + echo "\n"; + +} // show_album_select + +/** + * show_artist_select + * This is the same as show_album_select except it's *gasp* for artists! How + * inventive! + */ +function show_artist_select($name='artist', $artist_id=0, $allow_add=0, $song_id=0) +{ + static $artist_id_cnt = 0; + // Generate key to use for HTML element ID + if ($song_id) { + $key = "artist_select_" . $song_id; + } else { + $key = "artist_select_c" . ++$artist_id_cnt; + } + + echo "\n"; + +} // show_artist_select + +/** + * show_catalog_select + * Yet another one of these buggers. this shows a drop down of all of your + * catalogs. + */ +function show_catalog_select($name='catalog',$catalog_id=0,$style='') +{ + echo "\n"; + +} // show_catalog_select + +/** + * show_user_select + * This one is for users! shows a select/option statement so you can pick a user + * to blame + */ +function show_user_select($name,$selected='',$style='') +{ + echo "\n"; + +} // show_user_select + +/** + * show_playlist_select + * This one is for playlists! + */ +function show_playlist_select($name,$selected='',$style='') +{ + echo "\n"; + +} // show_playlist_select + +function xoutput_headers() +{ + $output = isset($_REQUEST['xoutput']) ? $_REQUEST['xoutput'] : 'xml'; + if ($output == 'xml') { + header("Content-type: text/xml; charset=" . AmpConfig::get('site_charset')); + header("Content-Disposition: attachment; filename=ajax.xml"); + } else { + header("Content-type: application/json; charset=" . AmpConfig::get('site_charset')); + } + + header("Expires: Tuesday, 27 Mar 1984 05:00:00 GMT"); + header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); + header("Cache-Control: no-store, no-cache, must-revalidate"); + header("Pragma: no-cache"); +} + +function xoutput_from_array($array, $callback = false, $type = '') +{ + $output = isset($_REQUEST['xoutput']) ? $_REQUEST['xoutput'] : 'xml'; + if ($output == 'xml') { + return xml_from_array($array, $callback, $type); + } elseif ($output == 'raw') { + $outputnode = $_REQUEST['xoutputnode']; + return $array[$outputnode]; + } else { + return json_from_array($array, $callback, $type); + } +} + +// FIXME: This should probably go in XML_Data +/** + * xml_from_array + * This takes a one dimensional array and creates a XML document from it. For + * use primarily by the ajax mojo. + */ +function xml_from_array($array, $callback = false, $type = '') +{ + $string = ''; + + // If we weren't passed an array then return + if (!is_array($array)) { return $string; } + + // The type is used for the different XML docs we pass + switch ($type) { + case 'itunes': + foreach ($array as $key=>$value) { + if (is_array($value)) { + $value = xoutput_from_array($value,1,$type); + $string .= "\t\t<$key>\n$value\t\t\n"; + } else { + if ($key == "key") { + $string .= "\t\t<$key>$value\n"; + } elseif (is_int($value)) { + $string .= "\t\t\t$key$value\n"; + } elseif ($key == "Date Added") { + $string .= "\t\t\t$key$value\n"; + } elseif (is_string($value)) { + /* We need to escape the value */ + $string .= "\t\t\t$key\n"; + } + } + + } // end foreach + + return $string; + case 'xspf': + foreach ($array as $key=>$value) { + if (is_array($value)) { + $value = xoutput_from_array($value,1,$type); + $string .= "\t\t<$key>\n$value\t\t\n"; + } else { + if ($key == "key") { + $string .= "\t\t<$key>$value\n"; + } elseif (is_numeric($value)) { + $string .= "\t\t\t<$key>$value\n"; + } elseif (is_string($value)) { + /* We need to escape the value */ + $string .= "\t\t\t<$key>\n"; + } + } + + } // end foreach + + return $string; + default: + foreach ($array as $key => $value) { + // No numeric keys + if (is_numeric($key)) { + $key = 'item'; + } + + if (is_array($value)) { + // Call ourself + $value = xoutput_from_array($value, true); + $string .= "\t$value\n"; + } else { + /* We need to escape the value */ + $string .= "\t\n"; + } + // end foreach elements + } + if (!$callback) { + $string = '' . + "\n\n" . $string . "\n"; + } + + return UI::clean_utf8($string); + } +} // xml_from_array + +function json_from_array($array, $callback = false, $type = '') +{ + return json_encode($array); +} + +/** + * xml_get_header + * This takes the type and returns the correct xml header + */ +function xml_get_header($type) +{ + switch ($type) { + case 'itunes': + $header = "\n" . + "\n" . + "\n" . + "\n" . + " Major Version1\n" . + " Minor Version1\n" . + " Application Version7.0.2\n" . + " Features1\n" . + " Show Content Ratings\n" . + " Tracks\n" . + " \n"; + return $header; + case 'xspf': + $header = "\n" . + ""; + "\n ". + "Ampache XSPF Playlist\n" . + "" . AmpConfig::get('site_title') . "\n" . + "" . AmpConfig::get('site_title') . "\n" . + "". AmpConfig::get('web_path') ."\n" . + "\n\n\n\n"; + return $header; + default: + $header = "\n"; + return $header; + } +} //xml_get_header + +/** + * xml_get_footer + * This takes the type and returns the correct xml footer + */ +function xml_get_footer($type) +{ + switch ($type) { + case 'itunes': + $footer = " \n" . + "\n" . + "\n"; + return $footer; + case 'xspf': + $footer = " \n" . + "\n"; + return $footer; + default: + + break; + } +} // xml_get_footer + +/** + * toggle_visible + * This is identical to the javascript command that it actually calls + */ +function toggle_visible($element) +{ + echo '\n"; + +} // toggle_visible + +/** + * print_bool + * This function takes a boolean value and then prints out a friendly text + * message. + */ +function print_bool($value) +{ + if ($value) { + $string = '' . T_('On') . ''; + } else { + $string = '' . T_('Off') . ''; + } + + return $string; + +} // print_bool + +/** + * show_now_playing + * This shows the now playing templates and does some garbage collecion + * this should really be somewhere else + */ +function show_now_playing() +{ + Session::gc(); + Stream::gc_now_playing(); + + $web_path = AmpConfig::get('web_path'); + $results = Stream::get_now_playing(); + require_once AmpConfig::get('prefix') . '/templates/show_now_playing.inc.php'; + +} // show_now_playing diff --git a/sources/locale/ar_SA/LC_MESSAGES/messages.mo b/sources/locale/ar_SA/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..e1429eb Binary files /dev/null and b/sources/locale/ar_SA/LC_MESSAGES/messages.mo differ diff --git a/sources/locale/ar_SA/LC_MESSAGES/messages.po b/sources/locale/ar_SA/LC_MESSAGES/messages.po new file mode 100644 index 0000000..bfba37e --- /dev/null +++ b/sources/locale/ar_SA/LC_MESSAGES/messages.po @@ -0,0 +1,6214 @@ +# Arabic translations for Ampache package. +# Copyright (C) 2009 THE Ampache'S COPYRIGHT HOLDER +# This file is distributed under the same license as the Ampache package. +# momo-i , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: Ampache SVN\n" +"Report-Msgid-Bugs-To: translations@ampache.org\n" +"POT-Creation-Date: 2014-05-26 12:19+0200\n" +"PO-Revision-Date: 2009-05-19 07:38+0900\n" +"Last-Translator: momo-i \n" +"Language-Team: Arabic\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: ../../admin/access.php:40 +msgid "Deleted" +msgstr "" + +#: ../../admin/access.php:40 +msgid "Your Access List Entry has been removed" +msgstr "" + +#: ../../admin/access.php:45 ../../admin/users.php:173 +msgid "Deletion Request" +msgstr "" + +#: ../../admin/access.php:45 +msgid "Are you sure you want to permanently delete" +msgstr "" + +#: ../../admin/access.php:72 ../../lib/class/search.class.php:322 +#: ../../templates/show_song.inc.php:101 +msgid "Added" +msgstr "" + +#: ../../admin/access.php:72 +msgid "Your new Access Control List(s) have been created" +msgstr "" + +#: ../../admin/access.php:86 ../../lib/class/catalog.class.php:889 +#: ../../lib/class/search.class.php:329 ../../preferences.php:120 +msgid "Updated" +msgstr "" + +#: ../../admin/access.php:86 +msgid "Access List Entry updated" +msgstr "" + +#: ../../admin/catalog.php:48 ../../admin/catalog.php:68 +#: ../../admin/catalog.php:94 ../../admin/catalog.php:167 +msgid "Catalog Updated" +msgstr "" + +#: ../../admin/catalog.php:111 +msgid "Catalog Deleted" +msgstr "" + +#: ../../admin/catalog.php:111 +msgid "The Catalog and all associated records have been deleted" +msgstr "" + +#: ../../admin/catalog.php:117 +msgid "Catalog Delete" +msgstr "" + +#: ../../admin/catalog.php:117 ../../admin/users.php:181 +#: ../../broadcast.php:34 ../../channel.php:69 ../../share.php:94 +msgid "Confirm Deletion Request" +msgstr "" + +#: ../../admin/catalog.php:128 +#, fuzzy +msgid " Song Enabled" +msgid_plural " Songs Enabled" +msgstr[0] "عنوان الأغنية" +msgstr[1] "عنوان الأغنية" + +#: ../../admin/catalog.php:130 +#, fuzzy +msgid "No Disabled Songs selected" +msgstr "بحث المعاقين أغاني" + +#: ../../admin/catalog.php:133 +#, fuzzy +msgid " Disabled Song Processed" +msgid_plural " Disabled Songs Processed" +msgstr[0] "بحث المعاقين أغاني" +msgstr[1] "بحث المعاقين أغاني" + +#: ../../admin/catalog.php:154 +msgid "Catalog Cleaned" +msgstr "" + +#: ../../admin/catalog.php:190 +msgid "Done." +msgstr "" + +#: ../../admin/catalog.php:199 +msgid "Error: Please select a catalog type" +msgstr "" + +#: ../../admin/catalog.php:203 +#, fuzzy +msgid "Error: Name not specified" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../admin/catalog.php:226 ../../admin/catalog.php:227 +msgid "Catalog Created" +msgstr "" + +#: ../../admin/catalog.php:242 +msgid "Catalog statistics cleared" +msgstr "" + +#: ../../admin/catalog.php:252 +msgid "Now Playing Cleared" +msgstr "" + +#: ../../admin/catalog.php:252 +msgid "All now playing data has been cleared" +msgstr "" + +#: ../../admin/catalog.php:262 +msgid "No Disabled songs found" +msgstr "" + +#: ../../admin/catalog.php:271 +msgid "Delete Catalog" +msgstr "" + +#: ../../admin/catalog.php:271 +msgid "Do you really want to delete this catalog?" +msgstr "" + +#: ../../admin/catalog.php:292 +msgid "Album Art Search Finished" +msgstr "" + +#: ../../admin/mail.php:60 +msgid "E-mail Sent" +msgstr "" + +#: ../../admin/mail.php:61 +msgid "Your E-mail was successfully sent." +msgstr "" + +#: ../../admin/mail.php:63 +msgid "E-mail Not Sent" +msgstr "" + +#: ../../admin/mail.php:64 +msgid "Your E-mail was not sent." +msgstr "" + +#: ../../admin/modules.php:38 +msgid "Install Failed, Controller Error" +msgstr "" + +#: ../../admin/modules.php:57 +#, fuzzy +msgid "Install Failed, Catalog Error" +msgstr "إحصائيات واضحة" + +#: ../../admin/modules.php:66 +msgid "Plugin Installed" +msgstr "" + +#: ../../admin/modules.php:73 ../../admin/modules.php:80 +#: ../../admin/modules.php:142 +msgid "Are you sure you want to remove this plugin?" +msgstr "" + +#: ../../admin/modules.php:92 ../../admin/modules.php:109 +#: ../../admin/modules.php:161 +msgid "Plugin Deactivated" +msgstr "" + +#: ../../admin/modules.php:101 +msgid "Uninstall Failed, Catalog Error" +msgstr "" + +#: ../../admin/modules.php:124 +msgid "Unable to Install Plugin" +msgstr "" + +#: ../../admin/modules.php:135 +msgid "Plugin Activated" +msgstr "" + +#: ../../admin/modules.php:176 +msgid "Plugin Upgraded" +msgstr "" + +#: ../../admin/modules.php:182 +msgid "Plugins" +msgstr "" + +#: ../../admin/modules.php:188 +msgid "Localplay Controllers" +msgstr "" + +#: ../../admin/modules.php:194 +#, fuzzy +msgid "Catalog Types" +msgstr "نوع التسويقي" + +#: ../../admin/shout.php:37 +msgid "Shoutbox Post Updated" +msgstr "" + +#: ../../admin/shout.php:49 +msgid "Shoutbox Post Deleted" +msgstr "" + +#: ../../admin/system.php:47 +msgid "Database Charset Updated" +msgstr "" + +#: ../../admin/system.php:47 +msgid "" +"Your Database and associated tables have been updated to match your " +"currently configured charset" +msgstr "" + +#: ../../admin/users.php:57 ../../admin/users.php:112 +#: ../../lib/class/user.class.php:492 +msgid "Error Username Required" +msgstr "" + +#: ../../admin/users.php:60 ../../admin/users.php:108 +#: ../../lib/class/user.class.php:496 +msgid "Error Passwords don't match" +msgstr "" + +#: ../../admin/users.php:89 +msgid "User Updated" +msgstr "" + +#: ../../admin/users.php:89 +msgid "updated" +msgstr "" + +#: ../../admin/users.php:117 ../../register.php:107 +msgid "Error Username already exists" +msgstr "" + +#: ../../admin/users.php:124 ../../register.php:136 +msgid "Error: Insert Failed" +msgstr "" + +#: ../../admin/users.php:134 ../../lib/class/democratic.class.php:158 +#: ../../templates/show_add_user.inc.php:82 +#: ../../templates/show_edit_user.inc.php:85 +#: ../../templates/show_preference_admin.inc.php:43 +#: ../../templates/show_preference_box.inc.php:62 +msgid "Guest" +msgstr "ضيف" + +#: ../../admin/users.php:134 ../../lib/class/democratic.class.php:161 +#: ../../lib/preferences.php:264 ../../templates/show_access_list.inc.php:55 +#: ../../templates/show_add_access.inc.php:42 +#: ../../templates/show_add_user.inc.php:83 +#: ../../templates/show_create_democratic.inc.php:42 +#: ../../templates/show_edit_access.inc.php:60 +#: ../../templates/show_edit_user.inc.php:86 +#: ../../templates/show_mail_users.inc.php:32 +#: ../../templates/show_manage_shoutbox.inc.php:28 +#: ../../templates/show_manage_shoutbox.inc.php:52 +#: ../../templates/show_missing_albums.inc.php:30 +#: ../../templates/show_preference_admin.inc.php:44 +#: ../../templates/show_preference_box.inc.php:63 +#: ../../templates/show_shared_objects.inc.php:28 +#: ../../templates/show_wanted_albums.inc.php:29 +msgid "User" +msgstr "مستخدم" + +#: ../../admin/users.php:134 ../../lib/class/democratic.class.php:170 +#: ../../lib/preferences.php:266 ../../templates/show_add_user.inc.php:86 +#: ../../templates/show_create_democratic.inc.php:45 +#: ../../templates/show_democratic_playlist.inc.php:62 +#: ../../templates/show_democratic_playlist.inc.php:109 +#: ../../templates/show_edit_user.inc.php:89 +#: ../../templates/show_mail_users.inc.php:33 +#: ../../templates/show_preference_admin.inc.php:45 +#: ../../templates/show_preference_box.inc.php:66 +#: ../../templates/sidebar.inc.php:33 +msgid "Admin" +msgstr "مشرف" + +#. HINT: %1 Username, %2 Access num +#: ../../admin/users.php:137 +msgid "New User Added" +msgstr "" + +#: ../../admin/users.php:137 +#, php-format +msgid "%1$s has been created with an access level of %2$s" +msgstr "" + +#: ../../admin/users.php:142 +msgid "User Enabled" +msgstr "" + +#: ../../admin/users.php:147 +msgid "User Disabled" +msgstr "" + +#: ../../admin/users.php:149 ../../templates/error_page.inc.php:49 +msgid "Error" +msgstr "" + +#: ../../admin/users.php:149 +msgid "Unable to Disabled last Administrator" +msgstr "" + +#: ../../admin/users.php:165 +msgid "User Deleted" +msgstr "" + +#: ../../admin/users.php:165 +#, php-format +msgid "%s has been Deleted" +msgstr "" + +#: ../../admin/users.php:167 +msgid "Delete Error" +msgstr "" + +#: ../../admin/users.php:167 +msgid "Unable to delete last Admin User" +msgstr "" + +#: ../../admin/users.php:174 +#, php-format +msgid "Are you sure you want to permanently delete %s?" +msgstr "" + +#: ../../admin/users.php:181 +msgid "User Avatar Delete" +msgstr "" + +#: ../../admin/users.php:195 +msgid "User Avater Deleted" +msgstr "" + +#: ../../admin/users.php:195 +msgid "User Avatar has been deleted" +msgstr "" + +#: ../../admin/users.php:201 ../../templates/show_edit_user.inc.php:109 +#, fuzzy +msgid "Generate new API Key" +msgstr "توليد ملف" + +#: ../../admin/users.php:201 +msgid "Confirm API Key Generation" +msgstr "" + +#: ../../admin/users.php:215 +msgid "API Key Generated" +msgstr "" + +#: ../../admin/users.php:215 +msgid "New user API Key has been generated." +msgstr "" + +#: ../../albums.php:33 +msgid "Album Art Cleared" +msgstr "" + +#: ../../albums.php:33 +msgid "Album Art information has been removed from the database" +msgstr "" + +#: ../../albums.php:40 ../../albums.php:57 ../../albums.php:133 +msgid "Album Art Not Located" +msgstr "" + +#: ../../albums.php:40 ../../albums.php:57 ../../albums.php:133 +msgid "" +"Album Art could not be located at this time. This may be due to write access " +"error, or the file is not received correctly." +msgstr "" + +#: ../../albums.php:53 ../../albums.php:84 +msgid "Album Art Inserted" +msgstr "" + +#: ../../artists.php:58 +msgid "Show Artists starting with" +msgstr "" + +#: ../../bin/catalog_update.inc:39 +#, fuzzy +msgid "- All Catalog Operations" +msgstr "كتالوج حفظ الإعدادات" + +#: ../../bin/catalog_update.inc:46 +#, fuzzy +msgid "- Catalog Clean" +msgstr "نوع التسويقي" + +#: ../../bin/catalog_update.inc:50 +#, fuzzy +msgid "- Catalog Verify" +msgstr "تحقق من الماضي" + +#: ../../bin/catalog_update.inc:54 +#, fuzzy +msgid "- Catalog Add" +msgstr "نوع التسويقي" + +#: ../../bin/catalog_update.inc:58 +#, fuzzy +msgid "- Catalog Art Gather" +msgstr "نوع التسويقي" + +#: ../../bin/catalog_update.inc:62 +#, fuzzy +msgid "- Playlist Import" +msgstr "نوع التشغيل" + +#: ../../bin/catalog_update.inc:84 +#, fuzzy +msgid "Starting Catalog Operations..." +msgstr "إعدادات %s" + +#: ../../bin/catalog_update.inc:97 ../../bin/print_tags.inc:43 +#, php-format +msgid "Reading: %s" +msgstr "" + +#: ../../bin/catalog_update.inc:102 +msgid "- Starting Clean - " +msgstr "" + +#: ../../bin/catalog_update.inc:110 +msgid "- Starting Verify - " +msgstr "" + +#: ../../bin/catalog_update.inc:118 +msgid "- Starting Add - " +msgstr "" + +#: ../../bin/catalog_update.inc:131 ../../templates/show_gather_art.inc.php:24 +msgid "Starting Album Art Search" +msgstr "بدءا من البحث في الفنون الألبوم" + +#: ../../bin/catalog_update.inc:157 +#, fuzzy +msgid "- Catalog Update -" +msgstr "نوع التسويقي" + +#: ../../bin/catalog_update.inc:159 +msgid "Usage: catalog_update.inc [CATALOG NAME] [-c|-v|-a|-g|-t|-i]" +msgstr "" + +#: ../../bin/catalog_update.inc:161 +msgid "Default behavior is to do all except playlist import" +msgstr "" + +#: ../../bin/catalog_update.inc:163 +#, fuzzy +msgid "Clean Catalogs" +msgstr "إحصائيات واضحة" + +#: ../../bin/catalog_update.inc:165 +msgid "Verify Catalogs" +msgstr "" + +#: ../../bin/catalog_update.inc:167 +msgid "Add to Catalogs" +msgstr "" + +#: ../../bin/catalog_update.inc:169 +#, fuzzy +msgid "Import Playlists" +msgstr "استيراد قائمة التشغيل" + +#: ../../bin/catalog_update.inc:171 +#: ../../templates/show_catalog_row.inc.php:38 +msgid "Gather Art" +msgstr "" + +#: ../../bin/channel_run.inc:44 +msgid "- Channel " +msgstr "" + +#: ../../bin/channel_run.inc:47 ../../bin/websocket_run.inc:42 +msgid "- Verbose" +msgstr "" + +#: ../../bin/channel_run.inc:52 ../../bin/websocket_run.inc:47 +msgid "- Port " +msgstr "" + +#: ../../bin/channel_run.inc:69 +#, fuzzy +msgid "Starting Channel..." +msgstr "إعدادات %s" + +#: ../../bin/channel_run.inc:73 +#, fuzzy +msgid "Unknown channel." +msgstr "غير معروف" + +#: ../../bin/channel_run.inc:88 +msgid "Found available port " +msgstr "" + +#: ../../bin/channel_run.inc:105 +msgid "Listening on " +msgstr "" + +#: ../../bin/channel_run.inc:474 +msgid "- Channel Listening -" +msgstr "" + +#: ../../bin/channel_run.inc:476 +msgid "Usage: channel_run.inc [-c {CHANNEL ID}|-p {PORT}|-v]" +msgstr "" + +#: ../../bin/channel_run.inc:479 +msgid "Channel id to start" +msgstr "" + +#: ../../bin/channel_run.inc:481 +msgid "Listening port, default get an available port automatically" +msgstr "" + +#: ../../bin/channel_run.inc:483 ../../bin/websocket_run.inc:74 +msgid "Verbose" +msgstr "" + +#: ../../bin/delete_disabled.inc:37 +msgid "DEBUG ENABLED WILL NOT DELETE FILES!" +msgstr "" + +#: ../../bin/delete_disabled.inc:45 +#, php-format +msgid "Would Delete: %s" +msgstr "" + +#: ../../bin/delete_disabled.inc:49 +#, php-format +msgid "Deleting: %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:42 +msgid "ERROR: Iconv required for this functionality, quiting" +msgstr "" + +#: ../../bin/fix_filenames.inc:51 +#, php-format +msgid "%s For the Love of Music" +msgstr "" + +#: ../../bin/fix_filenames.inc:52 +msgid "Testing Basic Translation, the two strings below should look the same" +msgstr "" + +#: ../../bin/fix_filenames.inc:54 +msgid "Original: For the Love of Music" +msgstr "" + +#: ../../bin/fix_filenames.inc:56 +#, php-format +msgid "Translated: %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:59 +#, php-format +msgid "Input Charset (%s):" +msgstr "" + +#: ../../bin/fix_filenames.inc:63 +#, php-format +msgid "Using %s as source character set" +msgstr "" + +#: ../../bin/fix_filenames.inc:72 +#, php-format +msgid "Checking %s (%s)" +msgstr "" + +#: ../../bin/fix_filenames.inc:78 +msgid "Finished checking filenames for valid chacters" +msgstr "" + +#: ../../bin/fix_filenames.inc:104 +#, php-format +msgid "ERROR: Unable to open %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:110 +#, php-format +msgid "ERROR: Unable to chdir to %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:133 +msgid "Translation failure, stripping non-valid characters" +msgstr "" + +#: ../../bin/fix_filenames.inc:138 +#, php-format +msgid "Attempting to Transcode to %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:141 +#, php-format +msgid "OLD: %s has invalid chars" +msgstr "" + +#: ../../bin/fix_filenames.inc:143 +#, php-format +msgid "NEW: %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:147 +msgid "Rename File (Y/N):" +msgstr "" + +#: ../../bin/fix_filenames.inc:150 +msgid "Not Renaming..." +msgstr "" + +#: ../../bin/fix_filenames.inc:182 +#, fuzzy, php-format +msgid "Error: Unable to create %s move failed, stopping" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../bin/fix_filenames.inc:194 +msgid "Error: Copy Failed, not deleteing old file" +msgstr "" + +#: ../../bin/fix_filenames.inc:203 ../../bin/sort_files.inc:289 +#, php-format +msgid "Error: Size Inconsistency, not deleting %s" +msgstr "" + +#: ../../bin/fix_filenames.inc:210 ../../bin/sort_files.inc:296 +#, fuzzy, php-format +msgid "Error: Unable to delete %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../bin/fix_filenames.inc:212 +msgid "File Moved..." +msgstr "" + +#: ../../bin/install/add_user.inc:50 +#, php-format +msgid "Created %s user %s with password %s" +msgstr "" + +#: ../../bin/install/add_user.inc:54 +msgid "User creation failed" +msgstr "" + +#: ../../bin/install/install_db.inc:68 +#, fuzzy +msgid "Existing Ampache installation found." +msgstr "Ampache التثبيت." + +#: ../../bin/install/install_db.inc:70 +msgid "Force specified, proceeding anyway." +msgstr "" + +#: ../../bin/install/install_db.inc:73 +msgid "Exiting." +msgstr "" + +#: ../../bin/install/install_db.inc:89 +msgid "Database creation failed" +msgstr "" + +#: ../../bin/install/install_db.inc:101 +msgid "Config file creation failed" +msgstr "" + +#: ../../bin/install/update_db.inc:44 +msgid "The following updates need to be performed:" +msgstr "" + +#: ../../bin/migrate_config.inc:50 +msgid "Parsing old config file..." +msgstr "" + +#: ../../bin/migrate_config.inc:80 +msgid "Parse complete, writing" +msgstr "" + +#: ../../bin/migrate_config.inc:88 +msgid "Write success, config migrated" +msgstr "" + +#: ../../bin/migrate_config.inc:92 +msgid "Access Denied, config migration failed" +msgstr "" + +#: ../../bin/print_tags.inc:37 +msgid "File not found." +msgstr "" + +#: ../../bin/print_tags.inc:58 +#, php-format +msgid "Using: %s AND %s for file pattern matching" +msgstr "" + +#: ../../bin/print_tags.inc:67 +msgid "Raw results:" +msgstr "" + +#: ../../bin/print_tags.inc:71 +#, php-format +msgid "Final results seen by Ampache using %s:" +msgstr "" + +#: ../../bin/print_tags.inc:78 +#, fuzzy, php-format +msgid "%s Version %s" +msgstr "النسخة" + +#: ../../bin/print_tags.inc:80 +#, fuzzy +msgid "Usage:" +msgstr "مستخدم" + +#: ../../bin/print_tags.inc:82 +msgid "php print_tags.inc " +msgstr "" + +#: ../../bin/sort_files.inc:61 +#, fuzzy, php-format +msgid "Starting Catalog: %s" +msgstr "إعدادات %s" + +#: ../../bin/sort_files.inc:76 +msgid "Moving File..." +msgstr "" + +#: ../../bin/sort_files.inc:78 +#, php-format +msgid "Source: %s" +msgstr "" + +#: ../../bin/sort_files.inc:80 +#, php-format +msgid "Dest: %s" +msgstr "" + +#: ../../bin/sort_files.inc:230 +#, php-format +msgid "Making %s Directory" +msgstr "" + +#: ../../bin/sort_files.inc:237 +#, fuzzy, php-format +msgid "Error: Unable to create %s move failed" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#. HINT: %1$s: file, %2$s: directory +#: ../../bin/sort_files.inc:250 +#, php-format +msgid "Copying %1$s to %2$s" +msgstr "" + +#: ../../bin/sort_files.inc:261 +#, php-format +msgid "Error: %s already exists" +msgstr "" + +#: ../../bin/sort_files.inc:282 +#, fuzzy, php-format +msgid "Error: Unable to copy file to %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../bin/websocket_run.inc:67 +msgid "- WebSocket server -" +msgstr "" + +#: ../../bin/websocket_run.inc:69 +msgid "Usage: websocket_run.inc [-p {PORT}|-v]" +msgstr "" + +#: ../../bin/websocket_run.inc:72 +msgid "Listening port, default 8100" +msgstr "" + +#: ../../bin/write_playlists.inc:38 +#, php-format +msgid "Error: Directory %s not writeable" +msgstr "" + +#: ../../bin/write_playlists.inc:59 +msgid "This will dump a collection of m3u playlists based on type" +msgstr "" + +#: ../../bin/write_playlists.inc:60 +#, fuzzy +msgid "Types:" +msgstr "نوع" + +#: ../../bin/write_playlists.inc:61 +msgid "Dumps all Albums as individual playlists" +msgstr "" + +#: ../../bin/write_playlists.inc:62 +msgid "Dumps all of your Playlists as m3u's" +msgstr "" + +#: ../../bin/write_playlists.inc:63 +msgid "Dumps all Artists as individual playlists" +msgstr "" + +#: ../../broadcast.php:34 +msgid "Broadcast Delete" +msgstr "" + +#: ../../broadcast.php:48 +msgid "Broadcast Deleted" +msgstr "" + +#: ../../broadcast.php:48 +msgid "The Broadcast has been deleted" +msgstr "" + +#: ../../browse.php:79 ../../lib/class/browse.class.php:202 +#: ../../templates/sidebar_home.inc.php:35 +msgid "Tag Cloud" +msgstr "الوسم الغيمة" + +#: ../../channel/index.php:75 ../../plex/web/init.php:38 +msgid "Unauthorized." +msgstr "" + +#: ../../channel.php:59 +msgid "Channel Created" +msgstr "" + +#: ../../channel.php:69 +#, fuzzy +msgid "Channel Delete" +msgstr "حذف" + +#: ../../channel.php:83 +msgid "Channel Deleted" +msgstr "" + +#: ../../channel.php:83 +msgid "The Channel has been deleted" +msgstr "" + +#: ../../democratic.php:57 +msgid "The Requested Playlist has been deleted." +msgstr "" + +#: ../../install.php:107 +msgid "Error: Ampache SQL Username or Password missing" +msgstr "" + +#: ../../install.php:163 +#, fuzzy +msgid "Error: Config file not found or unreadable" +msgstr "خطأ : لم يتم العثور على ملف أو غير صالح" + +#: ../../lib/class/access.class.php:110 ../../lib/class/access.class.php:114 +msgid "Invalid IPv4 / IPv6 Address Entered" +msgstr "" + +#: ../../lib/class/access.class.php:118 ../../lib/class/access.class.php:119 +msgid "IP Address Version Mismatch" +msgstr "" + +#: ../../lib/class/access.class.php:169 +msgid "Duplicate ACL defined" +msgstr "" + +#: ../../lib/class/access.class.php:387 ../../lib/class/access.class.php:407 +#: ../../lib/ui.lib.php:302 ../../templates/list_header.inc.php:148 +#: ../../templates/show_add_access.inc.php:38 +#: ../../templates/show_add_access.inc.php:55 +#: ../../templates/show_add_access.inc.php:61 +#: ../../templates/show_edit_access.inc.php:72 +#: ../../templates/show_export.inc.php:35 +#: ../../templates/show_mail_users.inc.php:31 +#: ../../templates/show_random.inc.php:48 +msgid "All" +msgstr "الكل" + +#: ../../lib/class/access.class.php:390 +#: ../../templates/show_add_access.inc.php:35 +#: ../../templates/show_edit_access.inc.php:69 +msgid "View" +msgstr "عرض" + +#: ../../lib/class/access.class.php:393 +#: ../../templates/show_add_access.inc.php:36 +#: ../../templates/show_edit_access.inc.php:70 +msgid "Read" +msgstr "يقرأ" + +#: ../../lib/class/access.class.php:396 +#: ../../templates/show_add_access.inc.php:37 +#: ../../templates/show_edit_access.inc.php:71 +msgid "Read/Write" +msgstr "قراءة وكتابة" + +#: ../../lib/class/access.class.php:422 +#: ../../templates/show_add_access.inc.php:53 +#: ../../templates/show_add_access.inc.php:54 +#: ../../templates/show_add_access.inc.php:55 +#: ../../templates/show_add_access.inc.php:67 +#: ../../templates/show_edit_access.inc.php:38 +msgid "API/RPC" +msgstr "" + +#: ../../lib/class/access.class.php:424 +#: ../../templates/show_add_access.inc.php:59 +#: ../../templates/show_add_access.inc.php:60 +#: ../../templates/show_add_access.inc.php:61 +#: ../../templates/show_add_access.inc.php:66 +#: ../../templates/show_edit_access.inc.php:37 +msgid "Local Network Definition" +msgstr "تعريف الشبكة المحلية" + +#: ../../lib/class/access.class.php:426 +#: ../../templates/show_add_access.inc.php:65 +#: ../../templates/show_edit_access.inc.php:36 +msgid "Web Interface" +msgstr "واجهة ويب" + +#: ../../lib/class/access.class.php:429 +#: ../../templates/show_add_access.inc.php:54 +#: ../../templates/show_add_access.inc.php:60 +#: ../../templates/show_add_access.inc.php:64 +#: ../../templates/show_edit_access.inc.php:35 +msgid "Stream Access" +msgstr "تيار الوصول" + +#: ../../lib/class/album.class.php:233 ../../lib/class/artist.class.php:357 +#: ../../templates/show_album_group_disks.inc.php:38 +#: ../../templates/show_album.inc.php:41 +#: ../../templates/show_lyrics.inc.php:33 +msgid "Unknown (Orphaned)" +msgstr "" + +#: ../../lib/class/album.class.php:427 +#: ../../lib/class/subsonic_xml_data.class.php:350 +#: ../../server/search.ajax.php:85 +#: ../../templates/show_album_group_disks.inc.php:99 +#: ../../templates/show_album.inc.php:28 +#: ../../templates/show_edit_album_row.inc.php:47 +#: ../../templates/show_song_previews.inc.php:36 +msgid "Disk" +msgstr "" + +#: ../../lib/class/album.class.php:440 ../../lib/class/browse.class.php:165 +#: ../../lib/ui.lib.php:158 ../../server/search.ajax.php:55 +#: ../../templates/header.inc.php:286 ../../templates/show_random.inc.php:29 +#: ../../templates/show_search.inc.php:30 +#: ../../templates/show_stats.inc.php:33 +#: ../../templates/sidebar_home.inc.php:34 +#: ../../templates/sidebar_home.inc.php:103 +msgid "Artists" +msgstr "الفنانون" + +#: ../../lib/class/album.class.php:440 ../../lib/class/album.class.php:441 +msgid "Various" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:70 +#: ../../lib/class/localplay.class.php:629 ../../templates/header.inc.php:36 +#: ../../templates/mainframes.inc.php:37 +#: ../../templates/show_localplay_status.inc.php:31 +#: ../../templates/show_now_playing.inc.php:33 +msgid "Now Playing" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:71 ../../templates/header.inc.php:37 +#: ../../templates/mainframes.inc.php:38 +#: ../../templates/show_recently_played.inc.php:24 +msgid "Recently Played" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:72 +msgid "Newest Albums" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:73 +msgid "Newest Artists" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:116 +msgid "RSS Feed" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "seconds ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "minutes ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "hours ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "days ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "weeks ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "months ago" +msgstr "" + +#: ../../lib/class/ampache_rss.class.php:189 +msgid "years ago" +msgstr "" + +#: ../../lib/class/ampconfig.class.php:78 +#, php-format +msgid "Trying to clobber '%s' without setting clobber" +msgstr "" + +#: ../../lib/class/api.class.php:129 +msgid "Login Failed: version too old" +msgstr "" + +#: ../../lib/class/api.class.php:157 +msgid "Login Failed: timestamp out of range" +msgstr "" + +#: ../../lib/class/api.class.php:168 ../../lib/class/api.class.php:236 +#, fuzzy +msgid "Invalid Username/Password" +msgstr "لا اسم المستخدم / كلمة السر المحددة" + +#: ../../lib/class/api.class.php:236 +msgid "Error Invalid Handshake - " +msgstr "" + +#: ../../lib/class/api.class.php:609 ../../lib/class/api.class.php:626 +#: ../../lib/class/api.class.php:644 +msgid "Access denied to this playlist." +msgstr "" + +#: ../../lib/class/api.class.php:731 ../../lib/class/api.class.php:792 +#: ../../server/xml.server.php:98 +msgid "Invalid Request" +msgstr "" + +#: ../../lib/class/api.class.php:752 ../../lib/class/api.class.php:770 +msgid "Media Object Invalid or Not Specified" +msgstr "" + +#: ../../lib/class/art.class.php:1008 +msgid "Error: Unable to open" +msgstr "" + +#: ../../lib/class/autoupdate.class.php:189 +msgid "Update available" +msgstr "" + +#: ../../lib/class/autoupdate.class.php:192 +msgid "See" +msgstr "" + +#: ../../lib/class/autoupdate.class.php:192 +#, fuzzy +msgid "changes" +msgstr "حفظ التغييرات" + +#: ../../lib/class/autoupdate.class.php:193 +msgid "or" +msgstr "" + +#: ../../lib/class/autoupdate.class.php:193 +#, fuzzy +msgid "download" +msgstr "تنزيل" + +#: ../../lib/class/broadcast.class.php:160 +#, fuzzy +msgid "Broadcast edit" +msgstr "الفنان والعنوان" + +#: ../../lib/class/broadcast.class.php:160 +#: ../../lib/class/channel.class.php:175 +#: ../../templates/show_access_list.inc.php:75 +#: ../../templates/show_album_group_disks.inc.php:128 +#: ../../templates/show_album.inc.php:129 +#: ../../templates/show_album_row.inc.php:80 +#: ../../templates/show_artist.inc.php:120 +#: ../../templates/show_artist_row.inc.php:62 +#: ../../templates/show_live_stream_row.inc.php:42 +#: ../../templates/show_playlist_row.inc.php:58 +#: ../../templates/show_shout_row.inc.php:31 +#: ../../templates/show_smartplaylist_row.inc.php:53 +#: ../../templates/show_song_row.inc.php:71 +#: ../../templates/show_tagcloud.inc.php:36 +#: ../../templates/show_user_row.inc.php:44 +msgid "Edit" +msgstr "تحرير" + +#: ../../lib/class/broadcast.class.php:161 +#: ../../lib/class/channel.class.php:176 ../../lib/class/share.class.php:191 +#: ../../templates/rightbar.inc.php:117 +#: ../../templates/show_access_list.inc.php:76 +#: ../../templates/show_catalog_row.inc.php:39 +#: ../../templates/show_democratic_playlist.inc.php:92 +#: ../../templates/show_edit_user.inc.php:99 +#: ../../templates/show_live_stream_row.inc.php:46 +#: ../../templates/show_localplay_instances.inc.php:40 +#: ../../templates/show_localplay_playlist.inc.php:50 +#: ../../templates/show_manage_democratic.inc.php:50 +#: ../../templates/show_playlist.inc.php:85 +#: ../../templates/show_playlist_row.inc.php:60 +#: ../../templates/show_playlist_song_row.inc.php:63 +#: ../../templates/show_shout_row.inc.php:34 +#: ../../templates/show_smartplaylist.inc.php:47 +#: ../../templates/show_smartplaylist_row.inc.php:55 +#: ../../templates/show_tagcloud.inc.php:40 +#: ../../templates/show_user_row.inc.php:54 +msgid "Delete" +msgstr "حذف" + +#: ../../lib/class/broadcast.class.php:169 +msgid "Broadcast" +msgstr "" + +#: ../../lib/class/broadcast.class.php:177 +msgid "Unbroadcast" +msgstr "" + +#: ../../lib/class/browse.class.php:150 ../../lib/class/browse.class.php:223 +#: ../../server/search.ajax.php:117 ../../templates/show_albums.inc.php:37 +#: ../../templates/show_albums.inc.php:79 +#: ../../templates/show_artists.inc.php:35 +#: ../../templates/show_artists.inc.php:74 +#: ../../templates/show_manage_democratic.inc.php:31 +#: ../../templates/show_random.inc.php:27 +#: ../../templates/show_recommended_artists.inc.php:32 +#: ../../templates/show_recommended_artists.inc.php:81 +#: ../../templates/show_search.inc.php:28 +#: ../../templates/show_stats.inc.php:34 ../../templates/show_stats.inc.php:70 +#: ../../templates/sidebar_home.inc.php:101 +msgid "Songs" +msgstr "الأغاني" + +#: ../../lib/class/browse.class.php:155 ../../lib/ui.lib.php:154 +#: ../../server/search.ajax.php:88 ../../templates/browse_filters.inc.php:63 +#: ../../templates/show_artist.inc.php:136 +#: ../../templates/show_artists.inc.php:36 +#: ../../templates/show_artists.inc.php:75 +#: ../../templates/show_random.inc.php:28 +#: ../../templates/show_recommended_artists.inc.php:33 +#: ../../templates/show_recommended_artists.inc.php:82 +#: ../../templates/show_search.inc.php:29 +#: ../../templates/show_stats.inc.php:32 +#: ../../templates/sidebar_home.inc.php:33 +#: ../../templates/sidebar_home.inc.php:102 +msgid "Albums" +msgstr "البوم" + +#: ../../lib/class/browse.class.php:161 +msgid "Manage Users" +msgstr "" + +#: ../../lib/class/browse.class.php:171 +#: ../../templates/sidebar_home.inc.php:42 +msgid "Radio Stations" +msgstr "محطات إذاعية" + +#: ../../lib/class/browse.class.php:176 ../../server/search.ajax.php:146 +#: ../../templates/header.inc.php:292 ../../templates/sidebar_home.inc.php:36 +#: ../../templates/sidebar_home.inc.php:104 +msgid "Playlists" +msgstr "التشغيل" + +#: ../../lib/class/browse.class.php:180 +msgid "Playlist Songs" +msgstr "" + +#: ../../lib/class/browse.class.php:184 +msgid "Current Playlist" +msgstr "" + +#: ../../lib/class/browse.class.php:189 +#: ../../templates/sidebar_home.inc.php:37 +#, fuzzy +msgid "Smart Playlists" +msgstr "استيراد قائمة التشغيل" + +#: ../../lib/class/browse.class.php:193 ../../templates/show_stats.inc.php:27 +#: ../../templates/sidebar_admin.inc.php:24 +msgid "Catalogs" +msgstr "" + +#: ../../lib/class/browse.class.php:197 +msgid "Shoutbox Records" +msgstr "" + +#: ../../lib/class/browse.class.php:207 ../../templates/show_search.inc.php:31 +#: ../../templates/show_stats.inc.php:35 ../../templates/show_stats.inc.php:71 +#: ../../templates/sidebar_home.inc.php:43 +#: ../../templates/sidebar_home.inc.php:105 +msgid "Videos" +msgstr "الفيديو" + +#: ../../lib/class/browse.class.php:211 +#: ../../templates/show_democratic.inc.php:23 +msgid "Democratic Playlist" +msgstr "" + +#: ../../lib/class/browse.class.php:215 +#, fuzzy +msgid "Wanted Albums" +msgstr "البوم" + +#: ../../lib/class/browse.class.php:219 +#: ../../templates/sidebar_home.inc.php:93 +msgid "Shared Objects" +msgstr "" + +#: ../../lib/class/browse.class.php:227 +#: ../../templates/sidebar_home.inc.php:38 +#, fuzzy +msgid "Channels" +msgstr "حفظ التغييرات" + +#: ../../lib/class/browse.class.php:231 +#: ../../templates/sidebar_home.inc.php:40 +msgid "Broadcasts" +msgstr "" + +#: ../../lib/class/browse.class.php:273 ../../templates/rightbar.inc.php:124 +#: ../../templates/show_recently_played.inc.php:158 +msgid "More" +msgstr "" + +#: ../../lib/class/catalog.class.php:342 ../../lib/class/catalog.class.php:345 +#: ../../lib/class/catalog.class.php:348 ../../lib/class/user.class.php:841 +#: ../../lib/preferences.php:291 ../../templates/show_user.inc.php:23 +#: ../../templates/show_users.inc.php:57 +msgid "Never" +msgstr "أبدا" + +#: ../../lib/class/catalog.class.php:393 +msgid "day" +msgid_plural "days" +msgstr[0] "" +msgstr[1] "" + +#: ../../lib/class/catalog.class.php:395 +msgid "hour" +msgid_plural "hours" +msgstr[0] "" +msgstr[1] "" + +#: ../../lib/class/catalog.class.php:431 +msgid "Catalog Insert Failed check debug logs" +msgstr "" + +#: ../../lib/class/catalog.class.php:896 +msgid "No Update Needed" +msgstr "" + +#: ../../lib/class/catalog.class.php:1051 +#, php-format +msgid "Catalog Clean Done. %d file removed." +msgid_plural "Catalog Clean Done. %d files removed." +msgstr[0] "" +msgstr[1] "" + +#: ../../lib/class/catalog.class.php:1076 +#, php-format +msgid "Catalog Verify Done. %d of %d files updated." +msgstr "" + +#: ../../lib/class/catalog.class.php:1225 +#, fuzzy +msgid "Failed to create playlist." +msgstr "إدارة التشغيل الديمقراطية" + +#: ../../lib/class/catalog.class.php:1242 +msgid "No valid songs found in playlist file." +msgstr "" + +#: ../../lib/class/channel.class.php:173 +#, fuzzy +msgid "Start Channel" +msgstr "حفظ التغييرات" + +#: ../../lib/class/channel.class.php:174 +msgid "Stop Channel" +msgstr "" + +#: ../../lib/class/channel.class.php:175 +msgid "Channel edit" +msgstr "" + +#: ../../lib/class/channel.class.php:265 +msgid "Running" +msgstr "" + +#: ../../lib/class/channel.class.php:267 +#: ../../lib/class/localplay.class.php:631 +msgid "Stopped" +msgstr "" + +#: ../../lib/class/democratic.class.php:153 ../../lib/class/video.class.php:92 +#: ../../templates/show_create_democratic.inc.php:36 +msgid "minutes" +msgstr "" + +#: ../../lib/class/democratic.class.php:154 +msgid "Primary" +msgstr "" + +#: ../../lib/class/democratic.class.php:164 +#: ../../templates/show_add_user.inc.php:84 +#: ../../templates/show_create_democratic.inc.php:43 +#: ../../templates/show_edit_user.inc.php:87 +#: ../../templates/show_preference_box.inc.php:64 +msgid "Content Manager" +msgstr "إدارة المحتوى" + +#: ../../lib/class/democratic.class.php:167 +#: ../../templates/show_add_user.inc.php:85 +#: ../../templates/show_create_democratic.inc.php:44 +#: ../../templates/show_edit_user.inc.php:88 +#: ../../templates/show_preference_box.inc.php:65 +msgid "Catalog Manager" +msgstr "المدير التسويقي" + +#: ../../lib/class/localplay.class.php:633 +msgid "Paused" +msgstr "" + +#: ../../lib/class/localplay.class.php:635 ../../lib/class/user.class.php:844 +#: ../../lib/general.lib.php:222 +#: ../../modules/localplay/mpd.controller.php:503 +#: ../../templates/show_user.inc.php:24 ../../templates/show_users.inc.php:58 +msgid "Unknown" +msgstr "غير معروف" + +#: ../../lib/class/playlist_object.abstract.php:47 +#: ../../templates/show_edit_playlist_row.inc.php:37 +#: ../../templates/show_edit_smartplaylist_row.inc.php:37 +msgid "Private" +msgstr "" + +#: ../../lib/class/query.class.php:78 +msgid "Browse not found or expired, try reloading the page" +msgstr "" + +#: ../../lib/class/radio.class.php:89 +msgid "Missing ID" +msgstr "" + +#: ../../lib/class/radio.class.php:93 ../../lib/class/radio.class.php:124 +#, fuzzy +msgid "Name Required" +msgstr "مطلوب" + +#: ../../lib/class/radio.class.php:101 +msgid "Invalid URL must be mms:// , https:// or http://" +msgstr "" + +#: ../../lib/class/radio.class.php:132 +msgid "Invalid URL must be http:// or https://" +msgstr "" + +#: ../../lib/class/radio.class.php:138 +#, fuzzy +msgid "Invalid Catalog" +msgstr "إحصائيات واضحة" + +#: ../../lib/class/registration.class.php:54 +#, php-format +msgid "New User Registration at %s" +msgstr "" + +#: ../../lib/class/registration.class.php:56 +#, php-format +msgid "" +"Thank you for registering\n" +"\n" +"\n" +"Please keep this e-mail for your records. Your account information is as " +"follows:\n" +"----------------------\n" +"Username: %s\n" +"----------------------\n" +"\n" +"Your account is currently inactive. You cannot use it until you've visited " +"the following link:\n" +"\n" +"%s\n" +"\n" +"Thank you for registering\n" +msgstr "" + +#: ../../lib/class/registration.class.php:76 +#, php-format +msgid "" +"A new user has registered\n" +"The following values were entered.\n" +"\n" +"Username: %s\n" +"Fullname: %s\n" +"E-mail: %s\n" +"Website: %s\n" +"\n" +msgstr "" + +#: ../../lib/class/search.class.php:60 +msgid "is greater than or equal to" +msgstr "" + +#: ../../lib/class/search.class.php:66 +msgid "is less than or equal to" +msgstr "" + +#: ../../lib/class/search.class.php:72 ../../lib/class/search.class.php:140 +#: ../../lib/class/search.class.php:159 ../../lib/class/search.class.php:172 +msgid "is" +msgstr "" + +#: ../../lib/class/search.class.php:78 ../../lib/class/search.class.php:165 +#: ../../lib/class/search.class.php:178 +msgid "is not" +msgstr "" + +#: ../../lib/class/search.class.php:84 +msgid "is greater than" +msgstr "" + +#: ../../lib/class/search.class.php:90 +msgid "is less than" +msgstr "" + +#: ../../lib/class/search.class.php:97 +msgid "is true" +msgstr "" + +#: ../../lib/class/search.class.php:102 +msgid "is false" +msgstr "" + +#: ../../lib/class/search.class.php:108 +msgid "contains" +msgstr "" + +#: ../../lib/class/search.class.php:116 +msgid "does not contain" +msgstr "" + +#: ../../lib/class/search.class.php:124 +#, fuzzy +msgid "starts with" +msgstr "يبدأ" + +#: ../../lib/class/search.class.php:132 +msgid "ends with" +msgstr "" + +#: ../../lib/class/search.class.php:146 +msgid "sounds like" +msgstr "" + +#: ../../lib/class/search.class.php:152 +msgid "does not sound like" +msgstr "" + +#: ../../lib/class/search.class.php:185 +msgid "before" +msgstr "" + +#: ../../lib/class/search.class.php:191 +msgid "after" +msgstr "" + +#: ../../lib/class/search.class.php:199 +msgid "Any searchable text" +msgstr "" + +#: ../../lib/class/search.class.php:206 ../../lib/class/search.class.php:383 +#: ../../templates/show_democratic_playlist.inc.php:57 +#: ../../templates/show_democratic_playlist.inc.php:104 +#: ../../templates/show_disabled_songs.inc.php:29 +#: ../../templates/show_disabled_songs.inc.php:55 +#: ../../templates/show_duplicate.inc.php:29 +#: ../../templates/show_edit_song_row.inc.php:27 +#: ../../templates/show_search_bar.inc.php:31 +#: ../../templates/show_song.inc.php:85 ../../templates/show_videos.inc.php:30 +#: ../../templates/show_videos.inc.php:59 +msgid "Title" +msgstr "العنوان" + +#: ../../lib/class/search.class.php:213 ../../templates/show_albums.inc.php:34 +#: ../../templates/show_albums.inc.php:76 +#: ../../templates/show_democratic_playlist.inc.php:58 +#: ../../templates/show_democratic_playlist.inc.php:105 +#: ../../templates/show_disabled_songs.inc.php:30 +#: ../../templates/show_disabled_songs.inc.php:56 +#: ../../templates/show_duplicates.inc.php:30 +#: ../../templates/show_duplicates.inc.php:69 +#: ../../templates/show_edit_song_row.inc.php:40 +#: ../../templates/show_get_albumart.inc.php:36 +#: ../../templates/show_lyrics.inc.php:51 +#: ../../templates/show_missing_albums.inc.php:27 +#: ../../templates/show_now_playing_row.inc.php:43 +#: ../../templates/show_playlist_songs.inc.php:34 +#: ../../templates/show_playlist_songs.inc.php:71 +#: ../../templates/show_recently_played.inc.php:32 +#: ../../templates/show_recently_played.inc.php:143 +#: ../../templates/show_search_bar.inc.php:32 +#: ../../templates/show_song.inc.php:87 +#: ../../templates/show_song_previews.inc.php:34 +#: ../../templates/show_songs.inc.php:34 ../../templates/show_songs.inc.php:78 +#: ../../templates/show_wanted_albums.inc.php:26 +#: ../../templates/sidebar_home.inc.php:71 +msgid "Album" +msgstr "الألبوم" + +#: ../../lib/class/search.class.php:220 +#: ../../templates/browse_filters.inc.php:66 +#: ../../templates/show_albums.inc.php:36 +#: ../../templates/show_albums.inc.php:78 +#: ../../templates/show_artists.inc.php:33 +#: ../../templates/show_artists.inc.php:72 +#: ../../templates/show_democratic_playlist.inc.php:59 +#: ../../templates/show_democratic_playlist.inc.php:106 +#: ../../templates/show_disabled_songs.inc.php:31 +#: ../../templates/show_disabled_songs.inc.php:57 +#: ../../templates/show_duplicates.inc.php:29 +#: ../../templates/show_duplicates.inc.php:68 +#: ../../templates/show_edit_album_row.inc.php:31 +#: ../../templates/show_edit_song_row.inc.php:31 +#: ../../templates/show_get_albumart.inc.php:28 +#: ../../templates/show_lyrics.inc.php:58 +#: ../../templates/show_missing_albums.inc.php:28 +#: ../../templates/show_now_playing_row.inc.php:47 +#: ../../templates/show_playlist_songs.inc.php:33 +#: ../../templates/show_playlist_songs.inc.php:70 +#: ../../templates/show_recently_played.inc.php:33 +#: ../../templates/show_recently_played.inc.php:144 +#: ../../templates/show_recommended_artists.inc.php:30 +#: ../../templates/show_recommended_artists.inc.php:79 +#: ../../templates/show_search_bar.inc.php:33 +#: ../../templates/show_song.inc.php:86 +#: ../../templates/show_song_previews.inc.php:33 +#: ../../templates/show_songs.inc.php:33 ../../templates/show_songs.inc.php:77 +#: ../../templates/show_wanted_albums.inc.php:27 +#: ../../templates/sidebar_home.inc.php:72 +msgid "Artist" +msgstr "الفنان" + +#: ../../lib/class/search.class.php:227 +#: ../../templates/show_manage_shoutbox.inc.php:30 +#: ../../templates/show_manage_shoutbox.inc.php:54 +#: ../../templates/show_song.inc.php:90 +msgid "Comment" +msgstr "" + +#: ../../lib/class/search.class.php:235 ../../lib/class/search.class.php:429 +#: ../../lib/class/search.class.php:451 +#: ../../templates/show_search_bar.inc.php:35 +msgid "Tag" +msgstr "الوسم" + +#: ../../lib/class/search.class.php:242 ../../lib/class/search.class.php:437 +#: ../../templates/show_disabled_songs.inc.php:32 +#: ../../templates/show_disabled_songs.inc.php:58 +#: ../../templates/show_duplicates.inc.php:34 +#: ../../templates/show_duplicates.inc.php:73 +#: ../../templates/show_import_playlist.inc.php:28 +#: ../../templates/show_song.inc.php:96 +msgid "Filename" +msgstr "اسم الملف" + +#: ../../lib/class/search.class.php:249 ../../lib/class/search.class.php:390 +#: ../../templates/show_albums.inc.php:38 +#: ../../templates/show_albums.inc.php:80 +#: ../../templates/show_edit_album_row.inc.php:43 +#: ../../templates/show_missing_albums.inc.php:29 +#: ../../templates/show_wanted_albums.inc.php:28 +msgid "Year" +msgstr "سنة" + +#: ../../lib/class/search.class.php:256 +msgid "Length (in minutes)" +msgstr "" + +#: ../../lib/class/search.class.php:264 ../../lib/class/search.class.php:398 +#: ../../templates/show_albums.inc.php:41 +#: ../../templates/show_albums.inc.php:83 +#: ../../templates/show_artists.inc.php:40 +#: ../../templates/show_artists.inc.php:79 +#: ../../templates/show_now_playing_row.inc.php:91 +#: ../../templates/show_playlist_songs.inc.php:40 +#: ../../templates/show_playlist_songs.inc.php:75 +#: ../../templates/show_recommended_artists.inc.php:37 +#: ../../templates/show_recommended_artists.inc.php:86 +#: ../../templates/show_song.inc.php:31 ../../templates/show_songs.inc.php:41 +#: ../../templates/show_songs.inc.php:82 +msgid "Rating" +msgstr "تصنيف" + +#: ../../lib/class/search.class.php:282 ../../templates/show_song.inc.php:103 +#, fuzzy +msgid "# Played" +msgstr "تشغيل" + +#: ../../lib/class/search.class.php:290 +#: ../../templates/show_add_channel.inc.php:102 +#: ../../templates/show_channels.inc.php:37 +#: ../../templates/show_duplicates.inc.php:32 +#: ../../templates/show_duplicates.inc.php:71 +#: ../../templates/show_edit_channel_row.inc.php:84 +#: ../../templates/show_song.inc.php:94 +msgid "Bitrate" +msgstr "" + +#: ../../lib/class/search.class.php:315 ../../templates/show_album.inc.php:66 +#: ../../templates/show_artist.inc.php:63 +msgid "Played" +msgstr "" + +#: ../../lib/class/search.class.php:342 ../../lib/class/search.class.php:421 +#: ../../templates/browse_filters.inc.php:72 +#: ../../templates/show_add_live_stream.inc.php:54 +#: ../../templates/show_export.inc.php:32 +msgid "Catalog" +msgstr "" + +#: ../../lib/class/search.class.php:355 ../../lib/ui.lib.php:113 +#: ../../templates/show_manage_democratic.inc.php:26 +#: ../../templates/show_search_bar.inc.php:34 +#: ../../templates/sidebar_home.inc.php:49 +#: ../../templates/sidebar_home.inc.php:73 +msgid "Playlist" +msgstr "التشغيل" + +#: ../../lib/class/search.class.php:362 +#: ../../templates/show_playlists.inc.php:28 +#: ../../templates/show_playlists.inc.php:56 +#: ../../templates/show_smartplaylists.inc.php:29 +#: ../../templates/show_smartplaylists.inc.php:55 +msgid "Playlist Name" +msgstr "الاسم التشغيل" + +#: ../../lib/class/search.class.php:375 +#, fuzzy +msgid "Smart Playlist" +msgstr "استيراد قائمة التشغيل" + +#: ../../lib/class/search.class.php:445 ../../lib/class/search.class.php:459 +#: ../../templates/show_access_list.inc.php:51 +#: ../../templates/show_account.inc.php:29 +#: ../../templates/show_add_access.inc.php:27 +#: ../../templates/show_add_channel.inc.php:35 +#: ../../templates/show_add_live_stream.inc.php:27 +#: ../../templates/show_add_playlist.inc.php:27 +#: ../../templates/show_broadcasts.inc.php:29 +#: ../../templates/show_catalogs.inc.php:27 +#: ../../templates/show_catalogs.inc.php:55 +#: ../../templates/show_channels.inc.php:30 +#: ../../templates/show_create_democratic.inc.php:27 +#: ../../templates/show_edit_access.inc.php:27 +#: ../../templates/show_edit_album_row.inc.php:27 +#: ../../templates/show_edit_artist_row.inc.php:27 +#: ../../templates/show_edit_broadcast_row.inc.php:27 +#: ../../templates/show_edit_catalog.inc.php:28 +#: ../../templates/show_edit_channel_row.inc.php:44 +#: ../../templates/show_edit_live_stream_row.inc.php:27 +#: ../../templates/show_edit_playlist_row.inc.php:27 +#: ../../templates/show_edit_smartplaylist_row.inc.php:27 +#: ../../templates/show_edit_tag_row.inc.php:27 +#: ../../templates/show_live_streams.inc.php:30 +#: ../../templates/show_live_streams.inc.php:56 +#: ../../templates/show_localplay_controllers.inc.php:29 +#: ../../templates/show_localplay_controllers.inc.php:63 +#: ../../templates/show_localplay_playlist.inc.php:32 +#: ../../templates/show_localplay_playlist.inc.php:62 +#: ../../templates/show_plugins.inc.php:29 +#: ../../templates/show_plugins.inc.php:69 +#: ../../templates/show_stats.inc.php:65 +msgid "Name" +msgstr "اسم" + +#: ../../lib/class/shoutbox.class.php:270 ../../templates/rightbar.inc.php:25 +#: ../../templates/show_album_group_disks.inc.php:54 +#: ../../templates/show_album_group_disks.inc.php:55 +#: ../../templates/show_album_group_disks.inc.php:105 +#: ../../templates/show_album.inc.php:75 ../../templates/show_album.inc.php:76 +#: ../../templates/show_album_row.inc.php:27 +#: ../../templates/show_artist_row.inc.php:27 +#: ../../templates/show_broadcast_row.inc.php:27 +#: ../../templates/show_channel_row.inc.php:27 +#: ../../templates/show_democratic.inc.php:41 +#: ../../templates/show_localplay_control.inc.php:27 +#: ../../templates/show_manage_democratic.inc.php:49 +#: ../../templates/show_missing_album.inc.php:56 +#: ../../templates/show_missing_album.inc.php:57 +#: ../../templates/show_playlist_row.inc.php:27 +#: ../../templates/show_playlist_song_row.inc.php:27 +#: ../../templates/show_playlist_songs.inc.php:67 +#: ../../templates/show_random_albums.inc.php:46 +#: ../../templates/show_recently_played.inc.php:98 +#: ../../templates/show_smartplaylist_row.inc.php:27 +#: ../../templates/show_song.inc.php:59 +#: ../../templates/show_song_preview_row.inc.php:26 +#: ../../templates/show_song_row.inc.php:27 +#: ../../templates/show_video_row.inc.php:27 +msgid "Play" +msgstr "تشغيل" + +#: ../../lib/class/shoutbox.class.php:271 +#: ../../templates/show_add_live_stream.inc.php:62 +#: ../../templates/show_catalog_row.inc.php:34 +#: ../../templates/show_live_stream.inc.php:27 +msgid "Add" +msgstr "إضافة" + +#: ../../lib/class/shoutbox.class.php:273 +#: ../../templates/show_album_group_disks.inc.php:118 +#: ../../templates/show_album.inc.php:117 +#: ../../templates/show_album.inc.php:118 +#: ../../templates/show_album_row.inc.php:67 +#: ../../templates/show_html5_player.inc.php:249 +#: ../../templates/show_html5_player.inc.php:255 +#: ../../templates/show_song.inc.php:68 +#: ../../templates/show_song_row.inc.php:61 +msgid "Post Shout" +msgstr "في مرحلة ما بعد الصيحة" + +#: ../../lib/class/song.class.php:886 +#: ../../templates/show_html5_player.inc.php:246 +msgid "Show Lyrics" +msgstr "" + +#: ../../lib/class/update.class.php:442 +msgid "No updates needed." +msgstr "" + +#: ../../lib/class/update.class.php:2428 ../../lib/class/update.class.php:2444 +msgid "File copy error." +msgstr "" + +#: ../../lib/class/update.class.php:2435 ../../lib/class/update.class.php:2451 +msgid "Cannot copy default .htaccess file." +msgstr "" + +#: ../../lib/class/user.class.php:865 ../../templates/show_objects.inc.php:44 +#: ../../templates/show_tagcloud.inc.php:48 +msgid "Not Enough Data" +msgstr "البيانات غير كافية" + +#: ../../lib/class/user.class.php:1155 +msgid "User avatar" +msgstr "" + +#: ../../lib/class/wanted.class.php:161 +#, fuzzy +msgid "Unknown Artist" +msgstr "غير معروف" + +#: ../../lib/class/wanted.class.php:288 +msgid "Accept" +msgstr "" + +#: ../../lib/class/wanted.class.php:292 +#: ../../lib/javascript/search-data.php:51 +msgid "Remove" +msgstr "" + +#: ../../lib/class/wanted.class.php:295 +msgid "Add to wanted list" +msgstr "" + +#: ../../lib/install.lib.php:72 +msgid "Config file already exists, install is probably completed" +msgstr "" + +#: ../../lib/install.lib.php:83 +msgid "Unable to connect to database, check your ampache config" +msgstr "Unable to connect to database, check your ampache config" + +#: ../../lib/install.lib.php:91 +#, fuzzy +msgid "Unable to query database, check your ampache config" +msgstr "تعذر اختيار قاعدة البيانات ، والتحقق من اتصالك ampache التهيئة" + +#: ../../lib/install.lib.php:98 +msgid "Existing Database detected, unable to continue installation" +msgstr "اكتشاف قاعدة البيانات الموجودة ، غير قادرة على الاستمرار في التثبيت" + +#: ../../lib/install.lib.php:154 ../../lib/install.lib.php:289 +msgid "Error writing config file" +msgstr "" + +#: ../../lib/install.lib.php:179 +#, fuzzy +msgid "Error: Invalid database name." +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../lib/install.lib.php:184 +#, fuzzy, php-format +msgid "Error: Unable to make database connection: %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../lib/install.lib.php:194 +msgid "Error: Database already exists and overwrite not checked" +msgstr "" + +#: ../../lib/install.lib.php:201 +#, fuzzy, php-format +msgid "Error: Unable to create database: %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../lib/install.lib.php:218 +#, php-format +msgid "" +"Error: Unable to create user %1$s with permissions to %2$s on %3$s: %4$s" +msgstr "" + +#: ../../lib/install.lib.php:269 +#, fuzzy +msgid "Invalid configuration settings" +msgstr "بدء التهيئة" + +#: ../../lib/install.lib.php:275 +msgid "Database Connection Failed Check Hostname, Username and Password" +msgstr "" + +#: ../../lib/install.lib.php:284 +#, fuzzy +msgid "Config file is not writable" +msgstr "خطأ : لم يتم العثور على ملف أو غير صالح" + +#: ../../lib/install.lib.php:310 +msgid "No Username/Password specified" +msgstr "لا اسم المستخدم / كلمة السر المحددة" + +#: ../../lib/install.lib.php:315 +msgid "Passwords do not match" +msgstr "كلمة السر لا تعمل" + +#: ../../lib/install.lib.php:320 +#, fuzzy, php-format +msgid "Database connection failed: %s" +msgstr "الديسيبل الربط" + +#: ../../lib/install.lib.php:325 +#, fuzzy, php-format +msgid "Database select failed: %s" +msgstr "اسم قاعدة البيانات المطلوبة" + +#: ../../lib/install.lib.php:335 +#, fuzzy, php-format +msgid "Administrative user creation failed: %s" +msgstr "MySQL الإدارية اسم المستخدم" + +#: ../../lib/login.php:84 +msgid "Error Username or Password incorrect, please try again" +msgstr "" + +#: ../../lib/login.php:104 +msgid "User Disabled please contact Admin" +msgstr "" + +#: ../../lib/login.php:112 +msgid "User Already Logged in" +msgstr "" + +#: ../../lib/login.php:133 +msgid "Unable to create local account" +msgstr "" + +#: ../../lib/preferences.php:186 ../../lib/preferences.php:305 +#: ../../templates/show_disabled_songs.inc.php:64 +#: ../../templates/show_user_row.inc.php:49 +msgid "Enable" +msgstr "يمكن" + +#: ../../lib/preferences.php:187 ../../lib/preferences.php:306 +#: ../../templates/show_catalog_types.inc.php:43 +#: ../../templates/show_duplicates.inc.php:27 +#: ../../templates/show_duplicates.inc.php:66 +#: ../../templates/show_localplay_controllers.inc.php:43 +#: ../../templates/show_user_row.inc.php:51 +msgid "Disable" +msgstr "يعطل" + +#: ../../lib/preferences.php:205 ../../lib/preferences.php:242 +#: ../../lib/ui.lib.php:329 ../../templates/show_adds_catalog.inc.php:27 +#: ../../templates/show_gather_art.inc.php:25 +#: ../../templates/show_install_config.inc.php:110 +#: ../../templates/sidebar_localplay.inc.php:50 +msgid "None" +msgstr "بلا" + +#: ../../lib/preferences.php:207 ../../templates/show_edit_user.inc.php:122 +#: ../../templates/show_playtype_switch.inc.php:33 +msgid "Stream" +msgstr "يتدفق" + +#: ../../lib/preferences.php:210 +#: ../../modules/localplay/httpq.controller.php:471 +#: ../../modules/localplay/mpd.controller.php:469 +#: ../../modules/localplay/vlc.controller.php:479 +#: ../../templates/show_edit_user.inc.php:119 +#: ../../templates/show_playtype_switch.inc.php:37 +#: ../../templates/sidebar_home.inc.php:53 +#: ../../templates/sidebar_modules.inc.php:39 +msgid "Democratic" +msgstr "الديمقراطية" + +#: ../../lib/preferences.php:213 ../../templates/show_edit_user.inc.php:120 +#: ../../templates/show_playtype_switch.inc.php:35 +#: ../../templates/sidebar_home.inc.php:62 ../../templates/sidebar.inc.php:30 +#: ../../templates/sidebar_localplay.inc.php:38 +msgid "Localplay" +msgstr "لعب المحلية" + +#: ../../lib/preferences.php:215 +#: ../../templates/show_playtype_switch.inc.php:39 +#, fuzzy +msgid "Web Player" +msgstr "الشبكة الدرب" + +#: ../../lib/preferences.php:222 +msgid "M3U" +msgstr "" + +#: ../../lib/preferences.php:223 +msgid "Simple M3U" +msgstr "" + +#: ../../lib/preferences.php:224 +msgid "PLS" +msgstr "" + +#: ../../lib/preferences.php:225 +msgid "Asx" +msgstr "" + +#: ../../lib/preferences.php:226 +msgid "RAM" +msgstr "" + +#: ../../lib/preferences.php:227 +msgid "XSPF" +msgstr "" + +#: ../../lib/preferences.php:263 +msgid "Disabled" +msgstr "" + +#: ../../lib/preferences.php:265 +msgid "Manager" +msgstr "" + +#: ../../lib/preferences.php:282 +msgid "Send on Add" +msgstr "" + +#: ../../lib/preferences.php:283 +msgid "Send and Clear on Add" +msgstr "" + +#: ../../lib/preferences.php:284 +msgid "Clear on Send" +msgstr "" + +#: ../../lib/preferences.php:285 ../../lib/preferences.php:292 +#: ../../lib/preferences.php:328 +#: ../../templates/show_manage_democratic.inc.php:30 +msgid "Default" +msgstr "افتراضي" + +#: ../../lib/preferences.php:293 +msgid "Always" +msgstr "" + +#: ../../lib/preferences.php:329 +msgid "Year ascending" +msgstr "" + +#: ../../lib/preferences.php:330 +msgid "Year descending" +msgstr "" + +#: ../../lib/preferences.php:331 +msgid "Name ascending" +msgstr "" + +#: ../../lib/preferences.php:332 +msgid "Name descending" +msgstr "" + +#: ../../lib/rating.lib.php:43 +msgid "Don't Play" +msgstr "" + +#: ../../lib/rating.lib.php:45 +msgid "It's Pretty Bad" +msgstr "" + +#: ../../lib/rating.lib.php:47 +msgid "It's Ok" +msgstr "" + +#: ../../lib/rating.lib.php:49 +msgid "It's Pretty Good" +msgstr "" + +#: ../../lib/rating.lib.php:51 +msgid "I Love It!" +msgstr "" + +#: ../../lib/rating.lib.php:53 +msgid "It's Insane" +msgstr "" + +#: ../../lib/rating.lib.php:56 +msgid "Off the Charts!" +msgstr "" + +#: ../../lib/ui.lib.php:101 ../../templates/header.inc.php:280 +#: ../../templates/sidebar.inc.php:29 +msgid "Home" +msgstr "منزل" + +#: ../../lib/ui.lib.php:104 +msgid "Upload" +msgstr "" + +#: ../../lib/ui.lib.php:107 +msgid "Local Play" +msgstr "" + +#: ../../lib/ui.lib.php:110 +msgid "Random Play" +msgstr "" + +#: ../../lib/ui.lib.php:116 ../../templates/show_search_bar.inc.php:37 +#: ../../templates/show_search.inc.php:53 +#: ../../templates/sidebar_home.inc.php:99 +msgid "Search" +msgstr "" + +#: ../../lib/ui.lib.php:119 ../../templates/show_user_row.inc.php:45 +#: ../../templates/sidebar.inc.php:31 +#: ../../templates/sidebar_preferences.inc.php:30 +msgid "Preferences" +msgstr "" + +#: ../../lib/ui.lib.php:122 ../../lib/ui.lib.php:126 +msgid "Admin-Catalog" +msgstr "" + +#: ../../lib/ui.lib.php:130 +msgid "Admin-User Management" +msgstr "" + +#: ../../lib/ui.lib.php:134 +msgid "Admin-Mail Users" +msgstr "" + +#: ../../lib/ui.lib.php:138 +msgid "Admin-Manage Access Lists" +msgstr "" + +#: ../../lib/ui.lib.php:142 +msgid "Admin-Site Preferences" +msgstr "" + +#: ../../lib/ui.lib.php:146 +msgid "Admin-Manage Modules" +msgstr "" + +#: ../../lib/ui.lib.php:150 +msgid "Browse Music" +msgstr "" + +#: ../../lib/ui.lib.php:162 ../../templates/show_stats.inc.php:26 +#: ../../templates/sidebar_home.inc.php:95 +msgid "Statistics" +msgstr "احصاءات" + +#: ../../lib/ui.lib.php:221 +msgid "Add New" +msgstr "" + +#: ../../lib/ui.lib.php:546 +msgid "On" +msgstr "" + +#: ../../lib/ui.lib.php:548 +msgid "Off" +msgstr "" + +#: ../../lostpassword.php:39 +msgid "Password has been sent" +msgstr "" + +#: ../../lostpassword.php:41 +#, fuzzy +msgid "Password has not been sent" +msgstr "كلمة السر لا تعمل" + +#: ../../lostpassword.php:60 +#, fuzzy +msgid "Lost Password" +msgstr "كلمة السر" + +#: ../../lostpassword.php:64 +#, php-format +msgid "A user from %s has requested a password reset for '%s'." +msgstr "" + +#: ../../lostpassword.php:66 +#, php-format +msgid "The password has been set to: %s" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:122 +#: ../../templates/show_edit_user.inc.php:108 +msgid "API Key" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:123 +#: ../../modules/catalog/soundcloud.catalog.php:114 +#: ../../templates/show_add_share.inc.php:35 +msgid "Secret" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:124 +#: ../../modules/catalog/local.catalog.php:107 +#: ../../templates/show_stats.inc.php:66 +msgid "Path" +msgstr "مسار" + +#: ../../modules/catalog/dropbox.catalog.php:125 +msgid "Get chunked files on analyze" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:169 +#, fuzzy +msgid "Error: API Key and Secret Required for Dropbox Catalogs" +msgstr "بعد لالكتالوجات" + +#: ../../modules/catalog/dropbox.catalog.php:175 +msgid "Invalid : " +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:185 +#: ../../modules/catalog/local.catalog.php:203 +#: ../../modules/catalog/remote.catalog.php:168 +#: ../../modules/catalog/soundcloud.catalog.php:171 +#: ../../modules/catalog/subsonic.catalog.php:169 +#, php-format +msgid "Error: Catalog with %s already exists" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:243 +msgid "Running Dropbox Remote Update" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:283 +#, php-format +msgid "Catalog Update Finished. Total Songs: [%s]" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:286 +msgid "No songs updated, do you respect the patterns?" +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:290 +#: ../../modules/catalog/dropbox.catalog.php:429 +msgid "API Error: cannot connect to Dropbox." +msgstr "" + +#: ../../modules/catalog/dropbox.catalog.php:324 +msgid "API Error: Cannot access file/folder at " +msgstr "" + +#: ../../modules/catalog/local.catalog.php:180 +msgid "Error: Path not specified" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:186 +msgid "Error: Defined Path is inside an existing catalog" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:193 +#, php-format +msgid "Error: %s is not readable or does not exist" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:242 +#, fuzzy, php-format +msgid "Error: Unable to open %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#: ../../modules/catalog/local.catalog.php:249 +#: ../../modules/catalog/local.catalog.php:295 +#, fuzzy, php-format +msgid "Error: Unable to change to directory %s" +msgstr "خطأ : تعذر الاتصال لجعل قاعدة البيانات" + +#. HINT: FullFile +#: ../../modules/catalog/local.catalog.php:320 +#, php-format +msgid "Error: Unable to get filesize for %s" +msgstr "" + +#. HINT: FullFile +#: ../../modules/catalog/local.catalog.php:327 +#, php-format +msgid "%s is not readable by ampache" +msgstr "" + +#. HINT: FullFile +#: ../../modules/catalog/local.catalog.php:349 +#, php-format +msgid "%s does not match site charset" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:451 +msgid "N/A" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:455 +#, php-format +msgid "" +"Catalog Update Finished. Total Time: [%s] Total Songs: [%s] Songs Per " +"Second: [%s]" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:531 +#, php-format +msgid "%s does not exist or is not readable" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:561 +msgid "Catalog Root unreadable, stopping clean" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:584 +msgid "All files would be removed. Doing nothing" +msgstr "" + +#: ../../modules/catalog/local.catalog.php:623 +#, php-format +msgid "Error File Not Found or 0 Bytes: %s" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:109 +#: ../../modules/catalog/subsonic.catalog.php:109 +msgid "Uri" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:110 +#: ../../modules/catalog/subsonic.catalog.php:110 +#: ../../modules/localplay/xbmc.controller.php:200 +#: ../../templates/show_add_user.inc.php:29 +#: ../../templates/show_edit_user.inc.php:32 +#: ../../templates/show_install_account.inc.php:51 +#: ../../templates/show_login_form.inc.php:79 +#: ../../templates/show_now_playing_row.inc.php:25 +#: ../../templates/show_recently_played.inc.php:34 +#: ../../templates/show_recently_played.inc.php:145 +#: ../../templates/show_user_registration.inc.php:71 +#: ../../templates/show_users.inc.php:41 ../../templates/show_users.inc.php:67 +msgid "Username" +msgstr "اسم المستخدم" + +#: ../../modules/catalog/remote.catalog.php:111 +#: ../../modules/catalog/subsonic.catalog.php:111 +#: ../../modules/localplay/httpq.controller.php:211 +#: ../../modules/localplay/mpd.controller.php:240 +#: ../../modules/localplay/vlc.controller.php:195 +#: ../../modules/localplay/xbmc.controller.php:201 +#: ../../templates/show_add_user.inc.php:60 +#: ../../templates/show_edit_user.inc.php:63 +#: ../../templates/show_install_account.inc.php:57 +#: ../../templates/show_login_form.inc.php:83 +#: ../../templates/show_user_registration.inc.php:94 +msgid "Password" +msgstr "كلمة السر" + +#: ../../modules/catalog/remote.catalog.php:152 +msgid "Error: Remote selected, but path is not a URL" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:157 +#, fuzzy +msgid "Error: Username and Password Required for Remote Catalogs" +msgstr "بعد لالكتالوجات" + +#: ../../modules/catalog/remote.catalog.php:184 +msgid "Running Remote Update" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:215 +msgid "Error connecting to remote server" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:242 +#, php-format +msgid "%u remote catalog(s) found (%u songs)" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:270 +#: ../../modules/catalog/soundcloud.catalog.php:280 +#: ../../modules/catalog/subsonic.catalog.php:245 +#, php-format +msgid "Unable to Insert Song - %s" +msgstr "" + +#: ../../modules/catalog/remote.catalog.php:278 +msgid "Completed updating remote catalog(s)." +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:113 +#, fuzzy +msgid "User ID" +msgstr "مستخدم" + +#: ../../modules/catalog/soundcloud.catalog.php:161 +#, fuzzy +msgid "Error: UserID and Secret Required for SoundCloud Catalogs" +msgstr "بعد لالكتالوجات" + +#: ../../modules/catalog/soundcloud.catalog.php:222 +msgid "Running SoundCloud Remote Update" +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:290 +msgid "Completed updating SoundCloud catalog(s)." +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:290 +#: ../../modules/catalog/subsonic.catalog.php:267 +msgid "Songs added." +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:296 +msgid "API Error: cannot get song list." +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:300 +#: ../../modules/catalog/soundcloud.catalog.php:358 +#: ../../modules/catalog/soundcloud.catalog.php:431 +msgid "API Error: cannot connect to SoundCloud." +msgstr "" + +#: ../../modules/catalog/soundcloud.catalog.php:304 +#: ../../modules/catalog/soundcloud.catalog.php:362 +#: ../../modules/catalog/soundcloud.catalog.php:435 +msgid "SoundCloud exception: " +msgstr "" + +#: ../../modules/catalog/subsonic.catalog.php:154 +msgid "Error: Subsonic selected, but path is not a URL" +msgstr "" + +#: ../../modules/catalog/subsonic.catalog.php:159 +#, fuzzy +msgid "Error: Username and Password Required for Subsonic Catalogs" +msgstr "بعد لالكتالوجات" + +#: ../../modules/catalog/subsonic.catalog.php:188 +msgid "Running Subsonic Remote Update" +msgstr "" + +#: ../../modules/catalog/subsonic.catalog.php:255 +msgid "Song Error." +msgstr "" + +#: ../../modules/catalog/subsonic.catalog.php:261 +#, fuzzy +msgid "Album Error." +msgstr "البوم الفن" + +#: ../../modules/catalog/subsonic.catalog.php:267 +msgid "Completed updating Subsonic catalog(s)." +msgstr "" + +#: ../../modules/catalog/subsonic.catalog.php:273 +#, fuzzy +msgid "Artist Error." +msgstr "الكتابة التهيئة" + +#: ../../modules/localplay/httpq.controller.php:208 +#: ../../modules/localplay/mpd.controller.php:237 +#: ../../modules/localplay/vlc.controller.php:192 +#: ../../modules/localplay/xbmc.controller.php:197 +msgid "Instance Name" +msgstr "" + +#: ../../modules/localplay/httpq.controller.php:209 +#: ../../modules/localplay/mpd.controller.php:238 +#: ../../modules/localplay/vlc.controller.php:193 +#: ../../modules/localplay/xbmc.controller.php:198 +msgid "Hostname" +msgstr "" + +#: ../../modules/localplay/httpq.controller.php:210 +#: ../../modules/localplay/mpd.controller.php:239 +#: ../../modules/localplay/vlc.controller.php:194 +#: ../../modules/localplay/xbmc.controller.php:199 +#: ../../templates/show_add_channel.inc.php:63 +#: ../../templates/show_channels.inc.php:32 +#: ../../templates/show_edit_channel_row.inc.php:60 +msgid "Port" +msgstr "" + +#: ../../modules/localplay/httpq.controller.php:475 +#: ../../modules/localplay/mpd.controller.php:473 +#: ../../modules/localplay/vlc.controller.php:483 +#: ../../templates/show_add_channel.inc.php:76 +#: ../../templates/show_channels.inc.php:34 +#: ../../templates/show_edit_channel_row.inc.php:69 +#: ../../templates/show_localplay_status.inc.php:48 +#: ../../templates/sidebar_home.inc.php:68 +msgid "Random" +msgstr "عشوائية" + +#: ../../playlist.php:54 +msgid "Playlist Created" +msgstr "" + +#: ../../playlist.php:54 +#, php-format +msgid "%1$s (%2$s) has been created" +msgstr "" + +#: ../../playlist.php:80 +msgid "Playlist Imported" +msgstr "" + +#: ../../playlist.php:83 +#, php-format +msgid "Successfully imported playlist with %d song." +msgid_plural "Successfully imported playlist with %d songs." +msgstr[0] "" +msgstr[1] "" + +#: ../../playlist.php:86 +msgid "Playlist Not Imported" +msgstr "" + +#: ../../playlist.php:135 +msgid "Empty Playlists Deleted" +msgstr "" + +#: ../../plex/web/index.php:32 +msgid "" +"Changing the server UUID could break clients connectivity. Do you confirm?" +msgstr "" + +#: ../../plex/web/index.php:83 +msgid "myPlex authentication completed." +msgstr "" + +#: ../../plex/web/index.php:96 +msgid "Server registration completed." +msgstr "" + +#: ../../plex/web/index.php:99 +msgid "Cannot register the server on myPlex." +msgstr "" + +#: ../../plex/web/index.php:105 +msgid "Cannot authenticate on myPlex." +msgstr "" + +#: ../../plex/web/index.php:133 +msgid "Server UUID changed." +msgstr "" + +#: ../../preferences.php:47 ../../preferences.php:85 +msgid "Server" +msgstr "" + +#: ../../preferences.php:117 +msgid "Error Update Failed" +msgstr "" + +#: ../../preferences.php:121 +msgid "Your Account has been updated" +msgstr "" + +#: ../../radio.php:55 +msgid "Radio Station Added" +msgstr "" + +#: ../../register.php:68 +msgid "Error Captcha Required" +msgstr "" + +#: ../../register.php:74 +msgid "Error Captcha Failed" +msgstr "" + +#: ../../register.php:81 +msgid "You must accept the user agreement" +msgstr "" + +#: ../../register.php:86 +msgid "You did not enter a username" +msgstr "" + +#: ../../register.php:90 +msgid "Please fill in your full name (Firstname Lastname)" +msgstr "" + +#: ../../register.php:95 +msgid "Invalid email address" +msgstr "" + +#: ../../register.php:99 +msgid "You must enter a password" +msgstr "" + +#: ../../register.php:103 +msgid "Your passwords do not match" +msgstr "" + +#: ../../rest/index.php:43 ../../server/xml.server.php:43 +msgid "Access Control not Enabled" +msgstr "" + +#: ../../search.php:43 +#, fuzzy +msgid "Search Saved" +msgstr "نوع البحث" + +#: ../../search.php:43 +#, php-format +msgid "Your Search has been saved as a track in %s" +msgstr "" + +#: ../../server/player.ajax.php:41 +#: ../../templates/show_html5_player.inc.php:661 +msgid "My Broadcast" +msgstr "" + +#: ../../server/xml.server.php:54 +msgid "Session Expired" +msgstr "" + +#: ../../server/xml.server.php:67 +msgid "Unauthorized access attempt to API - ACL Error" +msgstr "" + +#: ../../share.php:75 +msgid "Share created." +msgstr "" + +#: ../../share.php:76 +msgid "You can now start sharing the following url:" +msgstr "" + +#: ../../share.php:81 +msgid "" +"You can also embed this share as a web player into your website, with the " +"following html code:" +msgstr "" + +#: ../../share.php:84 +msgid "Object Shared" +msgstr "" + +#: ../../share.php:94 +#, fuzzy +msgid "Share Delete" +msgstr "حذف" + +#: ../../share.php:107 +#, fuzzy +msgid "Share Deleted" +msgstr "حذف" + +#: ../../share.php:107 +msgid "The Share has been deleted" +msgstr "" + +#: ../../shout.php:49 +msgid "Invalid Object Selected" +msgstr "" + +#: ../../templates/browse_filters.inc.php:30 +#: ../../templates/list_header.inc.php:179 +msgid "Filters" +msgstr "الفلاتر" + +#: ../../templates/browse_filters.inc.php:34 +msgid "Starts With" +msgstr "يبدأ" + +#: ../../templates/browse_filters.inc.php:40 +msgid "Minimum Count" +msgstr "الحد الأدنى للتعداد" + +#: ../../templates/browse_filters.inc.php:45 +msgid "Rated" +msgstr "صنف" + +#: ../../templates/browse_filters.inc.php:50 +msgid "Unplayed" +msgstr "غير لعب" + +#: ../../templates/browse_filters.inc.php:54 +msgid "All Playlists" +msgstr "جميع التشغيل" + +#: ../../templates/browse_filters.inc.php:60 +#: ../../templates/show_playlist_songs.inc.php:31 +#: ../../templates/show_playlist_songs.inc.php:68 +#: ../../templates/show_song_previews.inc.php:29 +#: ../../templates/show_songs.inc.php:31 ../../templates/show_songs.inc.php:75 +msgid "Song Title" +msgstr "عنوان الأغنية" + +#: ../../templates/browse_filters.inc.php:96 +msgid "Toggle Artwork" +msgstr "" + +#: ../../templates/error_page.inc.php:31 +#, fuzzy +msgid "Ampache error page" +msgstr "تحديث Ampache" + +#: ../../templates/error_page.inc.php:43 +#: ../../templates/show_denied.inc.php:38 +#: ../../templates/show_mail_users.inc.php:43 +#: ../../templates/show_test.inc.php:37 ../../update.php:58 +#, fuzzy +msgid "Ampache" +msgstr "Ampache التصحيح" + +#: ../../templates/error_page.inc.php:50 +msgid "" +"The folowing error has occured, you will automaticly be redirected after 10 " +"seconds." +msgstr "" + +#: ../../templates/error_page.inc.php:52 +msgid "Error messages" +msgstr "" + +#: ../../templates/footer.inc.php:28 +#: ../../templates/show_lostpassword_form.inc.php:75 +msgid "Queries:" +msgstr "الأسئلة :" + +#: ../../templates/footer.inc.php:28 +#: ../../templates/show_lostpassword_form.inc.php:76 +msgid "Cache Hits:" +msgstr "مخبأ عدد الزيارات :" + +#: ../../templates/footer.inc.php:33 +msgid "Load time:" +msgstr "" + +#: ../../templates/header.inc.php:83 +msgid "Save" +msgstr "" + +#: ../../templates/header.inc.php:84 +#: ../../templates/show_confirmation.inc.php:34 +msgid "Cancel" +msgstr "الغاء" + +#: ../../templates/header.inc.php:260 +msgid "Log out" +msgstr "" + +#: ../../templates/header.inc.php:298 ../../templates/sidebar_home.inc.php:87 +msgid "Favorites" +msgstr "" + +#: ../../templates/header.inc.php:317 +msgid "Error Config File Out of Date" +msgstr "" + +#: ../../templates/header.inc.php:318 +msgid "Generate New Config" +msgstr "" + +#: ../../templates/install_header.inc.php:41 +#: ../../templates/show_install_lang.inc.php:27 +msgid "Ampache Installation" +msgstr "Ampache التثبيت" + +#: ../../templates/list_header.inc.php:146 +msgid "Prev" +msgstr "" + +#: ../../templates/list_header.inc.php:147 +#: ../../templates/show_localplay_control.inc.php:28 +msgid "Next" +msgstr "التالي" + +#: ../../templates/list_header.inc.php:188 +msgid "Limit" +msgstr "" + +#: ../../templates/rightbar.inc.php:28 +msgid "Add to Playlist" +msgstr "" + +#: ../../templates/rightbar.inc.php:31 +#: ../../templates/show_playlists_dialog.inc.php:27 +msgid "Add to New Playlist" +msgstr "" + +#: ../../templates/rightbar.inc.php:49 +#: ../../templates/show_album_row.inc.php:75 +#: ../../templates/show_artist_row.inc.php:57 +#: ../../templates/show_playlist.inc.php:48 +#: ../../templates/show_playlist.inc.php:49 +#: ../../templates/show_playlist_row.inc.php:50 +#: ../../templates/show_search_options.inc.php:32 +#: ../../templates/show_search_options.inc.php:33 +#: ../../templates/show_smartplaylist.inc.php:34 +#: ../../templates/show_smartplaylist.inc.php:35 +#: ../../templates/show_smartplaylist_row.inc.php:48 +msgid "Batch Download" +msgstr "دفعة تنزيل" + +#: ../../templates/rightbar.inc.php:54 +#: ../../templates/show_democratic.inc.php:45 +#: ../../templates/show_democratic.inc.php:46 +#: ../../templates/show_localplay_status.inc.php:51 +msgid "Clear Playlist" +msgstr "" + +#: ../../templates/rightbar.inc.php:57 +msgid "Add Dynamic Items" +msgstr "" + +#: ../../templates/rightbar.inc.php:60 +#, fuzzy +msgid "Random Song" +msgstr "عشوائية" + +#: ../../templates/rightbar.inc.php:63 +#, fuzzy +msgid "Random Artist" +msgstr "الملف نمط" + +#: ../../templates/rightbar.inc.php:66 +#, fuzzy +msgid "Random Album" +msgstr "البوم" + +#: ../../templates/rightbar.inc.php:69 +#, fuzzy +msgid "Random Playlist" +msgstr "قاعدة التشغيل" + +#: ../../templates/rightbar.inc.php:120 +msgid "No items" +msgstr "" + +#: ../../templates/show_access_list.inc.php:23 +#: ../../templates/sidebar_admin.inc.php:37 +msgid "Access Control" +msgstr "" + +#: ../../templates/show_access_list.inc.php:27 +#: ../../templates/show_access_list.inc.php:28 +msgid "Add Current Host" +msgstr "" + +#: ../../templates/show_access_list.inc.php:31 +#: ../../templates/show_access_list.inc.php:32 +msgid "Add API / RPC Host" +msgstr "" + +#: ../../templates/show_access_list.inc.php:35 +#: ../../templates/show_access_list.inc.php:36 +msgid "Add Local Network Definition" +msgstr "إضافة تعريف الشبكة المحلية" + +#: ../../templates/show_access_list.inc.php:38 +#: ../../templates/show_access_list.inc.php:39 +msgid "Advanced Add" +msgstr "" + +#: ../../templates/show_access_list.inc.php:45 +msgid "Access Control Entries" +msgstr "" + +#: ../../templates/show_access_list.inc.php:52 +msgid "Start Address" +msgstr "" + +#: ../../templates/show_access_list.inc.php:53 +msgid "End Address" +msgstr "" + +#: ../../templates/show_access_list.inc.php:54 +#: ../../templates/show_add_access.inc.php:33 +#: ../../templates/show_create_democratic.inc.php:39 +#: ../../templates/show_edit_access.inc.php:66 +#: ../../templates/show_manage_democratic.inc.php:29 +#: ../../templates/show_preference_admin.inc.php:33 +#: ../../templates/show_preference_admin.inc.php:52 +msgid "Level" +msgstr "المستوى" + +#: ../../templates/show_access_list.inc.php:56 +#: ../../templates/show_add_playlist.inc.php:31 +#: ../../templates/show_catalog_types.inc.php:29 +#: ../../templates/show_catalog_types.inc.php:63 +#: ../../templates/show_edit_playlist_row.inc.php:31 +#: ../../templates/show_edit_smartplaylist_row.inc.php:31 +#: ../../templates/show_playlists.inc.php:30 +#: ../../templates/show_playlists.inc.php:58 +#: ../../templates/show_smartplaylists.inc.php:31 +#: ../../templates/show_smartplaylists.inc.php:57 +msgid "Type" +msgstr "نوع" + +#: ../../templates/show_access_list.inc.php:57 +#: ../../templates/show_artists.inc.php:45 +#: ../../templates/show_artists.inc.php:84 +#: ../../templates/show_catalog_types.inc.php:32 +#: ../../templates/show_catalog_types.inc.php:66 +#: ../../templates/show_democratic_playlist.inc.php:55 +#: ../../templates/show_democratic_playlist.inc.php:102 +#: ../../templates/show_live_streams.inc.php:34 +#: ../../templates/show_live_streams.inc.php:60 +#: ../../templates/show_localplay_controllers.inc.php:32 +#: ../../templates/show_localplay_controllers.inc.php:66 +#: ../../templates/show_localplay_instances.inc.php:29 +#: ../../templates/show_localplay_playlist.inc.php:33 +#: ../../templates/show_localplay_playlist.inc.php:63 +#: ../../templates/show_manage_democratic.inc.php:32 +#: ../../templates/show_manage_shoutbox.inc.php:32 +#: ../../templates/show_manage_shoutbox.inc.php:56 +#: ../../templates/show_playlist_songs.inc.php:47 +#: ../../templates/show_playlist_songs.inc.php:80 +#: ../../templates/show_plugins.inc.php:33 +#: ../../templates/show_plugins.inc.php:73 +#: ../../templates/show_recommended_artists.inc.php:42 +#: ../../templates/show_recommended_artists.inc.php:91 +#: ../../templates/show_song.inc.php:56 ../../templates/show_songs.inc.php:49 +#: ../../templates/show_users.inc.php:48 ../../templates/show_users.inc.php:74 +#: ../../templates/show_videos.inc.php:36 +#: ../../templates/show_videos.inc.php:65 +msgid "Action" +msgstr "العمل" + +#: ../../templates/show_account.inc.php:35 +#: ../../templates/show_add_user.inc.php:44 +#: ../../templates/show_edit_user.inc.php:47 +#: ../../templates/show_user_registration.inc.php:83 +msgid "E-mail" +msgstr "البريد الإلكتروني" + +#: ../../templates/show_account.inc.php:41 +#: ../../templates/show_add_user.inc.php:52 +#: ../../templates/show_edit_user.inc.php:55 +#: ../../templates/show_user_registration.inc.php:88 +msgid "Website" +msgstr "" + +#: ../../templates/show_account.inc.php:47 +msgid "New Password" +msgstr "" + +#: ../../templates/show_account.inc.php:54 +#: ../../templates/show_add_user.inc.php:69 +#: ../../templates/show_edit_user.inc.php:72 +#: ../../templates/show_install_account.inc.php:63 +#: ../../templates/show_user_registration.inc.php:100 +msgid "Confirm Password" +msgstr "تأكيد كلمة السر" + +#: ../../templates/show_account.inc.php:60 +#: ../../templates/show_edit_user.inc.php:133 +#: ../../templates/show_manage_catalogs.inc.php:32 +msgid "Clear Stats" +msgstr "إحصائيات واضحة" + +#: ../../templates/show_account.inc.php:70 +msgid "Update Account" +msgstr "" + +#: ../../templates/show_add_access.inc.php:22 +msgid "Add Access Control List" +msgstr "" + +#: ../../templates/show_add_access.inc.php:48 +#: ../../templates/show_edit_access.inc.php:31 +msgid "ACL Type" +msgstr "" + +#: ../../templates/show_add_access.inc.php:54 +#: ../../templates/show_add_access.inc.php:55 +#: ../../templates/show_add_access.inc.php:60 +#: ../../templates/show_add_access.inc.php:61 +#, php-format +msgid "%s + %s" +msgstr "" + +#: ../../templates/show_add_access.inc.php:73 +#: ../../templates/show_edit_access.inc.php:43 +msgid "IPv4 or IPv6 Addresses" +msgstr "عناوين IPv4 أو IPv6" + +#: ../../templates/show_add_access.inc.php:78 +#: ../../templates/show_edit_access.inc.php:48 +msgid "Start" +msgstr "يبدأ" + +#: ../../templates/show_add_access.inc.php:88 +#: ../../templates/show_edit_access.inc.php:53 +msgid "End" +msgstr "نهاية" + +#: ../../templates/show_add_access.inc.php:102 +msgid "Create ACL" +msgstr "خلق دورى أبطال العرب" + +#: ../../templates/show_add_catalog.inc.php:26 +#: ../../templates/sidebar_admin.inc.php:26 +msgid "Add a Catalog" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:27 +msgid "" +"In the form below enter either a local path (i.e. /data/music) or the URL to " +"a remote Ampache installation (i.e http://theotherampache.com)" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:33 +msgid "Catalog Name" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:36 +#: ../../templates/show_edit_catalog.inc.php:31 +msgid "Auto-inserted Fields" +msgstr "إدراج حقول السيارات" + +#: ../../templates/show_add_catalog.inc.php:37 +#: ../../templates/show_edit_catalog.inc.php:32 +msgid "album name" +msgstr "اسم الألبوم" + +#: ../../templates/show_add_catalog.inc.php:38 +#: ../../templates/show_edit_catalog.inc.php:33 +msgid "artist name" +msgstr "artist name" + +#: ../../templates/show_add_catalog.inc.php:39 +#: ../../templates/show_edit_catalog.inc.php:34 +msgid "id3 comment" +msgstr "ID3 التعليق" + +#: ../../templates/show_add_catalog.inc.php:40 +#: ../../templates/show_edit_catalog.inc.php:35 +msgid "track number (padded with leading 0)" +msgstr "المسار رقم (المحشوة مع كبار 0)" + +#: ../../templates/show_add_catalog.inc.php:41 +#: ../../templates/show_edit_catalog.inc.php:36 +msgid "song title" +msgstr "عنوان الأغنية" + +#: ../../templates/show_add_catalog.inc.php:42 +#: ../../templates/show_edit_catalog.inc.php:37 +msgid "year" +msgstr "سنة" + +#: ../../templates/show_add_catalog.inc.php:43 +#: ../../templates/show_edit_catalog.inc.php:38 +msgid "other" +msgstr "أخرى" + +#: ../../templates/show_add_catalog.inc.php:47 +#: ../../templates/show_edit_catalog.inc.php:42 +msgid "Catalog Type" +msgstr "نوع التسويقي" + +#: ../../templates/show_add_catalog.inc.php:51 +msgid "Filename Pattern" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:55 +#: ../../templates/show_edit_catalog.inc.php:53 +msgid "Folder Pattern" +msgstr "خطة مجلد" + +#: ../../templates/show_add_catalog.inc.php:55 +#: ../../templates/show_edit_catalog.inc.php:53 +msgid "(no leading or ending '/')" +msgstr "(لا يؤدي إنهاء أو '/')" + +#: ../../templates/show_add_catalog.inc.php:59 +msgid "Gather Album Art" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:63 +msgid "Build Playlists from playlist Files (m3u, asx, pls, xspf)" +msgstr "" + +#: ../../templates/show_add_catalog.inc.php:72 +msgid "Add Catalog" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:23 +#, fuzzy +msgid "Create Channel" +msgstr "خلق تاريخ" + +#: ../../templates/show_add_channel.inc.php:29 +#: ../../templates/show_channels.inc.php:33 +#: ../../templates/show_edit_channel_row.inc.php:27 +#, fuzzy +msgid "Stream Source" +msgstr "تيار الوصول" + +#: ../../templates/show_add_channel.inc.php:42 +#: ../../templates/show_catalog_types.inc.php:30 +#: ../../templates/show_catalog_types.inc.php:64 +#: ../../templates/show_edit_broadcast_row.inc.php:31 +#: ../../templates/show_edit_channel_row.inc.php:48 +#: ../../templates/show_localplay_controllers.inc.php:30 +#: ../../templates/show_localplay_controllers.inc.php:64 +#: ../../templates/show_plugins.inc.php:30 +#: ../../templates/show_plugins.inc.php:70 +msgid "Description" +msgstr "وصف" + +#: ../../templates/show_add_channel.inc.php:49 +#: ../../templates/show_edit_channel_row.inc.php:52 +msgid "Url" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:56 +#: ../../templates/show_channels.inc.php:31 +#: ../../templates/show_edit_channel_row.inc.php:56 +msgid "Interface" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:70 +#: ../../templates/show_channel_row.inc.php:44 +#: ../../templates/show_edit_broadcast_row.inc.php:36 +#: ../../templates/show_edit_channel_row.inc.php:65 +msgid "Authentication Required" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:82 +#: ../../templates/show_channels.inc.php:35 +#: ../../templates/show_edit_channel_row.inc.php:73 +msgid "Loop" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:88 +#: ../../templates/show_edit_channel_row.inc.php:76 +msgid "Max Listeners" +msgstr "" + +#: ../../templates/show_add_channel.inc.php:95 +#: ../../templates/show_channels.inc.php:36 +#: ../../templates/show_edit_channel_row.inc.php:80 +#, fuzzy +msgid "Stream Type" +msgstr "نوع البحث" + +#: ../../templates/show_add_channel.inc.php:111 +#: ../../templates/show_add_playlist.inc.php:41 +#: ../../templates/show_add_share.inc.php:71 +#: ../../templates/show_add_shout.inc.php:53 +msgid "Create" +msgstr "" + +#: ../../templates/show_add_live_stream.inc.php:23 +#: ../../templates/show_live_stream.inc.php:27 +msgid "Add Radio Station" +msgstr "" + +#: ../../templates/show_add_live_stream.inc.php:34 +#: ../../templates/show_edit_live_stream_row.inc.php:35 +msgid "Homepage" +msgstr "الصفحة الرئيسية" + +#: ../../templates/show_add_live_stream.inc.php:41 +#: ../../templates/show_edit_live_stream_row.inc.php:31 +#: ../../templates/show_live_streams.inc.php:32 +#: ../../templates/show_live_streams.inc.php:58 +msgid "Stream URL" +msgstr "تيار عنوان" + +#: ../../templates/show_add_live_stream.inc.php:48 +#: ../../templates/show_edit_live_stream_row.inc.php:39 +#: ../../templates/show_live_streams.inc.php:33 +#: ../../templates/show_live_streams.inc.php:59 +#: ../../templates/show_videos.inc.php:32 +#: ../../templates/show_videos.inc.php:61 +msgid "Codec" +msgstr "" + +#: ../../templates/show_add_playlist.inc.php:23 +msgid "Create a new playlist" +msgstr "" + +#: ../../templates/show_adds_catalog.inc.php:23 +#, fuzzy +msgid "Starting New Song Search" +msgstr "بدءا من البحث في الفنون الألبوم" + +#. HINT: Catalog Name +#: ../../templates/show_adds_catalog.inc.php:25 +#, php-format +msgid "Starting New Song Search on %s catalog" +msgstr "" + +#: ../../templates/show_adds_catalog.inc.php:27 +#: ../../templates/show_run_add_catalog.inc.php:25 +msgid "Found" +msgstr "" + +#: ../../templates/show_adds_catalog.inc.php:28 +#: ../../templates/show_clean_catalog.inc.php:28 +#: ../../templates/show_gather_art.inc.php:26 +#: ../../templates/show_run_add_catalog.inc.php:26 +#: ../../templates/show_verify_catalog.inc.php:30 +msgid "Reading" +msgstr "" + +#: ../../templates/show_add_share.inc.php:23 +#, fuzzy +msgid "Create Share" +msgstr "خلق تاريخ" + +#: ../../templates/show_add_share.inc.php:29 +msgid "Shared Object" +msgstr "" + +#: ../../templates/show_add_share.inc.php:42 +#: ../../templates/show_shared_objects.inc.php:32 +msgid "Max Counter" +msgstr "" + +#: ../../templates/show_add_share.inc.php:49 +#: ../../templates/show_shared_objects.inc.php:35 +msgid "Expire Days" +msgstr "" + +#: ../../templates/show_add_share.inc.php:55 +#: ../../templates/show_shared_objects.inc.php:33 +#, fuzzy +msgid "Allow Stream" +msgstr "يتدفق" + +#: ../../templates/show_add_share.inc.php:62 +#: ../../templates/show_shared_objects.inc.php:34 +#, fuzzy +msgid "Allow Download" +msgstr "تنزيل" + +#: ../../templates/show_add_shout.inc.php:28 +msgid "Post to Shoutbox" +msgstr "" + +#: ../../templates/show_add_shout.inc.php:37 +#: ../../templates/show_edit_shout.inc.php:31 +msgid "Comment:" +msgstr "" + +#: ../../templates/show_add_shout.inc.php:44 +#: ../../templates/show_edit_shout.inc.php:37 +#, fuzzy +msgid "Stick this comment" +msgstr "ID3 التعليق" + +#: ../../templates/show_add_shout.inc.php:62 +#: ../../templates/show_shoutbox.inc.php:23 +msgid "Shoutbox" +msgstr "" + +#: ../../templates/show_add_user.inc.php:23 +msgid "Adding a New User" +msgstr "" + +#: ../../templates/show_add_user.inc.php:37 +#: ../../templates/show_edit_user.inc.php:40 +#: ../../templates/show_user.inc.php:35 +#: ../../templates/show_user_registration.inc.php:77 +msgid "Full Name" +msgstr "الاسم الكامل" + +#: ../../templates/show_add_user.inc.php:77 +#: ../../templates/show_edit_user.inc.php:80 +msgid "User Access Level" +msgstr "مستوى الوصول للمستخدم" + +#: ../../templates/show_add_user.inc.php:92 +#: ../../templates/show_edit_user.inc.php:95 +msgid "Avatar" +msgstr "" + +#: ../../templates/show_add_user.inc.php:102 +#: ../../templates/sidebar_admin.inc.php:33 +msgid "Add User" +msgstr "" + +#: ../../templates/show_album_art.inc.php:28 +#, fuzzy +msgid "Select New Album Art" +msgstr "البوم الفن" + +#: ../../templates/show_album_art.inc.php:41 +#: ../../templates/show_big_art.inc.php:30 +msgid "Album Art" +msgstr "البوم الفن" + +#: ../../templates/show_album_art.inc.php:47 +msgid "Invalid" +msgstr "" + +#: ../../templates/show_album_art.inc.php:49 +#: ../../templates/show_disabled_songs.inc.php:28 +#: ../../templates/show_disabled_songs.inc.php:54 +msgid "Select" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:32 +#: ../../templates/show_album.inc.php:35 +#: ../../templates/show_artist.inc.php:37 +msgid "Search on Google ..." +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:33 +#: ../../templates/show_album.inc.php:36 +#: ../../templates/show_artist.inc.php:38 +msgid "Search on Wikipedia ..." +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:34 +#: ../../templates/show_album.inc.php:37 +#: ../../templates/show_artist.inc.php:39 +msgid "Search on Last.fm ..." +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:50 +#: ../../templates/show_album.inc.php:71 +#: ../../templates/show_albums.inc.php:46 +#: ../../templates/show_albums.inc.php:88 +#: ../../templates/show_artist.inc.php:68 +#: ../../templates/show_broadcasts.inc.php:33 +#: ../../templates/show_catalogs.inc.php:32 +#: ../../templates/show_catalogs.inc.php:60 +#: ../../templates/show_channels.inc.php:42 +#: ../../templates/show_missing_album.inc.php:51 +#: ../../templates/show_missing_albums.inc.php:31 +#: ../../templates/show_playlists.inc.php:33 +#: ../../templates/show_playlists.inc.php:61 +#: ../../templates/show_shared_objects.inc.php:37 +#: ../../templates/show_smartplaylists.inc.php:33 +#: ../../templates/show_smartplaylists.inc.php:59 +#: ../../templates/show_wanted_albums.inc.php:30 +msgid "Actions" +msgstr "الإجراءات" + +#: ../../templates/show_album_group_disks.inc.php:60 +#: ../../templates/show_album_group_disks.inc.php:61 +#: ../../templates/show_album_group_disks.inc.php:108 +#: ../../templates/show_album.inc.php:81 ../../templates/show_album.inc.php:82 +#: ../../templates/show_album_row.inc.php:29 +#: ../../templates/show_artist_row.inc.php:29 +#: ../../templates/show_missing_album.inc.php:62 +#: ../../templates/show_missing_album.inc.php:63 +#: ../../templates/show_playlist_row.inc.php:29 +#: ../../templates/show_playlist_song_row.inc.php:29 +#: ../../templates/show_random_albums.inc.php:48 +#: ../../templates/show_recently_played.inc.php:100 +#: ../../templates/show_smartplaylist_row.inc.php:29 +#: ../../templates/show_song.inc.php:61 +#: ../../templates/show_song_preview_row.inc.php:28 +#: ../../templates/show_song_row.inc.php:29 +#: ../../templates/show_video_row.inc.php:29 +#, fuzzy +msgid "Play last" +msgstr "التشغيل" + +#: ../../templates/show_album_group_disks.inc.php:65 +#: ../../templates/show_album_group_disks.inc.php:66 +#: ../../templates/show_album_group_disks.inc.php:110 +#: ../../templates/show_album.inc.php:86 ../../templates/show_album.inc.php:87 +#: ../../templates/show_album_row.inc.php:47 +#: ../../templates/show_artist_row.inc.php:37 +#: ../../templates/show_live_stream_row.inc.php:34 +#: ../../templates/show_missing_album.inc.php:67 +#: ../../templates/show_missing_album.inc.php:68 +#: ../../templates/show_playlist_row.inc.php:37 +#: ../../templates/show_playlist_song_row.inc.php:37 +#: ../../templates/show_random_albums.inc.php:51 +#: ../../templates/show_recently_played.inc.php:108 +#: ../../templates/show_smartplaylist_row.inc.php:37 +#: ../../templates/show_song.inc.php:65 +#: ../../templates/show_song_preview_row.inc.php:38 +#: ../../templates/show_song_row.inc.php:37 +#: ../../templates/show_video_row.inc.php:37 +#, fuzzy +msgid "Add to temporary playlist" +msgstr "استيراد قائمة التشغيل" + +#: ../../templates/show_album_group_disks.inc.php:69 +#: ../../templates/show_album_group_disks.inc.php:70 +#: ../../templates/show_album_group_disks.inc.php:111 +#: ../../templates/show_album.inc.php:90 ../../templates/show_album.inc.php:91 +#: ../../templates/show_album_row.inc.php:48 +#: ../../templates/show_artist_row.inc.php:38 +#: ../../templates/show_playlist_row.inc.php:38 +msgid "Random to temporary playlist" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:74 +#: ../../templates/show_album_group_disks.inc.php:75 +#: ../../templates/show_album.inc.php:102 +#: ../../templates/show_album.inc.php:103 +msgid "Do you really want to reset album art?" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:74 +#: ../../templates/show_album_group_disks.inc.php:75 +#: ../../templates/show_album.inc.php:102 +#: ../../templates/show_album.inc.php:103 +msgid "Reset Album Art" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:79 +#: ../../templates/show_album_group_disks.inc.php:80 +#: ../../templates/show_album.inc.php:107 +#: ../../templates/show_album.inc.php:108 +msgid "Find Album Art" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:84 +#: ../../templates/show_album_group_disks.inc.php:85 +#: ../../templates/show_album_group_disks.inc.php:124 +#: ../../templates/show_album.inc.php:138 +#: ../../templates/show_album.inc.php:139 +#: ../../templates/show_artist.inc.php:114 +#: ../../templates/show_artist.inc.php:115 +#: ../../templates/show_install_config.inc.php:134 +#: ../../templates/show_install_config.inc.php:149 +#: ../../templates/show_install_config.inc.php:165 +#: ../../templates/show_playlist_song_row.inc.php:56 +#: ../../templates/show_share.inc.php:32 ../../templates/show_share.inc.php:33 +#: ../../templates/show_song.inc.php:76 +#: ../../templates/show_song_row.inc.php:68 +#: ../../templates/show_video_row.inc.php:46 +msgid "Download" +msgstr "تنزيل" + +#: ../../templates/show_album_group_disks.inc.php:115 +#: ../../templates/show_album.inc.php:96 ../../templates/show_album.inc.php:97 +#: ../../templates/show_playlist.inc.php:41 +#: ../../templates/show_playlist.inc.php:42 +msgid "Save Tracks Order" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:121 +#: ../../templates/show_album.inc.php:122 +#: ../../templates/show_album.inc.php:123 +#: ../../templates/show_album_row.inc.php:71 +#: ../../templates/show_playlist_row.inc.php:54 +#: ../../templates/show_playlist_song_row.inc.php:60 +#: ../../templates/show_shares.inc.php:23 ../../templates/show_song.inc.php:72 +#: ../../templates/show_song_row.inc.php:65 +msgid "Share" +msgstr "" + +#: ../../templates/show_album_group_disks.inc.php:127 +#: ../../templates/show_album.inc.php:128 +#: ../../templates/show_album.inc.php:131 +#: ../../templates/show_album_row.inc.php:79 +#, fuzzy +msgid "Album edit" +msgstr "البوم الفن" + +#: ../../templates/show_album.inc.php:66 +#: ../../templates/show_artist.inc.php:63 +msgid "times" +msgstr "" + +#: ../../templates/show_album.inc.php:112 +#: ../../templates/show_album.inc.php:113 +#: ../../templates/show_artist.inc.php:108 +#: ../../templates/show_artist.inc.php:109 +msgid "Do you really want to update from tags?" +msgstr "" + +#: ../../templates/show_album.inc.php:112 +#: ../../templates/show_album.inc.php:113 +#: ../../templates/show_artist.inc.php:108 +#: ../../templates/show_artist.inc.php:109 +msgid "Update from tags" +msgstr "" + +#: ../../templates/show_album.inc.php:132 +#, fuzzy +msgid "Edit Album" +msgstr "الألبوم" + +#: ../../templates/show_album_row.inc.php:50 +#: ../../templates/show_artist_row.inc.php:40 +#: ../../templates/show_playlist_row.inc.php:40 +#: ../../templates/show_playlist_song_row.inc.php:39 +#: ../../templates/show_recently_played.inc.php:110 +#: ../../templates/show_smartplaylist_row.inc.php:39 +#: ../../templates/show_song_preview_row.inc.php:40 +#: ../../templates/show_song_row.inc.php:39 +msgid "Add to existing playlist" +msgstr "" + +#: ../../templates/show_albums.inc.php:32 +#: ../../templates/show_albums.inc.php:74 +msgid "Cover" +msgstr "غطاء" + +#: ../../templates/show_albums.inc.php:39 +#: ../../templates/show_albums.inc.php:81 +#: ../../templates/show_artists.inc.php:38 +#: ../../templates/show_artists.inc.php:77 +#: ../../templates/show_edit_album_row.inc.php:55 +#: ../../templates/show_edit_artist_row.inc.php:35 +#: ../../templates/show_edit_song_row.inc.php:57 +#: ../../templates/show_now_playing_row.inc.php:51 +#: ../../templates/show_playlist_songs.inc.php:35 +#: ../../templates/show_playlist_songs.inc.php:72 +#: ../../templates/show_recommended_artists.inc.php:35 +#: ../../templates/show_recommended_artists.inc.php:84 +#: ../../templates/show_songs.inc.php:35 ../../templates/show_songs.inc.php:79 +#: ../../templates/show_stats.inc.php:36 +#: ../../templates/show_videos.inc.php:35 +#: ../../templates/show_videos.inc.php:64 +msgid "Tags" +msgstr "العلامات" + +#: ../../templates/show_albums.inc.php:44 +#: ../../templates/show_albums.inc.php:86 +#: ../../templates/show_artists.inc.php:43 +#: ../../templates/show_artists.inc.php:82 +#: ../../templates/show_now_playing_row.inc.php:97 +#: ../../templates/show_playlist_songs.inc.php:45 +#: ../../templates/show_playlist_songs.inc.php:78 +#: ../../templates/show_recommended_artists.inc.php:40 +#: ../../templates/show_recommended_artists.inc.php:89 +#: ../../templates/show_song.inc.php:40 ../../templates/show_songs.inc.php:47 +msgid "Fav." +msgstr "" + +#: ../../templates/show_albums.inc.php:66 +#, fuzzy +msgid "No album found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_artist.inc.php:42 +#: ../../templates/show_artist.inc.php:162 +#: ../../templates/show_artist.inc.php:170 +#: ../../templates/show_artist.inc.php:178 +#: ../../templates/show_index.inc.php:32 +#: ../../templates/show_missing_artist.inc.php:33 +#: ../../templates/show_now_playing_row.inc.php:71 +#: ../../templates/show_now_playing_row.inc.php:77 +msgid "Loading..." +msgstr "" + +#: ../../templates/show_artist.inc.php:73 +#: ../../templates/show_artist.inc.php:75 +#, fuzzy +msgid "Show all" +msgstr "وتظهر قائمة التشغيل" + +#: ../../templates/show_artist.inc.php:79 +#: ../../templates/show_artist.inc.php:81 +#, fuzzy +msgid "Show albums" +msgstr "وتظهر قائمة التشغيل" + +#: ../../templates/show_artist.inc.php:86 +#: ../../templates/show_artist.inc.php:87 +#: ../../templates/show_playlist.inc.php:55 +#: ../../templates/show_playlist.inc.php:56 +#, fuzzy +msgid "Play all" +msgstr "تشغيل" + +#: ../../templates/show_artist.inc.php:92 +#: ../../templates/show_artist.inc.php:93 +#: ../../templates/show_playlist.inc.php:61 +#: ../../templates/show_playlist.inc.php:62 +#, fuzzy +msgid "Play all last" +msgstr "التشغيل" + +#. HINT: Artist Fullname +#: ../../templates/show_artist.inc.php:98 +#: ../../templates/show_artist.inc.php:99 +#: ../../templates/show_playlist.inc.php:66 +#: ../../templates/show_playlist.inc.php:67 +msgid "Add all to temporary playlist" +msgstr "" + +#. HINT: Artist Fullname +#: ../../templates/show_artist.inc.php:103 +#: ../../templates/show_artist.inc.php:104 +#: ../../templates/show_playlist.inc.php:70 +#: ../../templates/show_playlist.inc.php:71 +msgid "Random all to temporary playlist" +msgstr "" + +#: ../../templates/show_artist.inc.php:119 +#: ../../templates/show_artist.inc.php:122 +#: ../../templates/show_artist_row.inc.php:61 +#, fuzzy +msgid "Artist edit" +msgstr "الفنان والعنوان" + +#: ../../templates/show_artist.inc.php:123 +#, fuzzy +msgid "Edit Artist" +msgstr "الفنان" + +#: ../../templates/show_artist.inc.php:127 +msgid "Show Art" +msgstr "وتبين للفنون" + +#: ../../templates/show_artist.inc.php:138 +#: ../../templates/show_artist.inc.php:162 +#: ../../templates/show_missing_albums.inc.php:23 +#, fuzzy +msgid "Missing Albums" +msgstr "البوم" + +#: ../../templates/show_artist.inc.php:141 +#: ../../templates/show_artist.inc.php:170 +#: ../../templates/show_now_playing_row.inc.php:70 +#: ../../templates/show_now_playing_similar.inc.php:27 +#: ../../templates/show_recommended_artists.inc.php:25 +#, fuzzy +msgid "Similar Artists" +msgstr "الفنانون" + +#: ../../templates/show_artist.inc.php:144 +#: ../../templates/show_artist.inc.php:178 +msgid "Events" +msgstr "" + +#: ../../templates/show_artists.inc.php:37 +#: ../../templates/show_artists.inc.php:76 +#: ../../templates/show_democratic_playlist.inc.php:60 +#: ../../templates/show_democratic_playlist.inc.php:107 +#: ../../templates/show_playlist_songs.inc.php:36 +#: ../../templates/show_playlist_songs.inc.php:73 +#: ../../templates/show_recommended_artists.inc.php:34 +#: ../../templates/show_recommended_artists.inc.php:83 +#: ../../templates/show_songs.inc.php:36 ../../templates/show_songs.inc.php:80 +#: ../../templates/show_videos.inc.php:34 +#: ../../templates/show_videos.inc.php:63 +msgid "Time" +msgstr "وقت" + +#: ../../templates/show_artists.inc.php:65 +#, fuzzy +msgid "No artist found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_big_art.inc.php:34 +msgid "Click to close window" +msgstr "انقر لاغلاق النافذة" + +#: ../../templates/show_broadcast_row.inc.php:33 +#: ../../templates/show_channel_row.inc.php:36 +#: ../../templates/show_channel_row.inc.php:37 +msgid "Yes" +msgstr "" + +#: ../../templates/show_broadcast_row.inc.php:33 +#: ../../templates/show_channel_row.inc.php:36 +#: ../../templates/show_channel_row.inc.php:37 +#, fuzzy +msgid "No" +msgstr "بلا" + +#: ../../templates/show_broadcasts_dialog.inc.php:39 +msgid "New broadcast" +msgstr "" + +#: ../../templates/show_broadcasts.inc.php:30 +#: ../../templates/show_edit_broadcast_row.inc.php:39 +#: ../../templates/show_edit_channel_row.inc.php:88 +#: ../../templates/show_song.inc.php:88 +msgid "Genre" +msgstr "" + +#: ../../templates/show_broadcasts.inc.php:31 +#, fuzzy +msgid "Started" +msgstr "يبدأ" + +#: ../../templates/show_broadcasts.inc.php:32 +#: ../../templates/show_channels.inc.php:39 +msgid "Listeners" +msgstr "" + +#: ../../templates/show_broadcasts.inc.php:48 +#, fuzzy +msgid "No broadcast found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_catalog_row.inc.php:35 +msgid "Verify" +msgstr "" + +#: ../../templates/show_catalog_row.inc.php:36 +msgid "Clean" +msgstr "" + +#: ../../templates/show_catalog_row.inc.php:37 +#: ../../templates/show_create_democratic.inc.php:60 +#: ../../templates/show_edit_access.inc.php:78 +#: ../../templates/show_edit_shout.inc.php:41 +#: ../../templates/show_manage_catalogs.inc.php:46 +#: ../../templates/show_preference_admin.inc.php:57 +msgid "Update" +msgstr "" + +#: ../../templates/show_catalogs.inc.php:28 +#: ../../templates/show_catalogs.inc.php:56 +msgid "Info" +msgstr "" + +#: ../../templates/show_catalogs.inc.php:29 +#: ../../templates/show_catalogs.inc.php:57 +#: ../../templates/show_stats.inc.php:67 +msgid "Last Verify" +msgstr "تحقق من الماضي" + +#: ../../templates/show_catalogs.inc.php:30 +#: ../../templates/show_catalogs.inc.php:58 +#: ../../templates/show_stats.inc.php:68 +msgid "Last Add" +msgstr "أضف الماضي" + +#: ../../templates/show_catalogs.inc.php:31 +#: ../../templates/show_catalogs.inc.php:59 +#: ../../templates/show_stats.inc.php:69 +msgid "Last Clean" +msgstr "آخر النظيفة" + +#: ../../templates/show_catalogs.inc.php:48 +#, fuzzy +msgid "No catalog found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_catalog_types.inc.php:31 +#: ../../templates/show_catalog_types.inc.php:65 +#: ../../templates/show_localplay_controllers.inc.php:31 +#: ../../templates/show_localplay_controllers.inc.php:65 +#: ../../templates/show_plugins.inc.php:31 +#: ../../templates/show_plugins.inc.php:71 +msgid "Version" +msgstr "النسخة" + +#: ../../templates/show_catalog_types.inc.php:46 +#: ../../templates/show_localplay_controllers.inc.php:46 +#: ../../templates/show_plugins.inc.php:43 +msgid "Activate" +msgstr "تفعيل" + +#: ../../templates/show_catalog_types.inc.php:57 +#: ../../templates/show_disabled_songs.inc.php:48 +#: ../../templates/show_localplay_controllers.inc.php:57 +#: ../../templates/show_localplay_playlist.inc.php:55 +#: ../../templates/show_manage_shoutbox.inc.php:47 +#: ../../templates/show_plugins.inc.php:63 +msgid "No Records Found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_channels.inc.php:29 +msgid "#" +msgstr "" + +#: ../../templates/show_channels.inc.php:38 +#, fuzzy +msgid "Start Date" +msgstr "يبدأ" + +#: ../../templates/show_channels.inc.php:40 +#, fuzzy +msgid "Stream Url" +msgstr "تيار عنوان" + +#: ../../templates/show_channels.inc.php:41 +msgid "State" +msgstr "" + +#: ../../templates/show_channels.inc.php:57 +#, fuzzy +msgid "No channel found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_clean_catalog.inc.php:23 +#, fuzzy +msgid "Clean Catalog" +msgstr "إحصائيات واضحة" + +#. HINT: Catalog Name +#: ../../templates/show_clean_catalog.inc.php:25 +#, php-format +msgid "Cleaning the %s Catalog" +msgstr "" + +#: ../../templates/show_clean_catalog.inc.php:27 +msgid "Checking" +msgstr "" + +#: ../../templates/show_concerts.inc.php:23 +msgid "Coming Events" +msgstr "" + +#: ../../templates/show_concerts.inc.php:27 +#: ../../templates/show_concerts.inc.php:52 +#: ../../templates/show_ip_history.inc.php:45 +#: ../../templates/show_ip_history.inc.php:59 +msgid "Date" +msgstr "" + +#: ../../templates/show_concerts.inc.php:28 +#: ../../templates/show_concerts.inc.php:53 +msgid "Place" +msgstr "" + +#: ../../templates/show_concerts.inc.php:29 +#: ../../templates/show_concerts.inc.php:54 +#, fuzzy +msgid "Location" +msgstr "العمل" + +#: ../../templates/show_concerts.inc.php:42 +#, fuzzy +msgid "No coming events found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_concerts.inc.php:48 +#, fuzzy +msgid "Past Events" +msgstr "شوهد للمرة الاخيرة" + +#: ../../templates/show_concerts.inc.php:67 +#, fuzzy +msgid "No past events found" +msgstr "وتظهر قائمة التشغيل" + +#: ../../templates/show_confirmation.inc.php:29 +#: ../../templates/show_install_check.inc.php:65 +#: ../../templates/show_update_items.inc.php:28 +msgid "Continue" +msgstr "تواصل" + +#: ../../templates/show_create_democratic.inc.php:23 +#: ../../templates/show_democratic.inc.php:35 +#: ../../templates/show_democratic.inc.php:37 +msgid "Configure Democratic Playlist" +msgstr "" + +#: ../../templates/show_create_democratic.inc.php:31 +#: ../../templates/show_manage_democratic.inc.php:27 +msgid "Base Playlist" +msgstr "قاعدة التشغيل" + +#: ../../templates/show_create_democratic.inc.php:35 +msgid "Cooldown Time" +msgstr "" + +#: ../../templates/show_create_democratic.inc.php:49 +#, fuzzy +msgid "Make Default" +msgstr "افتراضي" + +#: ../../templates/show_create_democratic.inc.php:54 +msgid "Force Democratic Play" +msgstr "" + +#: ../../templates/show_debug.inc.php:23 +msgid "Debug Tools" +msgstr "" + +#: ../../templates/show_debug.inc.php:27 ../../templates/show_debug.inc.php:28 +msgid "Generate Configuration" +msgstr "" + +#: ../../templates/show_debug.inc.php:31 ../../templates/show_debug.inc.php:32 +msgid "Set Database Charset" +msgstr "" + +#: ../../templates/show_debug.inc.php:37 +msgid "PHP Settings" +msgstr "" + +#: ../../templates/show_debug.inc.php:45 +msgid "Setting" +msgstr "" + +#: ../../templates/show_debug.inc.php:46 ../../templates/show_debug.inc.php:99 +#: ../../templates/show_dynamic.inc.php:28 +#: ../../templates/show_preference_box.inc.php:43 +#: ../../templates/show_preference_box.inc.php:77 +#: ../../templates/show_user_preferences.inc.php:38 +msgid "Value" +msgstr "القيمة" + +#: ../../templates/show_debug.inc.php:51 +msgid "Memory Limit" +msgstr "" + +#: ../../templates/show_debug.inc.php:55 +msgid "Maximum Execution Time" +msgstr "" + +#: ../../templates/show_debug.inc.php:59 +msgid "Override Execution Time" +msgstr "" + +#: ../../templates/show_debug.inc.php:60 +msgid "Failed" +msgstr "" + +#: ../../templates/show_debug.inc.php:60 +msgid "Succeeded" +msgstr "" + +#: ../../templates/show_debug.inc.php:63 +msgid "Safe Mode" +msgstr "" + +#: ../../templates/show_debug.inc.php:71 +msgid "Zlib Support" +msgstr "" + +#: ../../templates/show_debug.inc.php:75 +msgid "GD Support" +msgstr "" + +#: ../../templates/show_debug.inc.php:79 +msgid "Iconv Support" +msgstr "" + +#: ../../templates/show_debug.inc.php:83 +msgid "Gettext Support" +msgstr "Gettext الدعم" + +#: ../../templates/show_debug.inc.php:90 +msgid "Current Configuration" +msgstr "" + +#: ../../templates/show_debug.inc.php:98 +#: ../../templates/show_preference_admin.inc.php:32 +#: ../../templates/show_preference_admin.inc.php:51 +#: ../../templates/show_preference_box.inc.php:42 +#: ../../templates/show_preference_box.inc.php:76 +#: ../../templates/show_user_preferences.inc.php:37 +msgid "Preference" +msgstr "الأفضلية" + +#: ../../templates/show_debug.inc.php:125 ../../update.php:64 +msgid "Ampache Update" +msgstr "تحديث Ampache" + +#: ../../templates/show_debug.inc.php:126 +#, fuzzy +msgid "Installed Ampache version" +msgstr "النسخة" + +#: ../../templates/show_debug.inc.php:127 +#, fuzzy +msgid "Latest Ampache version" +msgstr "Ampache التثبيت" + +#: ../../templates/show_debug.inc.php:128 +msgid "Force check" +msgstr "" + +#: ../../templates/show_democratic.inc.php:23 +#, php-format +msgid "%s Playlist" +msgstr "" + +#: ../../templates/show_democratic.inc.php:30 +#: ../../templates/show_manage_democratic.inc.php:28 +msgid "Cooldown" +msgstr "تهدئة" + +#: ../../templates/show_democratic.inc.php:42 +msgid "Play Democratic Playlist" +msgstr "" + +#: ../../templates/show_democratic_playlist.inc.php:44 +msgid "Playing from base Playlist" +msgstr "" + +#: ../../templates/show_democratic_playlist.inc.php:56 +#: ../../templates/show_democratic_playlist.inc.php:103 +msgid "Votes" +msgstr "" + +#: ../../templates/show_democratic_playlist.inc.php:80 +msgid "Remove Vote" +msgstr "" + +#: ../../templates/show_democratic_playlist.inc.php:82 +msgid "Add Vote" +msgstr "" + +#: ../../templates/show_denied.inc.php:44 +#: ../../templates/sidebar_localplay.inc.php:72 +msgid "Access Denied" +msgstr "تم رفض الوصول" + +#: ../../templates/show_denied.inc.php:45 +#: ../../templates/show_denied.inc.php:51 +msgid "This event has been logged." +msgstr "" + +#: ../../templates/show_denied.inc.php:49 +msgid "" +"You have been redirected to this page because you do not have access to this " +"function." +msgstr "" + +#: ../../templates/show_denied.inc.php:50 +msgid "" +"If you believe this is an error please contact an Ampache administrator." +msgstr "" + +#: ../../templates/show_denied.inc.php:53 +msgid "" +"You have been redirected to this page because you attempted to access a " +"function that is disabled in the demo." +msgstr "" + +#: ../../templates/show_disabled_songs.inc.php:33 +#: ../../templates/show_disabled_songs.inc.php:59 +msgid "Addition Time" +msgstr "" + +#: ../../templates/show_duplicate.inc.php:23 +#: ../../templates/show_duplicate.inc.php:36 +#: ../../templates/sidebar_modules.inc.php:33 +msgid "Find Duplicates" +msgstr "العثور على مكررات" + +#: ../../templates/show_duplicate.inc.php:27 +msgid "Search Type" +msgstr "نوع البحث" + +#: ../../templates/show_duplicate.inc.php:30 +msgid "Artist and Title" +msgstr "الفنان والعنوان" + +#: ../../templates/show_duplicate.inc.php:31 +msgid "Artist, Album and Title" +msgstr "فنانة ، وعنوان الألبوم" + +#: ../../templates/show_duplicates.inc.php:23 +msgid "Duplicate Songs" +msgstr "" + +#: ../../templates/show_duplicates.inc.php:28 +#: ../../templates/show_duplicates.inc.php:67 +#: ../../templates/show_lyrics.inc.php:44 +#: ../../templates/show_now_playing_row.inc.php:39 +#: ../../templates/show_recently_played.inc.php:30 +#: ../../templates/show_recently_played.inc.php:141 +#: ../../templates/sidebar_home.inc.php:70 +msgid "Song" +msgstr "" + +#: ../../templates/show_duplicates.inc.php:31 +#: ../../templates/show_duplicates.inc.php:70 +#: ../../templates/show_random.inc.php:54 ../../templates/show_song.inc.php:89 +msgid "Length" +msgstr "" + +#: ../../templates/show_duplicates.inc.php:33 +#: ../../templates/show_duplicates.inc.php:72 +msgid "Size" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:23 +msgid "Advanced Random Rules" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:26 +msgid "Field" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:27 +msgid "Operator" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:29 +msgid "Method" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:51 +msgid "Like" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:59 +msgid "OR" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:60 +msgid "AND" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:66 +msgid "Add Rule" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:69 +msgid "Save Rules As" +msgstr "" + +#: ../../templates/show_dynamic.inc.php:72 +msgid "Load Saved Rules" +msgstr "" + +#: ../../templates/show_edit_access.inc.php:23 +msgid "Edit Access Control List" +msgstr "" + +#: ../../templates/show_edit_album_row.inc.php:51 +#: ../../templates/show_edit_artist_row.inc.php:31 +#: ../../templates/show_edit_song_row.inc.php:53 +msgid "MusicBrainz ID" +msgstr "" + +#: ../../templates/show_edit_album_row.inc.php:62 +msgid " Apply tags to all childs (override tags for songs)" +msgstr "" + +#: ../../templates/show_edit_artist_row.inc.php:40 +msgid " Apply tags to all childs (override tags for albums and songs)" +msgstr "" + +#: ../../templates/show_edit_catalog.inc.php:23 +#, php-format +msgid "Settings for %s" +msgstr "إعدادات %s" + +#: ../../templates/show_edit_catalog.inc.php:46 +msgid "Filename pattern" +msgstr "الملف نمط" + +#: ../../templates/show_edit_catalog.inc.php:63 +msgid "Save Catalog Settings" +msgstr "كتالوج حفظ الإعدادات" + +#: ../../templates/show_edit_playlist_row.inc.php:36 +#: ../../templates/show_edit_smartplaylist_row.inc.php:36 +msgid "Public" +msgstr "" + +#: ../../templates/show_edit_shout.inc.php:23 +msgid "Edit existing Shoutbox Post" +msgstr "" + +#. HINT: Client link, Object link +#: ../../templates/show_edit_shout.inc.php:28 +#, php-format +msgid "Created by: %s for %s" +msgstr "" + +#: ../../templates/show_edit_song_row.inc.php:49 +#: ../../templates/show_localplay_playlist.inc.php:31 +#: ../../templates/show_localplay_playlist.inc.php:61 +#: ../../templates/show_song_previews.inc.php:35 +msgid "Track" +msgstr "" + +#: ../../templates/show_edit_user.inc.php:23 +msgid "Editing existing User" +msgstr "تحرير القائمة المستخدم" + +#: ../../templates/show_edit_user.inc.php:28 +msgid "User Properties" +msgstr "مستخدم للعقارات" + +#: ../../templates/show_edit_user.inc.php:104 +msgid "Other Options" +msgstr "خيارات أخرى" + +#: ../../templates/show_edit_user.inc.php:115 +msgid "Config Preset" +msgstr "التهيئة مسبقا" + +#: ../../templates/show_edit_user.inc.php:121 +msgid "Flash" +msgstr "Flash" + +#: ../../templates/show_edit_user.inc.php:127 +msgid "Prevent Preset Override" +msgstr "منع تجاوز مسبقا" + +#: ../../templates/show_edit_user.inc.php:129 +msgid "This Affects all non-Admin accounts" +msgstr "وهذا يؤثر على جميع المنظمات غير الادارية للحسابات" + +#: ../../templates/show_edit_user.inc.php:141 +msgid "Update User" +msgstr "تحديث المستخدم" + +#: ../../templates/show_export.inc.php:28 +#: ../../templates/sidebar_admin.inc.php:47 +msgid "Export Catalog" +msgstr "" + +#: ../../templates/show_export.inc.php:51 +msgid "Format" +msgstr "" + +#: ../../templates/show_export.inc.php:61 +msgid "Export" +msgstr "" + +#: ../../templates/show_gather_art.inc.php:23 +#, fuzzy +msgid "Album Art Search" +msgstr "بدءا من البحث في الفنون الألبوم" + +#: ../../templates/show_gather_art.inc.php:25 +msgid "Searched" +msgstr "" + +#: ../../templates/show_get_albumart.inc.php:23 +msgid "Customize Search" +msgstr "" + +#: ../../templates/show_get_albumart.inc.php:44 +msgid "Direct URL to Image" +msgstr "" + +#: ../../templates/show_get_albumart.inc.php:52 +msgid "Local Image" +msgstr "" + +#: ../../templates/show_get_albumart.inc.php:63 +msgid "Get Art" +msgstr "" + +#: ../../templates/show_highest.inc.php:23 +#: ../../templates/show_newest.inc.php:23 +#: ../../templates/show_popular.inc.php:23 +#: ../../templates/show_recent.inc.php:23 +#: ../../templates/sidebar_home.inc.php:78 +msgid "Information" +msgstr "معلومات" + +#: ../../templates/show_html5_player.inc.php:85 +msgid "Your browser doesn't support this feature." +msgstr "" + +#: ../../templates/show_html5_player.inc.php:518 +msgid "Media is currently playing. Are you sure you want to close" +msgstr "" + +#: ../../templates/show_html5_player.inc.php:678 +msgid "Slideshow" +msgstr "" + +#: ../../templates/show_html5_player.inc.php:682 +msgid "Equalizer" +msgstr "" + +#: ../../templates/show_html5_player.inc.php:685 +msgid "Visualizer" +msgstr "" + +#: ../../templates/show_html5_player.inc.php:688 +msgid "Visualizer Full-Screen" +msgstr "" + +#: ../../templates/show_import_playlist.inc.php:23 +msgid "Importing a Playlist from a File" +msgstr "استيراد الملف من قائمة التشغيل" + +#: ../../templates/show_import_playlist.inc.php:35 +msgid "Import Playlist" +msgstr "استيراد قائمة التشغيل" + +#: ../../templates/show_index.inc.php:32 +#: ../../templates/show_random_albums.inc.php:26 +msgid "Albums of the Moment" +msgstr "" + +#: ../../templates/show_install_account.inc.php:26 +#: ../../templates/show_install_config.inc.php:32 +#: ../../templates/show_install.inc.php:26 +#, fuzzy +msgid "Install progress" +msgstr "النسخة" + +#: ../../templates/show_install_account.inc.php:38 +#: ../../templates/show_install_config.inc.php:43 +#: ../../templates/show_install.inc.php:37 +#, fuzzy +msgid "Step 1 - Create the Ampache database" +msgstr "الخطوة 1 -- خلق وإدراج قاعدة بيانات Ampache" + +#: ../../templates/show_install_account.inc.php:39 +#: ../../templates/show_install_config.inc.php:44 +#: ../../templates/show_install.inc.php:42 +#, fuzzy +msgid "Step 2 - Create configuration files (ampache.cfg.php ...)" +msgstr "الخطوة 2 -- إنشاء ملف ampache.cfg.php" + +#: ../../templates/show_install_account.inc.php:41 +#: ../../templates/show_install_config.inc.php:49 +#: ../../templates/show_install.inc.php:43 +#, fuzzy +msgid "Step 3 - Set up the initial account" +msgstr "خطوة 3 -- الإعداد الأولي للحساب" + +#: ../../templates/show_install_account.inc.php:43 +#, fuzzy +msgid "" +"This step creates your initial Ampache admin account. Once your admin " +"account has been created you will be redirected to the login page." +msgstr "" +"تخلق هذه الخطوة الاصلية Ampache حساب مشرف. الحساب الإداري الخاص بك مرة واحدة " +"وقد خلقكم ستوجه إلى صفحة تسجيل الدخول" + +#: ../../templates/show_install_account.inc.php:47 +msgid "Create Admin Account" +msgstr "إنشاء حساب مشرف" + +#: ../../templates/show_install_account.inc.php:69 +msgid "Create Account" +msgstr "إنشاء حساب" + +#: ../../templates/show_install_check.inc.php:26 +msgid "Requirements" +msgstr "المتطلبات" + +#: ../../templates/show_install_check.inc.php:30 +#, fuzzy +msgid "" +"This page handles the installation of the Ampache database and the creation " +"of the ampache.cfg.php file. Before you continue please make sure that you " +"have the following prerequisites:" +msgstr "" +"وتتولى هذه الصفحة من Ampache تركيب ، وإنشاء قاعدة بيانات للampache.cfg.php " +"الملف. قبل المتابعة يرجى التأكد من أن لديك ما يلي الشروط المسبقة" + +#: ../../templates/show_install_check.inc.php:33 +#, fuzzy +msgid "" +"A MySQL server with a username and password that can create/modify databases" +msgstr "" +"وقال خادم MySQL مع اسم المستخدم وكلمة السر التي يمكن أن تخلق / تغيير قواعد " +"البيانات" + +#: ../../templates/show_install_check.inc.php:34 +#, fuzzy, php-format +msgid "Your webserver has read access to the files %s and %s" +msgstr "" +"لقد قرأ الويب الخاص بك للوصول إلى config/ampache.cfg.php.dist/ الملف وملف " +"sql/ampache.sql/" + +#: ../../templates/show_install_check.inc.php:37 +#, fuzzy, php-format +msgid "" +"Once you have ensured that the above requirements are met please fill out " +"the information below. You will only be asked for the required config " +"values. If you would like to make changes to your Ampache install at a later " +"date simply edit %s" +msgstr "" +"وبمجرد الانتهاء من التأكد من أن لديك الشروط المذكورة أعلاه يرجى ملء " +"المعلومات أدناه. عليك فقط طلب التهيئة المطلوبة القيم. إذا كنت ترغب في إجراء " +"تغييرات في تركيب ampache الخاصة بك في وقت لاحق لمجرد تحرير /config/ampache." +"cfg.php" + +#: ../../templates/show_install_check.inc.php:42 +#: ../../templates/show_test.inc.php:51 +msgid "CHECK" +msgstr "تفحص" + +#: ../../templates/show_install_check.inc.php:43 +#: ../../templates/show_test.inc.php:52 +msgid "STATUS" +msgstr "حالة" + +#: ../../templates/show_install_check.inc.php:44 +#: ../../templates/show_test.inc.php:53 +msgid "DESCRIPTION" +msgstr "افنص" + +#: ../../templates/show_install_check.inc.php:48 +#: ../../templates/show_install_check.inc.php:53 +#, php-format +msgid "%s is readable" +msgstr "" + +#: ../../templates/show_install_check.inc.php:50 +msgid "This tests whether the configuration template can be read." +msgstr "" + +#: ../../templates/show_install_check.inc.php:55 +msgid "" +"This tests whether the file needed to initialise the database structure is " +"available." +msgstr "" + +#: ../../templates/show_install_check.inc.php:58 +#, fuzzy +msgid "ampache.cfg.php is writable" +msgstr "يجد Ampache.cfg.php" + +#: ../../templates/show_install_check.inc.php:60 +msgid "" +"This tests whether PHP can write to config/. This is not strictly necessary, " +"but will help streamline the installation process." +msgstr "" + +#: ../../templates/show_install_config.inc.php:46 +#, fuzzy, php-format +msgid "" +"This step takes the basic config values and generates the config file. If " +"your config/ directory is writable, you can select \"write\" to have Ampache " +"write the config file directly to the correct location. If you select " +"\"download\" it will prompt you to download the config file, and you can " +"then manually place the config file in %s" +msgstr "" +"وتتخذ هذه الخطوات الأساسية والقيم والتهيئة يولد ملف. سوف مطالبتك لتحميل ملف. " +"يرجى وضع تحميل ملف في /config" + +#: ../../templates/show_install_config.inc.php:54 +msgid "Generate Config File" +msgstr "توليد ملف" + +#: ../../templates/show_install_config.inc.php:55 +#: ../../templates/show_test_table.inc.php:159 +#, fuzzy +msgid "Database connection" +msgstr "الديسيبل الربط" + +#: ../../templates/show_install_config.inc.php:59 +msgid "Web Path" +msgstr "الشبكة الدرب" + +#: ../../templates/show_install_config.inc.php:65 +#, fuzzy +msgid "Database Name" +msgstr "اسم قاعدة البيانات المطلوبة" + +#: ../../templates/show_install_config.inc.php:71 +#: ../../templates/show_install.inc.php:56 +msgid "MySQL Hostname" +msgstr "MySQL المضيف" + +#: ../../templates/show_install_config.inc.php:77 +msgid "MySQL Port (optional)" +msgstr "" + +#: ../../templates/show_install_config.inc.php:83 +msgid "MySQL Username" +msgstr "MySQL اسم المستخدم" + +#: ../../templates/show_install_config.inc.php:89 +msgid "MySQL Password" +msgstr "MySQL كلمة المرور" + +#: ../../templates/show_install_config.inc.php:99 +msgid "Transcoding" +msgstr "" + +#: ../../templates/show_install_config.inc.php:101 +msgid "" +"Transcoding allows you to convert one type of file to another. Ampache " +"supports on the fly transcoding of all file types based on user, IP address " +"or available bandwidth. In order to transcode Ampache takes advantage of " +"existing binary applications such as ffmpeg. In order for transcoding to " +"work you must first install the supporting applications and ensure that they " +"are executable by the webserver." +msgstr "" + +#: ../../templates/show_install_config.inc.php:103 +msgid "" +"This section apply default transcoding configuration according to the " +"application you want to use. You may need to customize settings once this " +"setup ended" +msgstr "" + +#: ../../templates/show_install_config.inc.php:103 +msgid "See wiki page" +msgstr "" + +#: ../../templates/show_install_config.inc.php:107 +#, fuzzy +msgid "Template Configuration" +msgstr "بدء التهيئة" + +#: ../../templates/show_install_config.inc.php:121 +msgid "" +"No default transcoding application found. You may need to install a popular " +"application (ffmpeg, avconv ...) or customize transcoding settings manually " +"after installation." +msgstr "" + +#: ../../templates/show_install_config.inc.php:127 +#, fuzzy +msgid "Files" +msgstr "اسم الملف" + +#: ../../templates/show_install_config.inc.php:131 +msgid "rest/.htaccess action" +msgstr "" + +#: ../../templates/show_install_config.inc.php:136 +#: ../../templates/show_install_config.inc.php:151 +#: ../../templates/show_install_config.inc.php:167 +#, fuzzy +msgid "Write" +msgstr "قراءة وكتابة" + +#: ../../templates/show_install_config.inc.php:139 +msgid "rest/.htaccess exists?" +msgstr "" + +#: ../../templates/show_install_config.inc.php:141 +msgid "rest/.htaccess configured?" +msgstr "" + +#: ../../templates/show_install_config.inc.php:146 +msgid "play/.htaccess action" +msgstr "" + +#: ../../templates/show_install_config.inc.php:154 +msgid "play/.htaccess exists?" +msgstr "" + +#: ../../templates/show_install_config.inc.php:156 +#, fuzzy +msgid "play/.htaccess configured?" +msgstr "Ampache.cfg.php مشكل؟" + +#: ../../templates/show_install_config.inc.php:162 +#, fuzzy +msgid "config/ampache.cfg.php action" +msgstr "Ampache.cfg.php مشكل؟" + +#: ../../templates/show_install_config.inc.php:170 +#, fuzzy +msgid "config/ampache.cfg.php exists?" +msgstr "يجد Ampache.cfg.php" + +#: ../../templates/show_install_config.inc.php:172 +#, fuzzy +msgid "config/ampache.cfg.php configured?" +msgstr "Ampache.cfg.php مشكل؟" + +#: ../../templates/show_install_config.inc.php:181 +#, fuzzy +msgid "Recheck Config" +msgstr "شيك التهيئة" + +#: ../../templates/show_install_config.inc.php:189 +msgid "Continue to Step 3" +msgstr "انتقل الى الخطوة 3" + +#: ../../templates/show_install.inc.php:39 +#, fuzzy +msgid "" +"This step creates and inserts the Ampache database, so please provide a " +"MySQL account with database creation rights. This step may take some time on " +"slower computers." +msgstr "" +"وتخلق هذه الخطوة إدراجات Ampache فإن قاعدة البيانات ، ولذلك يرجى تقديم mysql " +"حساب الحقوق وإنشاء قاعدة بيانات. هذه الخطوة قد تستغرق بعض الوقت اعتمادا على " +"سرعة الكمبيوتر" + +#: ../../templates/show_install.inc.php:47 +msgid "Insert Ampache Database" +msgstr "تضاف Ampache قاعدة البيانات" + +#: ../../templates/show_install.inc.php:50 +msgid "Desired Database Name" +msgstr "اسم قاعدة البيانات المطلوبة" + +#: ../../templates/show_install.inc.php:62 +msgid "MySQL port (optional)" +msgstr "" + +#: ../../templates/show_install.inc.php:68 +msgid "MySQL Administrative Username" +msgstr "MySQL الإدارية اسم المستخدم" + +#: ../../templates/show_install.inc.php:74 +msgid "MySQL Administrative Password" +msgstr "MySQL الإدارية كلمة المرور" + +#: ../../templates/show_install.inc.php:80 +#, fuzzy +msgid "Create Database" +msgstr "خلق تاريخ" + +#: ../../templates/show_install.inc.php:90 +msgid "Overwrite if database already exists" +msgstr "" + +#: ../../templates/show_install.inc.php:99 +#, fuzzy +msgid "Create Tables" +msgstr "خلق تاريخ" + +#: ../../templates/show_install.inc.php:108 +#, fuzzy +msgid "Create Database User" +msgstr "قاعدة بيانات Ampache اسم المستخدم" + +#: ../../templates/show_install.inc.php:118 +msgid "Ampache Database Username" +msgstr "قاعدة بيانات Ampache اسم المستخدم" + +#: ../../templates/show_install.inc.php:124 +msgid "Ampache Database User Password" +msgstr "Ampache قاعدة بيانات المستخدم كلمة السر" + +#: ../../templates/show_install.inc.php:130 +msgid "Skip" +msgstr "" + +#: ../../templates/show_install.inc.php:133 +msgid "Insert Database" +msgstr "تضاف قاعدة البيانات" + +#: ../../templates/show_install_lang.inc.php:30 +#, fuzzy +msgid "Choose Installation Language" +msgstr "اختيار وتركيب اللغة." + +#: ../../templates/show_install_lang.inc.php:50 +msgid "Start configuration" +msgstr "بدء التهيئة" + +#. HINT: Username +#: ../../templates/show_ip_history.inc.php:23 +#, php-format +msgid "%s IP History" +msgstr "" + +#: ../../templates/show_ip_history.inc.php:29 +msgid "Show Unique" +msgstr "" + +#: ../../templates/show_ip_history.inc.php:32 +msgid "Show All" +msgstr "" + +#: ../../templates/show_ip_history.inc.php:46 +#: ../../templates/show_ip_history.inc.php:60 +msgid "IP Address" +msgstr "" + +#: ../../templates/show_live_stream.inc.php:23 +msgid "Manage Radio Stations" +msgstr "" + +#: ../../templates/show_live_stream_row.inc.php:27 +#, fuzzy +msgid "Play live stream" +msgstr "الاسم التشغيل" + +#: ../../templates/show_live_stream_row.inc.php:41 +msgid "Live Stream edit" +msgstr "" + +#: ../../templates/show_live_streams.inc.php:49 +#, fuzzy +msgid "No live stream found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_localplay_add_instance.inc.php:23 +msgid "Add Localplay Instance" +msgstr "" + +#: ../../templates/show_localplay_add_instance.inc.php:34 +#: ../../templates/sidebar_localplay.inc.php:41 +msgid "Add Instance" +msgstr "إضافة درجة" + +#: ../../templates/show_localplay_control.inc.php:24 +msgid "Previous" +msgstr "السابق" + +#: ../../templates/show_localplay_control.inc.php:25 +msgid "Stop" +msgstr "توقف" + +#: ../../templates/show_localplay_control.inc.php:26 +msgid "Pause" +msgstr "وقفة" + +#: ../../templates/show_localplay_edit_instance.inc.php:23 +msgid "Edit Localplay Instance" +msgstr "" + +#: ../../templates/show_localplay_edit_instance.inc.php:34 +msgid "Update Instance" +msgstr "" + +#: ../../templates/show_localplay_instances.inc.php:23 +msgid "Show Localplay Instances" +msgstr "" + +#: ../../templates/show_localplay_instances.inc.php:39 +msgid "Edit Instance" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:30 +msgid "Localplay Control" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:35 +msgid "Mute" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:36 +msgid "Decrease Volume" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:37 +msgid "Increase Volume" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:38 +msgid "Volume" +msgstr "" + +#: ../../templates/show_localplay_status.inc.php:43 +msgid "Repeat" +msgstr "" + +#: ../../templates/show_login_form.inc.php:87 +msgid "Remember Me" +msgstr "" + +#: ../../templates/show_login_form.inc.php:94 +#, fuzzy +msgid "Lost password" +msgstr "كلمة السر" + +#: ../../templates/show_login_form.inc.php:95 +msgid "Login" +msgstr "" + +#: ../../templates/show_login_form.inc.php:100 +msgid "Register" +msgstr "" + +#: ../../templates/show_login_form.inc.php:111 +msgid "Message of the Day" +msgstr "" + +#: ../../templates/show_lostpassword_form.inc.php:67 +#, fuzzy +msgid "Email" +msgstr "البريد الإلكتروني" + +#: ../../templates/show_lostpassword_form.inc.php:71 +msgid "Submit" +msgstr "" + +#: ../../templates/show_lyrics.inc.php:66 +#, fuzzy +msgid "No lyrics found." +msgstr "لا توجد سجلات" + +#: ../../templates/show_lyrics.inc.php:68 +#, fuzzy +msgid "Show more" +msgstr "وتبين للفنون" + +#: ../../templates/show_mail_users.inc.php:24 +msgid "Send E-mail to Users" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:28 +msgid "Mail to" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:34 +msgid "Inactive Users" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:39 +msgid "From" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:42 +msgid "Yourself" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:48 +msgid "Subject" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:54 +msgid "Message" +msgstr "" + +#: ../../templates/show_mail_users.inc.php:61 +msgid "Send Mail" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:23 +#: ../../templates/sidebar_admin.inc.php:27 +msgid "Show Catalogs" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:26 +msgid "Gather All Art" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:27 +#, fuzzy +msgid "Show disabled songs" +msgstr "وتظهر الحالات" + +#: ../../templates/show_manage_catalogs.inc.php:28 +msgid "Add to All" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:29 +msgid "Verify All" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:30 +msgid "Clean All" +msgstr "" + +#: ../../templates/show_manage_catalogs.inc.php:31 +msgid "Update All" +msgstr "" + +#. HINT: /data/myNewMusic +#: ../../templates/show_manage_catalogs.inc.php:38 +#, php-format +msgid "Add from [%s]" +msgstr "" + +#. HINT: /data/myUpdatedMusic +#: ../../templates/show_manage_catalogs.inc.php:42 +#, php-format +msgid "Update from [%s]" +msgstr "" + +#: ../../templates/show_manage_democratic.inc.php:23 +msgid "Manage Democratic Playlists" +msgstr "إدارة التشغيل الديمقراطية" + +#: ../../templates/show_manage_democratic.inc.php:55 +#, fuzzy +msgid "No democratic found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_manage_democratic.inc.php:61 +msgid "Create New Playlist" +msgstr "خلق التشغيل الجديدة" + +#: ../../templates/show_manage_shoutbox.inc.php:27 +#: ../../templates/show_manage_shoutbox.inc.php:51 +#: ../../templates/show_shared_objects.inc.php:26 +msgid "Object" +msgstr "" + +#: ../../templates/show_manage_shoutbox.inc.php:29 +#: ../../templates/show_manage_shoutbox.inc.php:53 +msgid "Sticky" +msgstr "" + +#: ../../templates/show_manage_shoutbox.inc.php:31 +#: ../../templates/show_manage_shoutbox.inc.php:55 +msgid "Date Added" +msgstr "" + +#: ../../templates/show_missing_album.inc.php:72 +#, fuzzy +msgid "Wanted actions" +msgstr "محطات إذاعية" + +#: ../../templates/show_missing_albums.inc.php:44 +#, fuzzy +msgid "No missing album found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_now_playing_row.inc.php:76 +#: ../../templates/show_now_playing_similar.inc.php:52 +#, fuzzy +msgid "Similar Songs" +msgstr "# أغاني" + +#: ../../templates/show_object_rating.inc.php:43 +msgid "Current rating: " +msgstr "" + +#: ../../templates/show_object_rating.inc.php:45 +msgid "not rated yet" +msgstr "" + +#: ../../templates/show_object_rating.inc.php:46 +#, php-format +msgid "%s of 5" +msgstr "" + +#: ../../templates/show_playlist.inc.php:77 +#, fuzzy +msgid "Create channel" +msgstr "خلق تاريخ" + +#: ../../templates/show_playlist.inc.php:83 +#: ../../templates/show_playlist_row.inc.php:60 +msgid "Do you really want to delete the playlist?" +msgstr "" + +#: ../../templates/show_playlist_row.inc.php:57 +#, fuzzy +msgid "Playlist edit" +msgstr "نوع التشغيل" + +#: ../../templates/show_playlists.inc.php:31 +#: ../../templates/show_playlists.inc.php:59 +msgid "# Songs" +msgstr "# أغاني" + +#: ../../templates/show_playlists.inc.php:32 +#: ../../templates/show_playlists.inc.php:60 +#: ../../templates/show_smartplaylists.inc.php:32 +#: ../../templates/show_smartplaylists.inc.php:58 +msgid "Owner" +msgstr "مالك" + +#: ../../templates/show_playlists.inc.php:49 +#, fuzzy +msgid "No playlist found" +msgstr "وتظهر قائمة التشغيل" + +#: ../../templates/show_playlist_song_row.inc.php:67 +#: ../../templates/show_song_row.inc.php:82 +msgid "Reorder" +msgstr "" + +#: ../../templates/show_playlist_title.inc.php:23 +#, fuzzy, php-format +msgid "%s %s (Playlist)" +msgstr "%s$1 %s$2 التشغيل" + +#: ../../templates/show_plugins.inc.php:32 +#: ../../templates/show_plugins.inc.php:72 +#, fuzzy +msgid "Installed Version" +msgstr "النسخة" + +#: ../../templates/show_plugins.inc.php:46 +msgid "Deactivate" +msgstr "" + +#: ../../templates/show_plugins.inc.php:50 +msgid "Upgrade" +msgstr "" + +#: ../../templates/show_preference_admin.inc.php:23 +msgid "Preference Administration" +msgstr "" + +#: ../../templates/show_preference_box.inc.php:45 +#: ../../templates/show_preference_box.inc.php:79 +msgid "Apply to All" +msgstr "تنطبق على جميع" + +#: ../../templates/show_preference_box.inc.php:46 +#: ../../templates/show_preference_box.inc.php:80 +msgid "Access Level" +msgstr "مستوى الوصول" + +#. HINT: Username +#. HINT: Editing Username preferences +#: ../../templates/show_preferences.inc.php:29 +#: ../../templates/show_user_preferences.inc.php:29 +#, php-format +msgid "Editing %s preferences" +msgstr "تحرير تفضيلات %s" + +#: ../../templates/show_preferences.inc.php:35 +#: ../../templates/show_user_preferences.inc.php:51 +msgid "Update Preferences" +msgstr "تحديث تفضيلات" + +#: ../../templates/show_random_albums.inc.php:24 +msgid "Refresh" +msgstr "" + +#: ../../templates/show_random.inc.php:23 +msgid "Play Random Selection" +msgstr "" + +#: ../../templates/show_random.inc.php:35 +msgid "Item count" +msgstr "" + +#: ../../templates/show_random.inc.php:62 +#: ../../templates/show_random.inc.php:86 +#: ../../templates/show_search.inc.php:40 +msgid "Unlimited" +msgstr "" + +#: ../../templates/show_random.inc.php:68 +#, php-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_random.inc.php:70 +#, php-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_random.inc.php:79 +msgid "Size Limit" +msgstr "" + +#: ../../templates/show_random.inc.php:102 +msgid "Enqueue" +msgstr "" + +#: ../../templates/show_recently_played.inc.php:35 +#: ../../templates/show_recently_played.inc.php:146 +msgid "Last Played" +msgstr "" + +#: ../../templates/show_recently_played.inc.php:36 +#: ../../templates/show_recently_played.inc.php:147 +msgid "Agent" +msgstr "" + +#: ../../templates/show_recently_played.inc.php:65 +msgid "second ago" +msgid_plural "seconds ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:68 +msgid "minute ago" +msgid_plural "minutes ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:71 +msgid "hour ago" +msgid_plural "hours ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:74 +msgid "day ago" +msgid_plural "days ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:77 +msgid "week ago" +msgid_plural "weeks ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:80 +msgid "month ago" +msgid_plural "months ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:83 +#, fuzzy +msgid "year ago" +msgid_plural "years ago" +msgstr[0] "سنة" +msgstr[1] "سنة" + +#: ../../templates/show_recently_played.inc.php:86 +msgid "decade ago" +msgid_plural "decades ago" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_recently_played.inc.php:134 +#, fuzzy +msgid "No recently item found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_recommended_artists.inc.php:72 +#, fuzzy +msgid "No similar artist found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_registration_confirmation.inc.php:30 +#: ../../templates/show_user_activate.inc.php:30 +#: ../../templates/show_user_activate.inc.php:37 +#: ../../templates/show_user_registration.inc.php:30 +#: ../../templates/show_user_registration.inc.php:42 +msgid "Registration" +msgstr "" + +#: ../../templates/show_registration_confirmation.inc.php:37 +msgid "Registration Complete" +msgstr "" + +#: ../../templates/show_registration_confirmation.inc.php:43 +msgid "Your account has been created." +msgstr "" + +#: ../../templates/show_registration_confirmation.inc.php:46 +msgid "Please wait for an administrator to activate your account." +msgstr "" + +#: ../../templates/show_registration_confirmation.inc.php:48 +msgid "" +"An activation key has been sent to the e-mail address you provided. Please " +"check your e-mail for further information." +msgstr "" + +#: ../../templates/show_registration_confirmation.inc.php:52 +msgid "Return to Login Page" +msgstr "" + +#: ../../templates/show_rules.inc.php:33 +msgid "Rules" +msgstr "" + +#: ../../templates/show_rules.inc.php:37 +msgid "Match" +msgstr "" + +#: ../../templates/show_rules.inc.php:40 +msgid "all rules" +msgstr "" + +#: ../../templates/show_rules.inc.php:41 +msgid "any rule" +msgstr "" + +#: ../../templates/show_rules.inc.php:49 +msgid "Add Another Rule" +msgstr "" + +#: ../../templates/show_search_bar.inc.php:30 +msgid "Anywhere" +msgstr "" + +#: ../../templates/show_search_bar.inc.php:38 +msgid "Advanced Search" +msgstr "" + +#: ../../templates/show_search.inc.php:23 +msgid "Search Ampache" +msgstr "" + +#: ../../templates/show_search.inc.php:37 +msgid "Maximum Results" +msgstr "" + +#: ../../templates/show_search.inc.php:55 +#, fuzzy +msgid "Save as Smart Playlist" +msgstr "استيراد قائمة التشغيل" + +#: ../../templates/show_search_options.inc.php:23 +msgid "Options" +msgstr "" + +#: ../../templates/show_search_options.inc.php:27 +#: ../../templates/show_search_options.inc.php:28 +msgid "Add Search Results" +msgstr "" + +#: ../../templates/show_shared_objects.inc.php:27 +#, fuzzy +msgid "Object Type" +msgstr "نوع البحث" + +#: ../../templates/show_shared_objects.inc.php:29 +#, fuzzy +msgid "Creation Date" +msgstr "خلق تاريخ" + +#: ../../templates/show_shared_objects.inc.php:30 +#, fuzzy +msgid "Last Visit" +msgstr "تحقق من الماضي" + +#: ../../templates/show_shared_objects.inc.php:31 +msgid "Counter" +msgstr "" + +#: ../../templates/show_shared_objects.inc.php:36 +msgid "Public Url" +msgstr "" + +#: ../../templates/show_share.inc.php:26 +msgid "Shared on" +msgstr "" + +#: ../../templates/show_share.inc.php:27 +msgid "by" +msgstr "" + +#: ../../templates/show_smartplaylist.inc.php:39 +#: ../../templates/show_smartplaylist.inc.php:40 +msgid "Add All" +msgstr "إضافة الكل" + +#: ../../templates/show_smartplaylist.inc.php:56 +msgid "Save Changes" +msgstr "حفظ التغييرات" + +#: ../../templates/show_smartplaylist_row.inc.php:52 +#, fuzzy +msgid "Smart Playlist edit" +msgstr "استيراد قائمة التشغيل" + +#: ../../templates/show_smartplaylists.inc.php:48 +#, fuzzy +msgid "No smart playlist found" +msgstr "استيراد قائمة التشغيل" + +#: ../../templates/show_smartplaylist_title.inc.php:24 +#, fuzzy, php-format +msgid "%s %s (Smart Playlist)" +msgstr "%s$1 %s$2 التشغيل" + +#: ../../templates/show_song.inc.php:26 +msgid "Details" +msgstr "" + +#: ../../templates/show_song.inc.php:48 +msgid "Waveform" +msgstr "" + +#: ../../templates/show_song.inc.php:75 +msgid "Link" +msgstr "" + +#: ../../templates/show_song.inc.php:91 +msgid "Label" +msgstr "" + +#: ../../templates/show_song.inc.php:92 +msgid "Song Language" +msgstr "" + +#: ../../templates/show_song.inc.php:93 +msgid "Catalog Number" +msgstr "" + +#: ../../templates/show_song.inc.php:99 +msgid "Last Updated" +msgstr "" + +#: ../../templates/show_song.inc.php:107 +msgid "Lyrics" +msgstr "" + +#: ../../templates/show_song_row.inc.php:58 +msgid "Song Information" +msgstr "أغنية المعلومات" + +#: ../../templates/show_song_row.inc.php:70 +#, fuzzy +msgid "Song edit" +msgstr "عنوان الأغنية" + +#: ../../templates/show_songs.inc.php:68 +#, fuzzy +msgid "No song found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_stats.inc.php:30 +msgid "Connected Users" +msgstr "" + +#: ../../templates/show_stats.inc.php:31 +msgid "Total Users" +msgstr "" + +#: ../../templates/show_stats.inc.php:37 ../../templates/show_stats.inc.php:72 +msgid "Catalog Size" +msgstr "" + +#: ../../templates/show_stats.inc.php:38 +msgid "Catalog Time" +msgstr "" + +#: ../../templates/show_stats_popular.inc.php:26 +msgid "Most Popular Albums" +msgstr "" + +#: ../../templates/show_stats_popular.inc.php:32 +msgid "Most Popular Artists" +msgstr "" + +#: ../../templates/show_tagcloud.inc.php:35 +#, fuzzy +msgid "Tag edit" +msgstr "عنوان الأغنية" + +#: ../../templates/show_tagcloud.inc.php:40 +msgid "Do you really want to delete the tag?" +msgstr "" + +#: ../../templates/show_test_config.inc.php:40 +#: ../../templates/show_test.inc.php:43 +#: ../../templates/sidebar_admin.inc.php:45 +msgid "Ampache Debug" +msgstr "Ampache التصحيح" + +#: ../../templates/show_test.inc.php:46 +#, fuzzy +msgid "" +"You may have reached this page because a configuration error has occured. " +"Debug information is below." +msgstr "" +"كنت قد وصلت إلى هذه الصفحة ، لأن التكوين حدث خطأ. التصحيح المعلومات أدناه" + +#: ../../templates/show_test_table.inc.php:24 +#, fuzzy +msgid "PHP version" +msgstr "PHP الإصدار" + +#: ../../templates/show_test_table.inc.php:29 +#, fuzzy +msgid "" +"This tests whether you are running at least the minimum version of PHP " +"required by Ampache." +msgstr "" +"هذه الاختبارات للتأكد من أنك تقوم بتشغيل نسخة من المعروف ان PHP للعمل مع " +"Ampache." + +#: ../../templates/show_test_table.inc.php:33 +#, fuzzy +msgid "PHP hash extension" +msgstr "PHP دعم الدورة" + +#: ../../templates/show_test_table.inc.php:38 +#, fuzzy +msgid "" +"This tests whether you have the hash extension enabled. This extension is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:42 +#, fuzzy +msgid "SHA256" +msgstr "SHA256 الدعم" + +#: ../../templates/show_test_table.inc.php:47 +#, fuzzy +msgid "" +"This tests whether the hash extension supports SHA256. This algorithm is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:51 +#, fuzzy +msgid "PHP PDO extension" +msgstr "PHP PCRE الدعم" + +#: ../../templates/show_test_table.inc.php:56 +#, fuzzy +msgid "" +"This tests whether you have the PDO extension enabled. This extension is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:60 +msgid "MySQL" +msgstr "" + +#: ../../templates/show_test_table.inc.php:65 +#, fuzzy +msgid "" +"This tests whether the MySQL driver for PDO is enabled. This driver is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:69 +#, fuzzy +msgid "PHP session extension" +msgstr "PHP دعم الدورة" + +#: ../../templates/show_test_table.inc.php:74 +#, fuzzy +msgid "" +"This tests whether you have the session extension enabled. This extension is " +"required by Ampache." +msgstr "" +"هذا الاختبار فحوص للتأكد من أن لديك دعم PHP الدورة مكنت. الدورات اللازمة " +"لAmpache الى العمل." + +#: ../../templates/show_test_table.inc.php:78 +#, fuzzy +msgid "PHP iconv extension" +msgstr "PHP دعم الدورة" + +#: ../../templates/show_test_table.inc.php:83 +#, fuzzy +msgid "" +"This tests whether you have the iconv extension enabled. This extension is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:87 +#, fuzzy +msgid "PHP JSON extension" +msgstr "PHP PCRE الدعم" + +#: ../../templates/show_test_table.inc.php:92 +#, fuzzy +msgid "" +"This tests whether you have the JSON extension enabled. This extension is " +"required by Ampache." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:96 +#, fuzzy +msgid "PHP curl extension" +msgstr "PHP دعم الدورة" + +#: ../../templates/show_test_table.inc.php:101 +#, fuzzy +msgid "" +"This tests whether you have the curl extension enabled. This is not " +"strictly necessary, but may result in a better experience." +msgstr "" +"هذا الفحص اختبار لمعرفة إذا كان لديك ماي تمديدات تحميلها لPHP. ويلزم لهذه " +"Ampache للعمل." + +#: ../../templates/show_test_table.inc.php:105 +msgid "PHP safe mode disabled" +msgstr "" + +#: ../../templates/show_test_table.inc.php:110 +#, fuzzy +msgid "" +"This test makes sure that PHP is not running in safe mode. Some features of " +"Ampache will not work correctly in safe mode." +msgstr "" +"هذا الاختبار PHP بالتأكد من أنه لا تشارك في SafeMode وأننا قادرون على تغيير " +"حدود الذاكرة. بينما ليس من المطلوب ، من دون قدرة بعض هذه السمات ampache قد " +"لا تعمل بشكل صحيح" + +#: ../../templates/show_test_table.inc.php:114 +msgid "PHP memory limit override" +msgstr "" + +#: ../../templates/show_test_table.inc.php:119 +msgid "" +"This tests whether Ampache can override the memory limit. This is not " +"strictly necessary, but may result in a better experience." +msgstr "" + +#: ../../templates/show_test_table.inc.php:123 +msgid "PHP execution time override" +msgstr "" + +#: ../../templates/show_test_table.inc.php:128 +msgid "" +"This tests whether Ampache can override the limit on maximum execution " +"time. This is not strictly necessary, but may result in a better experience." +msgstr "" + +#: ../../templates/show_test_table.inc.php:135 +msgid "Configuration file readability" +msgstr "" + +#: ../../templates/show_test_table.inc.php:140 +#, fuzzy +msgid "" +"This test attempts to read config/ampache.cfg.php. If this fails the file " +"either is not in the correct location or is not currently readable." +msgstr "" +"هذه محاولة لقراءة وإذا فشلت هذه /config/ampache.cfg.php إما ampache.cfg.php " +"ليس في المواقع الصحيحة \n" +"\t أو أنها ليست في الوقت الراهن يمكن قراءتها من خلال خدمة الويب الخاص بك." + +#: ../../templates/show_test_table.inc.php:145 +msgid "Configuration file validity" +msgstr "" + +#: ../../templates/show_test_table.inc.php:155 +#, fuzzy +msgid "" +"This test makes sure that you have set all of the required configuration " +"variables and that we are able to completely parse your config file." +msgstr "" +"هذا الاختبار التأكد من أن لديك كل مجموعة من المتغيرات والتهيئة المطلوبة أننا " +"قادرون تماما على تحليل ملف الخاص بك" + +#: ../../templates/show_test_table.inc.php:164 +#, fuzzy +msgid "" +"This attempts to connect to your database using the values read from your " +"configuration file." +msgstr "هذه محاولة لربط لقاعدة البيانات باستخدام قيم من حسابك ampache.cfg.php" + +#: ../../templates/show_test_table.inc.php:168 +#, fuzzy +msgid "Database tables" +msgstr "اسم قاعدة البيانات المطلوبة" + +#: ../../templates/show_test_table.inc.php:173 +#, fuzzy +msgid "" +"This checks a few key tables to make sure that you have successfully " +"inserted the Ampache database and that the user has access to the database" +msgstr "" +"هذا الفحص بضع الجداول الرئيسية للتأكد من ان كنت قد نجحت في إدراج قاعدة " +"بيانات ampache وأن المستخدم الوصول إلى قاعدة البيانات" + +#: ../../templates/show_test_table.inc.php:178 +#, fuzzy +msgid "Web path" +msgstr "الشبكة الدرب" + +#: ../../templates/show_test_table.inc.php:190 +msgid "" +"This test makes sure that your web_path variable is set correctly and that " +"we are able to get to the index page. If you do not see a check mark here " +"then your web_path is not set correctly." +msgstr "" +"هذا الاختبار تأكد بأن ما تتمتعون به web_path متغير هو صحيح ، وأننا قادرون " +"على الوصول إلى صفحة فهرس. إذا كنت لا ترى علامة اختيار الخاص بك هنا ثم " +"web_path ليس صحيح." + +#: ../../templates/show_update_items.inc.php:23 +msgid "Starting Update from Tags" +msgstr "" + +#: ../../templates/show_update_items.inc.php:27 +msgid "Update from Tags Complete" +msgstr "" + +#: ../../templates/show_user_activate.inc.php:49 +msgid "User Activated" +msgstr "" + +#. HINT: Start A tag, End A tag +#: ../../templates/show_user_activate.inc.php:53 +#, php-format +msgid "This User ID is activated and can be used %sLogin%s" +msgstr "" + +#: ../../templates/show_user_activate.inc.php:56 +msgid "Validation Failed" +msgstr "" + +#: ../../templates/show_user_activate.inc.php:57 +msgid "The validation key used isn't correct" +msgstr "" + +#: ../../templates/show_userflag.inc.php:23 +#, fuzzy +msgid "User Favorites" +msgstr "مستخدم للعقارات" + +#: ../../templates/show_user.inc.php:38 +msgid "Create Date" +msgstr "خلق تاريخ" + +#: ../../templates/show_user.inc.php:41 ../../templates/show_users.inc.php:42 +#: ../../templates/show_users.inc.php:68 +msgid "Last Seen" +msgstr "شوهد للمرة الاخيرة" + +#: ../../templates/show_user.inc.php:44 ../../templates/show_users.inc.php:44 +#: ../../templates/show_users.inc.php:70 +msgid "Activity" +msgstr "النشاط" + +#: ../../templates/show_user.inc.php:47 +msgid "Status" +msgstr "" + +#: ../../templates/show_user.inc.php:50 +msgid "User is Online Now" +msgstr "مستخدم على الشبكة الآن" + +#: ../../templates/show_user.inc.php:52 +msgid "User is Offline Now" +msgstr "والآن ، دون اتصال المستخدم" + +#: ../../templates/show_user.inc.php:57 +msgid "Active Playlist" +msgstr "" + +#: ../../templates/show_user_registration.inc.php:57 +msgid "User Agreement" +msgstr "" + +#: ../../templates/show_user_registration.inc.php:64 +msgid "I Accept" +msgstr "" + +#: ../../templates/show_user_registration.inc.php:69 +msgid "User Information" +msgstr "" + +#: ../../templates/show_user_registration.inc.php:105 +msgid "* Required fields" +msgstr "" + +#: ../../templates/show_user_registration.inc.php:115 +msgid "Register User" +msgstr "" + +#: ../../templates/show_users.inc.php:41 ../../templates/show_users.inc.php:67 +msgid "Fullname" +msgstr "الاسم الكامل" + +#: ../../templates/show_users.inc.php:43 ../../templates/show_users.inc.php:69 +msgid "Registration Date" +msgstr "تاريخ التسجيل" + +#: ../../templates/show_users.inc.php:46 ../../templates/show_users.inc.php:72 +msgid "Last Ip" +msgstr "آخر للملكية الفكرية" + +#: ../../templates/show_users.inc.php:49 ../../templates/show_users.inc.php:75 +msgid "On-line" +msgstr "على الخط" + +#: ../../templates/show_verify_catalog.inc.php:23 +#, fuzzy +msgid "Verify Catalog" +msgstr "إحصائيات واضحة" + +#. HINT: Catalog Name +#: ../../templates/show_verify_catalog.inc.php:25 +#, php-format +msgid "Updating the %s catalog" +msgstr "" + +#: ../../templates/show_verify_catalog.inc.php:27 +#, php-format +msgid "%d item found checking tag information" +msgid_plural "%d items found checking tag information" +msgstr[0] "" +msgstr[1] "" + +#: ../../templates/show_verify_catalog.inc.php:29 +msgid "Verified" +msgstr "" + +#: ../../templates/show_videos.inc.php:33 +#: ../../templates/show_videos.inc.php:62 +msgid "Resolution" +msgstr "" + +#: ../../templates/show_videos.inc.php:52 +#, fuzzy +msgid "No video found" +msgstr "لا توجد سجلات" + +#: ../../templates/show_wanted.inc.php:23 +#: ../../templates/sidebar_home.inc.php:90 +msgid "Wanted List" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:31 +msgid "User Tools" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:34 +msgid "Browse Users" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:39 +msgid "Add ACL" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:40 +msgid "Show ACL(s)" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:43 +#: ../../templates/sidebar_modules.inc.php:31 +msgid "Other Tools" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:46 +msgid "Clear Now Playing" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:49 +msgid "Manage Shoutbox" +msgstr "" + +#: ../../templates/sidebar_admin.inc.php:54 +msgid "Server Config" +msgstr "" + +#: ../../templates/sidebar_home.inc.php:24 +msgid "Browse" +msgstr "تصفح" + +#: ../../templates/sidebar_home.inc.php:32 +msgid "Song Titles" +msgstr "الاغنية العنوان" + +#: ../../templates/sidebar_home.inc.php:51 +msgid "Currently Playing" +msgstr "اللعب في الوقت الحاضر" + +#: ../../templates/sidebar_home.inc.php:64 +msgid "Import" +msgstr "الواردات" + +#: ../../templates/sidebar_home.inc.php:74 +msgid "Advanced" +msgstr "المتقدمة" + +#: ../../templates/sidebar_home.inc.php:80 +msgid "Recent" +msgstr "" + +#: ../../templates/sidebar_home.inc.php:81 +msgid "Newest" +msgstr "أحدث" + +#: ../../templates/sidebar_home.inc.php:82 +msgid "Popular" +msgstr "الشعبية" + +#: ../../templates/sidebar_home.inc.php:84 +#, fuzzy +msgid "Top Rated" +msgstr "صنف" + +#: ../../templates/sidebar.inc.php:32 +#: ../../templates/sidebar_modules.inc.php:24 +msgid "Modules" +msgstr "وحدات" + +#: ../../templates/sidebar.inc.php:64 +msgid "Logout" +msgstr "" + +#: ../../templates/sidebar_localplay.inc.php:42 +msgid "Show instances" +msgstr "وتظهر الحالات" + +#: ../../templates/sidebar_localplay.inc.php:44 +msgid "Show Playlist" +msgstr "وتظهر قائمة التشغيل" + +#: ../../templates/sidebar_localplay.inc.php:48 +msgid "Active Instance" +msgstr "درجة نشطة" + +#: ../../templates/sidebar_localplay.inc.php:66 +msgid "Localplay Disabled" +msgstr "المحلية تلعب المعاقين" + +#: ../../templates/sidebar_localplay.inc.php:68 +msgid "Allow Localplay set to False" +msgstr "السماح لمجموعة لعب المحلية الزور" + +#: ../../templates/sidebar_localplay.inc.php:70 +msgid "Localplay Controller Not Defined" +msgstr "المراقب المالي المحلي اللعب غير محدد" + +#: ../../templates/sidebar_modules.inc.php:26 +msgid "Localplay Modules" +msgstr "" + +#: ../../templates/sidebar_modules.inc.php:27 +#, fuzzy +msgid "Catalog Modules" +msgstr "المدير التسويقي" + +#: ../../templates/sidebar_modules.inc.php:28 +msgid "Available Plugins" +msgstr "" + +#: ../../templates/sidebar_modules.inc.php:34 +msgid "Mail Users" +msgstr "" + +#: ../../templates/sidebar_modules.inc.php:41 +msgid "Manage Playlist" +msgstr "" + +#: ../../templates/sidebar_preferences.inc.php:39 +msgid "Account" +msgstr "" + +#: ../../update.php:67 +#, php-format +msgid "" +"This page handles all database updates to Ampache starting with " +"3.3.3.5. According to your database your current version " +"is: %s." +msgstr "" + +#: ../../update.php:68 +msgid "The following updates need to be performed" +msgstr "" + +#: ../../update.php:76 +msgid "Update Now!" +msgstr "" + +#: Database words +msgid "Allow Downloads" +msgstr "" + +#: Database words +msgid "Popular Threshold" +msgstr "" + +#: Database words +msgid "Transcode Bitrate" +msgstr "" + +#: Database words +msgid "Website Title" +msgstr "" + +#: Database words +msgid "Lock Songs" +msgstr "" + +#: Database words +msgid "Forces Http play regardless of port" +msgstr "" + +#: Database words +msgid "Non-Standard Http Port" +msgstr "" + +#: Database words +msgid "Type of Playback" +msgstr "" + +#: Database words +msgid "Language" +msgstr "" + +#: Database words +#, fuzzy +msgid "Playlist Type" +msgstr "الاسم التشغيل" + +#: Database words +msgid "Theme" +msgstr "" + +#: Database words +msgid "Localplay Access" +msgstr "" + +#: Database words +msgid "Localplay Type" +msgstr "" + +#: Database words +msgid "Allow Streaming" +msgstr "" + +#: Database words +msgid "Allow Democratic Play" +msgstr "" + +#: Database words +msgid "Allow Localplay Play" +msgstr "" + +#: Database words +msgid "Statistics Day Threshold" +msgstr "" + +#: Database words +msgid "Offset Limit" +msgstr "" + +#: Database words +msgid "Rate Limit" +msgstr "" + +#: Database words +msgid "Playlist Method" +msgstr "" + +#: Database words +#, fuzzy +msgid "Shoutcast Active Instance" +msgstr "درجة نشطة" + +#: Database words +msgid "Iframes" +msgstr "" + +#: Database words +msgid "Now playing filtered per user" +msgstr "" + +#: Database words +msgid "Album Default Sort" +msgstr "" + +#: Database words +#, fuzzy +msgid "Show # played" +msgstr "تشغيل" + +#: Database words +msgid "Show current song in Web player page title" +msgstr "" + +#: Database words +msgid "Use SubSonic backend" +msgstr "" + +#: Database words +msgid "Use Plex backend" +msgstr "" + +#: Database words +msgid "Authorize Flash Web Player(s)" +msgstr "" + +#: Database words +msgid "Authorize HTML5 Web Player(s)" +msgstr "" + +#: Database words +msgid "Personal information visibility - Now playing" +msgstr "" + +#: Database words +msgid "Personal information visibility - Recently played" +msgstr "" + +#: Database words +msgid "" +"Personal information visibility - Recently played - Allow to show streaming " +"date/time" +msgstr "" + +#: Database words +msgid "" +"Personal information visibility - Recently played - Allow to show streaming " +"agent" +msgstr "" + +#: Database words +msgid "Fix header/sidebars position on compatible themes" +msgstr "" + +#: Database words +msgid "Check for Ampache updates automatically" +msgstr "" + +#: Database words +msgid "AutoUpdate last check time" +msgstr "" + +#: Database words +msgid "AutoUpdate last version from last check" +msgstr "" + +#: Database words +msgid "AutoUpdate last version from last check is newer" +msgstr "" + +#: Database words +msgid "Confirmation when closing current playing window" +msgstr "" + +#: Database words +msgid "Auto-pause betweens tabs" +msgstr "" + +#: Database words +msgid "Use beautiful stream url" +msgstr "" + +#: Database words +#, fuzzy +msgid "Allow Share" +msgstr "يتدفق" + +#: Database words +msgid "Share links default expiration days (0=never)" +msgstr "" + +#: Database words +msgid "Artist slideshow inactivity time" +msgstr "" + +#: Database words +msgid "Broadcast web player by default" +msgstr "" + +#: Database words +msgid "Limit number of future events" +msgstr "" + +#: Database words +msgid "Limit number of past events" +msgstr "" + +#: Database words +msgid "Album - Group multiple disks" +msgstr "" + +#: Database words +msgid "Top menu" +msgstr "" + +#: Database words +msgid "Flickr api key" +msgstr "" + +#, fuzzy +#~ msgid "Renamed artist" +#~ msgstr "الملف نمط" + +#, fuzzy +#~ msgid "Error: Email and Password Required for Google Music Catalogs" +#~ msgstr "بعد لالكتالوجات" + +#, fuzzy +#~ msgid "Create Database User (for New Database)?" +#~ msgstr "إنشاء قاعدة بيانات المستخدم لقاعدة البيانات الجديدة" + +#~ msgid "Overwrite Existing" +#~ msgstr "فوق القائمة" + +#~ msgid "Use Existing Database" +#~ msgstr "استخدام قاعدة البيانات القائمة" + +#, fuzzy +#~ msgid "Play album" +#~ msgstr "الاسم التشغيل" + +#, fuzzy +#~ msgid "Play Add Album" +#~ msgstr "الاسم التشغيل" + +#, fuzzy +#~ msgid "Play add album" +#~ msgstr "الاسم التشغيل" + +#, fuzzy +#~ msgid "Play artist" +#~ msgstr "التشغيل" + +#, fuzzy +#~ msgid "Play add artist" +#~ msgstr "التشغيل" + +#, fuzzy +#~ msgid "Play Album Preview" +#~ msgstr "الاسم التشغيل" + +#~ msgid "Normalize Tracks" +#~ msgstr "تطبيع مسارات" + +#, fuzzy +#~ msgid "Play Add Playlist" +#~ msgstr "التشغيل" + +#~ msgid "Add Random" +#~ msgstr "أضف عشوائية" + +#, fuzzy +#~ msgid "Play playlist" +#~ msgstr "التشغيل" + +#, fuzzy +#~ msgid "Play song" +#~ msgstr "تشغيل" + +#, fuzzy +#~ msgid "Play add song" +#~ msgstr "تشغيل" + +#, fuzzy +#~ msgid "Play song Preview" +#~ msgstr "تشغيل" + +#, fuzzy +#~ msgid "Play video" +#~ msgstr "تشغيل" + +#~ msgid "Ampache Installation." +#~ msgstr "Ampache التثبيت." + +#, fuzzy +#~ msgid "Generate Thumbnails" +#~ msgstr "توليد ملف" + +#, fuzzy +#~ msgid "No Tag" +#~ msgstr "الوسم" + +#~ msgid "%1$s - %2$s Lyrics Detail" +#~ msgstr "%1$s -- كلمات %2$s التفاصيل" + +#~ msgid "Using Old Password Encryption, Please Reset your Password" +#~ msgstr "استخدام كلمة السر القديمة التشفير ، الرجاء إعادة تعيين كلمة المرور" + +#, fuzzy +#~ msgid "Remote Catalog Username" +#~ msgstr "قاعدة بيانات Ampache اسم المستخدم" + +#~ msgid "Required for Remote Catalogs" +#~ msgstr "بعد لالكتالوجات" + +#, fuzzy +#~ msgid "Remote Catalog Password" +#~ msgstr "كلمة السر" + +#~ msgid "Frequency" +#~ msgstr "تردد" + +#~ msgid "Callsign" +#~ msgstr "توقيع نداء" + +#~ msgid "Reject" +#~ msgstr "رفض" + +#~ msgid "Required" +#~ msgstr "مطلوب" + +#~ msgid "PHP Version" +#~ msgstr "PHP الإصدار" + +#~ msgid "Hash Function Exists" +#~ msgstr "توجد البعثرة" + +#, fuzzy +#~ msgid "PHP MySQL Support" +#~ msgstr "PHP PCRE الدعم" + +#~ msgid "PHP Session Support" +#~ msgstr "PHP دعم الدورة" + +#, fuzzy +#~ msgid "PHP iconv Support" +#~ msgstr "PHP دعم الدورة" + +#~ msgid "PHP PCRE Support" +#~ msgstr "PHP PCRE الدعم" + +#~ msgid "Optional" +#~ msgstr "اختياري" + +#, fuzzy +#~ msgid "PHP gettext Support" +#~ msgstr "Gettext الدعم" + +#, fuzzy +#~ msgid "gettext emulation will be used" +#~ msgstr "Gettext المحاكي ستستخدم" + +#, fuzzy +#~ msgid "PHP mbstring Support" +#~ msgstr "Mbstring الدعم" + +#, fuzzy +#~ msgid "Multibyte character encodings may not be autodetected correctly" +#~ msgstr "لا يجوز للطابع متعدد البايت كشف الصحيح" + +#, fuzzy +#~ msgid "Minimum requirements not met. Unable to install Ampache." +#~ msgstr "الحد الأدنى من الشروط لم تتحقق. غير قادر على تثبيت Ampache." + +#~ msgid "Mysql for PHP" +#~ msgstr "Mysql لPHP" + +#~ msgid "PHP ICONV Support" +#~ msgstr "PHP ICONV دعم " + +#~ msgid "" +#~ "This test checks to make sure you have Iconv support installed. Iconv " +#~ "support is required for Ampache" +#~ msgstr "" +#~ "هذا الاختبار فحوص للتأكد من لديك Iconv الدعم تركيبها. Iconv الدعم اللازم " +#~ "لAmpache" + +#~ msgid "" +#~ "This test makes sure you have PCRE support compiled into your version of " +#~ "PHP, this is required for Ampache." +#~ msgstr "" +#~ "يحرص في هذا الاختبار لديك دعم PCRE تجميعها نسخة PHP ، وهذا هو المطلوب " +#~ "لAmpache." + +#, fuzzy +#~ msgid "Override Execution Limit" +#~ msgstr "فوق القائمة" + +#~ msgid "Ampache.cfg.php Configured?" +#~ msgstr "Ampache.cfg.php مشكل؟" + +#~ msgid "DB Inserted" +#~ msgstr "مدخل الديسيبل" + +#~ msgid "RPC Options" +#~ msgstr "خيارات جنة الحماية من الإشعاع" + +#~ msgid "Remote Key" +#~ msgstr "النائية الرئيسية" + +#~ msgid "XML-RPC Key" +#~ msgstr "إكس إم أل الرئيسية للجنة الحماية من الإشعاع" + +#, fuzzy +#~ msgid "Ampache Security Information" +#~ msgstr "Ampache التثبيت" + +#, fuzzy +#~ msgid "Close this window" +#~ msgstr "انقر لاغلاق النافذة" + +#, fuzzy +#~ msgid "" +#~ "Compare that you are running a version of Ampache and currently a version " +#~ "of Ampache." +#~ msgstr "" +#~ "هذه الاختبارات للتأكد من أنك تقوم بتشغيل نسخة من المعروف ان PHP للعمل مع " +#~ "Ampache." + +#, fuzzy +#~ msgid "PHP putenv Support" +#~ msgstr "PHP PutENV دعم " + +#~ msgid "PHP PutENV Support" +#~ msgstr "PHP PutENV دعم " + +#~ msgid "Error: No Keyword Entered" +#~ msgstr "خطأ : لا يوجد كلمات دخلت" + +#~ msgid "Step 2 - Creating the Ampache.cfg.php file" +#~ msgstr "الخطوة 2 -- إنشاء ملف Ampache.cfg.php" + +#~ msgid "" +#~ "Once you have ensured that you have the above requirements please fill " +#~ "out the information below. You will only be asked for the required config " +#~ "values. If you would like to make changes to your ampache install at a " +#~ "later date simply edit /config/ampache.cfg.php" +#~ msgstr "" +#~ "وبمجرد الانتهاء من التأكد من أن لديك الشروط المذكورة أعلاه يرجى ملء " +#~ "المعلومات أدناه. عليك فقط طلب التهيئة المطلوبة القيم. إذا كنت ترغب في " +#~ "إجراء تغييرات في تركيب ampache الخاصة بك في وقت لاحق لمجرد تحرير /config/" +#~ "ampache.cfg.php" + +#~ msgid "" +#~ "Your webserver has read access to the /sql/ampache.sql file and the /" +#~ "config/ampache.cfg.dist.php file" +#~ msgstr "" +#~ "لقد قرأ الويب الخاص بك للوصول إلى /sql/ampache.sql الملف وملف /config/" +#~ "ampache.cfg.dist.php" diff --git a/sources/locale/base/LANGLIST b/sources/locale/base/LANGLIST new file mode 100644 index 0000000..11f67ba --- /dev/null +++ b/sources/locale/base/LANGLIST @@ -0,0 +1,20 @@ +Localization Status Report for Ampache. +Thu Feb 3 05:13:04 GMT 2011 + +LANG Trans Fuzzy Untrans Obsolete +ar_SA 241 56 645 18 # This is a test language. +ca_ES 761 105 76 79 +cs_CZ 769 97 76 76 +de_DE 693 173 76 81 +el_GR 580 190 172 178 +en_GB 786 84 72 78 +es_ES 516 244 182 50 +fa_IR 513 348 81 67 +fr_FR 665 204 73 77 +it_IT 259 266 417 54 +ja_JP 935 4 3 0 +nb_NO 741 123 78 80 +nl_NL 749 118 75 515 +pl_PL 770 97 75 78 +ru_RU 782 84 76 77 +sv_SE 769 97 76 79 diff --git a/sources/locale/base/TRANSLATIONS b/sources/locale/base/TRANSLATIONS new file mode 100644 index 0000000..84b2de3 --- /dev/null +++ b/sources/locale/base/TRANSLATIONS @@ -0,0 +1,121 @@ +------------------------------------------------------------------------------- +--------- TRANSLATIONS - Ampache Translation Guide ----------- +------------------------------------------------------------------------------- + +Contents: + + 1. Introduction + a) Getting the Necessary tools + b) Quick Reference + 2. Creating a New Translation + a) Translating + b) Creating a MO file + 3. Updating an Existing Translation + a) Merging existing file + b) Generating the MO file + 4. Questions? + +Introduction: + + Ampache uses gettext to handle translating between different languages if + you are interested in translating Ampache into a new language or updating + an existing translations simply follow the directions provided below. + + A) Getting the Necessary Tools + + Before attempting to translate Ampache into a new language we recommend + contacting us on IRC (chat.freenode.net #ampache) to make sure that nobody else is + already working on a translation. Once you are ready to start your + translation you will need to get a few tools + + - Gettext http://www.gnu.org/software/gettext/ + - xgettext (Generates PO files) + - msgmerge (Merges old and new PO files) + - msgfmt (Generates the MO file from a PO file) + + B) Quick Reference + + Below are listed all of the commands you may have to run when working on + a translation + + # Gather All info + ./gather-messages.sh --all + + # Create New po file + LANG=YOURLANG ./gather-messages.sh --init + Example: LANG=ja_JP.UTF-8 ./gather-messages.sh --init + locale/ja_JP/LC_MESSAGES/messages.po will create. + + # Merge with existing po file + ./gather-messages.sh --merge + + # Combine Old & New po files + msgmerge old.po messages.po --output-file=new.po + + # Generate MO file for use by gettext + ./gather-messages.sh --format + +Creating a New Translation: + + A) Translating + + We do our best to keep an up to date po file in /locale/base feel free to + use this file rather than attempting to generate your own. If you would + like to gather a new PO file simply run /locale/base/gather-messages.sh + (Linux only) + + Once you have an up to date PO file you will need to figure out the + country code for the language you are translating into. There are many + lists on the web. + http://www.gnu.org/software/gettext/manual/html_chapter/gettext_16.html + + Create the following directory structure and put your po file in the + LC_MESSAGES directory + /locale//LC_MESSAGES/ + + Start Translating! + + C) Creating a MO File + + Once you have finished translating the PO file you need to convert it into + a MO file in order for Gettext to be able to use it. Simply run the command + listed below. + + msgfmt messages.po -o /messages.mo + + Unfortunately currently Ampache doesn't automatically detect new languages + and thus you have to edit the code directly in order for it to pickup your + new language. Find /lib/preferences.php and then find "case 'lang':" under + the "create_preference_input" function and add a line for your own + language. For example to add en_US support add the following line + + echo "\t\n"; + + Make sure that it comes after the \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n \n
\n \n
\n\n', +".dg ul{list-style:none;margin:0;padding:0;width:100%;clear:both}.dg.ac{position:fixed;top:0;left:0;right:0;height:0;z-index:0}.dg:not(.ac) .main{overflow:hidden}.dg.main{-webkit-transition:opacity 0.1s linear;-o-transition:opacity 0.1s linear;-moz-transition:opacity 0.1s linear;transition:opacity 0.1s linear}.dg.main.taller-than-window{overflow-y:auto}.dg.main.taller-than-window .close-button{opacity:1;margin-top:-1px;border-top:1px solid #2c2c2c}.dg.main ul.closed .close-button{opacity:1 !important}.dg.main:hover .close-button,.dg.main .close-button.drag{opacity:1}.dg.main .close-button{-webkit-transition:opacity 0.1s linear;-o-transition:opacity 0.1s linear;-moz-transition:opacity 0.1s linear;transition:opacity 0.1s linear;border:0;position:absolute;line-height:19px;height:20px;cursor:pointer;text-align:center;background-color:#000}.dg.main .close-button:hover{background-color:#111}.dg.a{float:right;margin-right:15px;overflow-x:hidden}.dg.a.has-save ul{margin-top:27px}.dg.a.has-save ul.closed{margin-top:0}.dg.a .save-row{position:fixed;top:0;z-index:1002}.dg li{-webkit-transition:height 0.1s ease-out;-o-transition:height 0.1s ease-out;-moz-transition:height 0.1s ease-out;transition:height 0.1s ease-out}.dg li:not(.folder){cursor:auto;height:27px;line-height:27px;overflow:hidden;padding:0 4px 0 5px}.dg li.folder{padding:0;border-left:4px solid rgba(0,0,0,0)}.dg li.title{cursor:pointer;margin-left:-4px}.dg .closed li:not(.title),.dg .closed ul li,.dg .closed ul li > *{height:0;overflow:hidden;border:0}.dg .cr{clear:both;padding-left:3px;height:27px}.dg .property-name{cursor:default;float:left;clear:left;width:40%;overflow:hidden;text-overflow:ellipsis}.dg .c{float:left;width:60%}.dg .c input[type=text]{border:0;margin-top:4px;padding:3px;width:100%;float:right}.dg .has-slider input[type=text]{width:30%;margin-left:0}.dg .slider{float:left;width:66%;margin-left:-5px;margin-right:0;height:19px;margin-top:4px}.dg .slider-fg{height:100%}.dg .c input[type=checkbox]{margin-top:9px}.dg .c select{margin-top:5px}.dg .cr.function,.dg .cr.function .property-name,.dg .cr.function *,.dg .cr.boolean,.dg .cr.boolean *{cursor:pointer}.dg .selector{display:none;position:absolute;margin-left:-9px;margin-top:23px;z-index:10}.dg .c:hover .selector,.dg .selector.drag{display:block}.dg li.save-row{padding:0}.dg li.save-row .button{display:inline-block;padding:0px 6px}.dg.dialogue{background-color:#222;width:460px;padding:15px;font-size:13px;line-height:15px}#dg-new-constructor{padding:10px;color:#222;font-family:Monaco, monospace;font-size:10px;border:0;resize:none;box-shadow:inset 1px 1px 1px #888;word-wrap:break-word;margin:12px 0;display:block;width:440px;overflow-y:scroll;height:100px;position:relative}#dg-local-explain{display:none;font-size:11px;line-height:17px;border-radius:3px;background-color:#333;padding:8px;margin-top:10px}#dg-local-explain code{font-size:10px}#dat-gui-save-locally{display:none}.dg{color:#eee;font:11px 'Lucida Grande', sans-serif;text-shadow:0 -1px 0 #111}.dg.main::-webkit-scrollbar{width:5px;background:#1a1a1a}.dg.main::-webkit-scrollbar-corner{height:0;display:none}.dg.main::-webkit-scrollbar-thumb{border-radius:5px;background:#676767}.dg li:not(.folder){background:#1a1a1a;border-bottom:1px solid #2c2c2c}.dg li.save-row{line-height:25px;background:#dad5cb;border:0}.dg li.save-row select{margin-left:5px;width:108px}.dg li.save-row .button{margin-left:5px;margin-top:1px;border-radius:2px;font-size:9px;line-height:7px;padding:4px 4px 5px 4px;background:#c5bdad;color:#fff;text-shadow:0 1px 0 #b0a58f;box-shadow:0 -1px 0 #b0a58f;cursor:pointer}.dg li.save-row .button.gears{background:#c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;height:7px;width:8px}.dg li.save-row .button:hover{background-color:#bab19e;box-shadow:0 -1px 0 #b0a58f}.dg li.folder{border-bottom:0}.dg li.title{padding-left:16px;background:#000 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.2)}.dg .closed li.title{background-image:url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==)}.dg .cr.boolean{border-left:3px solid #806787}.dg .cr.function{border-left:3px solid #e61d5f}.dg .cr.number{border-left:3px solid #2fa1d6}.dg .cr.number input[type=text]{color:#2fa1d6}.dg .cr.string{border-left:3px solid #1ed36f}.dg .cr.string input[type=text]{color:#1ed36f}.dg .cr.function:hover,.dg .cr.boolean:hover{background:#111}.dg .c input[type=text]{background:#303030;outline:none}.dg .c input[type=text]:hover{background:#3c3c3c}.dg .c input[type=text]:focus{background:#494949;color:#fff}.dg .c .slider{background:#303030;cursor:ew-resize}.dg .c .slider-fg{background:#2fa1d6}.dg .c .slider:hover{background:#3c3c3c}.dg .c .slider:hover .slider-fg{background:#44abda}\n", +dat.controllers.factory=function(e,a,c,d,f,b,n){return function(h,j,m,l){var o=h[j];if(n.isArray(m)||n.isObject(m))return new e(h,j,m);if(n.isNumber(o))return n.isNumber(m)&&n.isNumber(l)?new c(h,j,m,l):new a(h,j,{min:m,max:l});if(n.isString(o))return new d(h,j);if(n.isFunction(o))return new f(h,j,"");if(n.isBoolean(o))return new b(h,j)}}(dat.controllers.OptionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.StringController=function(e,a,c){var d= +function(c,b){function e(){h.setValue(h.__input.value)}d.superclass.call(this,c,b);var h=this;this.__input=document.createElement("input");this.__input.setAttribute("type","text");a.bind(this.__input,"keyup",e);a.bind(this.__input,"change",e);a.bind(this.__input,"blur",function(){h.__onFinishChange&&h.__onFinishChange.call(h,h.getValue())});a.bind(this.__input,"keydown",function(a){a.keyCode===13&&this.blur()});this.updateDisplay();this.domElement.appendChild(this.__input)};d.superclass=e;c.extend(d.prototype, +e.prototype,{updateDisplay:function(){if(!a.isActive(this.__input))this.__input.value=this.getValue();return d.superclass.prototype.updateDisplay.call(this)}});return d}(dat.controllers.Controller,dat.dom.dom,dat.utils.common),dat.controllers.FunctionController,dat.controllers.BooleanController,dat.utils.common),dat.controllers.Controller,dat.controllers.BooleanController,dat.controllers.FunctionController,dat.controllers.NumberControllerBox,dat.controllers.NumberControllerSlider,dat.controllers.OptionController, +dat.controllers.ColorController=function(e,a,c,d,f){function b(a,b,c,d){a.style.background="";f.each(j,function(e){a.style.cssText+="background: "+e+"linear-gradient("+b+", "+c+" 0%, "+d+" 100%); "})}function n(a){a.style.background="";a.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);";a.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"; +a.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);";a.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}var h=function(e,l){function o(b){q(b);a.bind(window,"mousemove",q);a.bind(window, +"mouseup",j)}function j(){a.unbind(window,"mousemove",q);a.unbind(window,"mouseup",j)}function g(){var a=d(this.value);a!==false?(p.__color.__state=a,p.setValue(p.__color.toOriginal())):this.value=p.__color.toString()}function i(){a.unbind(window,"mousemove",s);a.unbind(window,"mouseup",i)}function q(b){b.preventDefault();var c=a.getWidth(p.__saturation_field),d=a.getOffset(p.__saturation_field),e=(b.clientX-d.left+document.body.scrollLeft)/c,b=1-(b.clientY-d.top+document.body.scrollTop)/c;b>1?b= +1:b<0&&(b=0);e>1?e=1:e<0&&(e=0);p.__color.v=b;p.__color.s=e;p.setValue(p.__color.toOriginal());return false}function s(b){b.preventDefault();var c=a.getHeight(p.__hue_field),d=a.getOffset(p.__hue_field),b=1-(b.clientY-d.top+document.body.scrollTop)/c;b>1?b=1:b<0&&(b=0);p.__color.h=b*360;p.setValue(p.__color.toOriginal());return false}h.superclass.call(this,e,l);this.__color=new c(this.getValue());this.__temp=new c(0);var p=this;this.domElement=document.createElement("div");a.makeSelectable(this.domElement, +false);this.__selector=document.createElement("div");this.__selector.className="selector";this.__saturation_field=document.createElement("div");this.__saturation_field.className="saturation-field";this.__field_knob=document.createElement("div");this.__field_knob.className="field-knob";this.__field_knob_border="2px solid ";this.__hue_knob=document.createElement("div");this.__hue_knob.className="hue-knob";this.__hue_field=document.createElement("div");this.__hue_field.className="hue-field";this.__input= +document.createElement("input");this.__input.type="text";this.__input_textShadow="0 1px 1px ";a.bind(this.__input,"keydown",function(a){a.keyCode===13&&g.call(this)});a.bind(this.__input,"blur",g);a.bind(this.__selector,"mousedown",function(){a.addClass(this,"drag").bind(window,"mouseup",function(){a.removeClass(p.__selector,"drag")})});var t=document.createElement("div");f.extend(this.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"}); +f.extend(this.__field_knob.style,{position:"absolute",width:"12px",height:"12px",border:this.__field_knob_border+(this.__color.v<0.5?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1});f.extend(this.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1});f.extend(this.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"});f.extend(t.style, +{width:"100%",height:"100%",background:"none"});b(t,"top","rgba(0,0,0,0)","#000");f.extend(this.__hue_field.style,{width:"15px",height:"100px",display:"inline-block",border:"1px solid #555",cursor:"ns-resize"});n(this.__hue_field);f.extend(this.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:this.__input_textShadow+"rgba(0,0,0,0.7)"});a.bind(this.__saturation_field,"mousedown",o);a.bind(this.__field_knob,"mousedown",o);a.bind(this.__hue_field,"mousedown", +function(b){s(b);a.bind(window,"mousemove",s);a.bind(window,"mouseup",i)});this.__saturation_field.appendChild(t);this.__selector.appendChild(this.__field_knob);this.__selector.appendChild(this.__saturation_field);this.__selector.appendChild(this.__hue_field);this.__hue_field.appendChild(this.__hue_knob);this.domElement.appendChild(this.__input);this.domElement.appendChild(this.__selector);this.updateDisplay()};h.superclass=e;f.extend(h.prototype,e.prototype,{updateDisplay:function(){var a=d(this.getValue()); +if(a!==false){var e=false;f.each(c.COMPONENTS,function(b){if(!f.isUndefined(a[b])&&!f.isUndefined(this.__color.__state[b])&&a[b]!==this.__color.__state[b])return e=true,{}},this);e&&f.extend(this.__color.__state,a)}f.extend(this.__temp.__state,this.__color.__state);this.__temp.a=1;var h=this.__color.v<0.5||this.__color.s>0.5?255:0,j=255-h;f.extend(this.__field_knob.style,{marginLeft:100*this.__color.s-7+"px",marginTop:100*(1-this.__color.v)-7+"px",backgroundColor:this.__temp.toString(),border:this.__field_knob_border+ +"rgb("+h+","+h+","+h+")"});this.__hue_knob.style.marginTop=(1-this.__color.h/360)*100+"px";this.__temp.s=1;this.__temp.v=1;b(this.__saturation_field,"left","#fff",this.__temp.toString());f.extend(this.__input.style,{backgroundColor:this.__input.value=this.__color.toString(),color:"rgb("+h+","+h+","+h+")",textShadow:this.__input_textShadow+"rgba("+j+","+j+","+j+",.7)"})}});var j=["-moz-","-o-","-webkit-","-ms-",""];return h}(dat.controllers.Controller,dat.dom.dom,dat.color.Color=function(e,a,c,d){function f(a, +b,c){Object.defineProperty(a,b,{get:function(){if(this.__state.space==="RGB")return this.__state[b];n(this,b,c);return this.__state[b]},set:function(a){if(this.__state.space!=="RGB")n(this,b,c),this.__state.space="RGB";this.__state[b]=a}})}function b(a,b){Object.defineProperty(a,b,{get:function(){if(this.__state.space==="HSV")return this.__state[b];h(this);return this.__state[b]},set:function(a){if(this.__state.space!=="HSV")h(this),this.__state.space="HSV";this.__state[b]=a}})}function n(b,c,e){if(b.__state.space=== +"HEX")b.__state[c]=a.component_from_hex(b.__state.hex,e);else if(b.__state.space==="HSV")d.extend(b.__state,a.hsv_to_rgb(b.__state.h,b.__state.s,b.__state.v));else throw"Corrupted color state";}function h(b){var c=a.rgb_to_hsv(b.r,b.g,b.b);d.extend(b.__state,{s:c.s,v:c.v});if(d.isNaN(c.h)){if(d.isUndefined(b.__state.h))b.__state.h=0}else b.__state.h=c.h}var j=function(){this.__state=e.apply(this,arguments);if(this.__state===false)throw"Failed to interpret color arguments";this.__state.a=this.__state.a|| +1};j.COMPONENTS="r,g,b,h,s,v,hex,a".split(",");d.extend(j.prototype,{toString:function(){return c(this)},toOriginal:function(){return this.__state.conversion.write(this)}});f(j.prototype,"r",2);f(j.prototype,"g",1);f(j.prototype,"b",0);b(j.prototype,"h");b(j.prototype,"s");b(j.prototype,"v");Object.defineProperty(j.prototype,"a",{get:function(){return this.__state.a},set:function(a){this.__state.a=a}});Object.defineProperty(j.prototype,"hex",{get:function(){if(!this.__state.space!=="HEX")this.__state.hex= +a.rgb_to_hex(this.r,this.g,this.b);return this.__state.hex},set:function(a){this.__state.space="HEX";this.__state.hex=a}});return j}(dat.color.interpret,dat.color.math=function(){var e;return{hsv_to_rgb:function(a,c,d){var e=a/60-Math.floor(a/60),b=d*(1-c),n=d*(1-e*c),c=d*(1-(1-e)*c),a=[[d,c,b],[n,d,b],[b,d,c],[b,n,d],[c,b,d],[d,b,n]][Math.floor(a/60)%6];return{r:a[0]*255,g:a[1]*255,b:a[2]*255}},rgb_to_hsv:function(a,c,d){var e=Math.min(a,c,d),b=Math.max(a,c,d),e=b-e;if(b==0)return{h:NaN,s:0,v:0}; +a=a==b?(c-d)/e:c==b?2+(d-a)/e:4+(a-c)/e;a/=6;a<0&&(a+=1);return{h:a*360,s:e/b,v:b/255}},rgb_to_hex:function(a,c,d){a=this.hex_with_component(0,2,a);a=this.hex_with_component(a,1,c);return a=this.hex_with_component(a,0,d)},component_from_hex:function(a,c){return a>>c*8&255},hex_with_component:function(a,c,d){return d<<(e=c*8)|a&~(255<c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); +k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= +"block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:11,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= +a+"px",m=b,r=0);return b},update:function(){l=this.end()}}}; diff --git a/sources/modules/UberViz/style.css b/sources/modules/UberViz/style.css new file mode 100644 index 0000000..b7b8f54 --- /dev/null +++ b/sources/modules/UberViz/style.css @@ -0,0 +1,99 @@ +#uberviz { + visibility: hidden; +} + +#uberviz #viz { + position:absolute; + background-color: #000; +} + +#uberviz #controls{ + position:absolute; + background-color: #000; + top: 0px; + right:0px; + width:250px; + height:100%; + display:none; +} + +#uberviz #preloader{ + position: absolute; + width: 40px; + height: 40px; + top: 50%; + left: 50%; + margin-left: -20px; + margin-top:-20px; + background:url(loader.gif) center center no-repeat; +} + +#uberviz #debugText{ + position:absolute; + background-color: #000; + height: 38px; + width:150px; + top:0px; + right: 80px; + padding:10px; + font-size: 16px; + padding-top: 15px; +} + +#uberviz #stats{ + position:absolute; + background-color: #003; + right:0; + top:0; +} + +#uberviz #audioDebug{ + position:absolute; + background-color: #000; + width: 250px; + top:0px; + right:0px; + opacity: 0.9; +} + +#uberviz #settings{ + position:absolute; + background-color: #003; + top: 248px; +} + +#uberviz #info{ + position:absolute; + left: 20px; + bottom: 20px; + line-height: 20px; +} + +#uberviz a { + color: #fff; + text-decoration: none; + border-bottom:thin dotted #999; +} + +#equalizer { + position: absolute; + z-index: 9999; + bottom: 1px; + right: 25%; + visibility: hidden; + margin-right: 5px; +} + +#equalizer .eq { + display: inline-block; + height: 45px; + width: 8px; + border: 2px solid orange; + background: none; + background-color: #000; +} + +#equalizer .ui-slider-handle { + width: 10px; + height: 10px; +} diff --git a/sources/modules/UberViz/three.js b/sources/modules/UberViz/three.js new file mode 100644 index 0000000..b2a2313 --- /dev/null +++ b/sources/modules/UberViz/three.js @@ -0,0 +1,705 @@ +// three.js - http://github.com/mrdoob/three.js +'use strict';var THREE=THREE||{REVISION:"60"};self.console=self.console||{info:function(){},log:function(){},debug:function(){},warn:function(){},error:function(){}};String.prototype.trim=String.prototype.trim||function(){return this.replace(/^\s+|\s+$/g,"")};THREE.extend=function(a,b){if(Object.keys)for(var c=Object.keys(b),d=0,e=c.length;d>16&255)/255;this.g=(a>>8&255)/255;this.b=(a&255)/255;return this},setRGB:function(a,b,c){this.r=a;this.g=b;this.b=c;return this},setHSL:function(a,b,c){if(0===b)this.r=this.g=this.b=c;else{var d=function(a,b,c){0>c&&(c+=1);1c?b:c<2/3?a+6*(b-a)*(2/3-c):a},b=0.5>=c?c*(1+b):c+b-c*b,c=2*c-b;this.r=d(c,b,a+1/3);this.g=d(c,b,a);this.b=d(c,b,a-1/3)}return this},setStyle:function(a){if(/^rgb\((\d+),(\d+),(\d+)\)$/i.test(a))return a=/^rgb\((\d+),(\d+),(\d+)\)$/i.exec(a),this.r=Math.min(255,parseInt(a[1],10))/255,this.g=Math.min(255,parseInt(a[2],10))/255,this.b=Math.min(255,parseInt(a[3],10))/255,this;if(/^rgb\((\d+)\%,(\d+)\%,(\d+)\%\)$/i.test(a))return a=/^rgb\((\d+)\%,(\d+)\%,(\d+)\%\)$/i.exec(a),this.r=Math.min(100, +parseInt(a[1],10))/100,this.g=Math.min(100,parseInt(a[2],10))/100,this.b=Math.min(100,parseInt(a[3],10))/100,this;if(/^\#([0-9a-f]{6})$/i.test(a))return a=/^\#([0-9a-f]{6})$/i.exec(a),this.setHex(parseInt(a[1],16)),this;if(/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.test(a))return a=/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(a),this.setHex(parseInt(a[1]+a[1]+a[2]+a[2]+a[3]+a[3],16)),this;if(/^(\w+)$/i.test(a))return this.setHex(THREE.ColorKeywords[a]),this},copy:function(a){this.r=a.r;this.g=a.g;this.b= +a.b;return this},copyGammaToLinear:function(a){this.r=a.r*a.r;this.g=a.g*a.g;this.b=a.b*a.b;return this},copyLinearToGamma:function(a){this.r=Math.sqrt(a.r);this.g=Math.sqrt(a.g);this.b=Math.sqrt(a.b);return this},convertGammaToLinear:function(){var a=this.r,b=this.g,c=this.b;this.r=a*a;this.g=b*b;this.b=c*c;return this},convertLinearToGamma:function(){this.r=Math.sqrt(this.r);this.g=Math.sqrt(this.g);this.b=Math.sqrt(this.b);return this},getHex:function(){return 255*this.r<<16^255*this.g<<8^255* +this.b<<0},getHexString:function(){return("000000"+this.getHex().toString(16)).slice(-6)},getHSL:function(){var a={h:0,s:0,l:0};return function(){var b=this.r,c=this.g,d=this.b,e=Math.max(b,c,d),f=Math.min(b,c,d),h,g=(f+e)/2;if(f===e)f=h=0;else{var i=e-f,f=0.5>=g?i/(e+f):i/(2-e-f);switch(e){case b:h=(c-d)/i+(cf&&c>b?(c=2*Math.sqrt(1+c-f-b),this._w=(i-h)/c,this._x=0.25*c, +this._y=(a+e)/c,this._z=(d+g)/c):f>b?(c=2*Math.sqrt(1+f-c-b),this._w=(d-g)/c,this._x=(a+e)/c,this._y=0.25*c,this._z=(h+i)/c):(c=2*Math.sqrt(1+b-c-f),this._w=(e-a)/c,this._x=(d+g)/c,this._y=(h+i)/c,this._z=0.25*c);this._updateEuler();return this},inverse:function(){this.conjugate().normalize();return this},conjugate:function(){this._x*=-1;this._y*=-1;this._z*=-1;this._updateEuler();return this},lengthSq:function(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w},length:function(){return Math.sqrt(this._x* +this._x+this._y*this._y+this._z*this._z+this._w*this._w)},normalize:function(){var a=this.length();0===a?(this._z=this._y=this._x=0,this._w=1):(a=1/a,this._x*=a,this._y*=a,this._z*=a,this._w*=a);return this},multiply:function(a,b){return void 0!==b?(console.warn("DEPRECATED: Quaternion's .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(a,b)):this.multiplyQuaternions(this,a)},multiplyQuaternions:function(a,b){var c=a._x,d=a._y,e=a._z,f= +a._w,h=b._x,g=b._y,i=b._z,k=b._w;this._x=c*k+f*h+d*i-e*g;this._y=d*k+f*g+e*h-c*i;this._z=e*k+f*i+c*g-d*h;this._w=f*k-c*h-d*g-e*i;this._updateEuler();return this},multiplyVector3:function(a){console.warn("DEPRECATED: Quaternion's .multiplyVector3() has been removed. Use is now vector.applyQuaternion( quaternion ) instead.");return a.applyQuaternion(this)},slerp:function(a,b){var c=this._x,d=this._y,e=this._z,f=this._w,h=f*a._w+c*a._x+d*a._y+e*a._z;0>h?(this._w=-a._w,this._x=-a._x,this._y=-a._y,this._z= +-a._z,h=-h):this.copy(a);if(1<=h)return this._w=f,this._x=c,this._y=d,this._z=e,this;var g=Math.acos(h),i=Math.sqrt(1-h*h);if(0.0010>Math.abs(i))return this._w=0.5*(f+this._w),this._x=0.5*(c+this._x),this._y=0.5*(d+this._y),this._z=0.5*(e+this._z),this;h=Math.sin((1-b)*g)/i;g=Math.sin(b*g)/i;this._w=f*h+this._w*g;this._x=c*h+this._x*g;this._y=d*h+this._y*g;this._z=e*h+this._z*g;this._updateEuler();return this},equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._w===this._w}, +fromArray:function(a){this._x=a[0];this._y=a[1];this._z=a[2];this._w=a[3];this._updateEuler();return this},toArray:function(){return[this._x,this._y,this._z,this._w]},clone:function(){return new THREE.Quaternion(this._x,this._y,this._z,this._w)}};THREE.Quaternion.slerp=function(a,b,c,d){return c.copy(a).slerp(b,d)};THREE.Vector2=function(a,b){this.x=a||0;this.y=b||0}; +THREE.Vector2.prototype={constructor:THREE.Vector2,set:function(a,b){this.x=a;this.y=b;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;default:throw Error("index is out of range: "+a);}},copy:function(a){this.x=a.x;this.y=a.y;return this},add:function(a, +b){if(void 0!==b)return console.warn("DEPRECATED: Vector2's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;return this},addScalar:function(a){this.x+=a;this.y+=a;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector2's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-= +a.y;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;return this},divideScalar:function(a){0!==a?(a=1/a,this.x*=a,this.y*=a):this.y=this.x=0;return this},min:function(a){this.x>a.x&&(this.x=a.x);this.y>a.y&&(this.y=a.y);return this},max:function(a){this.xb.x&&(this.x=b.x);this.yb.y&&(this.y=b.y); +return this},negate:function(){return this.multiplyScalar(-1)},dot:function(a){return this.x*a.x+this.y*a.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},normalize:function(){return this.divideScalar(this.length())},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,a=this.y-a.y;return b*b+a*a},setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/ +b);return this},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;return this},equals:function(a){return a.x===this.x&&a.y===this.y},fromArray:function(a){this.x=a[0];this.y=a[1];return this},toArray:function(){return[this.x,this.y]},clone:function(){return new THREE.Vector2(this.x,this.y)}};THREE.Vector3=function(a,b,c){this.x=a||0;this.y=b||0;this.z=c||0}; +THREE.Vector3.prototype={constructor:THREE.Vector3,set:function(a,b,c){this.x=a;this.y=b;this.z=c;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw Error("index is out of range: "+ +a);}},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this},add:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."), +this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;return this},multiply:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(a,b);this.x*=a.x;this.y*=a.y;this.z*=a.z;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;return this},multiplyVectors:function(a,b){this.x=a.x* +b.x;this.y=a.y*b.y;this.z=a.z*b.z;return this},applyMatrix3:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[3]*c+a[6]*d;this.y=a[1]*b+a[4]*c+a[7]*d;this.z=a[2]*b+a[5]*c+a[8]*d;return this},applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12];this.y=a[1]*b+a[5]*c+a[9]*d+a[13];this.z=a[2]*b+a[6]*c+a[10]*d+a[14];return this},applyProjection:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements,e=1/(a[3]*b+a[7]*c+a[11]*d+a[15]); +this.x=(a[0]*b+a[4]*c+a[8]*d+a[12])*e;this.y=(a[1]*b+a[5]*c+a[9]*d+a[13])*e;this.z=(a[2]*b+a[6]*c+a[10]*d+a[14])*e;return this},applyQuaternion:function(a){var b=this.x,c=this.y,d=this.z,e=a.x,f=a.y,h=a.z,a=a.w,g=a*b+f*d-h*c,i=a*c+h*b-e*d,k=a*d+e*c-f*b,b=-e*b-f*c-h*d;this.x=g*a+b*-e+i*-h-k*-f;this.y=i*a+b*-f+k*-e-g*-h;this.z=k*a+b*-h+g*-f-i*-e;return this},transformDirection:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d;this.y=a[1]*b+a[5]*c+a[9]*d;this.z=a[2]* +b+a[6]*c+a[10]*d;this.normalize();return this},divide:function(a){this.x/=a.x;this.y/=a.y;this.z/=a.z;return this},divideScalar:function(a){0!==a?(a=1/a,this.x*=a,this.y*=a,this.z*=a):this.z=this.y=this.x=0;return this},min:function(a){this.x>a.x&&(this.x=a.x);this.y>a.y&&(this.y=a.y);this.z>a.z&&(this.z=a.z);return this},max:function(a){this.xb.x&&(this.x=b.x);this.y< +a.y?this.y=a.y:this.y>b.y&&(this.y=b.y);this.zb.z&&(this.z=b.z);return this},negate:function(){return this.multiplyScalar(-1)},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScalar(this.length())}, +setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/b);return this},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;return this},cross:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."),this.crossVectors(a,b);var c=this.x,d=this.y,e=this.z;this.x=d*a.z-e*a.y;this.y=e*a.x-c*a.z;this.z=c*a.y-d*a.x;return this},crossVectors:function(a,b){var c= +a.x,d=a.y,e=a.z,f=b.x,h=b.y,g=b.z;this.x=d*g-e*h;this.y=e*f-c*g;this.z=c*h-d*f;return this},angleTo:function(a){a=this.dot(a)/(this.length()*a.length());return Math.acos(THREE.Math.clamp(a,-1,1))},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,c=this.y-a.y,a=this.z-a.z;return b*b+c*c+a*a},setEulerFromRotationMatrix:function(){console.error("REMOVED: Vector3's setEulerFromRotationMatrix has been removed in favor of Euler.setFromRotationMatrix(), please update your code.")}, +setEulerFromQuaternion:function(){console.error("REMOVED: Vector3's setEulerFromQuaternion: has been removed in favor of Euler.setFromQuaternion(), please update your code.")},getPositionFromMatrix:function(a){this.x=a.elements[12];this.y=a.elements[13];this.z=a.elements[14];return this},getScaleFromMatrix:function(a){var b=this.set(a.elements[0],a.elements[1],a.elements[2]).length(),c=this.set(a.elements[4],a.elements[5],a.elements[6]).length(),a=this.set(a.elements[8],a.elements[9],a.elements[10]).length(); +this.x=b;this.y=c;this.z=a;return this},getColumnFromMatrix:function(a,b){var c=4*a,d=b.elements;this.x=d[c];this.y=d[c+1];this.z=d[c+2];return this},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z},fromArray:function(a){this.x=a[0];this.y=a[1];this.z=a[2];return this},toArray:function(){return[this.x,this.y,this.z]},clone:function(){return new THREE.Vector3(this.x,this.y,this.z)}}; +THREE.extend(THREE.Vector3.prototype,{applyEuler:function(){var a=new THREE.Quaternion;return function(b){!1===b instanceof THREE.Euler&&console.error("ERROR: Vector3's .applyEuler() now expects a Euler rotation rather than a Vector3 and order. Please update your code.");this.applyQuaternion(a.setFromEuler(b));return this}}(),applyAxisAngle:function(){var a=new THREE.Quaternion;return function(b,c){this.applyQuaternion(a.setFromAxisAngle(b,c));return this}}(),projectOnVector:function(){var a=new THREE.Vector3; +return function(b){a.copy(b).normalize();b=this.dot(a);return this.copy(a).multiplyScalar(b)}}(),projectOnPlane:function(){var a=new THREE.Vector3;return function(b){a.copy(this).projectOnVector(b);return this.sub(a)}}(),reflect:function(){var a=new THREE.Vector3;return function(b){a.copy(this).projectOnVector(b).multiplyScalar(2);return this.subVectors(a,this)}}()});THREE.Vector4=function(a,b,c,d){this.x=a||0;this.y=b||0;this.z=c||0;this.w=void 0!==d?d:1}; +THREE.Vector4.prototype={constructor:THREE.Vector4,set:function(a,b,c,d){this.x=a;this.y=b;this.z=c;this.w=d;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setW:function(a){this.w=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;case 3:this.w=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x; +case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw Error("index is out of range: "+a);}},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;this.w=void 0!==a.w?a.w:1;return this},add:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector4's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;this.w+=a.w;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;this.w+=a;return this}, +addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;this.w=a.w+b.w;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector4's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;this.w-=a.w;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;this.w=a.w-b.w;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;this.w*=a;return this}, +applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z,e=this.w,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12]*e;this.y=a[1]*b+a[5]*c+a[9]*d+a[13]*e;this.z=a[2]*b+a[6]*c+a[10]*d+a[14]*e;this.w=a[3]*b+a[7]*c+a[11]*d+a[15]*e;return this},divideScalar:function(a){0!==a?(a=1/a,this.x*=a,this.y*=a,this.z*=a,this.w*=a):(this.z=this.y=this.x=0,this.w=1);return this},setAxisAngleFromQuaternion:function(a){this.w=2*Math.acos(a.w);var b=Math.sqrt(1-a.w*a.w);1E-4>b?(this.x=1,this.z=this.y=0):(this.x=a.x/b, +this.y=a.y/b,this.z=a.z/b);return this},setAxisAngleFromRotationMatrix:function(a){var b,c,d,a=a.elements,e=a[0];d=a[4];var f=a[8],h=a[1],g=a[5],i=a[9];c=a[2];b=a[6];var k=a[10];if(0.01>Math.abs(d-h)&&0.01>Math.abs(f-c)&&0.01>Math.abs(i-b)){if(0.1>Math.abs(d+h)&&0.1>Math.abs(f+c)&&0.1>Math.abs(i+b)&&0.1>Math.abs(e+g+k-3))return this.set(1,0,0,0),this;a=Math.PI;e=(e+1)/2;g=(g+1)/2;k=(k+1)/2;d=(d+h)/4;f=(f+c)/4;i=(i+b)/4;e>g&&e>k?0.01>e?(b=0,d=c=0.707106781):(b=Math.sqrt(e),c=d/b,d=f/b):g>k?0.01>g? +(b=0.707106781,c=0,d=0.707106781):(c=Math.sqrt(g),b=d/c,d=i/c):0.01>k?(c=b=0.707106781,d=0):(d=Math.sqrt(k),b=f/d,c=i/d);this.set(b,c,d,a);return this}a=Math.sqrt((b-i)*(b-i)+(f-c)*(f-c)+(h-d)*(h-d));0.0010>Math.abs(a)&&(a=1);this.x=(b-i)/a;this.y=(f-c)/a;this.z=(h-d)/a;this.w=Math.acos((e+g+k-1)/2);return this},min:function(a){this.x>a.x&&(this.x=a.x);this.y>a.y&&(this.y=a.y);this.z>a.z&&(this.z=a.z);this.w>a.w&&(this.w=a.w);return this},max:function(a){this.xb.x&&(this.x=b.x);this.yb.y&&(this.y=b.y);this.zb.z&&(this.z=b.z);this.wb.w&&(this.w=b.w);return this},negate:function(){return this.multiplyScalar(-1)},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z+this.w*a.w},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x* +this.x+this.y*this.y+this.z*this.z+this.w*this.w)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)},normalize:function(){return this.divideScalar(this.length())},setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/b);return this},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;this.w+=(a.w-this.w)*b;return this},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z&& +a.w===this.w},fromArray:function(a){this.x=a[0];this.y=a[1];this.z=a[2];this.w=a[3];return this},toArray:function(){return[this.x,this.y,this.z,this.w]},clone:function(){return new THREE.Vector4(this.x,this.y,this.z,this.w)}};THREE.Euler=function(a,b,c,d){this._x=a||0;this._y=b||0;this._z=c||0;this._order=d||THREE.Euler.DefaultOrder};THREE.Euler.RotationOrders="XYZ YZX ZXY XZY YXZ ZYX".split(" ");THREE.Euler.DefaultOrder="XYZ"; +THREE.Euler.prototype={constructor:THREE.Euler,_x:0,_y:0,_z:0,_order:THREE.Euler.DefaultOrder,_quaternion:void 0,_updateQuaternion:function(){void 0!==this._quaternion&&this._quaternion.setFromEuler(this,!1)},get x(){return this._x},set x(a){this._x=a;this._updateQuaternion()},get y(){return this._y},set y(a){this._y=a;this._updateQuaternion()},get z(){return this._z},set z(a){this._z=a;this._updateQuaternion()},get order(){return this._order},set order(a){this._order=a;this._updateQuaternion()}, +set:function(a,b,c,d){this._x=a;this._y=b;this._z=c;this._order=d||this._order;this._updateQuaternion();return this},copy:function(a){this._x=a._x;this._y=a._y;this._z=a._z;this._order=a._order;this._updateQuaternion();return this},setFromRotationMatrix:function(a,b){function c(a){return Math.min(Math.max(a,-1),1)}var d=a.elements,e=d[0],f=d[4],h=d[8],g=d[1],i=d[5],k=d[9],m=d[2],l=d[6],d=d[10],b=b||this._order;"XYZ"===b?(this._y=Math.asin(c(h)),0.99999>Math.abs(h)?(this._x=Math.atan2(-k,d),this._z= +Math.atan2(-f,e)):(this._x=Math.atan2(l,i),this._z=0)):"YXZ"===b?(this._x=Math.asin(-c(k)),0.99999>Math.abs(k)?(this._y=Math.atan2(h,d),this._z=Math.atan2(g,i)):(this._y=Math.atan2(-m,e),this._z=0)):"ZXY"===b?(this._x=Math.asin(c(l)),0.99999>Math.abs(l)?(this._y=Math.atan2(-m,d),this._z=Math.atan2(-f,i)):(this._y=0,this._z=Math.atan2(g,e))):"ZYX"===b?(this._y=Math.asin(-c(m)),0.99999>Math.abs(m)?(this._x=Math.atan2(l,d),this._z=Math.atan2(g,e)):(this._x=0,this._z=Math.atan2(-f,i))):"YZX"===b?(this._z= +Math.asin(c(g)),0.99999>Math.abs(g)?(this._x=Math.atan2(-k,i),this._y=Math.atan2(-m,e)):(this._x=0,this._y=Math.atan2(h,d))):"XZY"===b?(this._z=Math.asin(-c(f)),0.99999>Math.abs(f)?(this._x=Math.atan2(l,i),this._y=Math.atan2(h,e)):(this._x=Math.atan2(-k,d),this._y=0)):console.warn("WARNING: Euler.setFromRotationMatrix() given unsupported order: "+b);this._order=b;this._updateQuaternion();return this},setFromQuaternion:function(a,b,c){function d(a){return Math.min(Math.max(a,-1),1)}var e=a.x*a.x,f= +a.y*a.y,h=a.z*a.z,g=a.w*a.w,b=b||this._order;"XYZ"===b?(this._x=Math.atan2(2*(a.x*a.w-a.y*a.z),g-e-f+h),this._y=Math.asin(d(2*(a.x*a.z+a.y*a.w))),this._z=Math.atan2(2*(a.z*a.w-a.x*a.y),g+e-f-h)):"YXZ"===b?(this._x=Math.asin(d(2*(a.x*a.w-a.y*a.z))),this._y=Math.atan2(2*(a.x*a.z+a.y*a.w),g-e-f+h),this._z=Math.atan2(2*(a.x*a.y+a.z*a.w),g-e+f-h)):"ZXY"===b?(this._x=Math.asin(d(2*(a.x*a.w+a.y*a.z))),this._y=Math.atan2(2*(a.y*a.w-a.z*a.x),g-e-f+h),this._z=Math.atan2(2*(a.z*a.w-a.x*a.y),g-e+f-h)):"ZYX"=== +b?(this._x=Math.atan2(2*(a.x*a.w+a.z*a.y),g-e-f+h),this._y=Math.asin(d(2*(a.y*a.w-a.x*a.z))),this._z=Math.atan2(2*(a.x*a.y+a.z*a.w),g+e-f-h)):"YZX"===b?(this._x=Math.atan2(2*(a.x*a.w-a.z*a.y),g-e+f-h),this._y=Math.atan2(2*(a.y*a.w-a.x*a.z),g+e-f-h),this._z=Math.asin(d(2*(a.x*a.y+a.z*a.w)))):"XZY"===b?(this._x=Math.atan2(2*(a.x*a.w+a.y*a.z),g-e+f-h),this._y=Math.atan2(2*(a.x*a.z+a.y*a.w),g+e-f-h),this._z=Math.asin(d(2*(a.z*a.w-a.x*a.y)))):console.warn("WARNING: Euler.setFromQuaternion() given unsupported order: "+ +b);this._order=b;!1!==c&&this._updateQuaternion();return this},reorder:function(){var a=new THREE.Quaternion;return function(b){a.setFromEuler(this);this.setFromQuaternion(a,b)}}(),fromArray:function(a){this._x=a[0];this._y=a[1];this._z=a[2];void 0!==a[3]&&(this._order=a[3]);this._updateQuaternion();return this},toArray:function(){return[this._x,this._y,this._z,this._order]},equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._order===this._order},clone:function(){return new THREE.Euler(this._x, +this._y,this._z,this._order)}};THREE.Line3=function(a,b){this.start=void 0!==a?a:new THREE.Vector3;this.end=void 0!==b?b:new THREE.Vector3}; +THREE.Line3.prototype={constructor:THREE.Line3,set:function(a,b){this.start.copy(a);this.end.copy(b);return this},copy:function(a){this.start.copy(a.start);this.end.copy(a.end);return this},center:function(a){return(a||new THREE.Vector3).addVectors(this.start,this.end).multiplyScalar(0.5)},delta:function(a){return(a||new THREE.Vector3).subVectors(this.end,this.start)},distanceSq:function(){return this.start.distanceToSquared(this.end)},distance:function(){return this.start.distanceTo(this.end)},at:function(a, +b){var c=b||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},closestPointToPointParameter:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d){a.subVectors(c,this.start);b.subVectors(this.end,this.start);var e=b.dot(b),e=b.dot(a)/e;d&&(e=THREE.Math.clamp(e,0,1));return e}}(),closestPointToPoint:function(a,b,c){a=this.closestPointToPointParameter(a,b);c=c||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},applyMatrix4:function(a){this.start.applyMatrix4(a); +this.end.applyMatrix4(a);return this},equals:function(a){return a.start.equals(this.start)&&a.end.equals(this.end)},clone:function(){return(new THREE.Line3).copy(this)}};THREE.Box2=function(a,b){this.min=void 0!==a?a:new THREE.Vector2(Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector2(-Infinity,-Infinity)}; +THREE.Box2.prototype={constructor:THREE.Box2,set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromPoints:function(a){if(0this.max.x&&(this.max.x=b.x),b.ythis.max.y&&(this.max.y=b.y)}else this.makeEmpty();return this},setFromCenterAndSize:function(){var a=new THREE.Vector2;return function(b,c){var d=a.copy(c).multiplyScalar(0.5); +this.min.copy(b).sub(d);this.max.copy(b).add(d);return this}}(),copy:function(a){this.min.copy(a.min);this.max.copy(a.max);return this},makeEmpty:function(){this.min.x=this.min.y=Infinity;this.max.x=this.max.y=-Infinity;return this},empty:function(){return this.max.xthis.max.x||a.ythis.max.y?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y?!0:!1},getParameter:function(a){return new THREE.Vector2((a.x-this.min.x)/(this.max.x-this.min.x), +(a.y-this.min.y)/(this.max.y-this.min.y))},isIntersectionBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y?!1:!0},clampPoint:function(a,b){return(b||new THREE.Vector2).copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new THREE.Vector2;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max); +return this},translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)},clone:function(){return(new THREE.Box2).copy(this)}};THREE.Box3=function(a,b){this.min=void 0!==a?a:new THREE.Vector3(Infinity,Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector3(-Infinity,-Infinity,-Infinity)}; +THREE.Box3.prototype={constructor:THREE.Box3,set:function(a,b){this.min.copy(a);this.max.copy(b);return this},addPoint:function(a){a.xthis.max.x&&(this.max.x=a.x);a.ythis.max.y&&(this.max.y=a.y);a.zthis.max.z&&(this.max.z=a.z)},setFromPoints:function(a){if(0this.max.x||a.ythis.max.y||a.zthis.max.z?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y&&this.min.z<=a.min.z&&a.max.z<=this.max.z?!0:!1},getParameter:function(a){return new THREE.Vector3((a.x-this.min.x)/(this.max.x-this.min.x), +(a.y-this.min.y)/(this.max.y-this.min.y),(a.z-this.min.z)/(this.max.z-this.min.z))},isIntersectionBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y||a.max.zthis.max.z?!1:!0},clampPoint:function(a,b){return(b||new THREE.Vector3).copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new THREE.Vector3;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),getBoundingSphere:function(){var a= +new THREE.Vector3;return function(b){b=b||new THREE.Sphere;b.center=this.center();b.radius=0.5*this.size(a).length();return b}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},applyMatrix4:function(){var a=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];return function(b){a[0].set(this.min.x,this.min.y, +this.min.z).applyMatrix4(b);a[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(b);a[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(b);a[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(b);a[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(b);a[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(b);a[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(b);a[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(b);this.makeEmpty();this.setFromPoints(a);return this}}(),translate:function(a){this.min.add(a); +this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)},clone:function(){return(new THREE.Box3).copy(this)}};THREE.Matrix3=function(a,b,c,d,e,f,h,g,i){this.elements=new Float32Array(9);this.set(void 0!==a?a:1,b||0,c||0,d||0,void 0!==e?e:1,f||0,h||0,g||0,void 0!==i?i:1)}; +THREE.Matrix3.prototype={constructor:THREE.Matrix3,set:function(a,b,c,d,e,f,h,g,i){var k=this.elements;k[0]=a;k[3]=b;k[6]=c;k[1]=d;k[4]=e;k[7]=f;k[2]=h;k[5]=g;k[8]=i;return this},identity:function(){this.set(1,0,0,0,1,0,0,0,1);return this},copy:function(a){a=a.elements;this.set(a[0],a[3],a[6],a[1],a[4],a[7],a[2],a[5],a[8]);return this},multiplyVector3:function(a){console.warn("DEPRECATED: Matrix3's .multiplyVector3() has been removed. Use vector.applyMatrix3( matrix ) instead.");return a.applyMatrix3(this)}, +multiplyVector3Array:function(){var a=new THREE.Vector3;return function(b){for(var c=0,d=b.length;cd?c.copy(this.origin):c.copy(this.direction).multiplyScalar(d).add(this.origin)},distanceToPoint:function(){var a=new THREE.Vector3;return function(b){var c=a.subVectors(b,this.origin).dot(this.direction);if(0>c)return this.origin.distanceTo(b);a.copy(this.direction).multiplyScalar(c).add(this.origin);return a.distanceTo(b)}}(),distanceSqToSegment:function(a,b,c,d){var e=a.clone().add(b).multiplyScalar(0.5),f=b.clone().sub(a).normalize(),h=0.5*a.distanceTo(b), +g=this.origin.clone().sub(e),a=-this.direction.dot(f),b=g.dot(this.direction),i=-g.dot(f),k=g.lengthSq(),m=Math.abs(1-a*a),l,n;0<=m?(g=a*i-b,l=a*b-i,n=h*m,0<=g?l>=-n?l<=n?(h=1/m,g*=h,l*=h,a=g*(g+a*l+2*b)+l*(a*g+l+2*i)+k):(l=h,g=Math.max(0,-(a*l+b)),a=-g*g+l*(l+2*i)+k):(l=-h,g=Math.max(0,-(a*l+b)),a=-g*g+l*(l+2*i)+k):l<=-n?(g=Math.max(0,-(-a*h+b)),l=0a.normal.dot(this.direction)*b?!0:!1},distanceToPlane:function(a){var b=a.normal.dot(this.direction);if(0==b)return 0==a.distanceToPoint(this.origin)? +0:null;a=-(this.origin.dot(a.normal)+a.constant)/b;return 0<=a?a:null},intersectPlane:function(a,b){var c=this.distanceToPlane(a);return null===c?null:this.at(c,b)},isIntersectionBox:function(){var a=new THREE.Vector3;return function(b){return null!==this.intersectBox(b,a)}}(),intersectBox:function(a,b){var c,d,e,f,h;d=1/this.direction.x;f=1/this.direction.y;h=1/this.direction.z;var g=this.origin;0<=d?(c=(a.min.x-g.x)*d,d*=a.max.x-g.x):(c=(a.max.x-g.x)*d,d*=a.min.x-g.x);0<=f?(e=(a.min.y-g.y)*f,f*= +a.max.y-g.y):(e=(a.max.y-g.y)*f,f*=a.min.y-g.y);if(c>f||e>d)return null;if(e>c||c!==c)c=e;if(fh||e>d)return null;if(e>c||c!==c)c=e;if(hd?null:this.at(0<=c?c:d,b)},intersectTriangle:function(){var a=new THREE.Vector3,b=new THREE.Vector3,c=new THREE.Vector3,d=new THREE.Vector3;return function(e,f,h,g,i){b.subVectors(f,e);c.subVectors(h,e);d.crossVectors(b,c);f=this.direction.dot(d);if(0< +f){if(g)return null;g=1}else if(0>f)g=-1,f=-f;else return null;a.subVectors(this.origin,e);e=g*this.direction.dot(c.crossVectors(a,c));if(0>e)return null;h=g*this.direction.dot(b.cross(a));if(0>h||e+h>f)return null;e=-g*a.dot(d);return 0>e?null:this.at(e/f,i)}}(),applyMatrix4:function(a){this.direction.add(this.origin).applyMatrix4(a);this.origin.applyMatrix4(a);this.direction.sub(this.origin);this.direction.normalize();return this},equals:function(a){return a.origin.equals(this.origin)&&a.direction.equals(this.direction)}, +clone:function(){return(new THREE.Ray).copy(this)}};THREE.Sphere=function(a,b){this.center=void 0!==a?a:new THREE.Vector3;this.radius=void 0!==b?b:0}; +THREE.Sphere.prototype={constructor:THREE.Sphere,set:function(a,b){this.center.copy(a);this.radius=b;return this},setFromPoints:function(){var a=new THREE.Box3;return function(b,c){var d=this.center;void 0!==c?d.copy(c):a.setFromPoints(b).center(d);for(var e=0,f=0,h=b.length;f=this.radius},containsPoint:function(a){return a.distanceToSquared(this.center)<= +this.radius*this.radius},distanceToPoint:function(a){return a.distanceTo(this.center)-this.radius},intersectsSphere:function(a){var b=this.radius+a.radius;return a.center.distanceToSquared(this.center)<=b*b},clampPoint:function(a,b){var c=this.center.distanceToSquared(a),d=b||new THREE.Vector3;d.copy(a);c>this.radius*this.radius&&(d.sub(this.center).normalize(),d.multiplyScalar(this.radius).add(this.center));return d},getBoundingBox:function(a){a=a||new THREE.Box3;a.set(this.center,this.center);a.expandByScalar(this.radius); +return a},applyMatrix4:function(a){this.center.applyMatrix4(a);this.radius*=a.getMaxScaleOnAxis();return this},translate:function(a){this.center.add(a);return this},equals:function(a){return a.center.equals(this.center)&&a.radius===this.radius},clone:function(){return(new THREE.Sphere).copy(this)}};THREE.Frustum=function(a,b,c,d,e,f){this.planes=[void 0!==a?a:new THREE.Plane,void 0!==b?b:new THREE.Plane,void 0!==c?c:new THREE.Plane,void 0!==d?d:new THREE.Plane,void 0!==e?e:new THREE.Plane,void 0!==f?f:new THREE.Plane]}; +THREE.Frustum.prototype={constructor:THREE.Frustum,set:function(a,b,c,d,e,f){var h=this.planes;h[0].copy(a);h[1].copy(b);h[2].copy(c);h[3].copy(d);h[4].copy(e);h[5].copy(f);return this},copy:function(a){for(var b=this.planes,c=0;6>c;c++)b[c].copy(a.planes[c]);return this},setFromMatrix:function(a){var b=this.planes,c=a.elements,a=c[0],d=c[1],e=c[2],f=c[3],h=c[4],g=c[5],i=c[6],k=c[7],m=c[8],l=c[9],n=c[10],t=c[11],q=c[12],p=c[13],r=c[14],c=c[15];b[0].setComponents(f-a,k-h,t-m,c-q).normalize();b[1].setComponents(f+ +a,k+h,t+m,c+q).normalize();b[2].setComponents(f+d,k+g,t+l,c+p).normalize();b[3].setComponents(f-d,k-g,t-l,c-p).normalize();b[4].setComponents(f-e,k-i,t-n,c-r).normalize();b[5].setComponents(f+e,k+i,t+n,c+r).normalize();return this},intersectsObject:function(){var a=new THREE.Sphere;return function(b){var c=b.geometry;null===c.boundingSphere&&c.computeBoundingSphere();a.copy(c.boundingSphere);a.applyMatrix4(b.matrixWorld);return this.intersectsSphere(a)}}(),intersectsSphere:function(a){for(var b=this.planes, +c=a.center,a=-a.radius,d=0;6>d;d++)if(b[d].distanceToPoint(c)e;e++){var f=d[e];a.x=0h&&0>f)return!1}return!0}}(),containsPoint:function(a){for(var b= +this.planes,c=0;6>c;c++)if(0>b[c].distanceToPoint(a))return!1;return!0},clone:function(){return(new THREE.Frustum).copy(this)}};THREE.Plane=function(a,b){this.normal=void 0!==a?a:new THREE.Vector3(1,0,0);this.constant=void 0!==b?b:0}; +THREE.Plane.prototype={constructor:THREE.Plane,set:function(a,b){this.normal.copy(a);this.constant=b;return this},setComponents:function(a,b,c,d){this.normal.set(a,b,c);this.constant=d;return this},setFromNormalAndCoplanarPoint:function(a,b){this.normal.copy(a);this.constant=-b.dot(this.normal);return this},setFromCoplanarPoints:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d,e){d=a.subVectors(e,d).cross(b.subVectors(c,d)).normalize();this.setFromNormalAndCoplanarPoint(d, +c);return this}}(),copy:function(a){this.normal.copy(a.normal);this.constant=a.constant;return this},normalize:function(){var a=1/this.normal.length();this.normal.multiplyScalar(a);this.constant*=a;return this},negate:function(){this.constant*=-1;this.normal.negate();return this},distanceToPoint:function(a){return this.normal.dot(a)+this.constant},distanceToSphere:function(a){return this.distanceToPoint(a.center)-a.radius},projectPoint:function(a,b){return this.orthoPoint(a,b).sub(a).negate()},orthoPoint:function(a, +b){var c=this.distanceToPoint(a);return(b||new THREE.Vector3).copy(this.normal).multiplyScalar(c)},isIntersectionLine:function(a){var b=this.distanceToPoint(a.start),a=this.distanceToPoint(a.end);return 0>b&&0a&&0f||1e;e++)8==e||13==e||18==e||23==e?b[e]="-":14==e?b[e]="4":(2>=c&&(c=33554432+16777216*Math.random()|0),d=c&15,c>>=4,b[e]=a[19==e?d&3|8:d]);return b.join("")}}(),clamp:function(a,b,c){return ac?c:a},clampBottom:function(a,b){return a=c)return 1;a=(a-b)/(c-b);return a*a*(3-2*a)},smootherstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*a*(a*(6*a-15)+10)},random16:function(){return(65280*Math.random()+255*Math.random())/65535},randInt:function(a,b){return a+Math.floor(Math.random()*(b-a+1))},randFloat:function(a,b){return a+Math.random()*(b-a)},randFloatSpread:function(a){return a*(0.5-Math.random())},sign:function(a){return 0>a?-1:0this.points.length-2?this.points.length-1:f+1;c[3]=f>this.points.length-3?this.points.length-1: +f+2;k=this.points[c[0]];m=this.points[c[1]];l=this.points[c[2]];n=this.points[c[3]];g=h*h;i=h*g;d.x=b(k.x,m.x,l.x,n.x,h,g,i);d.y=b(k.y,m.y,l.y,n.y,h,g,i);d.z=b(k.z,m.z,l.z,n.z,h,g,i);return d};this.getControlPointsArray=function(){var a,b,c=this.points.length,d=[];for(a=0;a=b.x+b.y}}(); +THREE.Triangle.prototype={constructor:THREE.Triangle,set:function(a,b,c){this.a.copy(a);this.b.copy(b);this.c.copy(c);return this},setFromPointsAndIndices:function(a,b,c,d){this.a.copy(a[b]);this.b.copy(a[c]);this.c.copy(a[d]);return this},copy:function(a){this.a.copy(a.a);this.b.copy(a.b);this.c.copy(a.c);return this},area:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(){a.subVectors(this.c,this.b);b.subVectors(this.a,this.b);return 0.5*a.cross(b).length()}}(),midpoint:function(a){return(a|| +new THREE.Vector3).addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)},normal:function(a){return THREE.Triangle.normal(this.a,this.b,this.c,a)},plane:function(a){return(a||new THREE.Plane).setFromCoplanarPoints(this.a,this.b,this.c)},barycoordFromPoint:function(a,b){return THREE.Triangle.barycoordFromPoint(a,this.a,this.b,this.c,b)},containsPoint:function(a){return THREE.Triangle.containsPoint(a,this.a,this.b,this.c)},equals:function(a){return a.a.equals(this.a)&&a.b.equals(this.b)&&a.c.equals(this.c)}, +clone:function(){return(new THREE.Triangle).copy(this)}};THREE.Vertex=function(a){console.warn("THREE.Vertex has been DEPRECATED. Use THREE.Vector3 instead.");return a};THREE.UV=function(a,b){console.warn("THREE.UV has been DEPRECATED. Use THREE.Vector2 instead.");return new THREE.Vector2(a,b)};THREE.Clock=function(a){this.autoStart=void 0!==a?a:!0;this.elapsedTime=this.oldTime=this.startTime=0;this.running=!1}; +THREE.Clock.prototype={constructor:THREE.Clock,start:function(){this.oldTime=this.startTime=void 0!==self.performance&&void 0!==self.performance.now?self.performance.now():Date.now();this.running=!0},stop:function(){this.getElapsedTime();this.running=!1},getElapsedTime:function(){this.getDelta();return this.elapsedTime},getDelta:function(){var a=0;this.autoStart&&!this.running&&this.start();if(this.running){var b=void 0!==self.performance&&void 0!==self.performance.now?self.performance.now():Date.now(), +a=0.0010*(b-this.oldTime);this.oldTime=b;this.elapsedTime+=a}return a}};THREE.EventDispatcher=function(){}; +THREE.EventDispatcher.prototype={constructor:THREE.EventDispatcher,apply:function(a){a.addEventListener=THREE.EventDispatcher.prototype.addEventListener;a.hasEventListener=THREE.EventDispatcher.prototype.hasEventListener;a.removeEventListener=THREE.EventDispatcher.prototype.removeEventListener;a.dispatchEvent=THREE.EventDispatcher.prototype.dispatchEvent},addEventListener:function(a,b){void 0===this._listeners&&(this._listeners={});var c=this._listeners;void 0===c[a]&&(c[a]=[]);-1===c[a].indexOf(b)&& +c[a].push(b)},hasEventListener:function(a,b){if(void 0===this._listeners)return!1;var c=this._listeners;return void 0!==c[a]&&-1!==c[a].indexOf(b)?!0:!1},removeEventListener:function(a,b){if(void 0!==this._listeners){var c=this._listeners,d=c[a].indexOf(b);-1!==d&&c[a].splice(d,1)}},dispatchEvent:function(a){if(void 0!==this._listeners){var b=this._listeners[a.type];if(void 0!==b){a.target=this;for(var c=0,d=b.length;cf.scale.x)return t;t.push({distance:q,point:f.position,face:null,object:f})}else if(f instanceof +a.LOD)d.getPositionFromMatrix(f.matrixWorld),q=m.ray.origin.distanceTo(d),k(f.getObjectForDistance(q),m,t);else if(f instanceof a.Mesh){var p=f.geometry;null===p.boundingSphere&&p.computeBoundingSphere();b.copy(p.boundingSphere);b.applyMatrix4(f.matrixWorld);if(!1===m.ray.isIntersectionSphere(b))return t;e.getInverse(f.matrixWorld);c.copy(m.ray).applyMatrix4(e);if(null!==p.boundingBox&&!1===c.isIntersectionBox(p.boundingBox))return t;var r=p.vertices;if(p instanceof a.BufferGeometry){var s=f.material; +if(void 0===s||!1===p.dynamic)return t;var u,w,E=m.precision;if(void 0!==p.attributes.index)for(var r=p.offsets,D=p.attributes.index.array,F=p.attributes.position.array,y=p.offsets.length,x=p.attributes.index.array.length/3,x=0;xm.far)||t.push({distance:q,point:u,face:null,faceIndex:null,object:f}));else{F=p.attributes.position.array;x=p.attributes.position.array.length;for(p=0;pm.far)||t.push({distance:q,point:u,face:null, +faceIndex:null,object:f}))}}else if(p instanceof a.Geometry){D=f.material instanceof a.MeshFaceMaterial;F=!0===D?f.material.materials:null;E=m.precision;y=0;for(x=p.faces.length;ym.far)||t.push({distance:q,point:u,face:z,faceIndex:y,object:f})))}}else if(f instanceof +a.Line){E=m.linePrecision;s=E*E;p=f.geometry;null===p.boundingSphere&&p.computeBoundingSphere();b.copy(p.boundingSphere);b.applyMatrix4(f.matrixWorld);if(!1===m.ray.isIntersectionSphere(b))return t;e.getInverse(f.matrixWorld);c.copy(m.ray).applyMatrix4(e);r=p.vertices;E=r.length;u=new a.Vector3;w=new a.Vector3;x=f.type===a.LineStrip?1:2;for(p=0;ps||(q=c.origin.distanceTo(w),qm.far||t.push({distance:q,point:u.clone().applyMatrix4(f.matrixWorld), +face:null,faceIndex:null,object:f}))}},m=function(a,b,c){for(var a=a.getDescendants(),d=0,e=a.length;de&&0>f||0>h&&0>g)return!1;0>e?c=Math.max(c,e/(e-f)):0>f&&(d=Math.min(d,e/(e-f)));0>h?c=Math.max(c,h/(h-g)):0>g&&(d=Math.min(d,h/(h-g)));if(dg.positionScreen.x||1g.positionScreen.y||1g.positionScreen.z||1(ja.positionScreen.x-V.positionScreen.x)*(P.positionScreen.y-V.positionScreen.y)-(ja.positionScreen.y-V.positionScreen.y)*(P.positionScreen.x-V.positionScreen.x), +Y===THREE.DoubleSide||x===(Y===THREE.FrontSide)){if(n===q){var Ha=new THREE.RenderableFace3;t.push(Ha);q++;n++;l=Ha}else l=t[n++];l.id=T.id;l.v1.copy(V);l.v2.copy(P);l.v3.copy(ja);l.normalModel.copy($.normal);!1===x&&(Y===THREE.BackSide||Y===THREE.DoubleSide)&&l.normalModel.negate();l.normalModel.applyMatrix3(R).normalize();l.normalModelView.copy(l.normalModel).applyMatrix3(J);l.centroidModel.copy($.centroid).applyMatrix4(A);ja=$.vertexNormals;V=0;for(P=Math.min(ja.length,3);Vz.z&&(E===F?(va=new THREE.RenderableParticle,D.push(va),F++,E++,w=va):w=D[E++],w.id=T.id,w.x=z.x*ga,w.y=z.y*ga,w.z=z.z,w.object=T,w.rotation=T.rotation.z,w.scale.x=T.scale.x*Math.abs(w.x-(z.x+f.projectionMatrix.elements[0])/(z.w+f.projectionMatrix.elements[12])), +w.scale.y=T.scale.y*Math.abs(w.y-(z.y+f.projectionMatrix.elements[5])/(z.w+f.projectionMatrix.elements[13])),w.material=T.material,y.elements.push(w)));!0===m&&y.elements.sort(b);return y}};THREE.Face3=function(a,b,c,d,e,f){this.a=a;this.b=b;this.c=c;this.normal=d instanceof THREE.Vector3?d:new THREE.Vector3;this.vertexNormals=d instanceof Array?d:[];this.color=e instanceof THREE.Color?e:new THREE.Color;this.vertexColors=e instanceof Array?e:[];this.vertexTangents=[];this.materialIndex=void 0!==f?f:0;this.centroid=new THREE.Vector3}; +THREE.Face3.prototype={constructor:THREE.Face3,clone:function(){var a=new THREE.Face3(this.a,this.b,this.c);a.normal.copy(this.normal);a.color.copy(this.color);a.centroid.copy(this.centroid);a.materialIndex=this.materialIndex;var b,c;b=0;for(c=this.vertexNormals.length;bd?-1:1,e.vertexTangents[c]=new THREE.Vector4(E.x,E.y,E.z,d)}this.hasTangents=!0},computeLineDistances:function(){for(var a=0,b=this.vertices,c=0,d=b.length;cd;d++)if(e[d]==e[(d+1)%3]){a.push(f);break}}for(f=a.length-1;0<=f;f--){this.faces.splice(f,1);c=0;for(h=this.faceVertexUvs.length;cb.max.x&&(b.max.x=c),db.max.y&&(b.max.y=d),eb.max.z&&(b.max.z=e)}if(void 0===a||0===a.length)this.boundingBox.min.set(0,0,0),this.boundingBox.max.set(0,0,0)},computeBoundingSphere:function(){var a=new THREE.Box3,b=new THREE.Vector3;return function(){null=== +this.boundingSphere&&(this.boundingSphere=new THREE.Sphere);var c=this.attributes.position.array;if(c){for(var d=this.boundingSphere.center,e=0,f=c.length;eQ?-1:1;h[4*a]=J.x;h[4*a+1]=J.y;h[4*a+2]=J.z;h[4*a+3]=N}if(void 0===this.attributes.index||void 0===this.attributes.position||void 0===this.attributes.normal||void 0===this.attributes.uv)console.warn("Missing required attributes (index, position, normal or uv) in BufferGeometry.computeTangents()");else{var b=this.attributes.index.array,c=this.attributes.position.array,d=this.attributes.normal.array,e=this.attributes.uv.array,f=c.length/3;void 0===this.attributes.tangent&&(this.attributes.tangent= +{itemSize:4,array:new Float32Array(4*f)});for(var h=this.attributes.tangent.array,g=[],i=[],k=0;ka.length?".":a.join("/"))+"/"},initMaterials:function(a,b){for(var c=[],d=0;da.opacity)i.transparent=a.transparent;void 0!==a.depthTest&&(i.depthTest=a.depthTest);void 0!==a.depthWrite&&(i.depthWrite=a.depthWrite);void 0!==a.visible&&(i.visible=a.visible);void 0!==a.flipSided&&(i.side=THREE.BackSide); +void 0!==a.doubleSided&&(i.side=THREE.DoubleSide);void 0!==a.wireframe&&(i.wireframe=a.wireframe);void 0!==a.vertexColors&&("face"===a.vertexColors?i.vertexColors=THREE.FaceColors:a.vertexColors&&(i.vertexColors=THREE.VertexColors));a.colorDiffuse?i.color=f(a.colorDiffuse):a.DbgColor&&(i.color=a.DbgColor);a.colorSpecular&&(i.specular=f(a.colorSpecular));a.colorAmbient&&(i.ambient=f(a.colorAmbient));a.transparency&&(i.opacity=a.transparency);a.specularCoef&&(i.shininess=a.specularCoef);a.mapDiffuse&& +b&&e(i,"map",a.mapDiffuse,a.mapDiffuseRepeat,a.mapDiffuseOffset,a.mapDiffuseWrap,a.mapDiffuseAnisotropy);a.mapLight&&b&&e(i,"lightMap",a.mapLight,a.mapLightRepeat,a.mapLightOffset,a.mapLightWrap,a.mapLightAnisotropy);a.mapBump&&b&&e(i,"bumpMap",a.mapBump,a.mapBumpRepeat,a.mapBumpOffset,a.mapBumpWrap,a.mapBumpAnisotropy);a.mapNormal&&b&&e(i,"normalMap",a.mapNormal,a.mapNormalRepeat,a.mapNormalOffset,a.mapNormalWrap,a.mapNormalAnisotropy);a.mapSpecular&&b&&e(i,"specularMap",a.mapSpecular,a.mapSpecularRepeat, +a.mapSpecularOffset,a.mapSpecularWrap,a.mapSpecularAnisotropy);a.mapBumpScale&&(i.bumpScale=a.mapBumpScale);a.mapNormal?(g=THREE.ShaderLib.normalmap,k=THREE.UniformsUtils.clone(g.uniforms),k.tNormal.value=i.normalMap,a.mapNormalFactor&&k.uNormalScale.value.set(a.mapNormalFactor,a.mapNormalFactor),i.map&&(k.tDiffuse.value=i.map,k.enableDiffuse.value=!0),i.specularMap&&(k.tSpecular.value=i.specularMap,k.enableSpecular.value=!0),i.lightMap&&(k.tAO.value=i.lightMap,k.enableAO.value=!0),k.uDiffuseColor.value.setHex(i.color), +k.uSpecularColor.value.setHex(i.specular),k.uAmbientColor.value.setHex(i.ambient),k.uShininess.value=i.shininess,void 0!==i.opacity&&(k.uOpacity.value=i.opacity),g=new THREE.ShaderMaterial({fragmentShader:g.fragmentShader,vertexShader:g.vertexShader,uniforms:k,lights:!0,fog:!0}),i.transparent&&(g.transparent=!0)):g=new THREE[g](i);void 0!==a.DbgName&&(g.name=a.DbgName);return g}};THREE.XHRLoader=function(a){this.manager=void 0!==a?a:THREE.DefaultLoadingManager}; +THREE.XHRLoader.prototype={constructor:THREE.XHRLoader,load:function(a,b,c,d){var e=this,f=new XMLHttpRequest;void 0!==b&&f.addEventListener("load",function(c){b(c.target.responseText);e.manager.itemEnd(a)},!1);void 0!==c&&f.addEventListener("progress",function(a){c(a)},!1);void 0!==d&&f.addEventListener("error",function(a){d(a)},!1);void 0!==this.crossOrigin&&(f.crossOrigin=this.crossOrigin);f.open("GET",a,!0);f.send(null);e.manager.itemStart(a)},setCrossOrigin:function(a){this.crossOrigin=a}};THREE.ImageLoader=function(a){this.manager=void 0!==a?a:THREE.DefaultLoadingManager}; +THREE.ImageLoader.prototype={constructor:THREE.ImageLoader,load:function(a,b,c,d){var e=this,f=document.createElement("img");void 0!==b&&f.addEventListener("load",function(){e.manager.itemEnd(a);b(this)},!1);void 0!==c&&f.addEventListener("progress",function(a){c(a)},!1);void 0!==d&&f.addEventListener("error",function(a){d(a)},!1);void 0!==this.crossOrigin&&(f.crossOrigin=this.crossOrigin);f.src=a;e.manager.itemStart(a)},setCrossOrigin:function(a){this.crossOrigin=a}};THREE.JSONLoader=function(a){THREE.Loader.call(this,a);this.withCredentials=!1};THREE.JSONLoader.prototype=Object.create(THREE.Loader.prototype);THREE.JSONLoader.prototype.load=function(a,b,c){c=c&&"string"===typeof c?c:this.extractUrlBase(a);this.onLoadStart();this.loadAjaxJSON(this,a,b,c)}; +THREE.JSONLoader.prototype.loadAjaxJSON=function(a,b,c,d,e){var f=new XMLHttpRequest,h=0;f.onreadystatechange=function(){if(f.readyState===f.DONE)if(200===f.status||0===f.status){if(f.responseText){var g=JSON.parse(f.responseText),g=a.parse(g,d);c(g.geometry,g.materials)}else console.warn("THREE.JSONLoader: ["+b+"] seems to be unreachable or file there is empty");a.onLoadComplete()}else console.error("THREE.JSONLoader: Couldn't load ["+b+"] ["+f.status+"]");else f.readyState===f.LOADING?e&&(0===h&& +(h=f.getResponseHeader("Content-Length")),e({total:h,loaded:f.responseText.length})):f.readyState===f.HEADERS_RECEIVED&&void 0!==e&&(h=f.getResponseHeader("Content-Length"))};f.open("GET",b,!0);f.withCredentials=this.withCredentials;f.send(null)}; +THREE.JSONLoader.prototype.parse=function(a,b){var c=new THREE.Geometry,d=void 0!==a.scale?1/a.scale:1,e,f,h,g,i,k,m,l,n,t,q,p,r,s,u=a.faces;n=a.vertices;var w=a.normals,E=a.colors,D=0;if(void 0!==a.uvs){for(e=0;ef;f++)l=u[g++],s=r[2*l],l=r[2*l+1],s=new THREE.Vector2(s,l),2!==f&&c.faceVertexUvs[e][h].push(s),0!==f&&c.faceVertexUvs[e][h+1].push(s)}m&&(m=3*u[g++],t.normal.set(w[m++],w[m++],w[m]),p.normal.copy(t.normal));if(q)for(e=0;4>e;e++)m=3*u[g++],q=new THREE.Vector3(w[m++], +w[m++],w[m]),2!==e&&t.vertexNormals.push(q),0!==e&&p.vertexNormals.push(q);k&&(k=u[g++],k=E[k],t.color.setHex(k),p.color.setHex(k));if(n)for(e=0;4>e;e++)k=u[g++],k=E[k],2!==e&&t.vertexColors.push(new THREE.Color(k)),0!==e&&p.vertexColors.push(new THREE.Color(k));c.faces.push(t);c.faces.push(p)}else{t=new THREE.Face3;t.a=u[g++];t.b=u[g++];t.c=u[g++];h&&(h=u[g++],t.materialIndex=h);h=c.faces.length;if(e)for(e=0;ef;f++)l=u[g++],s=r[2*l],l=r[2*l+1], +s=new THREE.Vector2(s,l),c.faceVertexUvs[e][h].push(s)}m&&(m=3*u[g++],t.normal.set(w[m++],w[m++],w[m]));if(q)for(e=0;3>e;e++)m=3*u[g++],q=new THREE.Vector3(w[m++],w[m++],w[m]),t.vertexNormals.push(q);k&&(k=u[g++],t.color.setHex(E[k]));if(n)for(e=0;3>e;e++)k=u[g++],t.vertexColors.push(new THREE.Color(E[k]));c.faces.push(t)}if(a.skinWeights){g=0;for(i=a.skinWeights.length;gG.parameters.opacity&&(G.parameters.transparent=!0);G.parameters.normalMap?(C=THREE.ShaderLib.normalmap,v=THREE.UniformsUtils.clone(C.uniforms), +s=G.parameters.color,A=G.parameters.specular,r=G.parameters.ambient,I=G.parameters.shininess,v.tNormal.value=z.textures[G.parameters.normalMap],G.parameters.normalScale&&v.uNormalScale.value.set(G.parameters.normalScale[0],G.parameters.normalScale[1]),G.parameters.map&&(v.tDiffuse.value=G.parameters.map,v.enableDiffuse.value=!0),G.parameters.envMap&&(v.tCube.value=G.parameters.envMap,v.enableReflection.value=!0,v.uReflectivity.value=G.parameters.reflectivity),G.parameters.lightMap&&(v.tAO.value=G.parameters.lightMap, +v.enableAO.value=!0),G.parameters.specularMap&&(v.tSpecular.value=z.textures[G.parameters.specularMap],v.enableSpecular.value=!0),G.parameters.displacementMap&&(v.tDisplacement.value=z.textures[G.parameters.displacementMap],v.enableDisplacement.value=!0,v.uDisplacementBias.value=G.parameters.displacementBias,v.uDisplacementScale.value=G.parameters.displacementScale),v.uDiffuseColor.value.setHex(s),v.uSpecularColor.value.setHex(A),v.uAmbientColor.value.setHex(r),v.uShininess.value=I,G.parameters.opacity&& +(v.uOpacity.value=G.parameters.opacity),q=new THREE.ShaderMaterial({fragmentShader:C.fragmentShader,vertexShader:C.vertexShader,uniforms:v,lights:!0,fog:!0})):q=new THREE[G.type](G.parameters);q.name=R;z.materials[R]=q}for(R in B.materials)if(G=B.materials[R],G.parameters.materials){J=[];for(s=0;sg.end&&(g.end=e);b||(b=h)}}a.firstAnimation=b}; +THREE.MorphAnimMesh.prototype.setAnimationLabel=function(a,b,c){this.geometry.animations||(this.geometry.animations={});this.geometry.animations[a]={start:b,end:c}};THREE.MorphAnimMesh.prototype.playAnimation=function(a,b){var c=this.geometry.animations[a];c?(this.setFrameRange(c.start,c.end),this.duration=1E3*((c.end-c.start)/b),this.time=0):console.warn("animation["+a+"] undefined")}; +THREE.MorphAnimMesh.prototype.updateAnimation=function(a){var b=this.duration/this.length;this.time+=this.direction*a;if(this.mirroredLoop){if(this.time>this.duration||0>this.time)this.direction*=-1,this.time>this.duration&&(this.time=this.duration,this.directionBackwards=!0),0>this.time&&(this.time=0,this.directionBackwards=!1)}else this.time%=this.duration,0>this.time&&(this.time+=this.duration);a=this.startKeyframe+THREE.Math.clamp(Math.floor(this.time/b),0,this.length-1);a!==this.currentKeyframe&& +(this.morphTargetInfluences[this.lastKeyframe]=0,this.morphTargetInfluences[this.currentKeyframe]=1,this.morphTargetInfluences[a]=0,this.lastKeyframe=this.currentKeyframe,this.currentKeyframe=a);b=this.time%b/b;this.directionBackwards&&(b=1-b);this.morphTargetInfluences[this.currentKeyframe]=b;this.morphTargetInfluences[this.lastKeyframe]=1-b}; +THREE.MorphAnimMesh.prototype.clone=function(a){void 0===a&&(a=new THREE.MorphAnimMesh(this.geometry,this.material));a.duration=this.duration;a.mirroredLoop=this.mirroredLoop;a.time=this.time;a.lastKeyframe=this.lastKeyframe;a.currentKeyframe=this.currentKeyframe;a.direction=this.direction;a.directionBackwards=this.directionBackwards;THREE.Mesh.prototype.clone.call(this,a);return a};THREE.Ribbon=function(a,b){THREE.Object3D.call(this);this.geometry=a;this.material=b};THREE.Ribbon.prototype=Object.create(THREE.Object3D.prototype);THREE.Ribbon.prototype.clone=function(a){void 0===a&&(a=new THREE.Ribbon(this.geometry,this.material));THREE.Object3D.prototype.clone.call(this,a);return a};THREE.LOD=function(){THREE.Object3D.call(this);this.objects=[]};THREE.LOD.prototype=Object.create(THREE.Object3D.prototype);THREE.LOD.prototype.addLevel=function(a,b){void 0===b&&(b=0);for(var b=Math.abs(b),c=0;c=this.objects[d].distance)this.objects[d-1].object.visible=!1,this.objects[d].object.visible=!0;else break;for(;d=g||(g*=f.intensity,c.add(Pa.multiplyScalar(g)))}else f instanceof THREE.PointLight&&(h=wa.getPositionFromMatrix(f.matrixWorld),g=b.dot(wa.subVectors(h,a).normalize()),0>=g||(g*=0==f.distance?1:1-Math.min(a.distanceTo(h)/f.distance,1),0!=g&&(g*=f.intensity,c.add(Pa.multiplyScalar(g)))))}} +function c(a,b,c,d){m(b);l(c);n(d);t(a.getStyle());B.stroke();ta.expandByScalar(2*b)}function d(a){q(a.getStyle());B.fill()}function e(a,b,c,e,f,h,g,j,i,k,m,l,n){if(!(n instanceof THREE.DataTexture||void 0===n.image||0==n.image.width)){if(!0===n.needsUpdate){var p=n.wrapS==THREE.RepeatWrapping,t=n.wrapT==THREE.RepeatWrapping;Ja[n.id]=B.createPattern(n.image,!0===p&&!0===t?"repeat":!0===p&&!1===t?"repeat-x":!1===p&&!0===t?"repeat-y":"no-repeat");n.needsUpdate=!1}void 0===Ja[n.id]?q("rgba(0,0,0,1)"): +q(Ja[n.id]);var p=n.offset.x/n.repeat.x,t=n.offset.y/n.repeat.y,r=n.image.width*n.repeat.x,s=n.image.height*n.repeat.y,g=(g+p)*r,j=(1-j+t)*s,c=c-a,e=e-b,f=f-a,h=h-b,i=(i+p)*r-g,k=(1-k+t)*s-j,m=(m+p)*r-g,l=(1-l+t)*s-j,p=i*l-m*k;0===p?(void 0===ga[n.id]&&(b=document.createElement("canvas"),b.width=n.image.width,b.height=n.image.height,b=b.getContext("2d"),b.drawImage(n.image,0,0),ga[n.id]=b.getImageData(0,0,n.image.width,n.image.height).data),b=ga[n.id],g=4*(Math.floor(g)+Math.floor(j)*n.image.width), +V.setRGB(b[g]/255,b[g+1]/255,b[g+2]/255),d(V)):(p=1/p,n=(l*c-k*f)*p,k=(l*e-k*h)*p,c=(i*f-m*c)*p,e=(i*h-m*e)*p,a=a-n*g-c*j,g=b-k*g-e*j,B.save(),B.transform(n,k,c,e,a,g),B.fill(),B.restore())}}function f(a,b,c,d,e,f,h,g,j,i,k,m,l){var n,p;n=l.width-1;p=l.height-1;h*=n;g*=p;c-=a;d-=b;e-=a;f-=b;j=j*n-h;i=i*p-g;k=k*n-h;m=m*p-g;p=1/(j*m-k*i);n=(m*c-i*e)*p;i=(m*d-i*f)*p;c=(j*e-k*c)*p;d=(j*f-k*d)*p;a=a-n*h-c*g;b=b-i*h-d*g;B.save();B.transform(n,i,c,d,a,b);B.clip();B.drawImage(l,0,0);B.restore()}function h(a, +b,c,d){xa[0]=255*a.r|0;xa[1]=255*a.g|0;xa[2]=255*a.b|0;xa[4]=255*b.r|0;xa[5]=255*b.g|0;xa[6]=255*b.b|0;xa[8]=255*c.r|0;xa[9]=255*c.g|0;xa[10]=255*c.b|0;xa[12]=255*d.r|0;xa[13]=255*d.g|0;xa[14]=255*d.b|0;j.putImageData(Ra,0,0);Ia.drawImage(Sa,0,0);return ya}function g(a,b,c){var d=b.x-a.x,e=b.y-a.y,f=d*d+e*e;0!==f&&(c/=Math.sqrt(f),d*=c,e*=c,b.x+=d,b.y+=e,a.x-=d,a.y-=e)}function i(a){v!==a&&(v=B.globalAlpha=a)}function k(a){A!==a&&(a===THREE.NormalBlending?B.globalCompositeOperation="source-over": +a===THREE.AdditiveBlending?B.globalCompositeOperation="lighter":a===THREE.SubtractiveBlending&&(B.globalCompositeOperation="darker"),A=a)}function m(a){J!==a&&(J=B.lineWidth=a)}function l(a){ca!==a&&(ca=B.lineCap=a)}function n(a){qa!==a&&(qa=B.lineJoin=a)}function t(a){G!==a&&(G=B.strokeStyle=a)}function q(a){R!==a&&(R=B.fillStyle=a)}function p(a,b){if(ra!==a||N!==b)B.setLineDash([a,b]),ra=a,N=b}console.log("THREE.CanvasRenderer",THREE.REVISION);var r=THREE.Math.smoothstep,a=a||{},s=this,u,w,E,D= +new THREE.Projector,F=void 0!==a.canvas?a.canvas:document.createElement("canvas"),y,x,z,O,B=F.getContext("2d"),C=new THREE.Color(0),I=0,v=1,A=0,G=null,R=null,J=null,ca=null,qa=null,ra=null,N=0,M,Q,K,ea;new THREE.RenderableVertex;new THREE.RenderableVertex;var Da,Fa,ba,Ea,$,fa,V=new THREE.Color,P=new THREE.Color,Y=new THREE.Color,T=new THREE.Color,ma=new THREE.Color,va=new THREE.Color,ja=new THREE.Color,Pa=new THREE.Color,Ja={},ga={},Ha,Xa,Ta,za,hb,ib,tb,ub,vb,jb,Ka=new THREE.Box2,na=new THREE.Box2, +ta=new THREE.Box2,kb=new THREE.Color,ua=new THREE.Color,ha=new THREE.Color,wa=new THREE.Vector3,Sa,j,Ra,xa,ya,Ia,Ua=16;Sa=document.createElement("canvas");Sa.width=Sa.height=2;j=Sa.getContext("2d");j.fillStyle="rgba(0,0,0,1)";j.fillRect(0,0,2,2);Ra=j.getImageData(0,0,2,2);xa=Ra.data;ya=document.createElement("canvas");ya.width=ya.height=Ua;Ia=ya.getContext("2d");Ia.translate(-Ua/2,-Ua/2);Ia.scale(Ua,Ua);Ua--;void 0===B.setLineDash&&(B.setLineDash=void 0!==B.mozDash?function(a){B.mozDash=null!==a[0]? +a:null}:function(){});this.domElement=F;this.devicePixelRatio=void 0!==a.devicePixelRatio?a.devicePixelRatio:void 0!==window.devicePixelRatio?window.devicePixelRatio:1;this.sortElements=this.sortObjects=this.autoClear=!0;this.info={render:{vertices:0,faces:0}};this.supportsVertexTextures=function(){};this.setFaceCulling=function(){};this.setSize=function(a,b,c){y=a*this.devicePixelRatio;x=b*this.devicePixelRatio;z=Math.floor(y/2);O=Math.floor(x/2);F.width=y;F.height=x;1!==this.devicePixelRatio&&!1!== +c&&(F.style.width=a+"px",F.style.height=b+"px");Ka.set(new THREE.Vector2(-z,-O),new THREE.Vector2(z,O));na.set(new THREE.Vector2(-z,-O),new THREE.Vector2(z,O));v=1;A=0;qa=ca=J=R=G=null};this.setClearColor=function(a,b){C.set(a);I=void 0!==b?b:1;na.set(new THREE.Vector2(-z,-O),new THREE.Vector2(z,O))};this.setClearColorHex=function(a,b){console.warn("DEPRECATED: .setClearColorHex() is being removed. Use .setClearColor() instead.");this.setClearColor(a,b)};this.getMaxAnisotropy=function(){return 0}; +this.clear=function(){B.setTransform(1,0,0,-1,z,O);!1===na.empty()&&(na.intersect(Ka),na.expandByScalar(2),1>I&&B.clearRect(na.min.x|0,na.min.y|0,na.max.x-na.min.x|0,na.max.y-na.min.y|0),0>1,ca=I.height>>1,N=F.scale.x*z,C=F.scale.y*O,v=N*R,A=C*ca,ta.min.set(y.x-v,y.y-A),ta.max.set(y.x+v,y.y+A),!1===Ka.isIntersectionBox(ta)?ta.makeEmpty():(B.save(),B.translate(y.x,y.y),B.rotate(-F.rotation),B.scale(N,-C),B.translate(-R,-ca),B.drawImage(I,0,0),B.restore())):J instanceof THREE.ParticleCanvasMaterial&&(v=F.scale.x*z,A=F.scale.y*O,ta.min.set(y.x-v,y.y-A),ta.max.set(y.x+v,y.y+A),!1===Ka.isIntersectionBox(ta)?ta.makeEmpty():(t(J.color.getStyle()), +q(J.color.getStyle()),B.save(),B.translate(y.x,y.y),B.rotate(-F.rotation),B.scale(v,A),J.program(B),B.restore()))}else if(v instanceof THREE.RenderableLine){if(Q=v.v1,K=v.v2,Q.positionScreen.x*=z,Q.positionScreen.y*=O,K.positionScreen.x*=z,K.positionScreen.y*=O,ta.setFromPoints([Q.positionScreen,K.positionScreen]),!0===Ka.isIntersectionBox(ta))if(y=Q,F=K,J=v,v=A,i(v.opacity),k(v.blending),B.beginPath(),B.moveTo(y.positionScreen.x,y.positionScreen.y),B.lineTo(F.positionScreen.x,F.positionScreen.y), +v instanceof THREE.LineBasicMaterial){m(v.linewidth);l(v.linecap);n(v.linejoin);if(v.vertexColors!==THREE.VertexColors)t(v.color.getStyle());else if(A=J.vertexColors[0].getStyle(),J=J.vertexColors[1].getStyle(),A===J)t(A);else{try{var ga=B.createLinearGradient(y.positionScreen.x,y.positionScreen.y,F.positionScreen.x,F.positionScreen.y);ga.addColorStop(0,A);ga.addColorStop(1,J)}catch(qa){ga=A}t(ga)}B.stroke();ta.expandByScalar(2*v.linewidth)}else v instanceof THREE.LineDashedMaterial&&(m(v.linewidth), +l(v.linecap),n(v.linejoin),t(v.color.getStyle()),p(v.dashSize,v.gapSize),B.stroke(),ta.expandByScalar(2*v.linewidth),p(null,null))}else if(v instanceof THREE.RenderableFace3){Q=v.v1;K=v.v2;ea=v.v3;if(-1>Q.positionScreen.z||1K.positionScreen.z||1ea.positionScreen.z||1 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif", +lights_lambert_vertex:"vLightFront = vec3( 0.0 );\n#ifdef DOUBLE_SIDED\nvLightBack = vec3( 0.0 );\n#endif\ntransformedNormal = normalize( transformedNormal );\n#if MAX_DIR_LIGHTS > 0\nfor( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( transformedNormal, dirVector );\nvec3 directionalLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 directionalLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 directionalLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 directionalLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\ndirectionalLightWeighting = mix( directionalLightWeighting, directionalLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\ndirectionalLightWeightingBack = mix( directionalLightWeightingBack, directionalLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += directionalLightColor[ i ] * directionalLightWeighting;\n#ifdef DOUBLE_SIDED\nvLightBack += directionalLightColor[ i ] * directionalLightWeightingBack;\n#endif\n}\n#endif\n#if MAX_POINT_LIGHTS > 0\nfor( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\nfloat dotProduct = dot( transformedNormal, lVector );\nvec3 pointLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 pointLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 pointLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 pointLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\npointLightWeighting = mix( pointLightWeighting, pointLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\npointLightWeightingBack = mix( pointLightWeightingBack, pointLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += pointLightColor[ i ] * pointLightWeighting * lDistance;\n#ifdef DOUBLE_SIDED\nvLightBack += pointLightColor[ i ] * pointLightWeightingBack * lDistance;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nfor( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - worldPosition.xyz ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\nfloat dotProduct = dot( transformedNormal, lVector );\nvec3 spotLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 spotLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 spotLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 spotLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\nspotLightWeighting = mix( spotLightWeighting, spotLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\nspotLightWeightingBack = mix( spotLightWeightingBack, spotLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += spotLightColor[ i ] * spotLightWeighting * lDistance * spotEffect;\n#ifdef DOUBLE_SIDED\nvLightBack += spotLightColor[ i ] * spotLightWeightingBack * lDistance * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( transformedNormal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nfloat hemiDiffuseWeightBack = -0.5 * dotProduct + 0.5;\nvLightFront += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\n#ifdef DOUBLE_SIDED\nvLightBack += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeightBack );\n#endif\n}\n#endif\nvLightFront = vLightFront * diffuse + ambient * ambientLightColor + emissive;\n#ifdef DOUBLE_SIDED\nvLightBack = vLightBack * diffuse + ambient * ambientLightColor + emissive;\n#endif", +lights_phong_pars_vertex:"#ifndef PHONG_PER_PIXEL\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\nvarying vec4 vPointLight[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\nvarying vec4 vSpotLight[ MAX_SPOT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvarying vec3 vWorldPosition;\n#endif", +lights_phong_vertex:"#ifndef PHONG_PER_PIXEL\n#if MAX_POINT_LIGHTS > 0\nfor( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nvPointLight[ i ] = vec4( lVector, lDistance );\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nfor( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nvSpotLight[ i ] = vec4( lVector, lDistance );\n}\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvWorldPosition = worldPosition.xyz;\n#endif", +lights_phong_pars_fragment:"uniform vec3 ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\n#ifdef PHONG_PER_PIXEL\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#else\nvarying vec4 vPointLight[ MAX_POINT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n#ifdef PHONG_PER_PIXEL\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n#else\nvarying vec4 vSpotLight[ MAX_SPOT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvarying vec3 vWorldPosition;\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif\nvarying vec3 vViewPosition;\nvarying vec3 vNormal;", +lights_phong_fragment:"vec3 normal = normalize( vNormal );\nvec3 viewPosition = normalize( vViewPosition );\n#ifdef DOUBLE_SIDED\nnormal = normal * ( -1.0 + 2.0 * float( gl_FrontFacing ) );\n#endif\n#ifdef USE_NORMALMAP\nnormal = perturbNormal2Arb( -vViewPosition, normal );\n#elif defined( USE_BUMPMAP )\nnormal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );\n#endif\n#if MAX_POINT_LIGHTS > 0\nvec3 pointDiffuse = vec3( 0.0 );\nvec3 pointSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\n#ifdef PHONG_PER_PIXEL\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz + vViewPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\n#else\nvec3 lVector = normalize( vPointLight[ i ].xyz );\nfloat lDistance = vPointLight[ i ].w;\n#endif\nfloat dotProduct = dot( normal, lVector );\n#ifdef WRAP_AROUND\nfloat pointDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat pointDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 pointDiffuseWeight = mix( vec3 ( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB );\n#else\nfloat pointDiffuseWeight = max( dotProduct, 0.0 );\n#endif\npointDiffuse += diffuse * pointLightColor[ i ] * pointDiffuseWeight * lDistance;\nvec3 pointHalfVector = normalize( lVector + viewPosition );\nfloat pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );\nfloat pointSpecularWeight = specularStrength * max( pow( pointDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, pointHalfVector ), 5.0 );\npointSpecular += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * lDistance * specularNormalization;\n#else\npointSpecular += specular * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * lDistance;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nvec3 spotDiffuse = vec3( 0.0 );\nvec3 spotSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\n#ifdef PHONG_PER_PIXEL\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz + vViewPosition.xyz;\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\n#else\nvec3 lVector = normalize( vSpotLight[ i ].xyz );\nfloat lDistance = vSpotLight[ i ].w;\n#endif\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - vWorldPosition ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\nfloat dotProduct = dot( normal, lVector );\n#ifdef WRAP_AROUND\nfloat spotDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat spotDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 spotDiffuseWeight = mix( vec3 ( spotDiffuseWeightFull ), vec3( spotDiffuseWeightHalf ), wrapRGB );\n#else\nfloat spotDiffuseWeight = max( dotProduct, 0.0 );\n#endif\nspotDiffuse += diffuse * spotLightColor[ i ] * spotDiffuseWeight * lDistance * spotEffect;\nvec3 spotHalfVector = normalize( lVector + viewPosition );\nfloat spotDotNormalHalf = max( dot( normal, spotHalfVector ), 0.0 );\nfloat spotSpecularWeight = specularStrength * max( pow( spotDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, spotHalfVector ), 5.0 );\nspotSpecular += schlick * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * lDistance * specularNormalization * spotEffect;\n#else\nspotSpecular += specular * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * lDistance * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_DIR_LIGHTS > 0\nvec3 dirDiffuse = vec3( 0.0 );\nvec3 dirSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, dirVector );\n#ifdef WRAP_AROUND\nfloat dirDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat dirDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 dirDiffuseWeight = mix( vec3( dirDiffuseWeightFull ), vec3( dirDiffuseWeightHalf ), wrapRGB );\n#else\nfloat dirDiffuseWeight = max( dotProduct, 0.0 );\n#endif\ndirDiffuse += diffuse * directionalLightColor[ i ] * dirDiffuseWeight;\nvec3 dirHalfVector = normalize( dirVector + viewPosition );\nfloat dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );\nfloat dirSpecularWeight = specularStrength * max( pow( dirDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( dirVector, dirHalfVector ), 5.0 );\ndirSpecular += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization;\n#else\ndirSpecular += specular * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nvec3 hemiDiffuse = vec3( 0.0 );\nvec3 hemiSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nvec3 hemiColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\nhemiDiffuse += diffuse * hemiColor;\nvec3 hemiHalfVectorSky = normalize( lVector + viewPosition );\nfloat hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;\nfloat hemiSpecularWeightSky = specularStrength * max( pow( hemiDotNormalHalfSky, shininess ), 0.0 );\nvec3 lVectorGround = -lVector;\nvec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );\nfloat hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;\nfloat hemiSpecularWeightGround = specularStrength * max( pow( hemiDotNormalHalfGround, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat dotProductGround = dot( normal, lVectorGround );\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlickSky = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, hemiHalfVectorSky ), 5.0 );\nvec3 schlickGround = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVectorGround, hemiHalfVectorGround ), 5.0 );\nhemiSpecular += hemiColor * specularNormalization * ( schlickSky * hemiSpecularWeightSky * max( dotProduct, 0.0 ) + schlickGround * hemiSpecularWeightGround * max( dotProductGround, 0.0 ) );\n#else\nhemiSpecular += specular * hemiColor * ( hemiSpecularWeightSky + hemiSpecularWeightGround ) * hemiDiffuseWeight;\n#endif\n}\n#endif\nvec3 totalDiffuse = vec3( 0.0 );\nvec3 totalSpecular = vec3( 0.0 );\n#if MAX_DIR_LIGHTS > 0\ntotalDiffuse += dirDiffuse;\ntotalSpecular += dirSpecular;\n#endif\n#if MAX_HEMI_LIGHTS > 0\ntotalDiffuse += hemiDiffuse;\ntotalSpecular += hemiSpecular;\n#endif\n#if MAX_POINT_LIGHTS > 0\ntotalDiffuse += pointDiffuse;\ntotalSpecular += pointSpecular;\n#endif\n#if MAX_SPOT_LIGHTS > 0\ntotalDiffuse += spotDiffuse;\ntotalSpecular += spotSpecular;\n#endif\n#ifdef METAL\ngl_FragColor.xyz = gl_FragColor.xyz * ( emissive + totalDiffuse + ambientLightColor * ambient + totalSpecular );\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * ( emissive + totalDiffuse + ambientLightColor * ambient ) + totalSpecular;\n#endif", +color_pars_fragment:"#ifdef USE_COLOR\nvarying vec3 vColor;\n#endif",color_fragment:"#ifdef USE_COLOR\ngl_FragColor = gl_FragColor * vec4( vColor, opacity );\n#endif",color_pars_vertex:"#ifdef USE_COLOR\nvarying vec3 vColor;\n#endif",color_vertex:"#ifdef USE_COLOR\n#ifdef GAMMA_INPUT\nvColor = color * color;\n#else\nvColor = color;\n#endif\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n#ifdef BONE_TEXTURE\nuniform sampler2D boneTexture;\nmat4 getBoneMatrix( const in float i ) {\nfloat j = i * 4.0;\nfloat x = mod( j, N_BONE_PIXEL_X );\nfloat y = floor( j / N_BONE_PIXEL_X );\nconst float dx = 1.0 / N_BONE_PIXEL_X;\nconst float dy = 1.0 / N_BONE_PIXEL_Y;\ny = dy * ( y + 0.5 );\nvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\nvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\nvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\nvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\nmat4 bone = mat4( v1, v2, v3, v4 );\nreturn bone;\n}\n#else\nuniform mat4 boneGlobalMatrices[ MAX_BONES ];\nmat4 getBoneMatrix( const in float i ) {\nmat4 bone = boneGlobalMatrices[ int(i) ];\nreturn bone;\n}\n#endif\n#endif", +skinbase_vertex:"#ifdef USE_SKINNING\nmat4 boneMatX = getBoneMatrix( skinIndex.x );\nmat4 boneMatY = getBoneMatrix( skinIndex.y );\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n#ifdef USE_MORPHTARGETS\nvec4 skinVertex = vec4( morphed, 1.0 );\n#else\nvec4 skinVertex = vec4( position, 1.0 );\n#endif\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n#ifndef USE_MORPHNORMALS\nuniform float morphTargetInfluences[ 8 ];\n#else\nuniform float morphTargetInfluences[ 4 ];\n#endif\n#endif", +morphtarget_vertex:"#ifdef USE_MORPHTARGETS\nvec3 morphed = vec3( 0.0 );\nmorphed += ( morphTarget0 - position ) * morphTargetInfluences[ 0 ];\nmorphed += ( morphTarget1 - position ) * morphTargetInfluences[ 1 ];\nmorphed += ( morphTarget2 - position ) * morphTargetInfluences[ 2 ];\nmorphed += ( morphTarget3 - position ) * morphTargetInfluences[ 3 ];\n#ifndef USE_MORPHNORMALS\nmorphed += ( morphTarget4 - position ) * morphTargetInfluences[ 4 ];\nmorphed += ( morphTarget5 - position ) * morphTargetInfluences[ 5 ];\nmorphed += ( morphTarget6 - position ) * morphTargetInfluences[ 6 ];\nmorphed += ( morphTarget7 - position ) * morphTargetInfluences[ 7 ];\n#endif\nmorphed += position;\n#endif", +default_vertex:"vec4 mvPosition;\n#ifdef USE_SKINNING\nmvPosition = modelViewMatrix * skinned;\n#endif\n#if !defined( USE_SKINNING ) && defined( USE_MORPHTARGETS )\nmvPosition = modelViewMatrix * vec4( morphed, 1.0 );\n#endif\n#if !defined( USE_SKINNING ) && ! defined( USE_MORPHTARGETS )\nmvPosition = modelViewMatrix * vec4( position, 1.0 );\n#endif\ngl_Position = projectionMatrix * mvPosition;",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\nvec3 morphedNormal = vec3( 0.0 );\nmorphedNormal += ( morphNormal0 - normal ) * morphTargetInfluences[ 0 ];\nmorphedNormal += ( morphNormal1 - normal ) * morphTargetInfluences[ 1 ];\nmorphedNormal += ( morphNormal2 - normal ) * morphTargetInfluences[ 2 ];\nmorphedNormal += ( morphNormal3 - normal ) * morphTargetInfluences[ 3 ];\nmorphedNormal += normal;\n#endif", +skinnormal_vertex:"#ifdef USE_SKINNING\nmat4 skinMatrix = skinWeight.x * boneMatX;\nskinMatrix \t+= skinWeight.y * boneMatY;\n#ifdef USE_MORPHNORMALS\nvec4 skinnedNormal = skinMatrix * vec4( morphedNormal, 0.0 );\n#else\nvec4 skinnedNormal = skinMatrix * vec4( normal, 0.0 );\n#endif\n#endif",defaultnormal_vertex:"vec3 objectNormal;\n#ifdef USE_SKINNING\nobjectNormal = skinnedNormal.xyz;\n#endif\n#if !defined( USE_SKINNING ) && defined( USE_MORPHNORMALS )\nobjectNormal = morphedNormal;\n#endif\n#if !defined( USE_SKINNING ) && ! defined( USE_MORPHNORMALS )\nobjectNormal = normal;\n#endif\n#ifdef FLIP_SIDED\nobjectNormal = -objectNormal;\n#endif\nvec3 transformedNormal = normalMatrix * objectNormal;", +shadowmap_pars_fragment:"#ifdef USE_SHADOWMAP\nuniform sampler2D shadowMap[ MAX_SHADOWS ];\nuniform vec2 shadowMapSize[ MAX_SHADOWS ];\nuniform float shadowDarkness[ MAX_SHADOWS ];\nuniform float shadowBias[ MAX_SHADOWS ];\nvarying vec4 vShadowCoord[ MAX_SHADOWS ];\nfloat unpackDepth( const in vec4 rgba_depth ) {\nconst vec4 bit_shift = vec4( 1.0 / ( 256.0 * 256.0 * 256.0 ), 1.0 / ( 256.0 * 256.0 ), 1.0 / 256.0, 1.0 );\nfloat depth = dot( rgba_depth, bit_shift );\nreturn depth;\n}\n#endif",shadowmap_fragment:"#ifdef USE_SHADOWMAP\n#ifdef SHADOWMAP_DEBUG\nvec3 frustumColors[3];\nfrustumColors[0] = vec3( 1.0, 0.5, 0.0 );\nfrustumColors[1] = vec3( 0.0, 1.0, 0.8 );\nfrustumColors[2] = vec3( 0.0, 0.5, 1.0 );\n#endif\n#ifdef SHADOWMAP_CASCADE\nint inFrustumCount = 0;\n#endif\nfloat fDepth;\nvec3 shadowColor = vec3( 1.0 );\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvec3 shadowCoord = vShadowCoord[ i ].xyz / vShadowCoord[ i ].w;\nbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\nbool inFrustum = all( inFrustumVec );\n#ifdef SHADOWMAP_CASCADE\ninFrustumCount += int( inFrustum );\nbvec3 frustumTestVec = bvec3( inFrustum, inFrustumCount == 1, shadowCoord.z <= 1.0 );\n#else\nbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n#endif\nbool frustumTest = all( frustumTestVec );\nif ( frustumTest ) {\nshadowCoord.z += shadowBias[ i ];\n#if defined( SHADOWMAP_TYPE_PCF )\nfloat shadow = 0.0;\nconst float shadowDelta = 1.0 / 9.0;\nfloat xPixelOffset = 1.0 / shadowMapSize[ i ].x;\nfloat yPixelOffset = 1.0 / shadowMapSize[ i ].y;\nfloat dx0 = -1.25 * xPixelOffset;\nfloat dy0 = -1.25 * yPixelOffset;\nfloat dx1 = 1.25 * xPixelOffset;\nfloat dy1 = 1.25 * yPixelOffset;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nshadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\nfloat shadow = 0.0;\nfloat xPixelOffset = 1.0 / shadowMapSize[ i ].x;\nfloat yPixelOffset = 1.0 / shadowMapSize[ i ].y;\nfloat dx0 = -1.0 * xPixelOffset;\nfloat dy0 = -1.0 * yPixelOffset;\nfloat dx1 = 1.0 * xPixelOffset;\nfloat dy1 = 1.0 * yPixelOffset;\nmat3 shadowKernel;\nmat3 depthKernel;\ndepthKernel[0][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\ndepthKernel[0][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\ndepthKernel[0][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy1 ) ) );\ndepthKernel[1][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\ndepthKernel[1][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\ndepthKernel[1][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\ndepthKernel[2][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\ndepthKernel[2][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\ndepthKernel[2][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\nvec3 shadowZ = vec3( shadowCoord.z );\nshadowKernel[0] = vec3(lessThan(depthKernel[0], shadowZ ));\nshadowKernel[0] *= vec3(0.25);\nshadowKernel[1] = vec3(lessThan(depthKernel[1], shadowZ ));\nshadowKernel[1] *= vec3(0.25);\nshadowKernel[2] = vec3(lessThan(depthKernel[2], shadowZ ));\nshadowKernel[2] *= vec3(0.25);\nvec2 fractionalCoord = 1.0 - fract( shadowCoord.xy * shadowMapSize[i].xy );\nshadowKernel[0] = mix( shadowKernel[1], shadowKernel[0], fractionalCoord.x );\nshadowKernel[1] = mix( shadowKernel[2], shadowKernel[1], fractionalCoord.x );\nvec4 shadowValues;\nshadowValues.x = mix( shadowKernel[0][1], shadowKernel[0][0], fractionalCoord.y );\nshadowValues.y = mix( shadowKernel[0][2], shadowKernel[0][1], fractionalCoord.y );\nshadowValues.z = mix( shadowKernel[1][1], shadowKernel[1][0], fractionalCoord.y );\nshadowValues.w = mix( shadowKernel[1][2], shadowKernel[1][1], fractionalCoord.y );\nshadow = dot( shadowValues, vec4( 1.0 ) );\nshadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n#else\nvec4 rgbaDepth = texture2D( shadowMap[ i ], shadowCoord.xy );\nfloat fDepth = unpackDepth( rgbaDepth );\nif ( fDepth < shadowCoord.z )\nshadowColor = shadowColor * vec3( 1.0 - shadowDarkness[ i ] );\n#endif\n}\n#ifdef SHADOWMAP_DEBUG\n#ifdef SHADOWMAP_CASCADE\nif ( inFrustum && inFrustumCount == 1 ) gl_FragColor.xyz *= frustumColors[ i ];\n#else\nif ( inFrustum ) gl_FragColor.xyz *= frustumColors[ i ];\n#endif\n#endif\n}\n#ifdef GAMMA_OUTPUT\nshadowColor *= shadowColor;\n#endif\ngl_FragColor.xyz = gl_FragColor.xyz * shadowColor;\n#endif", +shadowmap_pars_vertex:"#ifdef USE_SHADOWMAP\nvarying vec4 vShadowCoord[ MAX_SHADOWS ];\nuniform mat4 shadowMatrix[ MAX_SHADOWS ];\n#endif",shadowmap_vertex:"#ifdef USE_SHADOWMAP\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvShadowCoord[ i ] = shadowMatrix[ i ] * worldPosition;\n}\n#endif",alphatest_fragment:"#ifdef ALPHATEST\nif ( gl_FragColor.a < ALPHATEST ) discard;\n#endif",linear_to_gamma_fragment:"#ifdef GAMMA_OUTPUT\ngl_FragColor.xyz = sqrt( gl_FragColor.xyz );\n#endif"}; +THREE.UniformsUtils={merge:function(a){var b,c,d,e={};for(b=0;b dashSize ) {\ndiscard;\n}\ngl_FragColor = vec4( diffuse, opacity );",THREE.ShaderChunk.color_fragment,THREE.ShaderChunk.fog_fragment,"}"].join("\n")},depth:{uniforms:{mNear:{type:"f",value:1},mFar:{type:"f",value:2E3},opacity:{type:"f", +value:1}},vertexShader:"void main() {\ngl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}",fragmentShader:"uniform float mNear;\nuniform float mFar;\nuniform float opacity;\nvoid main() {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat color = 1.0 - smoothstep( mNear, mFar, depth );\ngl_FragColor = vec4( vec3( color ), opacity );\n}"},normal:{uniforms:{opacity:{type:"f",value:1}},vertexShader:["varying vec3 vNormal;",THREE.ShaderChunk.morphtarget_pars_vertex,"void main() {\nvNormal = normalize( normalMatrix * normal );", +THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.default_vertex,"}"].join("\n"),fragmentShader:"uniform float opacity;\nvarying vec3 vNormal;\nvoid main() {\ngl_FragColor = vec4( 0.5 * normalize( vNormal ) + 0.5, opacity );\n}"},normalmap:{uniforms:THREE.UniformsUtils.merge([THREE.UniformsLib.fog,THREE.UniformsLib.lights,THREE.UniformsLib.shadowmap,{enableAO:{type:"i",value:0},enableDiffuse:{type:"i",value:0},enableSpecular:{type:"i",value:0},enableReflection:{type:"i",value:0},enableDisplacement:{type:"i", +value:0},tDisplacement:{type:"t",value:null},tDiffuse:{type:"t",value:null},tCube:{type:"t",value:null},tNormal:{type:"t",value:null},tSpecular:{type:"t",value:null},tAO:{type:"t",value:null},uNormalScale:{type:"v2",value:new THREE.Vector2(1,1)},uDisplacementBias:{type:"f",value:0},uDisplacementScale:{type:"f",value:1},uDiffuseColor:{type:"c",value:new THREE.Color(16777215)},uSpecularColor:{type:"c",value:new THREE.Color(1118481)},uAmbientColor:{type:"c",value:new THREE.Color(16777215)},uShininess:{type:"f", +value:30},uOpacity:{type:"f",value:1},useRefract:{type:"i",value:0},uRefractionRatio:{type:"f",value:0.98},uReflectivity:{type:"f",value:0.5},uOffset:{type:"v2",value:new THREE.Vector2(0,0)},uRepeat:{type:"v2",value:new THREE.Vector2(1,1)},wrapRGB:{type:"v3",value:new THREE.Vector3(1,1,1)}}]),fragmentShader:["uniform vec3 uAmbientColor;\nuniform vec3 uDiffuseColor;\nuniform vec3 uSpecularColor;\nuniform float uShininess;\nuniform float uOpacity;\nuniform bool enableDiffuse;\nuniform bool enableSpecular;\nuniform bool enableAO;\nuniform bool enableReflection;\nuniform sampler2D tDiffuse;\nuniform sampler2D tNormal;\nuniform sampler2D tSpecular;\nuniform sampler2D tAO;\nuniform samplerCube tCube;\nuniform vec2 uNormalScale;\nuniform bool useRefract;\nuniform float uRefractionRatio;\nuniform float uReflectivity;\nvarying vec3 vTangent;\nvarying vec3 vBinormal;\nvarying vec3 vNormal;\nvarying vec2 vUv;\nuniform vec3 ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif\nvarying vec3 vWorldPosition;\nvarying vec3 vViewPosition;", +THREE.ShaderChunk.shadowmap_pars_fragment,THREE.ShaderChunk.fog_pars_fragment,"void main() {\ngl_FragColor = vec4( vec3( 1.0 ), uOpacity );\nvec3 specularTex = vec3( 1.0 );\nvec3 normalTex = texture2D( tNormal, vUv ).xyz * 2.0 - 1.0;\nnormalTex.xy *= uNormalScale;\nnormalTex = normalize( normalTex );\nif( enableDiffuse ) {\n#ifdef GAMMA_INPUT\nvec4 texelColor = texture2D( tDiffuse, vUv );\ntexelColor.xyz *= texelColor.xyz;\ngl_FragColor = gl_FragColor * texelColor;\n#else\ngl_FragColor = gl_FragColor * texture2D( tDiffuse, vUv );\n#endif\n}\nif( enableAO ) {\n#ifdef GAMMA_INPUT\nvec4 aoColor = texture2D( tAO, vUv );\naoColor.xyz *= aoColor.xyz;\ngl_FragColor.xyz = gl_FragColor.xyz * aoColor.xyz;\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * texture2D( tAO, vUv ).xyz;\n#endif\n}\nif( enableSpecular )\nspecularTex = texture2D( tSpecular, vUv ).xyz;\nmat3 tsb = mat3( normalize( vTangent ), normalize( vBinormal ), normalize( vNormal ) );\nvec3 finalNormal = tsb * normalTex;\n#ifdef FLIP_SIDED\nfinalNormal = -finalNormal;\n#endif\nvec3 normal = normalize( finalNormal );\nvec3 viewPosition = normalize( vViewPosition );\n#if MAX_POINT_LIGHTS > 0\nvec3 pointDiffuse = vec3( 0.0 );\nvec3 pointSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 pointVector = lPosition.xyz + vViewPosition.xyz;\nfloat pointDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\npointDistance = 1.0 - min( ( length( pointVector ) / pointLightDistance[ i ] ), 1.0 );\npointVector = normalize( pointVector );\n#ifdef WRAP_AROUND\nfloat pointDiffuseWeightFull = max( dot( normal, pointVector ), 0.0 );\nfloat pointDiffuseWeightHalf = max( 0.5 * dot( normal, pointVector ) + 0.5, 0.0 );\nvec3 pointDiffuseWeight = mix( vec3 ( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB );\n#else\nfloat pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );\n#endif\npointDiffuse += pointDistance * pointLightColor[ i ] * uDiffuseColor * pointDiffuseWeight;\nvec3 pointHalfVector = normalize( pointVector + viewPosition );\nfloat pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );\nfloat pointSpecularWeight = specularTex.r * max( pow( pointDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( pointVector, pointHalfVector ), 5.0 );\npointSpecular += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * pointDistance * specularNormalization;\n#else\npointSpecular += pointDistance * pointLightColor[ i ] * uSpecularColor * pointSpecularWeight * pointDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nvec3 spotDiffuse = vec3( 0.0 );\nvec3 spotSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 spotVector = lPosition.xyz + vViewPosition.xyz;\nfloat spotDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nspotDistance = 1.0 - min( ( length( spotVector ) / spotLightDistance[ i ] ), 1.0 );\nspotVector = normalize( spotVector );\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - vWorldPosition ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\n#ifdef WRAP_AROUND\nfloat spotDiffuseWeightFull = max( dot( normal, spotVector ), 0.0 );\nfloat spotDiffuseWeightHalf = max( 0.5 * dot( normal, spotVector ) + 0.5, 0.0 );\nvec3 spotDiffuseWeight = mix( vec3 ( spotDiffuseWeightFull ), vec3( spotDiffuseWeightHalf ), wrapRGB );\n#else\nfloat spotDiffuseWeight = max( dot( normal, spotVector ), 0.0 );\n#endif\nspotDiffuse += spotDistance * spotLightColor[ i ] * uDiffuseColor * spotDiffuseWeight * spotEffect;\nvec3 spotHalfVector = normalize( spotVector + viewPosition );\nfloat spotDotNormalHalf = max( dot( normal, spotHalfVector ), 0.0 );\nfloat spotSpecularWeight = specularTex.r * max( pow( spotDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( spotVector, spotHalfVector ), 5.0 );\nspotSpecular += schlick * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * spotDistance * specularNormalization * spotEffect;\n#else\nspotSpecular += spotDistance * spotLightColor[ i ] * uSpecularColor * spotSpecularWeight * spotDiffuseWeight * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_DIR_LIGHTS > 0\nvec3 dirDiffuse = vec3( 0.0 );\nvec3 dirSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\n#ifdef WRAP_AROUND\nfloat directionalLightWeightingFull = max( dot( normal, dirVector ), 0.0 );\nfloat directionalLightWeightingHalf = max( 0.5 * dot( normal, dirVector ) + 0.5, 0.0 );\nvec3 dirDiffuseWeight = mix( vec3( directionalLightWeightingFull ), vec3( directionalLightWeightingHalf ), wrapRGB );\n#else\nfloat dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );\n#endif\ndirDiffuse += directionalLightColor[ i ] * uDiffuseColor * dirDiffuseWeight;\nvec3 dirHalfVector = normalize( dirVector + viewPosition );\nfloat dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );\nfloat dirSpecularWeight = specularTex.r * max( pow( dirDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( dirVector, dirHalfVector ), 5.0 );\ndirSpecular += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization;\n#else\ndirSpecular += directionalLightColor[ i ] * uSpecularColor * dirSpecularWeight * dirDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nvec3 hemiDiffuse = vec3( 0.0 );\nvec3 hemiSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nvec3 hemiColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\nhemiDiffuse += uDiffuseColor * hemiColor;\nvec3 hemiHalfVectorSky = normalize( lVector + viewPosition );\nfloat hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;\nfloat hemiSpecularWeightSky = specularTex.r * max( pow( hemiDotNormalHalfSky, uShininess ), 0.0 );\nvec3 lVectorGround = -lVector;\nvec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );\nfloat hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;\nfloat hemiSpecularWeightGround = specularTex.r * max( pow( hemiDotNormalHalfGround, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat dotProductGround = dot( normal, lVectorGround );\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlickSky = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( lVector, hemiHalfVectorSky ), 5.0 );\nvec3 schlickGround = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( lVectorGround, hemiHalfVectorGround ), 5.0 );\nhemiSpecular += hemiColor * specularNormalization * ( schlickSky * hemiSpecularWeightSky * max( dotProduct, 0.0 ) + schlickGround * hemiSpecularWeightGround * max( dotProductGround, 0.0 ) );\n#else\nhemiSpecular += uSpecularColor * hemiColor * ( hemiSpecularWeightSky + hemiSpecularWeightGround ) * hemiDiffuseWeight;\n#endif\n}\n#endif\nvec3 totalDiffuse = vec3( 0.0 );\nvec3 totalSpecular = vec3( 0.0 );\n#if MAX_DIR_LIGHTS > 0\ntotalDiffuse += dirDiffuse;\ntotalSpecular += dirSpecular;\n#endif\n#if MAX_HEMI_LIGHTS > 0\ntotalDiffuse += hemiDiffuse;\ntotalSpecular += hemiSpecular;\n#endif\n#if MAX_POINT_LIGHTS > 0\ntotalDiffuse += pointDiffuse;\ntotalSpecular += pointSpecular;\n#endif\n#if MAX_SPOT_LIGHTS > 0\ntotalDiffuse += spotDiffuse;\ntotalSpecular += spotSpecular;\n#endif\n#ifdef METAL\ngl_FragColor.xyz = gl_FragColor.xyz * ( totalDiffuse + ambientLightColor * uAmbientColor + totalSpecular );\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * ( totalDiffuse + ambientLightColor * uAmbientColor ) + totalSpecular;\n#endif\nif ( enableReflection ) {\nvec3 vReflect;\nvec3 cameraToVertex = normalize( vWorldPosition - cameraPosition );\nif ( useRefract ) {\nvReflect = refract( cameraToVertex, normal, uRefractionRatio );\n} else {\nvReflect = reflect( cameraToVertex, normal );\n}\nvec4 cubeColor = textureCube( tCube, vec3( -vReflect.x, vReflect.yz ) );\n#ifdef GAMMA_INPUT\ncubeColor.xyz *= cubeColor.xyz;\n#endif\ngl_FragColor.xyz = mix( gl_FragColor.xyz, cubeColor.xyz, specularTex.r * uReflectivity );\n}", +THREE.ShaderChunk.shadowmap_fragment,THREE.ShaderChunk.linear_to_gamma_fragment,THREE.ShaderChunk.fog_fragment,"}"].join("\n"),vertexShader:["attribute vec4 tangent;\nuniform vec2 uOffset;\nuniform vec2 uRepeat;\nuniform bool enableDisplacement;\n#ifdef VERTEX_TEXTURES\nuniform sampler2D tDisplacement;\nuniform float uDisplacementScale;\nuniform float uDisplacementBias;\n#endif\nvarying vec3 vTangent;\nvarying vec3 vBinormal;\nvarying vec3 vNormal;\nvarying vec2 vUv;\nvarying vec3 vWorldPosition;\nvarying vec3 vViewPosition;", +THREE.ShaderChunk.skinning_pars_vertex,THREE.ShaderChunk.shadowmap_pars_vertex,"void main() {",THREE.ShaderChunk.skinbase_vertex,THREE.ShaderChunk.skinnormal_vertex,"#ifdef USE_SKINNING\nvNormal = normalize( normalMatrix * skinnedNormal.xyz );\nvec4 skinnedTangent = skinMatrix * vec4( tangent.xyz, 0.0 );\nvTangent = normalize( normalMatrix * skinnedTangent.xyz );\n#else\nvNormal = normalize( normalMatrix * normal );\nvTangent = normalize( normalMatrix * tangent.xyz );\n#endif\nvBinormal = normalize( cross( vNormal, vTangent ) * tangent.w );\nvUv = uv * uRepeat + uOffset;\nvec3 displacedPosition;\n#ifdef VERTEX_TEXTURES\nif ( enableDisplacement ) {\nvec3 dv = texture2D( tDisplacement, uv ).xyz;\nfloat df = uDisplacementScale * dv.x + uDisplacementBias;\ndisplacedPosition = position + normalize( normal ) * df;\n} else {\n#ifdef USE_SKINNING\nvec4 skinVertex = vec4( position, 1.0 );\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\ndisplacedPosition = skinned.xyz;\n#else\ndisplacedPosition = position;\n#endif\n}\n#else\n#ifdef USE_SKINNING\nvec4 skinVertex = vec4( position, 1.0 );\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\ndisplacedPosition = skinned.xyz;\n#else\ndisplacedPosition = position;\n#endif\n#endif\nvec4 mvPosition = modelViewMatrix * vec4( displacedPosition, 1.0 );\nvec4 worldPosition = modelMatrix * vec4( displacedPosition, 1.0 );\ngl_Position = projectionMatrix * mvPosition;\nvWorldPosition = worldPosition.xyz;\nvViewPosition = -mvPosition.xyz;\n#ifdef USE_SHADOWMAP\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvShadowCoord[ i ] = shadowMatrix[ i ] * worldPosition;\n}\n#endif\n}"].join("\n")}, +cube:{uniforms:{tCube:{type:"t",value:null},tFlip:{type:"f",value:-1}},vertexShader:"varying vec3 vWorldPosition;\nvoid main() {\nvec4 worldPosition = modelMatrix * vec4( position, 1.0 );\nvWorldPosition = worldPosition.xyz;\ngl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}",fragmentShader:"uniform samplerCube tCube;\nuniform float tFlip;\nvarying vec3 vWorldPosition;\nvoid main() {\ngl_FragColor = textureCube( tCube, vec3( tFlip * vWorldPosition.x, vWorldPosition.yz ) );\n}"}, +depthRGBA:{uniforms:{},vertexShader:[THREE.ShaderChunk.morphtarget_pars_vertex,THREE.ShaderChunk.skinning_pars_vertex,"void main() {",THREE.ShaderChunk.skinbase_vertex,THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.skinning_vertex,THREE.ShaderChunk.default_vertex,"}"].join("\n"),fragmentShader:"vec4 pack_depth( const in float depth ) {\nconst vec4 bit_shift = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );\nconst vec4 bit_mask = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );\nvec4 res = fract( depth * bit_shift );\nres -= res.xxyz * bit_mask;\nreturn res;\n}\nvoid main() {\ngl_FragData[ 0 ] = pack_depth( gl_FragCoord.z );\n}"}};THREE.WebGLRenderer=function(a){function b(a,b){var c=a.vertices.length,d=b.material;if(d.attributes){void 0===a.__webglCustomAttributesList&&(a.__webglCustomAttributesList=[]);for(var e in d.attributes){var f=d.attributes[e];if(!f.__webglInitialized||f.createUniqueBuffers){f.__webglInitialized=!0;var h=1;"v2"===f.type?h=2:"v3"===f.type?h=3:"v4"===f.type?h=4:"c"===f.type&&(h=3);f.size=h;f.array=new Float32Array(c*h);f.buffer=j.createBuffer();f.buffer.belongsToAttribute=e;f.needsUpdate=!0}a.__webglCustomAttributesList.push(f)}}} +function c(a,b){var c=b.geometry,h=a.faces3,g=3*h.length,i=1*h.length,k=3*h.length,h=d(b,a),m=f(h),l=e(h),n=h.vertexColors?h.vertexColors:!1;a.__vertexArray=new Float32Array(3*g);l&&(a.__normalArray=new Float32Array(3*g));c.hasTangents&&(a.__tangentArray=new Float32Array(4*g));n&&(a.__colorArray=new Float32Array(3*g));m&&(0l;l++)K.autoScaleCubemaps&&!f?(n=k,q=l,s=c.image[l],w=rc,s.width<=w&&s.height<=w||(v=Math.max(s.width,s.height),u=Math.floor(s.width*w/v),w=Math.floor(s.height*w/v),v=document.createElement("canvas"),v.width= +u,v.height=w,v.getContext("2d").drawImage(s,0,0,s.width,s.height,0,0,u,w),s=v),n[q]=s):k[l]=c.image[l];l=k[0];n=0===(l.width&l.width-1)&&0===(l.height&l.height-1);q=A(c.format);s=A(c.type);C(j.TEXTURE_CUBE_MAP,c,n);for(l=0;6>l;l++)if(f){w=k[l].mipmaps;v=0;for(z=w.length;v=Wb&&console.warn("WebGLRenderer: trying to use "+a+" texture units while this GPU supports only "+Wb);P+=1;return a}function F(a,b,c,d){a[b]=c.r*c.r*d;a[b+1]=c.g*c.g*d;a[b+2]=c.b*c.b*d}function y(a,b,c,d){a[b]=c.r*d;a[b+1]=c.g*d;a[b+2]=c.b*d}function x(a){a!==za&&(j.lineWidth(a),za=a)}function z(a,b,c){Ha!==a&&(a?j.enable(j.POLYGON_OFFSET_FILL):j.disable(j.POLYGON_OFFSET_FILL), +Ha=a);if(a&&(Xa!==b||Ta!==c))j.polygonOffset(b,c),Xa=b,Ta=c}function O(a){for(var a=a.split("\n"),b=0,c=a.length;bb;b++)j.deleteFramebuffer(a.__webglFramebuffer[b]),j.deleteRenderbuffer(a.__webglRenderbuffer[b]);else j.deleteFramebuffer(a.__webglFramebuffer),j.deleteRenderbuffer(a.__webglRenderbuffer);K.info.memory.textures--},Mb=function(a){a= +a.target;a.removeEventListener("dispose",Mb);Nb(a)},Ob=function(a){void 0!==a.__webglVertexBuffer&&j.deleteBuffer(a.__webglVertexBuffer);void 0!==a.__webglNormalBuffer&&j.deleteBuffer(a.__webglNormalBuffer);void 0!==a.__webglTangentBuffer&&j.deleteBuffer(a.__webglTangentBuffer);void 0!==a.__webglColorBuffer&&j.deleteBuffer(a.__webglColorBuffer);void 0!==a.__webglUVBuffer&&j.deleteBuffer(a.__webglUVBuffer);void 0!==a.__webglUV2Buffer&&j.deleteBuffer(a.__webglUV2Buffer);void 0!==a.__webglSkinIndicesBuffer&& +j.deleteBuffer(a.__webglSkinIndicesBuffer);void 0!==a.__webglSkinWeightsBuffer&&j.deleteBuffer(a.__webglSkinWeightsBuffer);void 0!==a.__webglFaceBuffer&&j.deleteBuffer(a.__webglFaceBuffer);void 0!==a.__webglLineBuffer&&j.deleteBuffer(a.__webglLineBuffer);void 0!==a.__webglLineDistanceBuffer&&j.deleteBuffer(a.__webglLineDistanceBuffer);if(void 0!==a.__webglCustomAttributesList)for(var b in a.__webglCustomAttributesList)j.deleteBuffer(a.__webglCustomAttributesList[b].buffer);K.info.memory.geometries--}, +Nb=function(a){var b=a.program;if(void 0!==b){a.program=void 0;var c,d,e=!1,a=0;for(c=ea.length;ad.numSupportedMorphTargets?(l.sort(k),l.length=d.numSupportedMorphTargets):l.length>d.numSupportedMorphNormals?l.sort(k):0===l.length&&l.push([0,0]);for(m=0;mGa;Ga++)Ja=R[Ga],Ab[lb]=Ja.x,Ab[lb+1]=Ja.y,Ab[lb+2]=Ja.z,lb+=3;else for(Ga=0;3>Ga;Ga++)Ab[lb]=Q.x,Ab[lb+1]=Q.y,Ab[lb+2]=Q.z,lb+=3;j.bindBuffer(j.ARRAY_BUFFER, +x.__webglNormalBuffer);j.bufferData(j.ARRAY_BUFFER,Ab,D)}if(Eb&&Jb&&N){C=0;for(I=aa.length;CGa;Ga++)Ia=Da[Ga],hb[Ra]=Ia.x,hb[Ra+1]=Ia.y,Ra+=2;0Ga;Ga++)Pa=$[Ga],ib[Sa]=Pa.x,ib[Sa+1]=Pa.y,Sa+=2;0f;f++){a.__webglFramebuffer[f]=j.createFramebuffer();a.__webglRenderbuffer[f]=j.createRenderbuffer();j.texImage2D(j.TEXTURE_CUBE_MAP_POSITIVE_X+f,0,d,a.width, +a.height,0,d,e,null);var g=a,h=j.TEXTURE_CUBE_MAP_POSITIVE_X+f;j.bindFramebuffer(j.FRAMEBUFFER,a.__webglFramebuffer[f]);j.framebufferTexture2D(j.FRAMEBUFFER,j.COLOR_ATTACHMENT0,h,g.__webglTexture,0);I(a.__webglRenderbuffer[f],a)}c&&j.generateMipmap(j.TEXTURE_CUBE_MAP)}else a.__webglFramebuffer=j.createFramebuffer(),a.__webglRenderbuffer=a.shareDepthFrom?a.shareDepthFrom.__webglRenderbuffer:j.createRenderbuffer(),j.bindTexture(j.TEXTURE_2D,a.__webglTexture),C(j.TEXTURE_2D,a,c),j.texImage2D(j.TEXTURE_2D, +0,d,a.width,a.height,0,d,e,null),d=j.TEXTURE_2D,j.bindFramebuffer(j.FRAMEBUFFER,a.__webglFramebuffer),j.framebufferTexture2D(j.FRAMEBUFFER,j.COLOR_ATTACHMENT0,d,a.__webglTexture,0),a.shareDepthFrom?a.depthBuffer&&!a.stencilBuffer?j.framebufferRenderbuffer(j.FRAMEBUFFER,j.DEPTH_ATTACHMENT,j.RENDERBUFFER,a.__webglRenderbuffer):a.depthBuffer&&a.stencilBuffer&&j.framebufferRenderbuffer(j.FRAMEBUFFER,j.DEPTH_STENCIL_ATTACHMENT,j.RENDERBUFFER,a.__webglRenderbuffer):I(a.__webglRenderbuffer,a),c&&j.generateMipmap(j.TEXTURE_2D); +b?j.bindTexture(j.TEXTURE_CUBE_MAP,null):j.bindTexture(j.TEXTURE_2D,null);j.bindRenderbuffer(j.RENDERBUFFER,null);j.bindFramebuffer(j.FRAMEBUFFER,null)}a?(b=b?a.__webglFramebuffer[a.activeCubeFace]:a.__webglFramebuffer,c=a.width,a=a.height,e=d=0):(b=null,c=tb,a=ub,d=hb,e=ib);b!==ba&&(j.bindFramebuffer(j.FRAMEBUFFER,b),j.viewport(d,e,c,a),ba=b);vb=c;jb=a};this.shadowMapPlugin=new THREE.ShadowMapPlugin;this.addPrePlugin(this.shadowMapPlugin);this.addPostPlugin(new THREE.SpritePlugin);this.addPostPlugin(new THREE.LensFlarePlugin)};THREE.WebGLRenderTarget=function(a,b,c){this.width=a;this.height=b;c=c||{};this.wrapS=void 0!==c.wrapS?c.wrapS:THREE.ClampToEdgeWrapping;this.wrapT=void 0!==c.wrapT?c.wrapT:THREE.ClampToEdgeWrapping;this.magFilter=void 0!==c.magFilter?c.magFilter:THREE.LinearFilter;this.minFilter=void 0!==c.minFilter?c.minFilter:THREE.LinearMipMapLinearFilter;this.anisotropy=void 0!==c.anisotropy?c.anisotropy:1;this.offset=new THREE.Vector2(0,0);this.repeat=new THREE.Vector2(1,1);this.format=void 0!==c.format?c.format: +THREE.RGBAFormat;this.type=void 0!==c.type?c.type:THREE.UnsignedByteType;this.depthBuffer=void 0!==c.depthBuffer?c.depthBuffer:!0;this.stencilBuffer=void 0!==c.stencilBuffer?c.stencilBuffer:!0;this.generateMipmaps=!0;this.shareDepthFrom=null}; +THREE.WebGLRenderTarget.prototype={constructor:THREE.WebGLRenderTarget,clone:function(){var a=new THREE.WebGLRenderTarget(this.width,this.height);a.wrapS=this.wrapS;a.wrapT=this.wrapT;a.magFilter=this.magFilter;a.minFilter=this.minFilter;a.anisotropy=this.anisotropy;a.offset.copy(this.offset);a.repeat.copy(this.repeat);a.format=this.format;a.type=this.type;a.depthBuffer=this.depthBuffer;a.stencilBuffer=this.stencilBuffer;a.generateMipmaps=this.generateMipmaps;a.shareDepthFrom=this.shareDepthFrom; +return a},dispose:function(){this.dispatchEvent({type:"dispose"})}};THREE.EventDispatcher.prototype.apply(THREE.WebGLRenderTarget.prototype);THREE.WebGLRenderTargetCube=function(a,b,c){THREE.WebGLRenderTarget.call(this,a,b,c);this.activeCubeFace=0};THREE.WebGLRenderTargetCube.prototype=Object.create(THREE.WebGLRenderTarget.prototype);THREE.RenderableVertex=function(){this.positionWorld=new THREE.Vector3;this.positionScreen=new THREE.Vector4;this.visible=!0};THREE.RenderableVertex.prototype.copy=function(a){this.positionWorld.copy(a.positionWorld);this.positionScreen.copy(a.positionScreen)};THREE.RenderableFace3=function(){this.id=0;this.v1=new THREE.RenderableVertex;this.v2=new THREE.RenderableVertex;this.v3=new THREE.RenderableVertex;this.centroidModel=new THREE.Vector3;this.normalModel=new THREE.Vector3;this.normalModelView=new THREE.Vector3;this.vertexNormalsLength=0;this.vertexNormalsModel=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];this.vertexNormalsModelView=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];this.material=this.color=null;this.uvs=[[]];this.z= +0};THREE.RenderableObject=function(){this.id=0;this.object=null;this.z=0};THREE.RenderableParticle=function(){this.id=0;this.object=null;this.z=this.y=this.x=0;this.rotation=null;this.scale=new THREE.Vector2;this.material=null};THREE.RenderableLine=function(){this.id=0;this.v1=new THREE.RenderableVertex;this.v2=new THREE.RenderableVertex;this.vertexColors=[new THREE.Color,new THREE.Color];this.material=null;this.z=0};THREE.GeometryUtils={merge:function(a,b,c){var d,e,f=a.vertices.length,h=b instanceof THREE.Mesh?b.geometry:b,g=a.vertices,i=h.vertices,k=a.faces,m=h.faces,a=a.faceVertexUvs[0],h=h.faceVertexUvs[0];void 0===c&&(c=0);b instanceof THREE.Mesh&&(b.matrixAutoUpdate&&b.updateMatrix(),d=b.matrix,e=(new THREE.Matrix3).getNormalMatrix(d));for(var b=0,l=i.length;ba?b(c,e-1):k[e]>8&255,i>>16&255,i>>24&255)),d}d.mipmapCount=1;g[2]&131072&&!1!==b&&(d.mipmapCount=Math.max(1,g[7]));d.isCubemap=g[28]&512?!0:!1;d.width=g[4];d.height=g[3];for(var g=g[1]+4,f=d.width,h=d.height,i=d.isCubemap?6:1,k=0;kl-1?0:l-1,t=l+1>e-1?e-1:l+1,q=0>m-1?0:m-1,p=m+1>d-1?d-1:m+1,r=[],s=[0,0,g[4*(l*d+m)]/255*b];r.push([-1,0,g[4*(l*d+q)]/255*b]);r.push([-1,-1,g[4*(n*d+q)]/255*b]);r.push([0,-1,g[4*(n*d+m)]/255*b]);r.push([1,-1,g[4*(n*d+p)]/255*b]);r.push([1,0,g[4*(l*d+p)]/255*b]);r.push([1,1,g[4*(t*d+p)]/255*b]);r.push([0,1,g[4*(t*d+m)]/255*b]);r.push([-1,1,g[4*(t*d+q)]/255*b]);n=[];q=r.length;for(t=0;te)return null;var f=[],h=[],g=[],i,k,m;if(0=l--){console.log("Warning, unable to triangulate polygon!");break}i=k;e<=i&&(i=0);k=i+1;e<=k&&(k=0);m=k+1;e<=m&&(m=0);var n;a:{var t=n=void 0,q=void 0,p=void 0,r=void 0,s=void 0,u=void 0,w=void 0,E= +void 0,t=a[h[i]].x,q=a[h[i]].y,p=a[h[k]].x,r=a[h[k]].y,s=a[h[m]].x,u=a[h[m]].y;if(1E-10>(p-t)*(u-q)-(r-q)*(s-t))n=!1;else{var D=void 0,F=void 0,y=void 0,x=void 0,z=void 0,O=void 0,B=void 0,C=void 0,I=void 0,v=void 0,I=C=B=E=w=void 0,D=s-p,F=u-r,y=t-s,x=q-u,z=p-t,O=r-q;for(n=0;ni)h=d+1;else if(0b&&(b=0);1=b)return b=c[a]-b,a=this.curves[a],b=1-b/a.getLength(),a.getPointAt(b);a++}return null};THREE.CurvePath.prototype.getLength=function(){var a=this.getCurveLengths();return a[a.length-1]}; +THREE.CurvePath.prototype.getCurveLengths=function(){if(this.cacheLengths&&this.cacheLengths.length==this.curves.length)return this.cacheLengths;var a=[],b=0,c,d=this.curves.length;for(c=0;cb?b=g.x:g.xc?c=g.y:g.yd?d=g.z:g.zMath.abs(d.x-c[0].x)&&1E-10>Math.abs(d.y-c[0].y)&&c.splice(c.length-1,1);b&&c.push(c[0]);return c}; +THREE.Path.prototype.toShapes=function(a){var b,c,d,e,f=[],h=new THREE.Path;b=0;for(c=this.actions.length;b +g&&(g+=c.length);g%=c.length;0>h&&(h+=k.length);h%=k.length;e=0<=g-1?g-1:c.length-1;f=0<=h-1?h-1:k.length-1;p=[k[h],c[g],c[e]];p=THREE.FontUtils.Triangulate.area(p);r=[k[h],k[f],c[g]];r=THREE.FontUtils.Triangulate.area(r);l+n>p+r&&(g=t,h=m,0>g&&(g+=c.length),g%=c.length,0>h&&(h+=k.length),h%=k.length,e=0<=g-1?g-1:c.length-1,f=0<=h-1?h-1:k.length-1);l=c.slice(0,g);n=c.slice(g);t=k.slice(h);m=k.slice(0,h);f=[k[h],k[f],c[g]];q.push([k[h],c[g],c[e]]);q.push(f);c=l.concat(t).concat(m).concat(n)}return{shape:c, +isolatedPts:q,allpoints:d}},triangulateShape:function(a,b){var c=THREE.Shape.Utils.removeHoles(a,b),d=c.allpoints,e=c.isolatedPts,c=THREE.FontUtils.Triangulate(c.shape,!1),f,h,g,i,k={};f=0;for(h=d.length;fd;d++)i=g[d].x+":"+g[d].y,i=k[i],void 0!==i&&(g[d]=i)}f=0;for(h=e.length;fd;d++)i=g[d].x+":"+g[d].y,i=k[i],void 0!==i&&(g[d]=i)}return c.concat(e)}, +isClockWise:function(a){return 0>THREE.FontUtils.Triangulate.area(a)},b2p0:function(a,b){var c=1-a;return c*c*b},b2p1:function(a,b){return 2*(1-a)*a*b},b2p2:function(a,b){return a*a*b},b2:function(a,b,c,d){return this.b2p0(a,b)+this.b2p1(a,c)+this.b2p2(a,d)},b3p0:function(a,b){var c=1-a;return c*c*c*b},b3p1:function(a,b){var c=1-a;return 3*c*c*a*b},b3p2:function(a,b){return 3*(1-a)*a*a*b},b3p3:function(a,b){return a*a*a*b},b3:function(a,b,c,d,e){return this.b3p0(a,b)+this.b3p1(a,c)+this.b3p2(a,d)+ +this.b3p3(a,e)}};THREE.LineCurve=function(a,b){this.v1=a;this.v2=b};THREE.LineCurve.prototype=Object.create(THREE.Curve.prototype);THREE.LineCurve.prototype.getPoint=function(a){var b=this.v2.clone().sub(this.v1);b.multiplyScalar(a).add(this.v1);return b};THREE.LineCurve.prototype.getPointAt=function(a){return this.getPoint(a)};THREE.LineCurve.prototype.getTangent=function(){return this.v2.clone().sub(this.v1).normalize()};THREE.QuadraticBezierCurve=function(a,b,c){this.v0=a;this.v1=b;this.v2=c};THREE.QuadraticBezierCurve.prototype=Object.create(THREE.Curve.prototype);THREE.QuadraticBezierCurve.prototype.getPoint=function(a){var b;b=THREE.Shape.Utils.b2(a,this.v0.x,this.v1.x,this.v2.x);a=THREE.Shape.Utils.b2(a,this.v0.y,this.v1.y,this.v2.y);return new THREE.Vector2(b,a)}; +THREE.QuadraticBezierCurve.prototype.getTangent=function(a){var b;b=THREE.Curve.Utils.tangentQuadraticBezier(a,this.v0.x,this.v1.x,this.v2.x);a=THREE.Curve.Utils.tangentQuadraticBezier(a,this.v0.y,this.v1.y,this.v2.y);b=new THREE.Vector2(b,a);b.normalize();return b};THREE.CubicBezierCurve=function(a,b,c,d){this.v0=a;this.v1=b;this.v2=c;this.v3=d};THREE.CubicBezierCurve.prototype=Object.create(THREE.Curve.prototype);THREE.CubicBezierCurve.prototype.getPoint=function(a){var b;b=THREE.Shape.Utils.b3(a,this.v0.x,this.v1.x,this.v2.x,this.v3.x);a=THREE.Shape.Utils.b3(a,this.v0.y,this.v1.y,this.v2.y,this.v3.y);return new THREE.Vector2(b,a)}; +THREE.CubicBezierCurve.prototype.getTangent=function(a){var b;b=THREE.Curve.Utils.tangentCubicBezier(a,this.v0.x,this.v1.x,this.v2.x,this.v3.x);a=THREE.Curve.Utils.tangentCubicBezier(a,this.v0.y,this.v1.y,this.v2.y,this.v3.y);b=new THREE.Vector2(b,a);b.normalize();return b};THREE.SplineCurve=function(a){this.points=void 0==a?[]:a};THREE.SplineCurve.prototype=Object.create(THREE.Curve.prototype);THREE.SplineCurve.prototype.getPoint=function(a){var b=new THREE.Vector2,c=[],d=this.points,e;e=(d.length-1)*a;a=Math.floor(e);e-=a;c[0]=0==a?a:a-1;c[1]=a;c[2]=a>d.length-2?d.length-1:a+1;c[3]=a>d.length-3?d.length-1:a+2;b.x=THREE.Curve.Utils.interpolate(d[c[0]].x,d[c[1]].x,d[c[2]].x,d[c[3]].x,e);b.y=THREE.Curve.Utils.interpolate(d[c[0]].y,d[c[1]].y,d[c[2]].y,d[c[3]].y,e);return b};THREE.EllipseCurve=function(a,b,c,d,e,f,h){this.aX=a;this.aY=b;this.xRadius=c;this.yRadius=d;this.aStartAngle=e;this.aEndAngle=f;this.aClockwise=h};THREE.EllipseCurve.prototype=Object.create(THREE.Curve.prototype);THREE.EllipseCurve.prototype.getPoint=function(a){var b=this.aEndAngle-this.aStartAngle;this.aClockwise||(a=1-a);b=this.aStartAngle+a*b;a=this.aX+this.xRadius*Math.cos(b);b=this.aY+this.yRadius*Math.sin(b);return new THREE.Vector2(a,b)};THREE.ArcCurve=function(a,b,c,d,e,f){THREE.EllipseCurve.call(this,a,b,c,c,d,e,f)};THREE.ArcCurve.prototype=Object.create(THREE.EllipseCurve.prototype);THREE.LineCurve3=THREE.Curve.create(function(a,b){this.v1=a;this.v2=b},function(a){var b=new THREE.Vector3;b.subVectors(this.v2,this.v1);b.multiplyScalar(a);b.add(this.v1);return b});THREE.QuadraticBezierCurve3=THREE.Curve.create(function(a,b,c){this.v0=a;this.v1=b;this.v2=c},function(a){var b,c;b=THREE.Shape.Utils.b2(a,this.v0.x,this.v1.x,this.v2.x);c=THREE.Shape.Utils.b2(a,this.v0.y,this.v1.y,this.v2.y);a=THREE.Shape.Utils.b2(a,this.v0.z,this.v1.z,this.v2.z);return new THREE.Vector3(b,c,a)});THREE.CubicBezierCurve3=THREE.Curve.create(function(a,b,c,d){this.v0=a;this.v1=b;this.v2=c;this.v3=d},function(a){var b,c;b=THREE.Shape.Utils.b3(a,this.v0.x,this.v1.x,this.v2.x,this.v3.x);c=THREE.Shape.Utils.b3(a,this.v0.y,this.v1.y,this.v2.y,this.v3.y);a=THREE.Shape.Utils.b3(a,this.v0.z,this.v1.z,this.v2.z,this.v3.z);return new THREE.Vector3(b,c,a)});THREE.SplineCurve3=THREE.Curve.create(function(a){this.points=void 0==a?[]:a},function(a){var b=new THREE.Vector3,c=[],d=this.points,e,a=(d.length-1)*a;e=Math.floor(a);a-=e;c[0]=0==e?e:e-1;c[1]=e;c[2]=e>d.length-2?d.length-1:e+1;c[3]=e>d.length-3?d.length-1:e+2;e=d[c[0]];var f=d[c[1]],h=d[c[2]],c=d[c[3]];b.x=THREE.Curve.Utils.interpolate(e.x,f.x,h.x,c.x,a);b.y=THREE.Curve.Utils.interpolate(e.y,f.y,h.y,c.y,a);b.z=THREE.Curve.Utils.interpolate(e.z,f.z,h.z,c.z,a);return b});THREE.ClosedSplineCurve3=THREE.Curve.create(function(a){this.points=void 0==a?[]:a},function(a){var b=new THREE.Vector3,c=[],d=this.points,e;e=(d.length-0)*a;a=Math.floor(e);e-=a;a+=0a.hierarchy[c].keys[d].time&& +(a.hierarchy[c].keys[d].time=0),void 0!==a.hierarchy[c].keys[d].rot&&!(a.hierarchy[c].keys[d].rot instanceof THREE.Quaternion)){var g=a.hierarchy[c].keys[d].rot;a.hierarchy[c].keys[d].rot=new THREE.Quaternion(g[0],g[1],g[2],g[3])}if(a.hierarchy[c].keys.length&&void 0!==a.hierarchy[c].keys[0].morphTargets){g={};for(d=0;dt;t++){c=b[t];h=i.prevKey[c];g=i.nextKey[c];if(g.time<=m){if(kd||1d?0:1;if("pos"===c)if(c=a.position,this.interpolationType===THREE.AnimationHandler.LINEAR)c.x=e[0]+(f[0]-e[0])*d,c.y=e[1]+(f[1]-e[1])*d,c.z=e[2]+ +(f[2]-e[2])*d;else{if(this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD)this.points[0]=this.getPrevKeyWith("pos",l,h.index-1).pos,this.points[1]=e,this.points[2]=f,this.points[3]=this.getNextKeyWith("pos",l,g.index+1).pos,d=0.33*d+0.33,e=this.interpolateCatmullRom(this.points,d),c.x=e[0],c.y=e[1],c.z=e[2],this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD&&(d=this.interpolateCatmullRom(this.points,1.01*d), +this.target.set(d[0],d[1],d[2]),this.target.sub(c),this.target.y=0,this.target.normalize(),d=Math.atan2(this.target.x,this.target.z),a.rotation.set(0,d,0))}else"rot"===c?THREE.Quaternion.slerp(e,f,a.quaternion,d):"scl"===c&&(c=a.scale,c.x=e[0]+(f[0]-e[0])*d,c.y=e[1]+(f[1]-e[1])*d,c.z=e[2]+(f[2]-e[2])*d)}}}}; +THREE.Animation.prototype.interpolateCatmullRom=function(a,b){var c=[],d=[],e,f,h,g,i,k;e=(a.length-1)*b;f=Math.floor(e);e-=f;c[0]=0===f?f:f-1;c[1]=f;c[2]=f>a.length-2?f:f+1;c[3]=f>a.length-3?f:f+2;f=a[c[0]];g=a[c[1]];i=a[c[2]];k=a[c[3]];c=e*e;h=e*c;d[0]=this.interpolate(f[0],g[0],i[0],k[0],e,c,h);d[1]=this.interpolate(f[1],g[1],i[1],k[1],e,c,h);d[2]=this.interpolate(f[2],g[2],i[2],k[2],e,c,h);return d}; +THREE.Animation.prototype.interpolate=function(a,b,c,d,e,f,h){a=0.5*(c-a);d=0.5*(d-b);return(2*(b-c)+a+d)*h+(-3*(b-c)-2*a-d)*f+a*e+b};THREE.Animation.prototype.getNextKeyWith=function(a,b,c){for(var d=this.data.hierarchy[b].keys,c=this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD?c=h?b.interpolate(c,h):b.interpolate(c,c.time)}this.data.hierarchy[a].node.updateMatrix();d.matrixWorldNeedsUpdate=!0}}if(this.JITCompile&&void 0===f[0][e]){this.hierarchy[0].updateMatrixWorld(!0);for(a=0;ag?(b=Math.atan2(b.y-a.y,b.x-a.x),a=Math.atan2(c.y-a.y,c.x-a.x),b>a&&(a+=2*Math.PI),c=(b+a)/2,a=-Math.cos(c),c=-Math.sin(c),new THREE.Vector2(a,c)):d.multiplyScalar(g).add(h).sub(a).clone()}function e(c,d){var e,f;for(N=c.length;0<=--N;){e=N;f=N-1;0>f&&(f=c.length-1);for(var g=0,h=t+2*m, +g=0;gMath.abs(c-i)?[new THREE.Vector2(b,1-e),new THREE.Vector2(d,1-f),new THREE.Vector2(k,1-h),new THREE.Vector2(l,1-a)]:[new THREE.Vector2(c,1-e),new THREE.Vector2(i,1-f),new THREE.Vector2(m,1-h),new THREE.Vector2(n,1-a)]}};THREE.ExtrudeGeometry.__v1=new THREE.Vector2;THREE.ExtrudeGeometry.__v2=new THREE.Vector2;THREE.ExtrudeGeometry.__v3=new THREE.Vector2;THREE.ExtrudeGeometry.__v4=new THREE.Vector2; +THREE.ExtrudeGeometry.__v5=new THREE.Vector2;THREE.ExtrudeGeometry.__v6=new THREE.Vector2;THREE.ShapeGeometry=function(a,b){THREE.Geometry.call(this);!1===a instanceof Array&&(a=[a]);this.shapebb=a[a.length-1].getBoundingBox();this.addShapeList(a,b);this.computeCentroids();this.computeFaceNormals()};THREE.ShapeGeometry.prototype=Object.create(THREE.Geometry.prototype);THREE.ShapeGeometry.prototype.addShapeList=function(a,b){for(var c=0,d=a.length;cc&&1===a.x&&(a=new THREE.Vector2(a.x-1,a.y));0===b.x&&0===b.z&&(a=new THREE.Vector2(c/2/Math.PI+0.5,a.y));return a.clone()}THREE.Geometry.call(this);for(var c=c||1,d=d||0,g=this,i=0,k=a.length;in&&(0.2>a&&(d[0].x+=1),0.2>b&&(d[1].x+=1),0.2>m&&(d[2].x+=1));i=0;for(k=this.vertices.length;ic.y?this.quaternion.set(1,0,0,0):(a.set(c.z,0,-c.x).normalize(),b=Math.acos(c.y),this.quaternion.setFromAxisAngle(a,b))}}();THREE.ArrowHelper.prototype.setLength=function(a){this.scale.set(a,a,a)}; +THREE.ArrowHelper.prototype.setColor=function(a){this.line.material.color.setHex(a);this.cone.material.color.setHex(a)};THREE.BoxHelper=function(a){var b=[new THREE.Vector3(1,1,1),new THREE.Vector3(-1,1,1),new THREE.Vector3(-1,-1,1),new THREE.Vector3(1,-1,1),new THREE.Vector3(1,1,-1),new THREE.Vector3(-1,1,-1),new THREE.Vector3(-1,-1,-1),new THREE.Vector3(1,-1,-1)];this.vertices=b;var c=new THREE.Geometry;c.vertices.push(b[0],b[1],b[1],b[2],b[2],b[3],b[3],b[0],b[4],b[5],b[5],b[6],b[6],b[7],b[7],b[4],b[0],b[4],b[1],b[5],b[2],b[6],b[3],b[7]);THREE.Line.call(this,c,new THREE.LineBasicMaterial({color:16776960}),THREE.LinePieces); +void 0!==a&&this.update(a)};THREE.BoxHelper.prototype=Object.create(THREE.Line.prototype); +THREE.BoxHelper.prototype.update=function(a){var b=a.geometry;null===b.boundingBox&&b.computeBoundingBox();var c=b.boundingBox.min,b=b.boundingBox.max,d=this.vertices;d[0].set(b.x,b.y,b.z);d[1].set(c.x,b.y,b.z);d[2].set(c.x,c.y,b.z);d[3].set(b.x,c.y,b.z);d[4].set(b.x,b.y,c.z);d[5].set(c.x,b.y,c.z);d[6].set(c.x,c.y,c.z);d[7].set(b.x,c.y,c.z);this.geometry.computeBoundingSphere();this.geometry.verticesNeedUpdate=!0;this.matrixAutoUpdate=!1;this.matrixWorld=a.matrixWorld};THREE.BoundingBoxHelper=function(a,b){var c=b||8947848;this.object=a;this.box=new THREE.Box3;THREE.Mesh.call(this,new THREE.CubeGeometry(1,1,1),new THREE.MeshBasicMaterial({color:c,wireframe:!0}))};THREE.BoundingBoxHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.BoundingBoxHelper.prototype.update=function(){this.box.setFromObject(this.object);this.box.size(this.scale);this.box.center(this.position)};THREE.CameraHelper=function(a){function b(a,b,d){c(a,d);c(b,d)}function c(a,b){d.vertices.push(new THREE.Vector3);d.colors.push(new THREE.Color(b));void 0===f[a]&&(f[a]=[]);f[a].push(d.vertices.length-1)}var d=new THREE.Geometry,e=new THREE.LineBasicMaterial({color:16777215,vertexColors:THREE.FaceColors}),f={};b("n1","n2",16755200);b("n2","n4",16755200);b("n4","n3",16755200);b("n3","n1",16755200);b("f1","f2",16755200);b("f2","f4",16755200);b("f4","f3",16755200);b("f3","f1",16755200);b("n1","f1",16755200); +b("n2","f2",16755200);b("n3","f3",16755200);b("n4","f4",16755200);b("p","n1",16711680);b("p","n2",16711680);b("p","n3",16711680);b("p","n4",16711680);b("u1","u2",43775);b("u2","u3",43775);b("u3","u1",43775);b("c","t",16777215);b("p","c",3355443);b("cn1","cn2",3355443);b("cn3","cn4",3355443);b("cf1","cf2",3355443);b("cf3","cf4",3355443);THREE.Line.call(this,d,e,THREE.LinePieces);this.camera=a;this.matrixWorld=a.matrixWorld;this.matrixAutoUpdate=!1;this.pointMap=f;this.update()}; +THREE.CameraHelper.prototype=Object.create(THREE.Line.prototype); +THREE.CameraHelper.prototype.update=function(){var a=new THREE.Vector3,b=new THREE.Camera,c=new THREE.Projector;return function(){function d(d,h,g,i){a.set(h,g,i);c.unprojectVector(a,b);d=e.pointMap[d];if(void 0!==d){h=0;for(g=d.length;hd;d++)c.faces[d].color=this.colors[4>d?0:1];d=new THREE.MeshBasicMaterial({vertexColors:THREE.FaceColors,wireframe:!0});this.lightSphere=new THREE.Mesh(c,d);this.add(this.lightSphere); +this.update()};THREE.HemisphereLightHelper.prototype=Object.create(THREE.Object3D.prototype);THREE.HemisphereLightHelper.prototype.update=function(){var a=new THREE.Vector3;return function(){this.colors[0].copy(this.light.color).multiplyScalar(this.light.intensity);this.colors[1].copy(this.light.groundColor).multiplyScalar(this.light.intensity);this.lightSphere.lookAt(a.getPositionFromMatrix(this.light.matrixWorld).negate());this.lightSphere.geometry.colorsNeedUpdate=!0}}();THREE.PointLightHelper=function(a,b){this.light=a;this.light.updateMatrixWorld();var c=new THREE.SphereGeometry(b,4,2),d=new THREE.MeshBasicMaterial({wireframe:!0,fog:!1});d.color.copy(this.light.color).multiplyScalar(this.light.intensity);THREE.Mesh.call(this,c,d);this.matrixWorld=this.light.matrixWorld;this.matrixAutoUpdate=!1};THREE.PointLightHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.PointLightHelper.prototype.update=function(){this.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)};THREE.SpotLightHelper=function(a){THREE.Object3D.call(this);this.light=a;this.light.updateMatrixWorld();this.matrixWorld=a.matrixWorld;this.matrixAutoUpdate=!1;a=new THREE.CylinderGeometry(0,1,1,8,1,!0);a.applyMatrix((new THREE.Matrix4).makeTranslation(0,-0.5,0));a.applyMatrix((new THREE.Matrix4).makeRotationX(-Math.PI/2));var b=new THREE.MeshBasicMaterial({wireframe:!0,fog:!1});this.cone=new THREE.Mesh(a,b);this.add(this.cone);this.update()};THREE.SpotLightHelper.prototype=Object.create(THREE.Object3D.prototype); +THREE.SpotLightHelper.prototype.update=function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(){var c=this.light.distance?this.light.distance:1E4,d=c*Math.tan(this.light.angle);this.cone.scale.set(d,d,c);a.getPositionFromMatrix(this.light.matrixWorld);b.getPositionFromMatrix(this.light.target.matrixWorld);this.cone.lookAt(b.sub(a));this.cone.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)}}();THREE.VertexNormalsHelper=function(a,b,c,d){this.object=a;this.size=b||1;for(var b=c||16711680,d=d||1,c=new THREE.Geometry,a=a.geometry.faces,e=0,f=a.length;el;l++){b[0]=m[e[l]];b[1]=m[e[(l+1)%3]];b.sort(d);var n=b.toString();void 0===c[n]&&(f.vertices.push(h[b[0]]),f.vertices.push(h[b[1]]),c[n]=!0)}THREE.Line.call(this,f,new THREE.LineBasicMaterial({color:16777215}),THREE.LinePieces);this.matrixAutoUpdate=!1;this.matrixWorld=a.matrixWorld}; +THREE.WireframeHelper.prototype=Object.create(THREE.Line.prototype);THREE.ImmediateRenderObject=function(){THREE.Object3D.call(this);this.render=function(){}};THREE.ImmediateRenderObject.prototype=Object.create(THREE.Object3D.prototype);THREE.LensFlare=function(a,b,c,d,e){THREE.Object3D.call(this);this.lensFlares=[];this.positionScreen=new THREE.Vector3;this.customUpdateCallback=void 0;void 0!==a&&this.add(a,b,c,d,e)};THREE.LensFlare.prototype=Object.create(THREE.Object3D.prototype); +THREE.LensFlare.prototype.add=function(a,b,c,d,e,f){void 0===b&&(b=-1);void 0===c&&(c=0);void 0===f&&(f=1);void 0===e&&(e=new THREE.Color(16777215));void 0===d&&(d=THREE.NormalBlending);c=Math.min(c,Math.max(0,c));this.lensFlares.push({texture:a,size:b,distance:c,x:0,y:0,z:0,scale:1,rotation:1,opacity:f,color:e,blending:d})}; +THREE.LensFlare.prototype.updateLensFlares=function(){var a,b=this.lensFlares.length,c,d=2*-this.positionScreen.x,e=2*-this.positionScreen.y;for(a=0;ag.end&&(g.end=f);c||(c=i)}}for(i in d)g=d[i],this.createAnimation(i,g.start,g.end,a);this.firstAnimation=c}; +THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(a){if(a=this.animationsMap[a])a.direction=1,a.directionBackwards=!1};THREE.MorphBlendMesh.prototype.setAnimationDirectionBackward=function(a){if(a=this.animationsMap[a])a.direction=-1,a.directionBackwards=!0};THREE.MorphBlendMesh.prototype.setAnimationFPS=function(a,b){var c=this.animationsMap[a];c&&(c.fps=b,c.duration=(c.end-c.start)/c.fps)}; +THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.animationsMap[a];c&&(c.duration=b,c.fps=(c.end-c.start)/c.duration)};THREE.MorphBlendMesh.prototype.setAnimationWeight=function(a,b){var c=this.animationsMap[a];c&&(c.weight=b)};THREE.MorphBlendMesh.prototype.setAnimationTime=function(a,b){var c=this.animationsMap[a];c&&(c.time=b)};THREE.MorphBlendMesh.prototype.getAnimationTime=function(a){var b=0;if(a=this.animationsMap[a])b=a.time;return b}; +THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("animation["+a+"] undefined")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1}; +THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;bd.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.startFrame+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),h=d.weight; +f!==d.currentFrame&&(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*h,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);this.morphTargetInfluences[d.currentFrame]=e*h;this.morphTargetInfluences[d.lastFrame]=(1-e)*h}}};THREE.LensFlarePlugin=function(){function a(a,c){var d=b.createProgram(),e=b.createShader(b.FRAGMENT_SHADER),f=b.createShader(b.VERTEX_SHADER),g="precision "+c+" float;\n";b.shaderSource(e,g+a.fragmentShader);b.shaderSource(f,g+a.vertexShader);b.compileShader(e);b.compileShader(f);b.attachShader(d,e);b.attachShader(d,f);b.linkProgram(d);return d}var b,c,d,e,f,h,g,i,k,m,l,n,t;this.init=function(q){b=q.context;c=q;d=q.getPrecision();e=new Float32Array(16);f=new Uint16Array(6);q=0;e[q++]=-1;e[q++]=-1; +e[q++]=0;e[q++]=0;e[q++]=1;e[q++]=-1;e[q++]=1;e[q++]=0;e[q++]=1;e[q++]=1;e[q++]=1;e[q++]=1;e[q++]=-1;e[q++]=1;e[q++]=0;e[q++]=1;q=0;f[q++]=0;f[q++]=1;f[q++]=2;f[q++]=0;f[q++]=2;f[q++]=3;h=b.createBuffer();g=b.createBuffer();b.bindBuffer(b.ARRAY_BUFFER,h);b.bufferData(b.ARRAY_BUFFER,e,b.STATIC_DRAW);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,g);b.bufferData(b.ELEMENT_ARRAY_BUFFER,f,b.STATIC_DRAW);i=b.createTexture();k=b.createTexture();b.bindTexture(b.TEXTURE_2D,i);b.texImage2D(b.TEXTURE_2D,0,b.RGB,16,16, +0,b.RGB,b.UNSIGNED_BYTE,null);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);b.bindTexture(b.TEXTURE_2D,k);b.texImage2D(b.TEXTURE_2D,0,b.RGBA,16,16,0,b.RGBA,b.UNSIGNED_BYTE,null);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE); +b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);0>=b.getParameter(b.MAX_VERTEX_TEXTURE_IMAGE_UNITS)?(m=!1,l=a(THREE.ShaderFlares.lensFlare,d)):(m=!0,l=a(THREE.ShaderFlares.lensFlareVertexTexture,d));n={};t={};n.vertex=b.getAttribLocation(l,"position");n.uv=b.getAttribLocation(l,"uv");t.renderType=b.getUniformLocation(l,"renderType");t.map=b.getUniformLocation(l,"map");t.occlusionMap=b.getUniformLocation(l,"occlusionMap");t.opacity= +b.getUniformLocation(l,"opacity");t.color=b.getUniformLocation(l,"color");t.scale=b.getUniformLocation(l,"scale");t.rotation=b.getUniformLocation(l,"rotation");t.screenPosition=b.getUniformLocation(l,"screenPosition")};this.render=function(a,d,e,f){var a=a.__webglFlares,u=a.length;if(u){var w=new THREE.Vector3,E=f/e,D=0.5*e,F=0.5*f,y=16/f,x=new THREE.Vector2(y*E,y),z=new THREE.Vector3(1,1,0),O=new THREE.Vector2(1,1),B=t,y=n;b.useProgram(l);b.enableVertexAttribArray(n.vertex);b.enableVertexAttribArray(n.uv); +b.uniform1i(B.occlusionMap,0);b.uniform1i(B.map,1);b.bindBuffer(b.ARRAY_BUFFER,h);b.vertexAttribPointer(y.vertex,2,b.FLOAT,!1,16,0);b.vertexAttribPointer(y.uv,2,b.FLOAT,!1,16,8);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,g);b.disable(b.CULL_FACE);b.depthMask(!1);var C,I,v,A,G;for(C=0;Cx;x++)E[x]=new THREE.Vector3,u[x]=new THREE.Vector3;E=D.shadowCascadeNearZ[w];D=D.shadowCascadeFarZ[w];u[0].set(-1,-1,E);u[1].set(1,-1,E);u[2].set(-1, +1,E);u[3].set(1,1,E);u[4].set(-1,-1,D);u[5].set(1,-1,D);u[6].set(-1,1,D);u[7].set(1,1,D);y.originalCamera=n;u=new THREE.Gyroscope;u.position=p.shadowCascadeOffset;u.add(y);u.add(y.target);n.add(u);p.shadowCascadeArray[s]=y;console.log("Created virtualLight",y)}w=p;E=s;D=w.shadowCascadeArray[E];D.position.copy(w.position);D.target.position.copy(w.target.position);D.lookAt(D.target);D.shadowCameraVisible=w.shadowCameraVisible;D.shadowDarkness=w.shadowDarkness;D.shadowBias=w.shadowCascadeBias[E];u=w.shadowCascadeNearZ[E]; +w=w.shadowCascadeFarZ[E];D=D.pointsFrustum;D[0].z=u;D[1].z=u;D[2].z=u;D[3].z=u;D[4].z=w;D[5].z=w;D[6].z=w;D[7].z=w;F[r]=y;r++}else F[r]=p,r++;t=0;for(q=F.length;tw;w++)E=D[w],E.copy(u[w]),THREE.ShadowMapPlugin.__projector.unprojectVector(E,s),E.applyMatrix4(r.matrixWorldInverse),E.xk.x&&(k.x=E.x),E.yk.y&&(k.y=E.y),E.zk.z&& +(k.z=E.z);r.left=i.x;r.right=k.x;r.top=k.y;r.bottom=i.y;r.updateProjectionMatrix()}r=p.shadowMap;u=p.shadowMatrix;s=p.shadowCamera;s.position.getPositionFromMatrix(p.matrixWorld);m.getPositionFromMatrix(p.target.matrixWorld);s.lookAt(m);s.updateMatrixWorld();s.matrixWorldInverse.getInverse(s.matrixWorld);p.cameraHelper&&(p.cameraHelper.visible=p.shadowCameraVisible);p.shadowCameraVisible&&p.cameraHelper.update();u.set(0.5,0,0,0.5,0,0.5,0,0.5,0,0,0.5,0.5,0,0,0,1);u.multiply(s.projectionMatrix);u.multiply(s.matrixWorldInverse); +g.multiplyMatrices(s.projectionMatrix,s.matrixWorldInverse);h.setFromMatrix(g);b.setRenderTarget(r);b.clear();D=l.__webglObjects;p=0;for(r=D.length;p 0 ) {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat fogFactor = 0.0;\nif ( fogType == 1 ) {\nfogFactor = smoothstep( fogNear, fogFar, depth );\n} else {\nconst float LOG2 = 1.442695;\nfloat fogFactor = exp2( - fogDensity * fogDensity * depth * depth * LOG2 );\nfogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );\n}\ngl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );\n}\n}"}}; diff --git a/sources/modules/ampacheapi/AmpacheApi.lib.php b/sources/modules/ampacheapi/AmpacheApi.lib.php new file mode 100644 index 0000000..9d51079 --- /dev/null +++ b/sources/modules/ampacheapi/AmpacheApi.lib.php @@ -0,0 +1,393 @@ +_debug_output = $config['debug']; + } + + if (isset($config['debug_callback'])) { + $this->_debug_callback = $config['debug_callback']; + } + + // If we got something, then configure! + if (is_array($config) && count($config)) { + $this->configure($config); + } + + // If we've been READY'd then go ahead and attempt to connect + if ($this->state() == 'READY') { + $this->connect(); + } + } + + /** + * _debug + * + * Make debugging all nice and pretty. + */ + private function _debug($source, $message, $level = 5) + { + if ($this->_debug_output) { + echo "$source :: $message\n"; + } + + if (!is_null($this->_debug_callback)) { + call_user_func($this->_debug_callback, 'AmpacheApi', "$source :: $message", $level); + } + } + + /** + * connect + * + * This attempts to connect to the Ampache instance. + */ + public function connect() + { + $this->_debug('CONNECT', "Using $this->username / $this->password"); + + // Set up the handshake + $results = array(); + $timestamp = time(); + $passphrase = hash('sha256', $timestamp . $this->password); + + $options = array( + 'timestamp' => $timestamp, + 'auth' => $passphrase, + 'version' => self::$LIB_version, + 'user' => $this->username + ); + + $data = $this->send_command('handshake', $options); + + foreach ($data as $value) { + $results = array_merge($results, $value); + } + + if (!$results['auth']) { + $this->set_state('error'); + return false; + } + $this->api_auth = $results['auth']; + $this->set_state('connected'); + // Define when we pulled this, it is not wine, it does + // not get better with age + $this->handshake_time = time(); + $this->handshake = $results; + } + + /** + * configure + * + * This function takes an array of elements and configures the AmpacheApi + * object. It doesn't really do anything fancy, but it's a separate function + * so it can be called both from the constructor and directly. + */ + public function configure($config = array()) + { + $this->_debug('CONFIGURE', 'Checking passed config options'); + + if (!is_array($config)) { + trigger_error('AmpacheApi::configure received a non-array value'); + return false; + } + + // FIXME: Is the scrubbing of these variables actually sane? I'm pretty + // sure password at least shouldn't be messed with like that. + if (isset($config['username'])) { + $this->username = htmlentities($config['username'], ENT_QUOTES, 'UTF-8'); + } + if (isset($config['password'])) { + $this->password = htmlentities($config['password'], ENT_QUOTES, 'UTF-8'); + } + + if (isset($config['api_secure'])) { + // This should be a boolean response + $this->api_secure = $config['api_secure'] ? true : false; + } + $protocol = $this->api_secure ? 'https://' : 'http://'; + + if (isset($config['server'])) { + // Replace any http:// in the URL with '' + $config['server'] = str_replace($protocol, '', $config['server']); + $this->server = htmlentities($config['server'], ENT_QUOTES, 'UTF-8'); + } + + $this->api_url = $protocol . $this->server . '/server/xml.server.php'; + + // See if we have enough to authenticate, if so change the state + if ($this->username AND $this->password AND $this->server) { + $this->set_state('ready'); + } + + return true; + } + + /** + * set_state + * + * This sets the current state of the API, it is used mostly internally but + * the state can be accessed externally so it could be used to check and see + * where the API is at, at this moment + */ + public function set_state($state) + { + // Very simple for now, maybe we'll do something more with this later + $this->api_state = strtoupper($state); + } + + /** + * state + * + * This returns the state of the API. + */ + public function state() + { + return $this->api_state; + } + + /** + * info + * + * Returns the information gathered by the handshake. + * Not raw so we can format it if we want? + */ + public function info() + { + if ($this->state() != 'CONNECTED') { + throw new Exception('AmpacheApi::info API in non-ready state, unable to return info'); + } + + return $this->handshake; + } + + /** + * send_command + * + * This sends an API command with options to the currently connected + * host. + */ + public function send_command($command, $options = array()) + { + $this->_debug('SEND COMMAND', $command . ' ' . json_encode($options)); + + if ($this->state() != 'READY' AND $this->state() != 'CONNECTED') { + throw new Exception('AmpacheApi::send_command API in non-ready state, unable to send'); + } + $command = trim($command); + if (!$command) { + throw new Exception('AmpacheApi::send_command no command specified'); + } + if (!$this->validate_command($command)) { + throw new Exception('AmpacheApi::send_command Invalid/Unknown command ' . $command . ' issued'); + } + + $url = $this->api_url . '?action=' . urlencode($command); + + foreach ($options as $key => $value) { + $key = trim($key); + if (!$key) { + // Nonfatal, don't need to throw an exception + trigger_error('AmpacheApi::send_command unable to append empty variable to command'); + continue; + } + $url .= '&' . urlencode($key) . '=' . urlencode($value); + } + + // If auth is set then we append it so callers don't have to. + if ($this->api_auth) { + $url .= '&auth=' . urlencode($this->api_auth) . '&username=' . urlencode($this->username); + } + + $this->_debug('COMMAND URL', $url); + + $data = file_get_contents($url); + $this->raw_response = $data; + $this->parse_response($data); + return $this->get_response(); + } + + /** + * validate_command + * + * This takes the specified command and checks it against the known + * commands for the current version of Ampache. If no version is known yet + * it should return FALSE for everything except ping and handshake. + */ + public function validate_command($command) + { + // FIXME: actually do something + return true; + } + + /** + * parse_response + * + * This takes an XML document and dumps it into $this->results. Before + * it does that it will clean up anything that was there before, so I hope + * you didn't want any of that. + */ + public function parse_response($response) + { + // Reset the results + $this->XML_results = array(); + $this->XML_position = 0; + + $this->XML_create_parser(); + + if (!xml_parse($this->XML_parser, $response)) { + throw new Exception('AmpacheApi::parse_response was unable to parse XML document'); + } + + xml_parser_free($this->XML_parser); + $this->_debug('PARSE RESPONSE', json_encode($this->XML_results)); + return true; + } + + /** + * get_response + * + * This returns the last data we parsed. + */ + public function get_response() + { + return $this->XML_results; + } + + + ////////////////////////// XML PARSER FUNCTIONS //////////////////////////// + + /** + * XML_create_parser + * This creates the xml parser and sets the options + */ + public function XML_create_parser() + { + $this->XML_parser = xml_parser_create(); + xml_parser_set_option($this->XML_parser,XML_OPTION_CASE_FOLDING,false); + xml_set_object($this->XML_parser,$this); + xml_set_element_handler($this->XML_parser,'XML_start_element','XML_end_element'); + xml_set_character_data_handler($this->XML_parser,'XML_cdata'); + + } // XML_create_parser + + /** + * XML_cdata + * This is called for the content of the XML tag + */ + public function XML_cdata($parser,$cdata) + { + $cdata = trim($cdata); + + if (!$this->XML_currentTag || !$cdata) { return false; } + + if ($this->XML_subTag) { + $this->XML_results[$this->XML_position][$this->XML_currentTag][$this->XML_subTag] = $cdata; + } else { + $this->XML_results[$this->XML_position][$this->XML_currentTag] = $cdata; + } + + + } // XML_cdata + + public function XML_start_element($parser,$tag,$attributes) + { + // Skip it! + if (in_array($tag,$this->XML_skiptags)) { return false; } + + if (!in_array($tag,$this->XML_parenttags) OR $this->XML_currentTag) { + $this->XML_subTag = $tag; + } else { + $this->XML_currentTag = $tag; + } + + if (count($attributes)) { + if (!$this->XML_subTag) { + $this->XML_results[$this->XML_position][$this->XML_currentTag]['self'] = $attributes; + } else { + $this->XML_results[$this->XML_position][$this->XML_currentTag][$this->XML_subTag]['self'] = $attributes; + } + } + + } // start_element + + public function XML_end_element($parser,$tag) + { + if ($tag != $this->XML_currentTag) { + $this->XML_subTag = false; + } else { + $this->XML_currentTag = false; + $this->XML_position++; + } + + + } // end_element +} diff --git a/sources/modules/archive/archive.lib.php b/sources/modules/archive/archive.lib.php new file mode 100644 index 0000000..bb85b3c --- /dev/null +++ b/sources/modules/archive/archive.lib.php @@ -0,0 +1,727 @@ +options = array ( + 'basedir' => ".", + 'name' => $name, + 'prepend' => "", + 'inmemory' => 0, + 'overwrite' => 0, + 'recurse' => 1, + 'storepaths' => 1, + 'followlinks' => 0, + 'level' => 3, + 'method' => 1, + 'sfx' => "", + 'type' => "", + 'comment' => "" + ); + $this->files = array (); + $this->exclude = array (); + $this->storeonly = array (); + $this->error = array (); + } // archive + + public function set_options($options) { + + foreach ($options as $key => $value) { + $this->options[$key] = $value; + debug_event("archive.lib.php", "Setting option ".$key."[".$value."]...", "5"); + } + + if (!empty ($this->options['basedir'])) + { + $this->options['basedir'] = str_replace("\\", "/", $this->options['basedir']); + $this->options['basedir'] = preg_replace("/\/+/", "/", $this->options['basedir']); + $this->options['basedir'] = preg_replace("/\/$/", "", $this->options['basedir']); + } + if (!empty ($this->options['name'])) + { + $this->options['name'] = str_replace("\\", "/", $this->options['name']); + $this->options['name'] = preg_replace("/\/+/", "/", $this->options['name']); + } + if (!empty ($this->options['prepend'])) + { + $this->options['prepend'] = str_replace("\\", "/", $this->options['prepend']); + $this->options['prepend'] = preg_replace("/^(\.*\/+)+/", "", $this->options['prepend']); + $this->options['prepend'] = preg_replace("/\/+/", "/", $this->options['prepend']); + $this->options['prepend'] = preg_replace("/\/$/", "", $this->options['prepend']) . "/"; + } + + // Generate a tmpname + $this->options['tmpname'] = time() . '_' . session_id(); + + } // set_options + + public function create_archive() { + $this->make_list(); + + if ($this->options['inmemory'] == 0) + { + $pwd = getcwd(); + chdir($this->options['basedir']); + if ($this->options['overwrite'] == 0 && file_exists($this->options['tmpname'] . ($this->options['type'] == "gzip" || $this->options['type'] == "bzip" ? ".tmp" : ""))) + { + $this->error[] = "File {$this->options['tmpname']} already exists."; + chdir($pwd); + return 0; + } + else if ($this->archive = @fopen($this->options['tmpname'] . ($this->options['type'] == "gzip" || $this->options['type'] == "bzip" ? ".tmp" : ""), "wb+")) + chdir($pwd); + else + { + $this->error[] = "Could not open {$this->options['tmpname']} for writing."; + chdir($pwd); + return 0; + } + } + else { + $this->archive = ""; + } + + switch ($this->options['type']) + { + case "zip": + if (!$this->create_zip()) + { + $this->error[] = "Could not create zip file."; + return 0; + } + break; + case "bzip": + if (!$this->create_tar()) + { + $this->error[] = "Could not create tar file."; + return 0; + } + if (!$this->create_bzip()) + { + $this->error[] = "Could not create bzip2 file."; + return 0; + } + break; + case "gzip": + if (!$this->create_tar()) + { + $this->error[] = "Could not create tar file."; + return 0; + } + if (!$this->create_gzip()) + { + $this->error[] = "Could not create gzip file."; + return 0; + } + break; + case "tar": + if (!$this->create_tar()) + { + $this->error[] = "Could not create tar file."; + return 0; + } + } + + if ($this->options['inmemory'] == 0) + { + fclose($this->archive); + if ($this->options['type'] == "gzip" || $this->options['type'] == "bzip") { + unlink($this->options['basedir'] . "/" . $this->options['tmpname'] . ".tmp"); + } + } + + return true; + + } // create_archive + + function add_data($data) + { + if ($this->options['inmemory'] == 0) + fwrite($this->archive, $data); + else + $this->archive .= $data; + } + + function make_list() + { + if (!empty ($this->exclude)) + foreach ($this->files as $key => $value) + foreach ($this->exclude as $current) + if ($value['name'] == $current['name']) + unset ($this->files[$key]); + if (!empty ($this->storeonly)) + foreach ($this->files as $key => $value) + foreach ($this->storeonly as $current) + if ($value['name'] == $current['name']) + $this->files[$key]['method'] = 0; + unset ($this->exclude, $this->storeonly); + } + + function add_files($list, $prepend = '') + { + // Change the preprend directory temporary + if (!empty($prepend)) { + $oldprepend = $this->options['prepend']; + $this->set_options(array('prepend' => $prepend)); + } + $temp = $this->list_files($list); + foreach ($temp as $current) + $this->files[] = $current; + + // Restore previous prepend directory + if (isset($oldprepend)) { + $this->set_options(array('prepend' => $oldprepend)); + } + } + + function exclude_files($list) + { + $temp = $this->list_files($list); + foreach ($temp as $current) + $this->exclude[] = $current; + } + + function store_files($list) + { + $temp = $this->list_files($list); + foreach ($temp as $current) + $this->storeonly[] = $current; + } + + function list_files($list) + { + if (!is_array ($list)) + { + $temp = $list; + $list = array ($temp); + unset ($temp); + } + + $files = array (); + + $pwd = getcwd(); + chdir($this->options['basedir']); + + foreach ($list as $current) + { + debug_event("archive.lib.php", "Listing file {".$current."}...", "5"); + + $current = str_replace("\\", "/", $current); + $current = preg_replace("/\/+/", "/", $current); + $current = preg_replace("/\/$/", "", $current); + if (substr($current, 0, 1) == "/" ) { $current = "/" . $current; } + if (strstr($current, "*")) { + $regex = preg_replace("/([\\\^\$\.\[\]\|\(\)\?\+\{\}\/])/", "\\\\\\1", $current); + $regex = str_replace("*", ".*", $regex); + $dir = strstr($current, "/") ? substr($current, 0, strrpos($current, "/")) : "."; + $temp = $this->parse_dir($dir); + foreach ($temp as $current2) { + if (preg_match("/^{$regex}$/i", $current2['name'])) { + $files[] = $current2; + } + } + unset ($regex, $dir, $temp, $current); + } + else if (@is_dir($current)) { + $temp = $this->parse_dir($current); + foreach ($temp as $file) { + $files[] = $file; + } + unset ($temp, $file); + } + else if (@file_exists($current)) { + $files[] = array ('name' => $current, + 'name2' => $this->options['prepend'] . preg_replace("/(\.+\/+)+/", + "", + ($this->options['storepaths'] == 0 && strstr($current, "/")) ? substr($current, strrpos($current, "/") + 1) : $current), + 'type' => @is_link($current) && $this->options['followlinks'] == 0 ? 2 : 0, + 'ext' => substr($current, strrpos($current, ".")), 'stat' => stat($current)); + } + } + + chdir($pwd); + + unset ($current, $pwd); + + return $files; + } + + function parse_dir($dirname) + { + if ($this->options['storepaths'] == 1 && !preg_match("/^(\.+\/*)+$/", $dirname)) { + $files = array (array ('name' => $dirname, 'name2' => $this->options['prepend'] . + preg_replace("/(\.+\/+)+/", "", ($this->options['storepaths'] == 0 && strstr($dirname, "/")) ? + substr($dirname, strrpos($dirname, "/") + 1) : $dirname), 'type' => 5, 'stat' => stat($dirname))); + } + else { + $files = array (); + } + + $dir = @opendir($dirname); + + while ($file = @readdir($dir)) + { + $fullname = $dirname . "/" . $file; + if ($file == "." || $file == "..") { + continue; + } + else if (@is_dir($fullname)) { + if (empty ($this->options['recurse'])) + continue; + $temp = $this->parse_dir($fullname); + foreach ($temp as $file2) + $files[] = $file2; + } + else if (@file_exists($fullname)) { + $files[] = array ('name' => $fullname, 'name2' => $this->options['prepend'] . + preg_replace("/(\.+\/+)+/", "", ($this->options['storepaths'] == 0 && strstr($fullname, "/")) ? + substr($fullname, strrpos($fullname, "/") + 1) : $fullname), + 'type' => @is_link($fullname) && $this->options['followlinks'] == 0 ? 2 : 0, + 'ext' => substr($file, strrpos($file, ".")), 'stat' => stat($fullname)); + } + } + + @closedir($dir); + + return $files; + } + + /** + * download_file + * Modified by COF + */ + public function download_file() { + + // Always send this header + switch ($this->options['type']) { + case "zip": + header("Content-Type: application/zip"); + break; + case "bzip": + header("Content-Type: application/x-bzip2"); + break; + case "gzip": + header("Content-Type: application/x-gzip"); + break; + case "tar": + header("Content-Type: application/x-tar"); + } // end switch + + if ($this->options['inmemory'] == 0) { + + $full_arc_name = $this->options['basedir']."/".$this->options['tmpname']; + if (file_exists($full_arc_name)) { + $fsize = filesize($full_arc_name); + + //Send some headers which can be useful... + $header = "Content-Disposition: attachment; filename=\""; + $header .= strstr($this->options['name'], "/") ? substr($this->options['name'], strrpos($this->options['name'], "/") + 1) : $this->options['name']; + $header .= "\""; + header($header); + header("Content-Length: " . $fsize); + header("Content-Transfer-Encoding: binary"); + header("Cache-Control: no-cache, must-revalidate, max-age=60"); + header("Expires: Sat, 01 Jan 2000 12:00:00 GMT"); + + readfile($full_arc_name); + + //Now delete tempory file + unlink($full_arc_name); + } + else { + debug_event('ERROR','Archive does not exist, unable to download','1'); + return false; + } + return true; + } + // else if we're doing this baby in memory + else { + $header = "Content-Disposition: attachment; filename=\""; + $header .= strstr($this->options['name'], "/") ? substr($this->options['name'], strrpos($this->options['name'], "/") + 1) : $this->options['name']; + $header .= "\""; + header($header); + header("Content-Length: " . strlen($this->archive)); + header("Content-Transfer-Encoding: binary"); + header("Cache-Control: no-cache, must-revalidate, max-age=60"); + header("Expires: Sat, 01 Jan 2000 12:00:00 GMT"); + print($this->archive); + } + } // download file + +} // end zip_file class + +class tar_file extends archive +{ + function tar_file($name) + { + $this->archive($name); + $this->options['type'] = "tar"; + } + + function create_tar() + { + $pwd = getcwd(); + chdir($this->options['basedir']); + + foreach ($this->files as $current) + { + if ($current['name'] == $this->options['name']) + continue; + if (strlen($current['name2']) > 99) + { + $path = substr($current['name2'], 0, strpos($current['name2'], "/", strlen($current['name2']) - 100) + 1); + $current['name2'] = substr($current['name2'], strlen($path)); + if (strlen($path) > 154 || strlen($current['name2']) > 99) + { + $this->error[] = "Could not add {$path}{$current['name2']} to archive because the filename is too long."; + continue; + } + } + $block = pack("a100a8a8a8a12a12a8a1a100a6a2a32a32a8a8a155a12", $current['name2'], sprintf("%07o", + $current['stat'][2]), sprintf("%07o", $current['stat'][4]), sprintf("%07o", $current['stat'][5]), + sprintf("%011o", $current['type'] == 2 ? 0 : $current['stat'][7]), sprintf("%011o", $current['stat'][9]), + " ", $current['type'], $current['type'] == 2 ? @readlink($current['name']) : "", "ustar ", " ", + "Unknown", "Unknown", "", "", !empty ($path) ? $path : "", ""); + + $checksum = 0; + for ($i = 0; $i < 512; $i++) + $checksum += ord(substr($block, $i, 1)); + $checksum = pack("a8", sprintf("%07o", $checksum)); + $block = substr_replace($block, $checksum, 148, 8); + + if ($current['type'] == 2 || $current['stat'][7] == 0) + $this->add_data($block); + else if ($fp = @fopen($current['name'], "rb")) + { + $this->add_data($block); + while ($temp = fread($fp, 1048576)) + $this->add_data($temp); + if ($current['stat'][7] % 512 > 0) + { + $temp = ""; + for ($i = 0; $i < 512 - $current['stat'][7] % 512; $i++) + $temp .= "\0"; + $this->add_data($temp); + } + fclose($fp); + } + else + $this->error[] = "Could not open file {$current['name']} for reading. It was not added."; + } + + $this->add_data(pack("a1024", "")); + + chdir($pwd); + + return 1; + } + + function extract_files() + { + $pwd = getcwd(); + chdir($this->options['basedir']); + + if ($fp = $this->open_archive()) + { + if ($this->options['inmemory'] == 1) + $this->files = array (); + + while ($block = fread($fp, 512)) + { + $temp = unpack("a100name/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1type/a100symlink/a6magic/a2temp/a32temp/a32temp/a8temp/a8temp/a155prefix/a12temp", $block); + $file = array ( + 'name' => $temp['prefix'] . $temp['name'], + 'stat' => array ( + 2 => $temp['mode'], + 4 => octdec($temp['uid']), + 5 => octdec($temp['gid']), + 7 => octdec($temp['size']), + 9 => octdec($temp['mtime']), + ), + 'checksum' => octdec($temp['checksum']), + 'type' => $temp['type'], + 'magic' => $temp['magic'], + ); + if ($file['checksum'] == 0x00000000) + break; + else if (substr($file['magic'], 0, 5) != "ustar") + { + $this->error[] = "This script does not support extracting this type of tar file."; + break; + } + $block = substr_replace($block, " ", 148, 8); + $checksum = 0; + for ($i = 0; $i < 512; $i++) + $checksum += ord(substr($block, $i, 1)); + if ($file['checksum'] != $checksum) + $this->error[] = "Could not extract from {$this->options['name']}, it is corrupt."; + + if ($this->options['inmemory'] == 1) + { + $file['data'] = fread($fp, $file['stat'][7]); + fread($fp, (512 - $file['stat'][7] % 512) == 512 ? 0 : (512 - $file['stat'][7] % 512)); + unset ($file['checksum'], $file['magic']); + $this->files[] = $file; + } + else if ($file['type'] == 5) + { + if (!is_dir($file['name'])) + mkdir($file['name'], $file['stat'][2]); + } + else if ($this->options['overwrite'] == 0 && file_exists($file['name'])) + { + $this->error[] = "{$file['name']} already exists."; + continue; + } + else if ($file['type'] == 2) + { + symlink($temp['symlink'], $file['name']); + chmod($file['name'], $file['stat'][2]); + } + else if ($new = @fopen($file['name'], "wb")) + { + fwrite($new, fread($fp, $file['stat'][7])); + fread($fp, (512 - $file['stat'][7] % 512) == 512 ? 0 : (512 - $file['stat'][7] % 512)); + fclose($new); + chmod($file['name'], $file['stat'][2]); + } + else + { + $this->error[] = "Could not open {$file['name']} for writing."; + continue; + } + chown($file['name'], $file['stat'][4]); + chgrp($file['name'], $file['stat'][5]); + touch($file['name'], $file['stat'][9]); + unset ($file); + } + } + else + $this->error[] = "Could not open file {$this->options['name']}"; + + chdir($pwd); + } + + function open_archive() + { + return @fopen($this->options['name'], "rb"); + } +} + +class gzip_file extends tar_file +{ + function gzip_file($name) + { + $this->tar_file($name); + $this->options['type'] = "gzip"; + } + + function create_gzip() + { + if ($this->options['inmemory'] == 0) + { + $pwd = getcwd(); + chdir($this->options['basedir']); + if ($fp = gzopen($this->options['name'], "wb{$this->options['level']}")) + { + fseek($this->archive, 0); + while ($temp = fread($this->archive, 1048576)) + gzwrite($fp, $temp); + gzclose($fp); + chdir($pwd); + } + else + { + $this->error[] = "Could not open {$this->options['name']} for writing."; + chdir($pwd); + return 0; + } + } + else + $this->archive = gzencode($this->archive, $this->options['level']); + + return 1; + } + + function open_archive() + { + return @gzopen($this->options['name'], "rb"); + } +} + +class bzip_file extends tar_file +{ + function bzip_file($name) + { + $this->tar_file($name); + $this->options['type'] = "bzip"; + } + + function create_bzip() + { + if ($this->options['inmemory'] == 0) + { + $pwd = getcwd(); + chdir($this->options['basedir']); + if ($fp = bzopen($this->options['name'], "wb")) + { + fseek($this->archive, 0); + while ($temp = fread($this->archive, 1048576)) + bzwrite($fp, $temp); + bzclose($fp); + chdir($pwd); + } + else + { + $this->error[] = "Could not open {$this->options['name']} for writing."; + chdir($pwd); + return 0; + } + } + else + $this->archive = bzcompress($this->archive, $this->options['level']); + + return 1; + } + + function open_archive() + { + return @bzopen($this->options['name'], "rb"); + } +} + +class zip_file extends archive +{ + function zip_file($name) + { + $this->archive($name); + $this->options['type'] = "zip"; + } + + function create_zip() + { + $files = 0; + $offset = 0; + $central = ""; + + if (!empty ($this->options['sfx'])) + if ($fp = @fopen($this->options['sfx'], "rb")) + { + $temp = fread($fp, filesize($this->options['sfx'])); + fclose($fp); + $this->add_data($temp); + $offset += strlen($temp); + unset ($temp); + } + else { + $this->error[] = "Could not open sfx module from {$this->options['sfx']}."; + } + + $pwd = getcwd(); + chdir($this->options['basedir']); + + foreach ($this->files as $current) + { + foreach ($current as $key => $value) { + debug_event("archive.lib.php", "Processing ".$key."[".$value."]...", "5"); + } + + if (function_exists('iconv')) { + // Fix encoding issue for zip archives + $current['name2'] = iconv('UTF-8', 'CP852', $current['name2']); + } + + if ($current['name'] == $this->options['name']) + continue; + + $timedate = explode(" ", date("Y n j G i s", $current['stat'][9])); + $timedate = ($timedate[0] - 1980 << 25) | ($timedate[1] << 21) | ($timedate[2] << 16) | + ($timedate[3] << 11) | ($timedate[4] << 5) | ($timedate[5]); + + $block = pack("VvvvV", 0x04034b50, 0x000A, 0x0000, (isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate); + + if ($current['stat'][7] == 0 && $current['type'] == 5) + { + $block .= pack("VVVvv", 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']) + 1, 0x0000); + $block .= $current['name2'] . "/"; + $this->add_data($block); + $central .= pack("VvvvvVVVVvvvvvVV", 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000, + (isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate, + 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']) + 1, 0x0000, 0x0000, 0x0000, 0x0000, $current['type'] == 5 ? 0x00000010 : 0x00000000, $offset); + $central .= $current['name2'] . "/"; + $files++; + $offset += (31 + strlen($current['name2'])); + } + else if ($current['stat'][7] == 0) + { + $block .= pack("VVVvv", 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']), 0x0000); + $block .= $current['name2']; + $this->add_data($block); + $central .= pack("VvvvvVVVVvvvvvVV", 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000, + (isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate, + 0x00000000, 0x00000000, 0x00000000, strlen($current['name2']), 0x0000, 0x0000, 0x0000, 0x0000, $current['type'] == 5 ? 0x00000010 : 0x00000000, $offset); + $central .= $current['name2']; + $files++; + $offset += (30 + strlen($current['name2'])); + } + else if ($fp = @fopen($current['name'], "rb")) + { + $temp = fread($fp, $current['stat'][7]); + fclose($fp); + $crc32 = crc32($temp); + if (!isset($current['method']) && $this->options['method'] == 1) + { + $temp = gzcompress($temp, $this->options['level']); + $size = strlen($temp) - 6; + $temp = substr($temp, 2, $size); + } + else + $size = strlen($temp); + $block .= pack("VVVvv", $crc32, $size, $current['stat'][7], strlen($current['name2']), 0x0000); + $block .= $current['name2']; + $this->add_data($block); + $this->add_data($temp); + unset ($temp); + $central .= pack("VvvvvVVVVvvvvvVV", 0x02014b50, 0x0014, $this->options['method'] == 0 ? 0x0000 : 0x000A, 0x0000, + (isset($current['method']) || $this->options['method'] == 0) ? 0x0000 : 0x0008, $timedate, + $crc32, $size, $current['stat'][7], strlen($current['name2']), 0x0000, 0x0000, 0x0000, 0x0000, 0x00000000, $offset); + $central .= $current['name2']; + $files++; + $offset += (30 + strlen($current['name2']) + $size); + } + else { + $this->error[] = "Could not open file {$current['name']} for reading. It was not added."; + } + } + + $this->add_data($central); + + $this->add_data(pack("VvvvvVVv", 0x06054b50, 0x0000, 0x0000, $files, $files, strlen($central), $offset, + !empty ($this->options['comment']) ? strlen($this->options['comment']) : 0x0000)); + + if (!empty ($this->options['comment'])) + $this->add_data($this->options['comment']); + + chdir($pwd); + + return 1; + } +} ?> diff --git a/sources/modules/bootstrap/css/bootstrap-theme.css b/sources/modules/bootstrap/css/bootstrap-theme.css new file mode 100644 index 0000000..a406992 --- /dev/null +++ b/sources/modules/bootstrap/css/bootstrap-theme.css @@ -0,0 +1,347 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +.btn-default, +.btn-primary, +.btn-success, +.btn-info, +.btn-warning, +.btn-danger { + text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); +} +.btn-default:active, +.btn-primary:active, +.btn-success:active, +.btn-info:active, +.btn-warning:active, +.btn-danger:active, +.btn-default.active, +.btn-primary.active, +.btn-success.active, +.btn-info.active, +.btn-warning.active, +.btn-danger.active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn:active, +.btn.active { + background-image: none; +} +.btn-default { + text-shadow: 0 1px 0 #fff; + background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); + background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #dbdbdb; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus { + background-color: #e0e0e0; + background-position: 0 -15px; +} +.btn-default:active, +.btn-default.active { + background-color: #e0e0e0; + border-color: #dbdbdb; +} +.btn-primary { + background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #2b669a; +} +.btn-primary:hover, +.btn-primary:focus { + background-color: #2d6ca2; + background-position: 0 -15px; +} +.btn-primary:active, +.btn-primary.active { + background-color: #2d6ca2; + border-color: #2b669a; +} +.btn-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); + background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #3e8f3e; +} +.btn-success:hover, +.btn-success:focus { + background-color: #419641; + background-position: 0 -15px; +} +.btn-success:active, +.btn-success.active { + background-color: #419641; + border-color: #3e8f3e; +} +.btn-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); + background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #28a4c9; +} +.btn-info:hover, +.btn-info:focus { + background-color: #2aabd2; + background-position: 0 -15px; +} +.btn-info:active, +.btn-info.active { + background-color: #2aabd2; + border-color: #28a4c9; +} +.btn-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #e38d13; +} +.btn-warning:hover, +.btn-warning:focus { + background-color: #eb9316; + background-position: 0 -15px; +} +.btn-warning:active, +.btn-warning.active { + background-color: #eb9316; + border-color: #e38d13; +} +.btn-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); + background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-color: #b92c28; +} +.btn-danger:hover, +.btn-danger:focus { + background-color: #c12e2a; + background-position: 0 -15px; +} +.btn-danger:active, +.btn-danger.active { + background-color: #c12e2a; + border-color: #b92c28; +} +.thumbnail, +.img-thumbnail { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); + box-shadow: 0 1px 2px rgba(0, 0, 0, .075); +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + background-color: #e8e8e8; + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + background-color: #357ebd; + background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); + background-repeat: repeat-x; +} +.navbar-default { + background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); + background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); +} +.navbar-default .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); +} +.navbar-brand, +.navbar-nav > li > a { + text-shadow: 0 1px 0 rgba(255, 255, 255, .25); +} +.navbar-inverse { + background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); + background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + background-repeat: repeat-x; +} +.navbar-inverse .navbar-nav > .active > a { + background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); + background-image: linear-gradient(to bottom, #222 0%, #282828 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); + box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); +} +.navbar-inverse .navbar-brand, +.navbar-inverse .navbar-nav > li > a { + text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); +} +.navbar-static-top, +.navbar-fixed-top, +.navbar-fixed-bottom { + border-radius: 0; +} +.alert { + text-shadow: 0 1px 0 rgba(255, 255, 255, .2); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); +} +.alert-success { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); + background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); + background-repeat: repeat-x; + border-color: #b2dba1; +} +.alert-info { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); + background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); + background-repeat: repeat-x; + border-color: #9acfea; +} +.alert-warning { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); + background-repeat: repeat-x; + border-color: #f5e79e; +} +.alert-danger { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); + background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); + background-repeat: repeat-x; + border-color: #dca7a7; +} +.progress { + background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); + background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar { + background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-success { + background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); + background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-info { + background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); + background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-warning { + background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); + background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); + background-repeat: repeat-x; +} +.progress-bar-danger { + background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); + background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); + background-repeat: repeat-x; +} +.list-group { + border-radius: 4px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); + box-shadow: 0 1px 2px rgba(0, 0, 0, .075); +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + text-shadow: 0 -1px 0 #3071a9; + background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); + background-repeat: repeat-x; + border-color: #3278b3; +} +.panel { + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); + box-shadow: 0 1px 2px rgba(0, 0, 0, .05); +} +.panel-default > .panel-heading { + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + background-repeat: repeat-x; +} +.panel-primary > .panel-heading { + background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); + background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); + background-repeat: repeat-x; +} +.panel-success > .panel-heading { + background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); + background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); + background-repeat: repeat-x; +} +.panel-info > .panel-heading { + background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); + background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); + background-repeat: repeat-x; +} +.panel-warning > .panel-heading { + background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); + background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + background-repeat: repeat-x; +} +.panel-danger > .panel-heading { + background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); + background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + background-repeat: repeat-x; +} +.well { + background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); + background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); + background-repeat: repeat-x; + border-color: #dcdcdc; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); +} +/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/sources/modules/bootstrap/css/bootstrap-theme.css.map b/sources/modules/bootstrap/css/bootstrap-theme.css.map new file mode 100644 index 0000000..b36fc9a --- /dev/null +++ b/sources/modules/bootstrap/css/bootstrap-theme.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["less/theme.less","less/mixins.less"],"names":[],"mappings":"AAeA;AACA;AACA;AACA;AACA;AACA;EACE,wCAAA;ECoGA,2FAAA;EACQ,mFAAA;;ADhGR,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;AACD,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;EC8FD,wDAAA;EACQ,gDAAA;;ADnER,IAAC;AACD,IAAC;EACC,sBAAA;;AAKJ;EC4PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;EAyB2C,yBAAA;EAA2B,kBAAA;;AAvBtE,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAeJ;EC2PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAgBJ;EC0PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAiBJ;ECyPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,SAAC;AACD,SAAC;EACC,yBAAA;EACA,qBAAA;;AAkBJ;ECwPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAmBJ;ECuPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,WAAC;AACD,WAAC;EACC,yBAAA;EACA,qBAAA;;AA2BJ;AACA;EC6CE,kDAAA;EACQ,0CAAA;;ADpCV,cAAe,KAAK,IAAG;AACvB,cAAe,KAAK,IAAG;ECmOnB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EDpOF,yBAAA;;AAEF,cAAe,UAAU;AACzB,cAAe,UAAU,IAAG;AAC5B,cAAe,UAAU,IAAG;EC6NxB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED9NF,yBAAA;;AAUF;ECiNI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;EDrPA,kBAAA;ECaA,2FAAA;EACQ,mFAAA;;ADjBV,eAOE,YAAY,UAAU;EC0MpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,wDAAA;EACQ,gDAAA;;ADLV;AACA,WAAY,KAAK;EACf,8CAAA;;AAIF;EC+LI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;;ADtOF,eAIE,YAAY,UAAU;EC2LpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,uDAAA;EACQ,+CAAA;;ADCV,eASE;AATF,eAUE,YAAY,KAAK;EACf,yCAAA;;AAKJ;AACA;AACA;EACE,gBAAA;;AAUF;EACE,6CAAA;EChCA,0FAAA;EACQ,kFAAA;;AD2CV;ECqJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAKF;ECoJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAMF;ECmJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAOF;ECkJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAgBF;ECyII,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADlIJ;EC+HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADjIJ;EC8HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADhIJ;EC6HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD/HJ;EC4HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD9HJ;EC2HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtHJ;EACE,kBAAA;EC/EA,kDAAA;EACQ,0CAAA;;ADiFV,gBAAgB;AAChB,gBAAgB,OAAO;AACvB,gBAAgB,OAAO;EACrB,6BAAA;EC4GE,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED7GF,qBAAA;;AAUF;ECjGE,iDAAA;EACQ,yCAAA;;AD0GV,cAAe;ECsFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADxFJ,cAAe;ECqFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADvFJ,cAAe;ECoFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtFJ,WAAY;ECmFR,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADrFJ,cAAe;ECkFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADpFJ,aAAc;ECiFV,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD5EJ;ECyEI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED1EF,qBAAA;EC1HA,yFAAA;EACQ,iFAAA","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n\n// WebKit-style focus\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n width: @width;\n height: @height;\n}\n.square(@size) {\n .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support the\n// standard `box-shadow` property.\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n -webkit-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n -webkit-transform: scale(@ratio, @ratio-y);\n -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n -webkit-transform: skew(@x, @y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n resize: @direction; // Options: horizontal, vertical, both\n overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse .panel-body {\n border-top-color: @border;\n }\n }\n & > .panel-footer {\n + .panel-collapse .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a& {\n color: @color;\n\n .list-group-item-heading { color: inherit; }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:hover,\n &:focus,\n &:active,\n &.active,\n .open .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 8%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &:active,\n &.active {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n background-color: @color;\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n background-color: @color;\n .progress-striped & {\n #gradient > .striped();\n }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n @media (min-width: @screen-xs-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-push(@columns) {\n @media (min-width: @screen-xs-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-pull(@columns) {\n @media (min-width: @screen-xs-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n // Color the label and help text\n .help-block,\n .control-label,\n .radio,\n .checkbox,\n .radio-inline,\n .checkbox-inline {\n color: @text-color;\n }\n // Set the border and box shadow on specific inputs to match\n .form-control {\n border-color: @border-color;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n &:focus {\n border-color: darken(@border-color, 10%);\n @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n .box-shadow(@shadow);\n }\n }\n // Set validation states also for addons\n .input-group-addon {\n color: @text-color;\n border-color: @border-color;\n background-color: @background-color;\n }\n // Optional feedback icon\n .form-control-feedback {\n color: @text-color;\n }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n &:focus {\n border-color: @color;\n outline: 0;\n .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. ``\n// element gets special love because it's special, and that's a fact!\n\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n height: @input-height;\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n\n select& {\n height: @input-height;\n line-height: @input-height;\n }\n\n textarea&,\n select[multiple]& {\n height: auto;\n }\n}\n","//\n// Variables\n// --------------------------------------------------\n\n\n//== Colors\n//\n//## Gray and brand colors for use across Bootstrap.\n\n@gray-darker: lighten(#000, 13.5%); // #222\n@gray-dark: lighten(#000, 20%); // #333\n@gray: lighten(#000, 33.5%); // #555\n@gray-light: lighten(#000, 60%); // #999\n@gray-lighter: lighten(#000, 93.5%); // #eee\n\n@brand-primary: #428bca;\n@brand-success: #5cb85c;\n@brand-info: #5bc0de;\n@brand-warning: #f0ad4e;\n@brand-danger: #d9534f;\n\n\n//== Scaffolding\n//\n// ## Settings for some of the most global styles.\n\n//** Background color for ``.\n@body-bg: #fff;\n//** Global text color on ``.\n@text-color: @gray-dark;\n\n//** Global textual link color.\n@link-color: @brand-primary;\n//** Link hover color set via `darken()` function.\n@link-hover-color: darken(@link-color, 15%);\n\n\n//== Typography\n//\n//## Font, line-height, and color for body text, headings, and more.\n\n@font-family-sans-serif: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n@font-family-serif: Georgia, \"Times New Roman\", Times, serif;\n//** Default monospace fonts for ``, ``, and `
`.\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n@font-family-base:        @font-family-sans-serif;\n\n@font-size-base:          14px;\n@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px\n@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px\n\n@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px\n@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px\n@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px\n@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px\n@font-size-h5:            @font-size-base;\n@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px\n\n//** Unit-less `line-height` for use in components like buttons.\n@line-height-base:        1.428571429; // 20/14\n//** Computed \"line-height\" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.\n@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px\n\n//** By default, this inherits from the ``.\n@headings-font-family:    inherit;\n@headings-font-weight:    500;\n@headings-line-height:    1.1;\n@headings-color:          inherit;\n\n\n//-- Iconography\n//\n//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.\n\n@icon-font-path:          \"../fonts/\";\n@icon-font-name:          \"glyphicons-halflings-regular\";\n@icon-font-svg-id:        \"glyphicons_halflingsregular\";\n\n//== Components\n//\n//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).\n\n@padding-base-vertical:     6px;\n@padding-base-horizontal:   12px;\n\n@padding-large-vertical:    10px;\n@padding-large-horizontal:  16px;\n\n@padding-small-vertical:    5px;\n@padding-small-horizontal:  10px;\n\n@padding-xs-vertical:       1px;\n@padding-xs-horizontal:     5px;\n\n@line-height-large:         1.33;\n@line-height-small:         1.5;\n\n@border-radius-base:        4px;\n@border-radius-large:       6px;\n@border-radius-small:       3px;\n\n//** Global color for active items (e.g., navs or dropdowns).\n@component-active-color:    #fff;\n//** Global background color for active items (e.g., navs or dropdowns).\n@component-active-bg:       @brand-primary;\n\n//** Width of the `border` for generating carets that indicator dropdowns.\n@caret-width-base:          4px;\n//** Carets increase slightly in size for larger components.\n@caret-width-large:         5px;\n\n\n//== Tables\n//\n//## Customizes the `.table` component with basic values, each used across all table variations.\n\n//** Padding for ``s and ``s.\n@table-cell-padding:            8px;\n//** Padding for cells in `.table-condensed`.\n@table-condensed-cell-padding:  5px;\n\n//** Default background color used for all tables.\n@table-bg:                      transparent;\n//** Background color used for `.table-striped`.\n@table-bg-accent:               #f9f9f9;\n//** Background color used for `.table-hover`.\n@table-bg-hover:                #f5f5f5;\n@table-bg-active:               @table-bg-hover;\n\n//** Border color for table and cell borders.\n@table-border-color:            #ddd;\n\n\n//== Buttons\n//\n//## For each of Bootstrap's buttons, define text, background and border color.\n\n@btn-font-weight:                normal;\n\n@btn-default-color:              #333;\n@btn-default-bg:                 #fff;\n@btn-default-border:             #ccc;\n\n@btn-primary-color:              #fff;\n@btn-primary-bg:                 @brand-primary;\n@btn-primary-border:             darken(@btn-primary-bg, 5%);\n\n@btn-success-color:              #fff;\n@btn-success-bg:                 @brand-success;\n@btn-success-border:             darken(@btn-success-bg, 5%);\n\n@btn-info-color:                 #fff;\n@btn-info-bg:                    @brand-info;\n@btn-info-border:                darken(@btn-info-bg, 5%);\n\n@btn-warning-color:              #fff;\n@btn-warning-bg:                 @brand-warning;\n@btn-warning-border:             darken(@btn-warning-bg, 5%);\n\n@btn-danger-color:               #fff;\n@btn-danger-bg:                  @brand-danger;\n@btn-danger-border:              darken(@btn-danger-bg, 5%);\n\n@btn-link-disabled-color:        @gray-light;\n\n\n//== Forms\n//\n//##\n\n//** `` background color\n@input-bg:                       #fff;\n//** `` background color\n@input-bg-disabled:              @gray-lighter;\n\n//** Text color for ``s\n@input-color:                    @gray;\n//** `` border color\n@input-border:                   #ccc;\n//** `` border radius\n@input-border-radius:            @border-radius-base;\n//** Border color for inputs on focus\n@input-border-focus:             #66afe9;\n\n//** Placeholder text color\n@input-color-placeholder:        @gray-light;\n\n//** Default `.form-control` height\n@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);\n//** Large `.form-control` height\n@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);\n//** Small `.form-control` height\n@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);\n\n@legend-color:                   @gray-dark;\n@legend-border-color:            #e5e5e5;\n\n//** Background color for textual input addons\n@input-group-addon-bg:           @gray-lighter;\n//** Border color for textual input addons\n@input-group-addon-border-color: @input-border;\n\n\n//== Dropdowns\n//\n//## Dropdown menu container and contents.\n\n//** Background for the dropdown menu.\n@dropdown-bg:                    #fff;\n//** Dropdown menu `border-color`.\n@dropdown-border:                rgba(0,0,0,.15);\n//** Dropdown menu `border-color` **for IE8**.\n@dropdown-fallback-border:       #ccc;\n//** Divider color for between dropdown items.\n@dropdown-divider-bg:            #e5e5e5;\n\n//** Dropdown link text color.\n@dropdown-link-color:            @gray-dark;\n//** Hover color for dropdown links.\n@dropdown-link-hover-color:      darken(@gray-dark, 5%);\n//** Hover background for dropdown links.\n@dropdown-link-hover-bg:         #f5f5f5;\n\n//** Active dropdown menu item text color.\n@dropdown-link-active-color:     @component-active-color;\n//** Active dropdown menu item background color.\n@dropdown-link-active-bg:        @component-active-bg;\n\n//** Disabled dropdown menu item background color.\n@dropdown-link-disabled-color:   @gray-light;\n\n//** Text color for headers within dropdown menus.\n@dropdown-header-color:          @gray-light;\n\n// Note: Deprecated @dropdown-caret-color as of v3.1.0\n@dropdown-caret-color:           #000;\n\n\n//-- Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n//\n// Note: These variables are not generated into the Customizer.\n\n@zindex-navbar:            1000;\n@zindex-dropdown:          1000;\n@zindex-popover:           1010;\n@zindex-tooltip:           1030;\n@zindex-navbar-fixed:      1030;\n@zindex-modal-background:  1040;\n@zindex-modal:             1050;\n\n\n//== Media queries breakpoints\n//\n//## Define the breakpoints at which your layout will change, adapting to different screen sizes.\n\n// Extra small screen / phone\n// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1\n@screen-xs:                  480px;\n@screen-xs-min:              @screen-xs;\n@screen-phone:               @screen-xs-min;\n\n// Small screen / tablet\n// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1\n@screen-sm:                  768px;\n@screen-sm-min:              @screen-sm;\n@screen-tablet:              @screen-sm-min;\n\n// Medium screen / desktop\n// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1\n@screen-md:                  992px;\n@screen-md-min:              @screen-md;\n@screen-desktop:             @screen-md-min;\n\n// Large screen / wide desktop\n// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1\n@screen-lg:                  1200px;\n@screen-lg-min:              @screen-lg;\n@screen-lg-desktop:          @screen-lg-min;\n\n// So media queries don't overlap when required, provide a maximum\n@screen-xs-max:              (@screen-sm-min - 1);\n@screen-sm-max:              (@screen-md-min - 1);\n@screen-md-max:              (@screen-lg-min - 1);\n\n\n//== Grid system\n//\n//## Define your custom responsive grid.\n\n//** Number of columns in the grid.\n@grid-columns:              12;\n//** Padding between columns. Gets divided in half for the left and right.\n@grid-gutter-width:         30px;\n// Navbar collapse\n//** Point at which the navbar becomes uncollapsed.\n@grid-float-breakpoint:     @screen-sm-min;\n//** Point at which the navbar begins collapsing.\n@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);\n\n\n//== Container sizes\n//\n//## Define the maximum width of `.container` for different screen sizes.\n\n// Small screen / tablet\n@container-tablet:             ((720px + @grid-gutter-width));\n//** For `@screen-sm-min` and up.\n@container-sm:                 @container-tablet;\n\n// Medium screen / desktop\n@container-desktop:            ((940px + @grid-gutter-width));\n//** For `@screen-md-min` and up.\n@container-md:                 @container-desktop;\n\n// Large screen / wide desktop\n@container-large-desktop:      ((1140px + @grid-gutter-width));\n//** For `@screen-lg-min` and up.\n@container-lg:                 @container-large-desktop;\n\n\n//== Navbar\n//\n//##\n\n// Basics of a navbar\n@navbar-height:                    50px;\n@navbar-margin-bottom:             @line-height-computed;\n@navbar-border-radius:             @border-radius-base;\n@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));\n@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);\n@navbar-collapse-max-height:       340px;\n\n@navbar-default-color:             #777;\n@navbar-default-bg:                #f8f8f8;\n@navbar-default-border:            darken(@navbar-default-bg, 6.5%);\n\n// Navbar links\n@navbar-default-link-color:                #777;\n@navbar-default-link-hover-color:          #333;\n@navbar-default-link-hover-bg:             transparent;\n@navbar-default-link-active-color:         #555;\n@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);\n@navbar-default-link-disabled-color:       #ccc;\n@navbar-default-link-disabled-bg:          transparent;\n\n// Navbar brand label\n@navbar-default-brand-color:               @navbar-default-link-color;\n@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);\n@navbar-default-brand-hover-bg:            transparent;\n\n// Navbar toggle\n@navbar-default-toggle-hover-bg:           #ddd;\n@navbar-default-toggle-icon-bar-bg:        #888;\n@navbar-default-toggle-border-color:       #ddd;\n\n\n// Inverted navbar\n// Reset inverted navbar basics\n@navbar-inverse-color:                      @gray-light;\n@navbar-inverse-bg:                         #222;\n@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);\n\n// Inverted navbar links\n@navbar-inverse-link-color:                 @gray-light;\n@navbar-inverse-link-hover-color:           #fff;\n@navbar-inverse-link-hover-bg:              transparent;\n@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;\n@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);\n@navbar-inverse-link-disabled-color:        #444;\n@navbar-inverse-link-disabled-bg:           transparent;\n\n// Inverted navbar brand label\n@navbar-inverse-brand-color:                @navbar-inverse-link-color;\n@navbar-inverse-brand-hover-color:          #fff;\n@navbar-inverse-brand-hover-bg:             transparent;\n\n// Inverted navbar toggle\n@navbar-inverse-toggle-hover-bg:            #333;\n@navbar-inverse-toggle-icon-bar-bg:         #fff;\n@navbar-inverse-toggle-border-color:        #333;\n\n\n//== Navs\n//\n//##\n\n//=== Shared nav styles\n@nav-link-padding:                          10px 15px;\n@nav-link-hover-bg:                         @gray-lighter;\n\n@nav-disabled-link-color:                   @gray-light;\n@nav-disabled-link-hover-color:             @gray-light;\n\n@nav-open-link-hover-color:                 #fff;\n\n//== Tabs\n@nav-tabs-border-color:                     #ddd;\n\n@nav-tabs-link-hover-border-color:          @gray-lighter;\n\n@nav-tabs-active-link-hover-bg:             @body-bg;\n@nav-tabs-active-link-hover-color:          @gray;\n@nav-tabs-active-link-hover-border-color:   #ddd;\n\n@nav-tabs-justified-link-border-color:            #ddd;\n@nav-tabs-justified-active-link-border-color:     @body-bg;\n\n//== Pills\n@nav-pills-border-radius:                   @border-radius-base;\n@nav-pills-active-link-hover-bg:            @component-active-bg;\n@nav-pills-active-link-hover-color:         @component-active-color;\n\n\n//== Pagination\n//\n//##\n\n@pagination-color:                     @link-color;\n@pagination-bg:                        #fff;\n@pagination-border:                    #ddd;\n\n@pagination-hover-color:               @link-hover-color;\n@pagination-hover-bg:                  @gray-lighter;\n@pagination-hover-border:              #ddd;\n\n@pagination-active-color:              #fff;\n@pagination-active-bg:                 @brand-primary;\n@pagination-active-border:             @brand-primary;\n\n@pagination-disabled-color:            @gray-light;\n@pagination-disabled-bg:               #fff;\n@pagination-disabled-border:           #ddd;\n\n\n//== Pager\n//\n//##\n\n@pager-bg:                             @pagination-bg;\n@pager-border:                         @pagination-border;\n@pager-border-radius:                  15px;\n\n@pager-hover-bg:                       @pagination-hover-bg;\n\n@pager-active-bg:                      @pagination-active-bg;\n@pager-active-color:                   @pagination-active-color;\n\n@pager-disabled-color:                 @pagination-disabled-color;\n\n\n//== Jumbotron\n//\n//##\n\n@jumbotron-padding:              30px;\n@jumbotron-color:                inherit;\n@jumbotron-bg:                   @gray-lighter;\n@jumbotron-heading-color:        inherit;\n@jumbotron-font-size:            ceil((@font-size-base * 1.5));\n\n\n//== Form states and alerts\n//\n//## Define colors for form feedback states and, by default, alerts.\n\n@state-success-text:             #3c763d;\n@state-success-bg:               #dff0d8;\n@state-success-border:           darken(spin(@state-success-bg, -10), 5%);\n\n@state-info-text:                #31708f;\n@state-info-bg:                  #d9edf7;\n@state-info-border:              darken(spin(@state-info-bg, -10), 7%);\n\n@state-warning-text:             #8a6d3b;\n@state-warning-bg:               #fcf8e3;\n@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);\n\n@state-danger-text:              #a94442;\n@state-danger-bg:                #f2dede;\n@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);\n\n\n//== Tooltips\n//\n//##\n\n//** Tooltip max width\n@tooltip-max-width:           200px;\n//** Tooltip text color\n@tooltip-color:               #fff;\n//** Tooltip background color\n@tooltip-bg:                  #000;\n@tooltip-opacity:             .9;\n\n//** Tooltip arrow width\n@tooltip-arrow-width:         5px;\n//** Tooltip arrow color\n@tooltip-arrow-color:         @tooltip-bg;\n\n\n//== Popovers\n//\n//##\n\n//** Popover body background color\n@popover-bg:                          #fff;\n//** Popover maximum width\n@popover-max-width:                   276px;\n//** Popover border color\n@popover-border-color:                rgba(0,0,0,.2);\n//** Popover fallback border color\n@popover-fallback-border-color:       #ccc;\n\n//** Popover title background color\n@popover-title-bg:                    darken(@popover-bg, 3%);\n\n//** Popover arrow width\n@popover-arrow-width:                 10px;\n//** Popover arrow color\n@popover-arrow-color:                 #fff;\n\n//** Popover outer arrow width\n@popover-arrow-outer-width:           (@popover-arrow-width + 1);\n//** Popover outer arrow color\n@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);\n//** Popover outer arrow fallback color\n@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);\n\n\n//== Labels\n//\n//##\n\n//** Default label background color\n@label-default-bg:            @gray-light;\n//** Primary label background color\n@label-primary-bg:            @brand-primary;\n//** Success label background color\n@label-success-bg:            @brand-success;\n//** Info label background color\n@label-info-bg:               @brand-info;\n//** Warning label background color\n@label-warning-bg:            @brand-warning;\n//** Danger label background color\n@label-danger-bg:             @brand-danger;\n\n//** Default label text color\n@label-color:                 #fff;\n//** Default text color of a linked label\n@label-link-hover-color:      #fff;\n\n\n//== Modals\n//\n//##\n\n//** Padding applied to the modal body\n@modal-inner-padding:         20px;\n\n//** Padding applied to the modal title\n@modal-title-padding:         15px;\n//** Modal title line-height\n@modal-title-line-height:     @line-height-base;\n\n//** Background color of modal content area\n@modal-content-bg:                             #fff;\n//** Modal content border color\n@modal-content-border-color:                   rgba(0,0,0,.2);\n//** Modal content border color **for IE8**\n@modal-content-fallback-border-color:          #999;\n\n//** Modal backdrop background color\n@modal-backdrop-bg:           #000;\n//** Modal backdrop opacity\n@modal-backdrop-opacity:      .5;\n//** Modal header border color\n@modal-header-border-color:   #e5e5e5;\n//** Modal footer border color\n@modal-footer-border-color:   @modal-header-border-color;\n\n@modal-lg:                    900px;\n@modal-md:                    600px;\n@modal-sm:                    300px;\n\n\n//== Alerts\n//\n//## Define alert colors, border radius, and padding.\n\n@alert-padding:               15px;\n@alert-border-radius:         @border-radius-base;\n@alert-link-font-weight:      bold;\n\n@alert-success-bg:            @state-success-bg;\n@alert-success-text:          @state-success-text;\n@alert-success-border:        @state-success-border;\n\n@alert-info-bg:               @state-info-bg;\n@alert-info-text:             @state-info-text;\n@alert-info-border:           @state-info-border;\n\n@alert-warning-bg:            @state-warning-bg;\n@alert-warning-text:          @state-warning-text;\n@alert-warning-border:        @state-warning-border;\n\n@alert-danger-bg:             @state-danger-bg;\n@alert-danger-text:           @state-danger-text;\n@alert-danger-border:         @state-danger-border;\n\n\n//== Progress bars\n//\n//##\n\n//** Background color of the whole progress component\n@progress-bg:                 #f5f5f5;\n//** Progress bar text color\n@progress-bar-color:          #fff;\n\n//** Default progress bar color\n@progress-bar-bg:             @brand-primary;\n//** Success progress bar color\n@progress-bar-success-bg:     @brand-success;\n//** Warning progress bar color\n@progress-bar-warning-bg:     @brand-warning;\n//** Danger progress bar color\n@progress-bar-danger-bg:      @brand-danger;\n//** Info progress bar color\n@progress-bar-info-bg:        @brand-info;\n\n\n//== List group\n//\n//##\n\n//** Background color on `.list-group-item`\n@list-group-bg:                 #fff;\n//** `.list-group-item` border color\n@list-group-border:             #ddd;\n//** List group border radius\n@list-group-border-radius:      @border-radius-base;\n\n//** Background color of single list elements on hover\n@list-group-hover-bg:           #f5f5f5;\n//** Text color of active list elements\n@list-group-active-color:       @component-active-color;\n//** Background color of active list elements\n@list-group-active-bg:          @component-active-bg;\n//** Border color of active list elements\n@list-group-active-border:      @list-group-active-bg;\n@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);\n\n@list-group-link-color:         #555;\n@list-group-link-heading-color: #333;\n\n\n//== Panels\n//\n//##\n\n@panel-bg:                    #fff;\n@panel-body-padding:          15px;\n@panel-border-radius:         @border-radius-base;\n\n//** Border color for elements within panels\n@panel-inner-border:          #ddd;\n@panel-footer-bg:             #f5f5f5;\n\n@panel-default-text:          @gray-dark;\n@panel-default-border:        #ddd;\n@panel-default-heading-bg:    #f5f5f5;\n\n@panel-primary-text:          #fff;\n@panel-primary-border:        @brand-primary;\n@panel-primary-heading-bg:    @brand-primary;\n\n@panel-success-text:          @state-success-text;\n@panel-success-border:        @state-success-border;\n@panel-success-heading-bg:    @state-success-bg;\n\n@panel-info-text:             @state-info-text;\n@panel-info-border:           @state-info-border;\n@panel-info-heading-bg:       @state-info-bg;\n\n@panel-warning-text:          @state-warning-text;\n@panel-warning-border:        @state-warning-border;\n@panel-warning-heading-bg:    @state-warning-bg;\n\n@panel-danger-text:           @state-danger-text;\n@panel-danger-border:         @state-danger-border;\n@panel-danger-heading-bg:     @state-danger-bg;\n\n\n//== Thumbnails\n//\n//##\n\n//** Padding around the thumbnail image\n@thumbnail-padding:           4px;\n//** Thumbnail background color\n@thumbnail-bg:                @body-bg;\n//** Thumbnail border color\n@thumbnail-border:            #ddd;\n//** Thumbnail border radius\n@thumbnail-border-radius:     @border-radius-base;\n\n//** Custom text color for thumbnail captions\n@thumbnail-caption-color:     @text-color;\n//** Padding around the thumbnail caption\n@thumbnail-caption-padding:   9px;\n\n\n//== Wells\n//\n//##\n\n@well-bg:                     #f5f5f5;\n@well-border:                 darken(@well-bg, 7%);\n\n\n//== Badges\n//\n//##\n\n@badge-color:                 #fff;\n//** Linked badge text color on hover\n@badge-link-hover-color:      #fff;\n@badge-bg:                    @gray-light;\n\n//** Badge text color in active nav link\n@badge-active-color:          @link-color;\n//** Badge background color in active nav link\n@badge-active-bg:             #fff;\n\n@badge-font-weight:           bold;\n@badge-line-height:           1;\n@badge-border-radius:         10px;\n\n\n//== Breadcrumbs\n//\n//##\n\n@breadcrumb-padding-vertical:   8px;\n@breadcrumb-padding-horizontal: 15px;\n//** Breadcrumb background color\n@breadcrumb-bg:                 #f5f5f5;\n//** Breadcrumb text color\n@breadcrumb-color:              #ccc;\n//** Text color of current page in the breadcrumb\n@breadcrumb-active-color:       @gray-light;\n//** Textual separator for between breadcrumb elements\n@breadcrumb-separator:          \"/\";\n\n\n//== Carousel\n//\n//##\n\n@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);\n\n@carousel-control-color:                      #fff;\n@carousel-control-width:                      15%;\n@carousel-control-opacity:                    .5;\n@carousel-control-font-size:                  20px;\n\n@carousel-indicator-active-bg:                #fff;\n@carousel-indicator-border-color:             #fff;\n\n@carousel-caption-color:                      #fff;\n\n\n//== Close\n//\n//##\n\n@close-font-weight:           bold;\n@close-color:                 #000;\n@close-text-shadow:           0 1px 0 #fff;\n\n\n//== Code\n//\n//##\n\n@code-color:                  #c7254e;\n@code-bg:                     #f9f2f4;\n\n@kbd-color:                   #fff;\n@kbd-bg:                      #333;\n\n@pre-bg:                      #f5f5f5;\n@pre-color:                   @gray-dark;\n@pre-border-color:            #ccc;\n@pre-scrollable-max-height:   340px;\n\n\n//== Type\n//\n//##\n\n//** Text muted color\n@text-muted:                  @gray-light;\n//** Abbreviations and acronyms border color\n@abbr-border-color:           @gray-light;\n//** Headings small color\n@headings-small-color:        @gray-light;\n//** Blockquote small color\n@blockquote-small-color:      @gray-light;\n//** Blockquote font size\n@blockquote-font-size:        (@font-size-base * 1.25);\n//** Blockquote border color\n@blockquote-border-color:     @gray-lighter;\n//** Page header border color\n@page-header-border-color:    @gray-lighter;\n\n\n//== Miscellaneous\n//\n//##\n\n//** Horizontal line color.\n@hr-border:                   @gray-lighter;\n\n//** Horizontal offset for forms and lists.\n@component-offset-horizontal: 180px;\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n  display: block;\n  padding: @thumbnail-padding;\n  margin-bottom: @line-height-computed;\n  line-height: @line-height-base;\n  background-color: @thumbnail-bg;\n  border: 1px solid @thumbnail-border;\n  border-radius: @thumbnail-border-radius;\n  .transition(all .2s ease-in-out);\n\n  > img,\n  a > img {\n    &:extend(.img-responsive);\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  // Add a hover state for linked versions only\n  a&:hover,\n  a&:focus,\n  a&.active {\n    border-color: @link-color;\n  }\n\n  // Image captions\n  .caption {\n    padding: @thumbnail-caption-padding;\n    color: @thumbnail-caption-color;\n  }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n  position: relative;\n}\n\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n\n  > .item {\n    display: none;\n    position: relative;\n    .transition(.6s ease-in-out left);\n\n    // Account for jankitude on images\n    > img,\n    > a > img {\n      &:extend(.img-responsive);\n      line-height: 1;\n    }\n  }\n\n  > .active,\n  > .next,\n  > .prev { display: block; }\n\n  > .active {\n    left: 0;\n  }\n\n  > .next,\n  > .prev {\n    position: absolute;\n    top: 0;\n    width: 100%;\n  }\n\n  > .next {\n    left: 100%;\n  }\n  > .prev {\n    left: -100%;\n  }\n  > .next.left,\n  > .prev.right {\n    left: 0;\n  }\n\n  > .active.left {\n    left: -100%;\n  }\n  > .active.right {\n    left: 100%;\n  }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: @carousel-control-width;\n  .opacity(@carousel-control-opacity);\n  font-size: @carousel-control-font-size;\n  color: @carousel-control-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  // We can't have this transition here because WebKit cancels the carousel\n  // animation if you trip this while in the middle of another animation.\n\n  // Set gradients for backgrounds\n  &.left {\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n  }\n  &.right {\n    left: auto;\n    right: 0;\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n  }\n\n  // Hover/focus state\n  &:hover,\n  &:focus {\n    outline: none;\n    color: @carousel-control-color;\n    text-decoration: none;\n    .opacity(.9);\n  }\n\n  // Toggles\n  .icon-prev,\n  .icon-next,\n  .glyphicon-chevron-left,\n  .glyphicon-chevron-right {\n    position: absolute;\n    top: 50%;\n    z-index: 5;\n    display: inline-block;\n  }\n  .icon-prev,\n  .glyphicon-chevron-left {\n    left: 50%;\n  }\n  .icon-next,\n  .glyphicon-chevron-right {\n    right: 50%;\n  }\n  .icon-prev,\n  .icon-next {\n    width:  20px;\n    height: 20px;\n    margin-top: -10px;\n    margin-left: -10px;\n    font-family: serif;\n  }\n\n  .icon-prev {\n    &:before {\n      content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n    }\n  }\n  .icon-next {\n    &:before {\n      content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n    }\n  }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n\n  li {\n    display: inline-block;\n    width:  10px;\n    height: 10px;\n    margin: 1px;\n    text-indent: -999px;\n    border: 1px solid @carousel-indicator-border-color;\n    border-radius: 10px;\n    cursor: pointer;\n\n    // IE8-9 hack for event handling\n    //\n    // Internet Explorer 8-9 does not support clicks on elements without a set\n    // `background-color`. We cannot use `filter` since that's not viewed as a\n    // background color by the browser. Thus, a hack is needed.\n    //\n    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n    // set alpha transparency for the best results possible.\n    background-color: #000 \\9; // IE8\n    background-color: rgba(0,0,0,0); // IE9\n  }\n  .active {\n    margin: 0;\n    width:  12px;\n    height: 12px;\n    background-color: @carousel-indicator-active-bg;\n  }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: @carousel-caption-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  & .btn {\n    text-shadow: none; // No shadow for button elements in carousel-caption\n  }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n  // Scale up the controls a smidge\n  .carousel-control {\n    .glyphicon-chevron-left,\n    .glyphicon-chevron-right,\n    .icon-prev,\n    .icon-next {\n      width: 30px;\n      height: 30px;\n      margin-top: -15px;\n      margin-left: -15px;\n      font-size: 30px;\n    }\n  }\n\n  // Show and left align the captions\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n\n  // Move up the indicators\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n  font-family: @headings-font-family;\n  font-weight: @headings-font-weight;\n  line-height: @headings-line-height;\n  color: @headings-color;\n\n  small,\n  .small {\n    font-weight: normal;\n    line-height: 1;\n    color: @headings-small-color;\n  }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n  margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n  margin-bottom: @line-height-computed;\n  font-size: floor((@font-size-base * 1.15));\n  font-weight: 200;\n  line-height: 1.4;\n\n  @media (min-width: @screen-sm-min) {\n    font-size: (@font-size-base * 1.5);\n  }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: 14px base font * 85% = about 12px\nsmall,\n.small  { font-size: 85%; }\n\n// Undo browser default styling\ncite    { font-style: normal; }\n\n// Alignment\n.text-left           { text-align: left; }\n.text-right          { text-align: right; }\n.text-center         { text-align: center; }\n.text-justify        { text-align: justify; }\n\n// Contextual colors\n.text-muted {\n  color: @text-muted;\n}\n.text-primary {\n  .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n  .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n  .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n  .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n  .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n  // Given the contrast here, this is the only class to have its color inverted\n  // automatically.\n  color: #fff;\n  .bg-variant(@brand-primary);\n}\n.bg-success {\n  .bg-variant(@state-success-bg);\n}\n.bg-info {\n  .bg-variant(@state-info-bg);\n}\n.bg-warning {\n  .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n  .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n  padding-bottom: ((@line-height-computed / 2) - 1);\n  margin: (@line-height-computed * 2) 0 @line-height-computed;\n  border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// --------------------------------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n  margin-top: 0;\n  margin-bottom: (@line-height-computed / 2);\n  ul,\n  ol {\n    margin-bottom: 0;\n  }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n  .list-unstyled();\n  margin-left: -5px;\n\n  > li {\n    display: inline-block;\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n}\n\n// Description Lists\ndl {\n  margin-top: 0; // Remove browser default\n  margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n  line-height: @line-height-base;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n@media (min-width: @grid-float-breakpoint) {\n  .dl-horizontal {\n    dt {\n      float: left;\n      width: (@component-offset-horizontal - 20);\n      clear: left;\n      text-align: right;\n      .text-overflow();\n    }\n    dd {\n      margin-left: @component-offset-horizontal;\n      &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n    }\n  }\n}\n\n// MISC\n// ----\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n  padding: (@line-height-computed / 2) @line-height-computed;\n  margin: 0 0 @line-height-computed;\n  font-size: @blockquote-font-size;\n  border-left: 5px solid @blockquote-border-color;\n\n  p,\n  ul,\n  ol {\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  // Note: Deprecated small and .small as of v3.1.0\n  // Context: https://github.com/twbs/bootstrap/issues/11660\n  footer,\n  small,\n  .small {\n    display: block;\n    font-size: 80%; // back to default font-size\n    line-height: @line-height-base;\n    color: @blockquote-small-color;\n\n    &:before {\n      content: '\\2014 \\00A0'; // em dash, nbsp\n    }\n  }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid @blockquote-border-color;\n  border-left: 0;\n  text-align: right;\n\n  // Account for citation\n  footer,\n  small,\n  .small {\n    &:before { content: ''; }\n    &:after {\n      content: '\\00A0 \\2014'; // nbsp, em dash\n    }\n  }\n}\n\n// Quotes\nblockquote:before,\nblockquote:after {\n  content: \"\";\n}\n\n// Addresses\naddress {\n  margin-bottom: @line-height-computed;\n  font-style: normal;\n  line-height: @line-height-base;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n  font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @code-color;\n  background-color: @code-bg;\n  white-space: nowrap;\n  border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @kbd-color;\n  background-color: @kbd-bg;\n  border-radius: @border-radius-small;\n  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n}\n\n// Blocks of code\npre {\n  display: block;\n  padding: ((@line-height-computed - 1) / 2);\n  margin: 0 0 (@line-height-computed / 2);\n  font-size: (@font-size-base - 1); // 14px to 13px\n  line-height: @line-height-base;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: @pre-color;\n  background-color: @pre-bg;\n  border: 1px solid @pre-border-color;\n  border-radius: @border-radius-base;\n\n  // Account for some code outputs that place code tags in pre tags\n  code {\n    padding: 0;\n    font-size: inherit;\n    color: inherit;\n    white-space: pre-wrap;\n    background-color: transparent;\n    border-radius: 0;\n  }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n  max-height: @pre-scrollable-max-height;\n  overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n  .container-fixed();\n\n  @media (min-width: @screen-sm-min) {\n    width: @container-sm;\n  }\n  @media (min-width: @screen-md-min) {\n    width: @container-md;\n  }\n  @media (min-width: @screen-lg-min) {\n    width: @container-lg;\n  }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n  .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n  .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n  .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n  .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n  .make-grid(lg);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n  max-width: 100%;\n  background-color: @table-bg;\n}\nth {\n  text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n  width: 100%;\n  margin-bottom: @line-height-computed;\n  // Cells\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-cell-padding;\n        line-height: @line-height-base;\n        vertical-align: top;\n        border-top: 1px solid @table-border-color;\n      }\n    }\n  }\n  // Bottom align for column headings\n  > thead > tr > th {\n    vertical-align: bottom;\n    border-bottom: 2px solid @table-border-color;\n  }\n  // Remove top border from thead by default\n  > caption + thead,\n  > colgroup + thead,\n  > thead:first-child {\n    > tr:first-child {\n      > th,\n      > td {\n        border-top: 0;\n      }\n    }\n  }\n  // Account for multiple tbody instances\n  > tbody + tbody {\n    border-top: 2px solid @table-border-color;\n  }\n\n  // Nesting\n  .table {\n    background-color: @body-bg;\n  }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-condensed-cell-padding;\n      }\n    }\n  }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n  border: 1px solid @table-border-color;\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        border: 1px solid @table-border-color;\n      }\n    }\n  }\n  > thead > tr {\n    > th,\n    > td {\n      border-bottom-width: 2px;\n    }\n  }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n  > tbody > tr:nth-child(odd) {\n    > td,\n    > th {\n      background-color: @table-bg-accent;\n    }\n  }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n  > tbody > tr:hover {\n    > td,\n    > th {\n      background-color: @table-bg-hover;\n    }\n  }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n  position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n  float: none;\n  display: table-column;\n}\ntable {\n  td,\n  th {\n    &[class*=\"col-\"] {\n      position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n      float: none;\n      display: table-cell;\n    }\n  }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n@media (max-width: @screen-xs-max) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: (@line-height-computed * 0.75);\n    overflow-y: hidden;\n    overflow-x: scroll;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid @table-border-color;\n    -webkit-overflow-scrolling: touch;\n\n    // Tighten up spacing\n    > .table {\n      margin-bottom: 0;\n\n      // Ensure the content doesn't wrap\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th,\n          > td {\n            white-space: nowrap;\n          }\n        }\n      }\n    }\n\n    // Special overrides for the bordered tables\n    > .table-bordered {\n      border: 0;\n\n      // Nuke the appropriate borders so that the parent can handle them\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th:first-child,\n          > td:first-child {\n            border-left: 0;\n          }\n          > th:last-child,\n          > td:last-child {\n            border-right: 0;\n          }\n        }\n      }\n\n      // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n      // chances are there will be only one `tr` in a `thead` and that would\n      // remove the border altogether.\n      > tbody,\n      > tfoot {\n        > tr:last-child {\n          > th,\n          > td {\n            border-bottom: 0;\n          }\n        }\n      }\n\n    }\n  }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets,\n  // so we reset that to ensure it behaves more like a standard block element.\n  // See https://github.com/twbs/bootstrap/issues/12359.\n  min-width: 0;\n}\n\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: @line-height-computed;\n  font-size: (@font-size-base * 1.5);\n  line-height: inherit;\n  color: @legend-color;\n  border: 0;\n  border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n  display: inline-block;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n  .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9; /* IE8-9 */\n  line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n  display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n  height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  .tab-focus();\n}\n\n// Adjust output element\noutput {\n  display: block;\n  padding-top: (@padding-base-vertical + 1);\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n  display: block;\n  width: 100%;\n  height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n  background-color: @input-bg;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid @input-border;\n  border-radius: @input-border-radius;\n  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n  .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n  // Customize the `:focus` state to imitate native WebKit styles.\n  .form-control-focus();\n\n  // Placeholder\n  .placeholder();\n\n  // Disabled and read-only inputs\n  //\n  // HTML5 says that controls under a fieldset > legend:first-child won't be\n  // disabled if the fieldset is disabled. Due to implementation difficulty, we\n  // don't honor that edge case; we style them as disabled anyway.\n  &[disabled],\n  &[readonly],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    background-color: @input-bg-disabled;\n    opacity: 1; // iOS fix for unreadable disabled content\n  }\n\n  // Reset height for `textarea`s\n  textarea& {\n    height: auto;\n  }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n\n\n// Special styles for iOS date input\n//\n// In Mobile Safari, date inputs require a pixel line-height that matches the\n// given height of the input.\n\ninput[type=\"date\"] {\n  line-height: @input-height-base;\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n  margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n  display: block;\n  min-height: @line-height-computed; // clear the floating input if there is no label text\n  margin-top: 10px;\n  margin-bottom: 10px;\n  padding-left: 20px;\n  label {\n    display: inline;\n    font-weight: normal;\n    cursor: pointer;\n  }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  float: left;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n//\n// Note: Neither radios nor checkboxes can be readonly.\ninput[type=\"radio\"],\ninput[type=\"checkbox\"],\n.radio,\n.radio-inline,\n.checkbox,\n.checkbox-inline {\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n  }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n\n.input-sm {\n  .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n.input-lg {\n  .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n\n// Form control feedback states\n//\n// Apply contextual and semantic states to individual form controls.\n\n.has-feedback {\n  // Enable absolute positioning\n  position: relative;\n\n  // Ensure icons don't overlap text\n  .form-control {\n    padding-right: (@input-height-base * 1.25);\n  }\n\n  // Feedback icon (requires .glyphicon classes)\n  .form-control-feedback {\n    position: absolute;\n    top: (@line-height-computed + 5); // Height of the `label` and its margin\n    right: 0;\n    display: block;\n    width: @input-height-base;\n    height: @input-height-base;\n    line-height: @input-height-base;\n    text-align: center;\n  }\n}\n\n// Feedback states\n.has-success {\n  .form-control-validation(@state-success-text; @state-success-text; @state-success-bg);\n}\n.has-warning {\n  .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);\n}\n.has-error {\n  .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);\n}\n\n\n// Static form control text\n//\n// Apply class to a `p` element to make any string of text align with labels in\n// a horizontal form layout.\n\n.form-control-static {\n  margin-bottom: 0; // Remove default margin from `p`\n}\n\n\n// Help text\n//\n// Apply to any element you wish to create light text for placement immediately\n// below a form control. Use for general help, formatting, or instructional text.\n\n.help-block {\n  display: block; // account for any element using help-block\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: lighten(@text-color, 25%); // lighten the text some for contrast\n}\n\n\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n//\n// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.\n\n.form-inline {\n\n  // Kick in the inline\n  @media (min-width: @screen-sm-min) {\n    // Inline-block all the things for \"inline\"\n    .form-group {\n      display: inline-block;\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // In navbar-form, allow folks to *not* use `.form-group`\n    .form-control {\n      display: inline-block;\n      width: auto; // Prevent labels from stacking above inputs in `.form-group`\n      vertical-align: middle;\n    }\n    // Input groups need that 100% width though\n    .input-group > .form-control {\n      width: 100%;\n    }\n\n    .control-label {\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // Remove default margin on radios/checkboxes that were used for stacking, and\n    // then undo the floating of radios and checkboxes to match (which also avoids\n    // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969).\n    .radio,\n    .checkbox {\n      display: inline-block;\n      margin-top: 0;\n      margin-bottom: 0;\n      padding-left: 0;\n      vertical-align: middle;\n    }\n    .radio input[type=\"radio\"],\n    .checkbox input[type=\"checkbox\"] {\n      float: none;\n      margin-left: 0;\n    }\n\n    // Validation states\n    //\n    // Reposition the icon because it's now within a grid column and columns have\n    // `position: relative;` on them. Also accounts for the grid gutter padding.\n    .has-feedback .form-control-feedback {\n      top: 0;\n    }\n  }\n}\n\n\n// Horizontal forms\n//\n// Horizontal forms are built on grid classes and allow you to create forms with\n// labels on the left and inputs on the right.\n\n.form-horizontal {\n\n  // Consistent vertical alignment of labels, radios, and checkboxes\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-top: (@padding-base-vertical + 1); // Default padding plus a border\n  }\n  // Account for padding we're adding to ensure the alignment and of help text\n  // and other content below items\n  .radio,\n  .checkbox {\n    min-height: (@line-height-computed + (@padding-base-vertical + 1));\n  }\n\n  // Make form groups behave like rows\n  .form-group {\n    .make-row();\n  }\n\n  .form-control-static {\n    padding-top: (@padding-base-vertical + 1);\n  }\n\n  // Only right align form labels here when the columns stop stacking\n  @media (min-width: @screen-sm-min) {\n    .control-label {\n      text-align: right;\n    }\n  }\n\n  // Validation states\n  //\n  // Reposition the icon because it's now within a grid column and columns have\n  // `position: relative;` on them. Also accounts for the grid gutter padding.\n  .has-feedback .form-control-feedback {\n    top: 0;\n    right: (@grid-gutter-width / 2);\n  }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n  display: inline-block;\n  margin-bottom: 0; // For input.btn\n  font-weight: @btn-font-weight;\n  text-align: center;\n  vertical-align: middle;\n  cursor: pointer;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  white-space: nowrap;\n  .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base);\n  .user-select(none);\n\n  &,\n  &:active,\n  &.active {\n    &:focus {\n      .tab-focus();\n    }\n  }\n\n  &:hover,\n  &:focus {\n    color: @btn-default-color;\n    text-decoration: none;\n  }\n\n  &:active,\n  &.active {\n    outline: 0;\n    background-image: none;\n    .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n  }\n\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    pointer-events: none; // Future-proof disabling of clicks\n    .opacity(.65);\n    .box-shadow(none);\n  }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n  .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n  .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n  .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n  .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n  .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n  .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n  color: @link-color;\n  font-weight: normal;\n  cursor: pointer;\n  border-radius: 0;\n\n  &,\n  &:active,\n  &[disabled],\n  fieldset[disabled] & {\n    background-color: transparent;\n    .box-shadow(none);\n  }\n  &,\n  &:hover,\n  &:focus,\n  &:active {\n    border-color: transparent;\n  }\n  &:hover,\n  &:focus {\n    color: @link-hover-color;\n    text-decoration: underline;\n    background-color: transparent;\n  }\n  &[disabled],\n  fieldset[disabled] & {\n    &:hover,\n    &:focus {\n      color: @btn-link-disabled-color;\n      text-decoration: none;\n    }\n  }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n  // line-height: ensure even-numbered height of button next to large input\n  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n.btn-sm {\n  // line-height: ensure proper height of button next to small input\n  .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n.btn-xs {\n  .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n  display: block;\n  width: 100%;\n  padding-left: 0;\n  padding-right: 0;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n  &.btn-block {\n    width: 100%;\n  }\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle; // match .btn alignment given font-size hack above\n  > .btn {\n    position: relative;\n    float: left;\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      z-index: 2;\n    }\n    &:focus {\n      // Remove focus outline when dropdown JS adds it after closing the menu\n      outline: none;\n    }\n  }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n  .btn + .btn,\n  .btn + .btn-group,\n  .btn-group + .btn,\n  .btn-group + .btn-group {\n    margin-left: -1px;\n  }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n  margin-left: -5px; // Offset the first child's margin\n  &:extend(.clearfix all);\n\n  .btn-group,\n  .input-group {\n    float: left;\n  }\n  > .btn,\n  > .btn-group,\n  > .input-group {\n    margin-left: 5px;\n  }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n  margin-left: 0;\n  &:not(:last-child):not(.dropdown-toggle) {\n    .border-right-radius(0);\n  }\n}\n// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-right-radius(0);\n  }\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n  .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n  .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n  // Show no shadow for `.btn-link` since it has no other button styles.\n  &.btn-link {\n    .box-shadow(none);\n  }\n}\n\n\n// Reposition the caret\n.btn .caret {\n  margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n  border-width: @caret-width-large @caret-width-large 0;\n  border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n  border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n  > .btn,\n  > .btn-group,\n  > .btn-group > .btn {\n    display: block;\n    float: none;\n    width: 100%;\n    max-width: 100%;\n  }\n\n  // Clear floats so dropdown menus can be properly placed\n  > .btn-group {\n    &:extend(.clearfix all);\n    > .btn {\n      float: none;\n    }\n  }\n\n  > .btn + .btn,\n  > .btn + .btn-group,\n  > .btn-group + .btn,\n  > .btn-group + .btn-group {\n    margin-top: -1px;\n    margin-left: 0;\n  }\n}\n\n.btn-group-vertical > .btn {\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n  &:first-child:not(:last-child) {\n    border-top-right-radius: @border-radius-base;\n    .border-bottom-radius(0);\n  }\n  &:last-child:not(:first-child) {\n    border-bottom-left-radius: @border-radius-base;\n    .border-top-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-bottom-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  .border-top-radius(0);\n}\n\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n  > .btn,\n  > .btn-group {\n    float: none;\n    display: table-cell;\n    width: 1%;\n  }\n  > .btn-group .btn {\n    width: 100%;\n  }\n}\n\n\n// Checkbox and radio options\n[data-toggle=\"buttons\"] > .btn > input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn > input[type=\"checkbox\"] {\n  display: none;\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552.\n\n.fade {\n  opacity: 0;\n  .transition(opacity .15s linear);\n  &.in {\n    opacity: 1;\n  }\n}\n\n.collapse {\n  display: none;\n  &.in {\n    display: block;\n  }\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  .transition(height .35s ease);\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n//  Star\n\n// Import the fonts\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot')\";\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')\";\n}\n\n// Catchall baseclass\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk               { &:before { content: \"\\2a\"; } }\n.glyphicon-plus                   { &:before { content: \"\\2b\"; } }\n.glyphicon-euro                   { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus                  { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud                  { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope               { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil                 { &:before { content: \"\\270f\"; } }\n.glyphicon-glass                  { &:before { content: \"\\e001\"; } }\n.glyphicon-music                  { &:before { content: \"\\e002\"; } }\n.glyphicon-search                 { &:before { content: \"\\e003\"; } }\n.glyphicon-heart                  { &:before { content: \"\\e005\"; } }\n.glyphicon-star                   { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty             { &:before { content: \"\\e007\"; } }\n.glyphicon-user                   { &:before { content: \"\\e008\"; } }\n.glyphicon-film                   { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large               { &:before { content: \"\\e010\"; } }\n.glyphicon-th                     { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list                { &:before { content: \"\\e012\"; } }\n.glyphicon-ok                     { &:before { content: \"\\e013\"; } }\n.glyphicon-remove                 { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in                { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out               { &:before { content: \"\\e016\"; } }\n.glyphicon-off                    { &:before { content: \"\\e017\"; } }\n.glyphicon-signal                 { &:before { content: \"\\e018\"; } }\n.glyphicon-cog                    { &:before { content: \"\\e019\"; } }\n.glyphicon-trash                  { &:before { content: \"\\e020\"; } }\n.glyphicon-home                   { &:before { content: \"\\e021\"; } }\n.glyphicon-file                   { &:before { content: \"\\e022\"; } }\n.glyphicon-time                   { &:before { content: \"\\e023\"; } }\n.glyphicon-road                   { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt           { &:before { content: \"\\e025\"; } }\n.glyphicon-download               { &:before { content: \"\\e026\"; } }\n.glyphicon-upload                 { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox                  { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle            { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat                 { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh                { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt               { &:before { content: \"\\e032\"; } }\n.glyphicon-lock                   { &:before { content: \"\\e033\"; } }\n.glyphicon-flag                   { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones             { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off             { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down            { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up              { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode                 { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode                { &:before { content: \"\\e040\"; } }\n.glyphicon-tag                    { &:before { content: \"\\e041\"; } }\n.glyphicon-tags                   { &:before { content: \"\\e042\"; } }\n.glyphicon-book                   { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark               { &:before { content: \"\\e044\"; } }\n.glyphicon-print                  { &:before { content: \"\\e045\"; } }\n.glyphicon-camera                 { &:before { content: \"\\e046\"; } }\n.glyphicon-font                   { &:before { content: \"\\e047\"; } }\n.glyphicon-bold                   { &:before { content: \"\\e048\"; } }\n.glyphicon-italic                 { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height            { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width             { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left             { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center           { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right            { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify          { &:before { content: \"\\e055\"; } }\n.glyphicon-list                   { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left            { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right           { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video         { &:before { content: \"\\e059\"; } }\n.glyphicon-picture                { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker             { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust                 { &:before { content: \"\\e063\"; } }\n.glyphicon-tint                   { &:before { content: \"\\e064\"; } }\n.glyphicon-edit                   { &:before { content: \"\\e065\"; } }\n.glyphicon-share                  { &:before { content: \"\\e066\"; } }\n.glyphicon-check                  { &:before { content: \"\\e067\"; } }\n.glyphicon-move                   { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward          { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward          { &:before { content: \"\\e070\"; } }\n.glyphicon-backward               { &:before { content: \"\\e071\"; } }\n.glyphicon-play                   { &:before { content: \"\\e072\"; } }\n.glyphicon-pause                  { &:before { content: \"\\e073\"; } }\n.glyphicon-stop                   { &:before { content: \"\\e074\"; } }\n.glyphicon-forward                { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward           { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward           { &:before { content: \"\\e077\"; } }\n.glyphicon-eject                  { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left           { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right          { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign              { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign             { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign            { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign                { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign          { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign              { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot             { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle          { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle              { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle             { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left             { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right            { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up               { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down             { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt              { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full            { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small           { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign       { &:before { content: \"\\e101\"; } }\n.glyphicon-gift                   { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf                   { &:before { content: \"\\e103\"; } }\n.glyphicon-fire                   { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open               { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close              { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign           { &:before { content: \"\\e107\"; } }\n.glyphicon-plane                  { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar               { &:before { content: \"\\e109\"; } }\n.glyphicon-random                 { &:before { content: \"\\e110\"; } }\n.glyphicon-comment                { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet                 { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up             { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down           { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet                { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart          { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close           { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open            { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical        { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal      { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd                    { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn               { &:before { content: \"\\e122\"; } }\n.glyphicon-bell                   { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate            { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up              { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down            { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right             { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left              { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up                { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down              { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right     { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left      { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up        { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down      { &:before { content: \"\\e134\"; } }\n.glyphicon-globe                  { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench                 { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks                  { &:before { content: \"\\e137\"; } }\n.glyphicon-filter                 { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase              { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen             { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard              { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip              { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty            { &:before { content: \"\\e143\"; } }\n.glyphicon-link                   { &:before { content: \"\\e144\"; } }\n.glyphicon-phone                  { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin                { &:before { content: \"\\e146\"; } }\n.glyphicon-usd                    { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp                    { &:before { content: \"\\e149\"; } }\n.glyphicon-sort                   { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet       { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt   { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order          { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt      { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes     { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked              { &:before { content: \"\\e157\"; } }\n.glyphicon-expand                 { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down          { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up            { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in                 { &:before { content: \"\\e161\"; } }\n.glyphicon-flash                  { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out                { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window             { &:before { content: \"\\e164\"; } }\n.glyphicon-record                 { &:before { content: \"\\e165\"; } }\n.glyphicon-save                   { &:before { content: \"\\e166\"; } }\n.glyphicon-open                   { &:before { content: \"\\e167\"; } }\n.glyphicon-saved                  { &:before { content: \"\\e168\"; } }\n.glyphicon-import                 { &:before { content: \"\\e169\"; } }\n.glyphicon-export                 { &:before { content: \"\\e170\"; } }\n.glyphicon-send                   { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk            { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved           { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove          { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save            { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open            { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card            { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer               { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery                { &:before { content: \"\\e179\"; } }\n.glyphicon-header                 { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed             { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone               { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt              { &:before { content: \"\\e183\"; } }\n.glyphicon-tower                  { &:before { content: \"\\e184\"; } }\n.glyphicon-stats                  { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video               { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video               { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles              { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo           { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby            { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1              { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1              { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1              { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark         { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark      { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download         { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload           { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer           { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous         { &:before { content: \"\\e200\"; } }\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top:   @caret-width-base solid;\n  border-right: @caret-width-base solid transparent;\n  border-left:  @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropdown {\n  position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n  outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: @zindex-dropdown;\n  display: none; // none by default, but block on \"open\" of the menu\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0; // override default ul\n  list-style: none;\n  font-size: @font-size-base;\n  background-color: @dropdown-bg;\n  border: 1px solid @dropdown-fallback-border; // IE8 fallback\n  border: 1px solid @dropdown-border;\n  border-radius: @border-radius-base;\n  .box-shadow(0 6px 12px rgba(0,0,0,.175));\n  background-clip: padding-box;\n\n  // Aligns the dropdown menu to right\n  //\n  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n  &.pull-right {\n    right: 0;\n    left: auto;\n  }\n\n  // Dividers (basically an hr) within the dropdown\n  .divider {\n    .nav-divider(@dropdown-divider-bg);\n  }\n\n  // Links within the dropdown menu\n  > li > a {\n    display: block;\n    padding: 3px 20px;\n    clear: both;\n    font-weight: normal;\n    line-height: @line-height-base;\n    color: @dropdown-link-color;\n    white-space: nowrap; // prevent links from randomly breaking onto new lines\n  }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    color: @dropdown-link-hover-color;\n    background-color: @dropdown-link-hover-bg;\n  }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-active-color;\n    text-decoration: none;\n    outline: 0;\n    background-color: @dropdown-link-active-bg;\n  }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-disabled-color;\n  }\n}\n// Nuke hover/focus effects\n.dropdown-menu > .disabled > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: transparent;\n    background-image: none; // Remove CSS gradient\n    .reset-filter();\n    cursor: not-allowed;\n  }\n}\n\n// Open state for the dropdown\n.open {\n  // Show the menu\n  > .dropdown-menu {\n    display: block;\n  }\n\n  // Remove the outline when :focus is triggered\n  > a {\n    outline: 0;\n  }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n  left: auto; // Reset the default from `.dropdown-menu`\n  right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: @font-size-small;\n  line-height: @line-height-base;\n  color: @dropdown-header-color;\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n  // Reverse the caret\n  .caret {\n    border-top: 0;\n    border-bottom: @caret-width-base solid;\n    content: \"\";\n  }\n  // Different positioning for bottom up menu\n  .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n    margin-bottom: 1px;\n  }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-right {\n    .dropdown-menu {\n      .dropdown-menu-right();\n    }\n    // Necessary for overrides of the default right aligned menu.\n    // Will remove come v4 in all likelihood.\n    .dropdown-menu-left {\n      .dropdown-menu-left();\n    }\n  }\n}\n\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n  position: relative; // For dropdowns\n  display: table;\n  border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n  // Undo padding and float of grid classes\n  &[class*=\"col-\"] {\n    float: none;\n    padding-left: 0;\n    padding-right: 0;\n  }\n\n  .form-control {\n    // Ensure that the input is always above the *appended* addon button for\n    // proper border colors.\n    position: relative;\n    z-index: 2;\n\n    // IE9 fubars the placeholder attribute in text inputs and the arrows on\n    // select elements in input groups. To fix it, we float the input. Details:\n    // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n    float: left;\n\n    width: 100%;\n    margin-bottom: 0;\n  }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn { .input-lg(); }\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn { .input-sm(); }\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  font-weight: normal;\n  line-height: 1;\n  color: @input-color;\n  text-align: center;\n  background-color: @input-group-addon-bg;\n  border: 1px solid @input-group-addon-border-color;\n  border-radius: @border-radius-base;\n\n  // Sizing\n  &.input-sm {\n    padding: @padding-small-vertical @padding-small-horizontal;\n    font-size: @font-size-small;\n    border-radius: @border-radius-small;\n  }\n  &.input-lg {\n    padding: @padding-large-vertical @padding-large-horizontal;\n    font-size: @font-size-large;\n    border-radius: @border-radius-large;\n  }\n\n  // Nuke default margins from checkboxes and radios to vertically center within.\n  input[type=\"radio\"],\n  input[type=\"checkbox\"] {\n    margin-top: 0;\n  }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  .border-right-radius(0);\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  .border-left-radius(0);\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n  position: relative;\n  // Jankily prevent input button groups from wrapping with `white-space` and\n  // `font-size` in combination with `inline-block` on buttons.\n  font-size: 0;\n  white-space: nowrap;\n\n  // Negative margin for spacing, position for bringing hovered/focused/actived\n  // element above the siblings.\n  > .btn {\n    position: relative;\n    + .btn {\n      margin-left: -1px;\n    }\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active {\n      z-index: 2;\n    }\n  }\n\n  // Negative margin to only have a 1px border between the two\n  &:first-child {\n    > .btn,\n    > .btn-group {\n      margin-right: -1px;\n    }\n  }\n  &:last-child {\n    > .btn,\n    > .btn-group {\n      margin-left: -1px;\n    }\n  }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n  margin-bottom: 0;\n  padding-left: 0; // Override default ul/ol\n  list-style: none;\n  &:extend(.clearfix all);\n\n  > li {\n    position: relative;\n    display: block;\n\n    > a {\n      position: relative;\n      display: block;\n      padding: @nav-link-padding;\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        background-color: @nav-link-hover-bg;\n      }\n    }\n\n    // Disabled state sets text to gray and nukes hover/tab effects\n    &.disabled > a {\n      color: @nav-disabled-link-color;\n\n      &:hover,\n      &:focus {\n        color: @nav-disabled-link-hover-color;\n        text-decoration: none;\n        background-color: transparent;\n        cursor: not-allowed;\n      }\n    }\n  }\n\n  // Open dropdowns\n  .open > a {\n    &,\n    &:hover,\n    &:focus {\n      background-color: @nav-link-hover-bg;\n      border-color: @link-color;\n    }\n  }\n\n  // Nav dividers (deprecated with v3.0.1)\n  //\n  // This should have been removed in v3 with the dropping of `.nav-list`, but\n  // we missed it. We don't currently support this anywhere, but in the interest\n  // of maintaining backward compatibility in case you use it, it's deprecated.\n  .nav-divider {\n    .nav-divider();\n  }\n\n  // Prevent IE8 from misplacing imgs\n  //\n  // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n  > li > a > img {\n    max-width: none;\n  }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n  border-bottom: 1px solid @nav-tabs-border-color;\n  > li {\n    float: left;\n    // Make the list-items overlay the bottom border\n    margin-bottom: -1px;\n\n    // Actual tabs (as links)\n    > a {\n      margin-right: 2px;\n      line-height: @line-height-base;\n      border: 1px solid transparent;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n      &:hover {\n        border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n      }\n    }\n\n    // Active state, and its :hover to override normal :hover\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-tabs-active-link-hover-color;\n        background-color: @nav-tabs-active-link-hover-bg;\n        border: 1px solid @nav-tabs-active-link-hover-border-color;\n        border-bottom-color: transparent;\n        cursor: default;\n      }\n    }\n  }\n  // pulling this in mainly for less shorthand\n  &.nav-justified {\n    .nav-justified();\n    .nav-tabs-justified();\n  }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n  > li {\n    float: left;\n\n    // Links rendered as pills\n    > a {\n      border-radius: @nav-pills-border-radius;\n    }\n    + li {\n      margin-left: 2px;\n    }\n\n    // Active state\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-pills-active-link-hover-color;\n        background-color: @nav-pills-active-link-hover-bg;\n      }\n    }\n  }\n}\n\n\n// Stacked pills\n.nav-stacked {\n  > li {\n    float: none;\n    + li {\n      margin-top: 2px;\n      margin-left: 0; // no need for this gap between nav items\n    }\n  }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n  width: 100%;\n\n  > li {\n    float: none;\n     > a {\n      text-align: center;\n      margin-bottom: 5px;\n    }\n  }\n\n  > .dropdown .dropdown-menu {\n    top: auto;\n    left: auto;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li {\n      display: table-cell;\n      width: 1%;\n      > a {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n  border-bottom: 0;\n\n  > li > a {\n    // Override margin from .nav-tabs\n    margin-right: 0;\n    border-radius: @border-radius-base;\n  }\n\n  > .active > a,\n  > .active > a:hover,\n  > .active > a:focus {\n    border: 1px solid @nav-tabs-justified-link-border-color;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li > a {\n      border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n    }\n    > .active > a,\n    > .active > a:hover,\n    > .active > a:focus {\n      border-bottom-color: @nav-tabs-justified-active-link-border-color;\n    }\n  }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n  > .tab-pane {\n    display: none;\n  }\n  > .active {\n    display: block;\n  }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n  // make dropdown border overlap tab border\n  margin-top: -1px;\n  // Remove the top rounded corners here since there is a hard edge above the menu\n  .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n  position: relative;\n  min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n  margin-bottom: @navbar-margin-bottom;\n  border: 1px solid transparent;\n\n  // Prevent floats from breaking the navbar\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: @navbar-border-radius;\n  }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n  }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n  max-height: @navbar-collapse-max-height;\n  overflow-x: visible;\n  padding-right: @navbar-padding-horizontal;\n  padding-left:  @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n  &:extend(.clearfix all);\n  -webkit-overflow-scrolling: touch;\n\n  &.in {\n    overflow-y: auto;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n\n    &.collapse {\n      display: block !important;\n      height: auto !important;\n      padding-bottom: 0; // Override default setting\n      overflow: visible !important;\n    }\n\n    &.in {\n      overflow-y: visible;\n    }\n\n    // Undo the collapse side padding for navbars with containers to ensure\n    // alignment of right-aligned contents.\n    .navbar-fixed-top &,\n    .navbar-static-top &,\n    .navbar-fixed-bottom & {\n      padding-left: 0;\n      padding-right: 0;\n    }\n  }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n  > .navbar-header,\n  > .navbar-collapse {\n    margin-right: -@navbar-padding-horizontal;\n    margin-left:  -@navbar-padding-horizontal;\n\n    @media (min-width: @grid-float-breakpoint) {\n      margin-right: 0;\n      margin-left:  0;\n    }\n  }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n  z-index: @zindex-navbar;\n  border-width: 0 0 1px;\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: @zindex-navbar-fixed;\n\n  // Undo the rounded corners\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0; // override .navbar defaults\n  border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n  float: left;\n  padding: @navbar-padding-vertical @navbar-padding-horizontal;\n  font-size: @font-size-large;\n  line-height: @line-height-computed;\n  height: @navbar-height;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    .navbar > .container &,\n    .navbar > .container-fluid & {\n      margin-left: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: @navbar-padding-horizontal;\n  padding: 9px 10px;\n  .navbar-vertical-align(34px);\n  background-color: transparent;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  border-radius: @border-radius-base;\n\n  // We remove the `outline` here, but later compensate by attaching `:hover`\n  // styles to `:focus`.\n  &:focus {\n    outline: none;\n  }\n\n  // Bars\n  .icon-bar {\n    display: block;\n    width: 22px;\n    height: 2px;\n    border-radius: 1px;\n  }\n  .icon-bar + .icon-bar {\n    margin-top: 4px;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    display: none;\n  }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n  margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n  > li > a {\n    padding-top:    10px;\n    padding-bottom: 10px;\n    line-height: @line-height-computed;\n  }\n\n  @media (max-width: @grid-float-breakpoint-max) {\n    // Dropdowns get custom display when collapsed\n    .open .dropdown-menu {\n      position: static;\n      float: none;\n      width: auto;\n      margin-top: 0;\n      background-color: transparent;\n      border: 0;\n      box-shadow: none;\n      > li > a,\n      .dropdown-header {\n        padding: 5px 15px 5px 25px;\n      }\n      > li > a {\n        line-height: @line-height-computed;\n        &:hover,\n        &:focus {\n          background-image: none;\n        }\n      }\n    }\n  }\n\n  // Uncollapse the nav\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin: 0;\n\n    > li {\n      float: left;\n      > a {\n        padding-top:    @navbar-padding-vertical;\n        padding-bottom: @navbar-padding-vertical;\n      }\n    }\n\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-left  { .pull-left(); }\n  .navbar-right { .pull-right(); }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n  margin-left: -@navbar-padding-horizontal;\n  margin-right: -@navbar-padding-horizontal;\n  padding: 10px @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n  .box-shadow(@shadow);\n\n  // Mixin behavior for optimum display\n  .form-inline();\n\n  .form-group {\n    @media (max-width: @grid-float-breakpoint-max) {\n      margin-bottom: 5px;\n    }\n  }\n\n  // Vertically center in expanded, horizontal navbar\n  .navbar-vertical-align(@input-height-base);\n\n  // Undo 100% width for pull classes\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    .box-shadow(none);\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n  .navbar-vertical-align(@input-height-base);\n\n  &.btn-sm {\n    .navbar-vertical-align(@input-height-small);\n  }\n  &.btn-xs {\n    .navbar-vertical-align(22);\n  }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n  .navbar-vertical-align(@line-height-computed);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin-left: @navbar-padding-horizontal;\n    margin-right: @navbar-padding-horizontal;\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n  background-color: @navbar-default-bg;\n  border-color: @navbar-default-border;\n\n  .navbar-brand {\n    color: @navbar-default-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-default-brand-hover-color;\n      background-color: @navbar-default-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-default-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-default-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-hover-color;\n        background-color: @navbar-default-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-active-color;\n        background-color: @navbar-default-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-disabled-color;\n        background-color: @navbar-default-link-disabled-bg;\n      }\n    }\n  }\n\n  .navbar-toggle {\n    border-color: @navbar-default-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-default-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-default-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: @navbar-default-border;\n  }\n\n  // Dropdown menu items\n  .navbar-nav {\n    // Remove background color from open dropdown\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-default-link-active-bg;\n        color: @navbar-default-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display when collapsed\n      .open .dropdown-menu {\n        > li > a {\n          color: @navbar-default-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-hover-color;\n            background-color: @navbar-default-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-active-color;\n            background-color: @navbar-default-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-disabled-color;\n            background-color: @navbar-default-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n\n  // Links in navbars\n  //\n  // Add a class to ensure links outside the navbar nav are colored correctly.\n\n  .navbar-link {\n    color: @navbar-default-link-color;\n    &:hover {\n      color: @navbar-default-link-hover-color;\n    }\n  }\n\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n  background-color: @navbar-inverse-bg;\n  border-color: @navbar-inverse-border;\n\n  .navbar-brand {\n    color: @navbar-inverse-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-inverse-brand-hover-color;\n      background-color: @navbar-inverse-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-inverse-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-inverse-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-hover-color;\n        background-color: @navbar-inverse-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-active-color;\n        background-color: @navbar-inverse-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-disabled-color;\n        background-color: @navbar-inverse-link-disabled-bg;\n      }\n    }\n  }\n\n  // Darken the responsive nav toggle\n  .navbar-toggle {\n    border-color: @navbar-inverse-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-inverse-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-inverse-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: darken(@navbar-inverse-bg, 7%);\n  }\n\n  // Dropdowns\n  .navbar-nav {\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-inverse-link-active-bg;\n        color: @navbar-inverse-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display\n      .open .dropdown-menu {\n        > .dropdown-header {\n          border-color: @navbar-inverse-border;\n        }\n        .divider {\n          background-color: @navbar-inverse-border;\n        }\n        > li > a {\n          color: @navbar-inverse-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-hover-color;\n            background-color: @navbar-inverse-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-active-color;\n            background-color: @navbar-inverse-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-disabled-color;\n            background-color: @navbar-inverse-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n  .navbar-link {\n    color: @navbar-inverse-link-color;\n    &:hover {\n      color: @navbar-inverse-link-hover-color;\n    }\n  }\n\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n  .clearfix();\n}\n.center-block {\n  .center-block();\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n  display: none !important;\n  visibility: hidden !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n  position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n  padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n  margin-bottom: @line-height-computed;\n  list-style: none;\n  background-color: @breadcrumb-bg;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline-block;\n\n    + li:before {\n      content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n      padding: 0 5px;\n      color: @breadcrumb-color;\n    }\n  }\n\n  > .active {\n    color: @breadcrumb-active-color;\n  }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline; // Remove list-style and block-level defaults\n    > a,\n    > span {\n      position: relative;\n      float: left; // Collapse white-space\n      padding: @padding-base-vertical @padding-base-horizontal;\n      line-height: @line-height-base;\n      text-decoration: none;\n      color: @pagination-color;\n      background-color: @pagination-bg;\n      border: 1px solid @pagination-border;\n      margin-left: -1px;\n    }\n    &:first-child {\n      > a,\n      > span {\n        margin-left: 0;\n        .border-left-radius(@border-radius-base);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius-base);\n      }\n    }\n  }\n\n  > li > a,\n  > li > span {\n    &:hover,\n    &:focus {\n      color: @pagination-hover-color;\n      background-color: @pagination-hover-bg;\n      border-color: @pagination-hover-border;\n    }\n  }\n\n  > .active > a,\n  > .active > span {\n    &,\n    &:hover,\n    &:focus {\n      z-index: 2;\n      color: @pagination-active-color;\n      background-color: @pagination-active-bg;\n      border-color: @pagination-active-border;\n      cursor: default;\n    }\n  }\n\n  > .disabled {\n    > span,\n    > span:hover,\n    > span:focus,\n    > a,\n    > a:hover,\n    > a:focus {\n      color: @pagination-disabled-color;\n      background-color: @pagination-disabled-bg;\n      border-color: @pagination-disabled-border;\n      cursor: not-allowed;\n    }\n  }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n  .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n  .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small);\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  list-style: none;\n  text-align: center;\n  &:extend(.clearfix all);\n  li {\n    display: inline;\n    > a,\n    > span {\n      display: inline-block;\n      padding: 5px 14px;\n      background-color: @pager-bg;\n      border: 1px solid @pager-border;\n      border-radius: @pager-border-radius;\n    }\n\n    > a:hover,\n    > a:focus {\n      text-decoration: none;\n      background-color: @pager-hover-bg;\n    }\n  }\n\n  .next {\n    > a,\n    > span {\n      float: right;\n    }\n  }\n\n  .previous {\n    > a,\n    > span {\n      float: left;\n    }\n  }\n\n  .disabled {\n    > a,\n    > a:hover,\n    > a:focus,\n    > span {\n      color: @pager-disabled-color;\n      background-color: @pager-bg;\n      cursor: not-allowed;\n    }\n  }\n\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: @label-color;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n\n  // Add hover effects, but only for links\n  &[href] {\n    &:hover,\n    &:focus {\n      color: @label-link-hover-color;\n      text-decoration: none;\n      cursor: pointer;\n    }\n  }\n\n  // Empty labels collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for labels in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n  .label-variant(@label-default-bg);\n}\n\n.label-primary {\n  .label-variant(@label-primary-bg);\n}\n\n.label-success {\n  .label-variant(@label-success-bg);\n}\n\n.label-info {\n  .label-variant(@label-info-bg);\n}\n\n.label-warning {\n  .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n  .label-variant(@label-danger-bg);\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base classes\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: @font-size-small;\n  font-weight: @badge-font-weight;\n  color: @badge-color;\n  line-height: @badge-line-height;\n  vertical-align: baseline;\n  white-space: nowrap;\n  text-align: center;\n  background-color: @badge-bg;\n  border-radius: @badge-border-radius;\n\n  // Empty badges collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for badges in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n  .btn-xs & {\n    top: 0;\n    padding: 1px 5px;\n  }\n}\n\n// Hover state, but only for links\na.badge {\n  &:hover,\n  &:focus {\n    color: @badge-link-hover-color;\n    text-decoration: none;\n    cursor: pointer;\n  }\n}\n\n// Account for counters in navs\na.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: @badge-active-color;\n  background-color: @badge-active-bg;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n  padding: @jumbotron-padding;\n  margin-bottom: @jumbotron-padding;\n  color: @jumbotron-color;\n  background-color: @jumbotron-bg;\n\n  h1,\n  .h1 {\n    color: @jumbotron-heading-color;\n  }\n  p {\n    margin-bottom: (@jumbotron-padding / 2);\n    font-size: @jumbotron-font-size;\n    font-weight: 200;\n  }\n\n  .container & {\n    border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n  }\n\n  .container {\n    max-width: 100%;\n  }\n\n  @media screen and (min-width: @screen-sm-min) {\n    padding-top:    (@jumbotron-padding * 1.6);\n    padding-bottom: (@jumbotron-padding * 1.6);\n\n    .container & {\n      padding-left:  (@jumbotron-padding * 2);\n      padding-right: (@jumbotron-padding * 2);\n    }\n\n    h1,\n    .h1 {\n      font-size: (@font-size-base * 4.5);\n    }\n  }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n  padding: @alert-padding;\n  margin-bottom: @line-height-computed;\n  border: 1px solid transparent;\n  border-radius: @alert-border-radius;\n\n  // Headings for larger alerts\n  h4 {\n    margin-top: 0;\n    // Specified for the h4 to prevent conflicts of changing @headings-color\n    color: inherit;\n  }\n  // Provide class for links that match alerts\n  .alert-link {\n    font-weight: @alert-link-font-weight;\n  }\n\n  // Improve alignment and spacing of inner content\n  > p,\n  > ul {\n    margin-bottom: 0;\n  }\n  > p + p {\n    margin-top: 5px;\n  }\n}\n\n// Dismissable alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable {\n padding-right: (@alert-padding + 20);\n\n  // Adjust close link position\n  .close {\n    position: relative;\n    top: -2px;\n    right: -21px;\n    color: inherit;\n  }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n  .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n.alert-info {\n  .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n.alert-warning {\n  .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n.alert-danger {\n  .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n  overflow: hidden;\n  height: @line-height-computed;\n  margin-bottom: @line-height-computed;\n  background-color: @progress-bg;\n  border-radius: @border-radius-base;\n  .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: @font-size-small;\n  line-height: @line-height-computed;\n  color: @progress-bar-color;\n  text-align: center;\n  background-color: @progress-bar-bg;\n  .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n  .transition(width .6s ease);\n}\n\n// Striped bars\n.progress-striped .progress-bar {\n  #gradient > .striped();\n  background-size: 40px 40px;\n}\n\n// Call animation for the active one\n.progress.active .progress-bar {\n  .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n  .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n  .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n  .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n  .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Media objects\n// Source: http://stubbornella.org/content/?p=497\n// --------------------------------------------------\n\n\n// Common styles\n// -------------------------\n\n// Clear the floats\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n\n// Proper spacing between instances of .media\n.media,\n.media .media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n\n// For images and videos, set to block\n.media-object {\n  display: block;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n  margin: 0 0 5px;\n}\n\n\n// Media image alignment\n// -------------------------\n\n.media {\n  > .pull-left {\n    margin-right: 10px;\n  }\n  > .pull-right {\n    margin-left: 10px;\n  }\n}\n\n\n// Media list variation\n// -------------------------\n\n// Undo default ul/ol styles\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on 
    ,
      , or
      .\n\n.list-group {\n // No need to set list-style: none; since .list-group-item is block level\n margin-bottom: 20px;\n padding-left: 0; // reset padding because ul and ol\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n // Place the border on the list items and negative margin up for better styling\n margin-bottom: -1px;\n background-color: @list-group-bg;\n border: 1px solid @list-group-border;\n\n // Round the first and last items\n &:first-child {\n .border-top-radius(@list-group-border-radius);\n }\n &:last-child {\n margin-bottom: 0;\n .border-bottom-radius(@list-group-border-radius);\n }\n\n // Align badges within list items\n > .badge {\n float: right;\n }\n > .badge + .badge {\n margin-right: 5px;\n }\n}\n\n\n// Linked list items\n//\n// Use anchor elements instead of `li`s or `div`s to create linked list items.\n// Includes an extra `.active` modifier class for showing selected items.\n\na.list-group-item {\n color: @list-group-link-color;\n\n .list-group-item-heading {\n color: @list-group-link-heading-color;\n }\n\n // Hover state\n &:hover,\n &:focus {\n text-decoration: none;\n background-color: @list-group-hover-bg;\n }\n\n // Active class on item itself, not parent\n &.active,\n &.active:hover,\n &.active:focus {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: @list-group-active-color;\n background-color: @list-group-active-bg;\n border-color: @list-group-active-border;\n\n // Force color to inherit for custom content\n .list-group-item-heading {\n color: inherit;\n }\n .list-group-item-text {\n color: @list-group-active-text-color;\n }\n }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n.list-group-item-variant(success; @state-success-bg; @state-success-text);\n.list-group-item-variant(info; @state-info-bg; @state-info-text);\n.list-group-item-variant(warning; @state-warning-bg; @state-warning-text);\n.list-group-item-variant(danger; @state-danger-bg; @state-danger-text);\n\n\n// Custom content options\n//\n// Extra classes for creating well-formatted content within `.list-group-item`s.\n\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n","//\n// Panels\n// --------------------------------------------------\n\n\n// Base class\n.panel {\n margin-bottom: @line-height-computed;\n background-color: @panel-bg;\n border: 1px solid transparent;\n border-radius: @panel-border-radius;\n .box-shadow(0 1px 1px rgba(0,0,0,.05));\n}\n\n// Panel contents\n.panel-body {\n padding: @panel-body-padding;\n &:extend(.clearfix all);\n}\n\n// Optional heading\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n .border-top-radius((@panel-border-radius - 1));\n\n > .dropdown .dropdown-toggle {\n color: inherit;\n }\n}\n\n// Within heading, strip any `h*` tag of its default margins for spacing.\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: ceil((@font-size-base * 1.125));\n color: inherit;\n\n > a {\n color: inherit;\n }\n}\n\n// Optional footer (stays gray in every modifier class)\n.panel-footer {\n padding: 10px 15px;\n background-color: @panel-footer-bg;\n border-top: 1px solid @panel-inner-border;\n .border-bottom-radius((@panel-border-radius - 1));\n}\n\n\n// List groups in panels\n//\n// By default, space out list group content from panel headings to account for\n// any kind of custom content between the two.\n\n.panel {\n > .list-group {\n margin-bottom: 0;\n\n .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n }\n\n // Add border top radius for first one\n &:first-child {\n .list-group-item:first-child {\n border-top: 0;\n .border-top-radius((@panel-border-radius - 1));\n }\n }\n // Add border bottom radius for last one\n &:last-child {\n .list-group-item:last-child {\n border-bottom: 0;\n .border-bottom-radius((@panel-border-radius - 1));\n }\n }\n }\n}\n// Collapse space between when there's no additional content.\n.panel-heading + .list-group {\n .list-group-item:first-child {\n border-top-width: 0;\n }\n}\n\n\n// Tables in panels\n//\n// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and\n// watch it go full width.\n\n.panel {\n > .table,\n > .table-responsive > .table {\n margin-bottom: 0;\n }\n // Add border top radius for first one\n > .table:first-child,\n > .table-responsive:first-child > .table:first-child {\n .border-top-radius((@panel-border-radius - 1));\n\n > thead:first-child,\n > tbody:first-child {\n > tr:first-child {\n td:first-child,\n th:first-child {\n border-top-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-top-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n // Add border bottom radius for last one\n > .table:last-child,\n > .table-responsive:last-child > .table:last-child {\n .border-bottom-radius((@panel-border-radius - 1));\n\n > tbody:last-child,\n > tfoot:last-child {\n > tr:last-child {\n td:first-child,\n th:first-child {\n border-bottom-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-bottom-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n > .panel-body + .table,\n > .panel-body + .table-responsive {\n border-top: 1px solid @table-border-color;\n }\n > .table > tbody:first-child > tr:first-child th,\n > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n }\n > .table-bordered,\n > .table-responsive > .table-bordered {\n border: 0;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n > thead,\n > tbody {\n > tr:first-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n > tbody,\n > tfoot {\n > tr:last-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n }\n > .table-responsive {\n border: 0;\n margin-bottom: 0;\n }\n}\n\n\n// Collapsable panels (aka, accordion)\n//\n// Wrap a series of panels in `.panel-group` to turn them into an accordion with\n// the help of our collapse JavaScript plugin.\n\n.panel-group {\n margin-bottom: @line-height-computed;\n\n // Tighten up margin so it's only between panels\n .panel {\n margin-bottom: 0;\n border-radius: @panel-border-radius;\n overflow: hidden; // crop contents when collapsed\n + .panel {\n margin-top: 5px;\n }\n }\n\n .panel-heading {\n border-bottom: 0;\n + .panel-collapse .panel-body {\n border-top: 1px solid @panel-inner-border;\n }\n }\n .panel-footer {\n border-top: 0;\n + .panel-collapse .panel-body {\n border-bottom: 1px solid @panel-inner-border;\n }\n }\n}\n\n\n// Contextual variations\n.panel-default {\n .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border);\n}\n.panel-primary {\n .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border);\n}\n.panel-success {\n .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border);\n}\n.panel-info {\n .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border);\n}\n.panel-warning {\n .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border);\n}\n.panel-danger {\n .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border);\n}\n","//\n// Wells\n// --------------------------------------------------\n\n\n// Base class\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: @well-bg;\n border: 1px solid @well-border;\n border-radius: @border-radius-base;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));\n blockquote {\n border-color: #ddd;\n border-color: rgba(0,0,0,.15);\n }\n}\n\n// Sizes\n.well-lg {\n padding: 24px;\n border-radius: @border-radius-large;\n}\n.well-sm {\n padding: 9px;\n border-radius: @border-radius-small;\n}\n","//\n// Close icons\n// --------------------------------------------------\n\n\n.close {\n float: right;\n font-size: (@font-size-base * 1.5);\n font-weight: @close-font-weight;\n line-height: 1;\n color: @close-color;\n text-shadow: @close-text-shadow;\n .opacity(.2);\n\n &:hover,\n &:focus {\n color: @close-color;\n text-decoration: none;\n cursor: pointer;\n .opacity(.5);\n }\n\n // Additional properties for button version\n // iOS requires the button element instead of an anchor tag.\n // If you want the anchor version, it requires `href=\"#\"`.\n button& {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n }\n}\n","//\n// Modals\n// --------------------------------------------------\n\n// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n display: none;\n overflow: auto;\n overflow-y: scroll;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: @zindex-modal;\n -webkit-overflow-scrolling: touch;\n\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n\n // When fading in the modal, animate it to slide down\n &.fade .modal-dialog {\n .translate(0, -25%);\n .transition-transform(~\"0.3s ease-out\");\n }\n &.in .modal-dialog { .translate(0, 0)}\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n background-color: @modal-content-bg;\n border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)\n border: 1px solid @modal-content-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 3px 9px rgba(0,0,0,.5));\n background-clip: padding-box;\n // Remove focus outline from opened modal\n outline: none;\n}\n\n// Modal background\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: @zindex-modal-background;\n background-color: @modal-backdrop-bg;\n // Fade for backdrop\n &.fade { .opacity(0); }\n &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n padding: @modal-title-padding;\n border-bottom: 1px solid @modal-header-border-color;\n min-height: (@modal-title-padding + @modal-title-line-height);\n}\n// Close icon\n.modal-header .close {\n margin-top: -2px;\n}\n\n// Title text within header\n.modal-title {\n margin: 0;\n line-height: @modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n padding: @modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n margin-top: 15px;\n padding: (@modal-inner-padding - 1) @modal-inner-padding @modal-inner-padding;\n text-align: right; // right align buttons\n border-top: 1px solid @modal-footer-border-color;\n &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons\n\n // Properly space out buttons\n .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0; // account for input[type=\"submit\"] which gets the bottom margin like all other inputs\n }\n // but override that for button groups\n .btn-group .btn + .btn {\n margin-left: -1px;\n }\n // and override it for block buttons as well\n .btn-block + .btn-block {\n margin-left: 0;\n }\n}\n\n// Scale up the modal\n@media (min-width: @screen-sm-min) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n width: @modal-md;\n margin: 30px auto;\n }\n .modal-content {\n .box-shadow(0 5px 15px rgba(0,0,0,.5));\n }\n\n // Modal sizes\n .modal-sm { width: @modal-sm; }\n}\n\n@media (min-width: @screen-md-min) {\n .modal-lg { width: @modal-lg; }\n}\n","//\n// Tooltips\n// --------------------------------------------------\n\n\n// Base class\n.tooltip {\n position: absolute;\n z-index: @zindex-tooltip;\n display: block;\n visibility: visible;\n font-size: @font-size-small;\n line-height: 1.4;\n .opacity(0);\n\n &.in { .opacity(@tooltip-opacity); }\n &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; }\n &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; }\n &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; }\n &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: @tooltip-max-width;\n padding: 3px 8px;\n color: @tooltip-color;\n text-align: center;\n text-decoration: none;\n background-color: @tooltip-bg;\n border-radius: @border-radius-base;\n}\n\n// Arrows\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip {\n &.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-left .tooltip-arrow {\n bottom: 0;\n left: @tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-right .tooltip-arrow {\n bottom: 0;\n right: @tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0;\n border-right-color: @tooltip-arrow-color;\n }\n &.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-left-color: @tooltip-arrow-color;\n }\n &.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-left .tooltip-arrow {\n top: 0;\n left: @tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-right .tooltip-arrow {\n top: 0;\n right: @tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n}\n","//\n// Popovers\n// --------------------------------------------------\n\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: @zindex-popover;\n display: none;\n max-width: @popover-max-width;\n padding: 1px;\n text-align: left; // Reset given new insertion method\n background-color: @popover-bg;\n background-clip: padding-box;\n border: 1px solid @popover-fallback-border-color;\n border: 1px solid @popover-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 5px 10px rgba(0,0,0,.2));\n\n // Overrides for proper insertion\n white-space: normal;\n\n // Offset the popover to account for the popover arrow\n &.top { margin-top: -@popover-arrow-width; }\n &.right { margin-left: @popover-arrow-width; }\n &.bottom { margin-top: @popover-arrow-width; }\n &.left { margin-left: -@popover-arrow-width; }\n}\n\n.popover-title {\n margin: 0; // reset heading margin\n padding: 8px 14px;\n font-size: @font-size-base;\n font-weight: normal;\n line-height: 18px;\n background-color: @popover-title-bg;\n border-bottom: 1px solid darken(@popover-title-bg, 5%);\n border-radius: 5px 5px 0 0;\n}\n\n.popover-content {\n padding: 9px 14px;\n}\n\n// Arrows\n//\n// .arrow is outer, .arrow:after is inner\n\n.popover > .arrow {\n &,\n &:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n }\n}\n.popover > .arrow {\n border-width: @popover-arrow-outer-width;\n}\n.popover > .arrow:after {\n border-width: @popover-arrow-width;\n content: \"\";\n}\n\n.popover {\n &.top > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-top-color: @popover-arrow-outer-color;\n bottom: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n bottom: 1px;\n margin-left: -@popover-arrow-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-color;\n }\n }\n &.right > .arrow {\n top: 50%;\n left: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-right-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n left: 1px;\n bottom: -@popover-arrow-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-color;\n }\n }\n &.bottom > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-bottom-color: @popover-arrow-outer-color;\n top: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n top: 1px;\n margin-left: -@popover-arrow-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-color;\n }\n }\n\n &.left > .arrow {\n top: 50%;\n right: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-right-width: 0;\n border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-left-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: @popover-arrow-color;\n bottom: -@popover-arrow-width;\n }\n }\n\n}\n","//\n// Responsive: Utility classes\n// --------------------------------------------------\n\n\n// IE10 in Windows (Phone) 8\n//\n// Support for responsive views via media queries is kind of borked in IE10, for\n// Surface/desktop in split view and for Windows Phone 8. This particular fix\n// must be accompanied by a snippet of JavaScript to sniff the user agent and\n// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at\n// our Getting Started page for more information on this bug.\n//\n// For more information, see the following:\n//\n// Issue: https://github.com/twbs/bootstrap/issues/10497\n// Docs: http://getbootstrap.com/getting-started/#browsers\n// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/\n\n@-ms-viewport {\n width: device-width;\n}\n\n\n// Visibility utilities\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n .responsive-invisibility();\n}\n\n.visible-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-visibility();\n }\n}\n.visible-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-visibility();\n }\n}\n.visible-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-visibility();\n }\n}\n.visible-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-visibility();\n }\n}\n\n.hidden-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-invisibility();\n }\n}\n.hidden-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-invisibility();\n }\n}\n.hidden-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-invisibility();\n }\n}\n.hidden-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-invisibility();\n }\n}\n\n\n// Print utilities\n//\n// Media queries are placed on the inside to be mixin-friendly.\n\n.visible-print {\n .responsive-invisibility();\n\n @media print {\n .responsive-visibility();\n }\n}\n\n.hidden-print {\n @media print {\n .responsive-invisibility();\n }\n}\n"]} \ No newline at end of file diff --git a/sources/modules/bootstrap/css/bootstrap.min.css b/sources/modules/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..679272d --- /dev/null +++ b/sources/modules/bootstrap/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.eot b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..4a4ca86 Binary files /dev/null and b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.eot differ diff --git a/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.svg b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..e3e2dc7 --- /dev/null +++ b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.ttf b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..67fa00b Binary files /dev/null and b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.ttf differ diff --git a/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.woff b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..8c54182 Binary files /dev/null and b/sources/modules/bootstrap/fonts/glyphicons-halflings-regular.woff differ diff --git a/sources/modules/bootstrap/js/bootstrap.js b/sources/modules/bootstrap/js/bootstrap.js new file mode 100644 index 0000000..8ae571b --- /dev/null +++ b/sources/modules/bootstrap/js/bootstrap.js @@ -0,0 +1,1951 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } + +/* ======================================================================== + * Bootstrap: transition.js v3.1.1 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd', + 'MozTransition' : 'transitionend', + 'OTransition' : 'oTransitionEnd otransitionend', + 'transition' : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false, $el = this + $(this).one($.support.transition.end, function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.1.1 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent.trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one($.support.transition.end, removeElement) + .emulateTransitionEnd(150) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.1.1 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (!data.resetText) $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } + + if (changed) this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + e.preventDefault() + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.1.1 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getActiveIndex = function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + + return this.$items.index(this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getActiveIndex() + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + if ($next.hasClass('active')) return this.sliding = false + + var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid.bs.carousel', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) + }) + .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid.bs.carousel') + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + }) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + $carousel.carousel($carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.1.1 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null + + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() + } + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing') + [dimension](0) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in') + [dimension]('auto') + this.transitioning = 0 + this.$element.trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + [dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element + [dimension](this.$element[dimension]()) + [0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') option = !option + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + var target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + $target.collapse(option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.1.1 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(' diff --git a/sources/templates/show_box_top.inc.php b/sources/templates/show_box_top.inc.php new file mode 100644 index 0000000..b470fee --- /dev/null +++ b/sources/templates/show_box_top.inc.php @@ -0,0 +1,35 @@ + + +
      +
      +
      +
      +
      +
      + +

      + +
      diff --git a/sources/templates/show_broadcast_row.inc.php b/sources/templates/show_broadcast_row.inc.php new file mode 100644 index 0000000..543ffe9 --- /dev/null +++ b/sources/templates/show_broadcast_row.inc.php @@ -0,0 +1,35 @@ + + +   +
      + + id,'play', T_('Play'),'play_broadcast_' . $channel->id); ?> + +
      + +name; ?> +f_tags; ?> +started ? T_('Yes') : T_('No')); ?> +listeners; ?> +show_action_buttons(); ?> diff --git a/sources/templates/show_broadcasts.inc.php b/sources/templates/show_broadcasts.inc.php new file mode 100644 index 0000000..73a828b --- /dev/null +++ b/sources/templates/show_broadcasts.inc.php @@ -0,0 +1,54 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + +
      id . '&type=broadcast&sort=name', T_('Name'),'broadcast_sort_name'); ?>id . '&type=broadcast&sort=started', T_('Started'),'broadcast_sort_started'); ?>id . '&type=broadcast&sort=listeners', T_('Listeners'),'broadcast_sort_listeners'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> diff --git a/sources/templates/show_broadcasts_dialog.inc.php b/sources/templates/show_broadcasts_dialog.inc.php new file mode 100644 index 0000000..085eabd --- /dev/null +++ b/sources/templates/show_broadcasts_dialog.inc.php @@ -0,0 +1,40 @@ + + +
        +id); + foreach ($broadcasts as $broadcast_id) { + $broadcast = new Broadcast($broadcast_id); + $broadcast->format(); +?> +
      • + + f_name; ?> + +
      • + +

      + + + diff --git a/sources/templates/show_catalog_row.inc.php b/sources/templates/show_catalog_row.inc.php new file mode 100644 index 0000000..7205fec --- /dev/null +++ b/sources/templates/show_catalog_row.inc.php @@ -0,0 +1,45 @@ +enabled ? 'disable' : 'enable'; +$button_flip_state_id = 'button_flip_state_' . $catalog->id; +?> +f_name_link; ?> +f_info); ?> +f_update); ?> +f_add); ?> +f_clean); ?> + + + | + | + | + | + | + + | + id, $icon, T_(ucfirst($icon)),'flip_state_' . $catalog->id); ?> + + + diff --git a/sources/templates/show_catalog_types.inc.php b/sources/templates/show_catalog_types.inc.php new file mode 100644 index 0000000..e36bccc --- /dev/null +++ b/sources/templates/show_catalog_types.inc.php @@ -0,0 +1,70 @@ + + + + + + + + + + + + + format(); + if ($catalog->is_installed()) { + $action = 'confirm_uninstall_catalog_type'; + $action_txt = T_('Disable'); + } else { + $action = 'install_catalog_type'; + $action_txt = T_('Activate'); + } + ?> + + + + + + + + + + + + + + + + + + + + +
      get_type()); ?>get_description()); ?>get_version()); ?>
      +
      diff --git a/sources/templates/show_catalogs.inc.php b/sources/templates/show_catalogs.inc.php new file mode 100644 index 0000000..ddec4cb --- /dev/null +++ b/sources/templates/show_catalogs.inc.php @@ -0,0 +1,65 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + +
      + + + +
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_channel_row.inc.php b/sources/templates/show_channel_row.inc.php new file mode 100644 index 0000000..76bbfcc --- /dev/null +++ b/sources/templates/show_channel_row.inc.php @@ -0,0 +1,48 @@ + + +   +
      + + id,'play', T_('Play'),'play_channel_' . $channel->id); ?> + +
      + +id; ?> +name; ?> +interface; ?> +port; ?> +get_target_object()->f_name_link; ?> + +stream_type; ?> +bitrate; ?> +start_date); ?> +listeners; ?> + + get_stream_url(); ?>
      + is_private) { echo UI::get_icon('lock', T_('Authentication Required')); } ?> + get_stream_proxy_url(); ?> + +
      get_channel_state(); ?>
      +show_action_buttons(); ?> diff --git a/sources/templates/show_channels.inc.php b/sources/templates/show_channels.inc.php new file mode 100644 index 0000000..fbd0566 --- /dev/null +++ b/sources/templates/show_channels.inc.php @@ -0,0 +1,63 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> + + + + + + + + + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + +
      id . '&type=channel&sort=id', T_('#'),'channel_sort_id'); ?>id . '&type=channel&sort=name', T_('Name'),'channel_sort_name'); ?>id . '&type=channel&sort=interface', T_('Interface'),'channel_sort_interface'); ?>id . '&type=channel&sort=port', T_('Port'),'channel_sort_port'); ?>id . '&type=channel&sort=listeners', T_('Listeners'),'channel_sort_listeners'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> diff --git a/sources/templates/show_clean_catalog.inc.php b/sources/templates/show_clean_catalog.inc.php new file mode 100644 index 0000000..3c08994 --- /dev/null +++ b/sources/templates/show_clean_catalog.inc.php @@ -0,0 +1,29 @@ +[ $this->name ]"); +echo "...
      "; +echo T_('Checking') . ':
      '; +echo T_('Reading') . ':
      '; +UI::show_box_bottom(); diff --git a/sources/templates/show_concert_row.inc.php b/sources/templates/show_concert_row.inc.php new file mode 100644 index 0000000..202a2e7 --- /dev/null +++ b/sources/templates/show_concert_row.inc.php @@ -0,0 +1,26 @@ + + +startDate; ?> +venue->image) >= 1 && !empty($concert->venue->image[1])) ? '' : ''; ?> venue->name; ?> +venue->location->city; ?>, venue->location->country; ?> diff --git a/sources/templates/show_concerts.inc.php b/sources/templates/show_concerts.inc.php new file mode 100644 index 0000000..77d6f3b --- /dev/null +++ b/sources/templates/show_concerts.inc.php @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + +
      + diff --git a/sources/templates/show_confirmation.inc.php b/sources/templates/show_confirmation.inc.php new file mode 100644 index 0000000..f67d3d0 --- /dev/null +++ b/sources/templates/show_confirmation.inc.php @@ -0,0 +1,38 @@ + + + +
      +
      + + +
      + +
      + + +
      + + diff --git a/sources/templates/show_create_democratic.inc.php b/sources/templates/show_create_democratic.inc.php new file mode 100644 index 0000000..62d9263 --- /dev/null +++ b/sources/templates/show_create_democratic.inc.php @@ -0,0 +1,63 @@ + +
      + + + + + + + + + + + + + + + + + + + + + + + + +
      base_playlist); ?>
       ()
      + + +
      primary) echo "checked" ?> />
        
      +
      + + +
      +
      + diff --git a/sources/templates/show_debug.inc.php b/sources/templates/show_debug.inc.php new file mode 100644 index 0000000..07087be --- /dev/null +++ b/sources/templates/show_debug.inc.php @@ -0,0 +1,134 @@ + + +
      +
        +
      • + + +
      • +
      • + + +
      • +
      +
      + + + ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Open Basedir
      + + + + ++ + + + + + + + + + +$value) { + if ($key == 'database_password' || $key == 'mysql_password') { $value = '*********'; } + if (is_array($value)) { + $string = ''; + foreach ($value as $setting) { + $string .= $setting . '
      '; + } + $value = $string; + } + if (Preference::is_boolean($key)) { + $value = print_bool($value); + } +?> + + + + + + +
      + + + +
      : .
      +
      : .
      + + + diff --git a/sources/templates/show_democratic.inc.php b/sources/templates/show_democratic.inc.php new file mode 100644 index 0000000..399c612 --- /dev/null +++ b/sources/templates/show_democratic.inc.php @@ -0,0 +1,53 @@ +is_enabled() ? sprintf(T_('%s Playlist') ,$democratic->name) : T_('Democratic Playlist'); +UI::show_box_top($string , 'info-box'); +?> +
      +
        +is_enabled()) { ?> +
      • + :f_cooldown; ?> +
      • + + +
      • + +   + +
      • +is_enabled()) { ?> +
      • + id,'all', T_('Play'),'play_democratic'); ?> + id, T_('Play Democratic Playlist'),'play_democratic_full_text'); ?> +
      • +
      • + id,'delete', T_('Clear Playlist'),'clear_democratic'); ?> + id, T_('Clear Playlist'),'clear_democratic_full_text'); ?> +
      • + + +
      + +
      + diff --git a/sources/templates/show_democratic_playlist.inc.php b/sources/templates/show_democratic_playlist.inc.php new file mode 100644 index 0000000..bac1a68 --- /dev/null +++ b/sources/templates/show_democratic_playlist.inc.php @@ -0,0 +1,117 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + ++ + + + + + + + + + +base_playlist); +?> + + + + + + + + + + + + + + + + + + +set_parent(); +foreach ($object_ids as $item) { + if (!is_array($item)) { + $item = (array) $item; + } + $media = new $item['object_type']($item['object_id']); + $media->format(); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + . + +
      + has_vote($item['object_id'], $item['object_type'])) { ?> + + + id . '&type=' . scrub_out($item['object_type']),'tick', T_('Add Vote'),'remove_vote_' . $item['id']); ?> + + get_vote($item['id'])); ?>f_link; ?>f_album_link; ?>f_artist_link; ?>f_time; ?> + +
      +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_denied.inc.php b/sources/templates/show_denied.inc.php new file mode 100644 index 0000000..b2d8022 --- /dev/null +++ b/sources/templates/show_denied.inc.php @@ -0,0 +1,58 @@ + + + + + + Ampache -- Debug Page + + + + + + + +
      +
      +

      +

      +
      +
      + +

      +

      +

      + +

      + +
      +
      + + diff --git a/sources/templates/show_disabled_songs.inc.php b/sources/templates/show_disabled_songs.inc.php new file mode 100644 index 0000000..f892ae8 --- /dev/null +++ b/sources/templates/show_disabled_songs.inc.php @@ -0,0 +1,67 @@ + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      title; ?>get_album_name($song->album); ?>get_artist_name($song->album); ?>file; ?>addition_time); ?>
      +
      +    + +
      +
      diff --git a/sources/templates/show_duplicate.inc.php b/sources/templates/show_duplicate.inc.php new file mode 100644 index 0000000..52690a8 --- /dev/null +++ b/sources/templates/show_duplicate.inc.php @@ -0,0 +1,39 @@ + + +
      + + + + + +
      : +
      +
      +
      +
      +
      + +
      +
      + diff --git a/sources/templates/show_duplicates.inc.php b/sources/templates/show_duplicates.inc.php new file mode 100644 index 0000000..cb33065 --- /dev/null +++ b/sources/templates/show_duplicates.inc.php @@ -0,0 +1,77 @@ + + +
      + + + + + + + + + + + + $song_id) { + $song = new Song($song_id); + $song->format(); + $row_key = 'duplicate_' . $song_id; + $button_flip_state_id = 'button_flip_state_' . $song_id; + $current_class = ($key == '0') ? 'row-highlight' : UI::flip_class(); + $button = $song->enabled ? 'disable' : 'enable'; + ?> + + + + + + + + + + + + + + + + + + + + + +
      + + f_link; ?>f_artist_link; ?>f_album_link; ?>f_time; ?>f_bitrate; ?>f_size; ?>file); ?>
      +
      + diff --git a/sources/templates/show_dynamic.inc.php b/sources/templates/show_dynamic.inc.php new file mode 100644 index 0000000..c6551ee --- /dev/null +++ b/sources/templates/show_dynamic.inc.php @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + + + +
      + + + + + +
      +
      +
      + diff --git a/sources/templates/show_edit_access.inc.php b/sources/templates/show_edit_access.inc.php new file mode 100644 index 0000000..d6cc226 --- /dev/null +++ b/sources/templates/show_edit_access.inc.php @@ -0,0 +1,81 @@ + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      :
      : + +

      + (255.255.255.255) / (ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) +
      : + + + : + + +
      : + user); ?> +
      : + level; ${$name} = 'checked="checked"'; ?> + > + > + > + > +
      +
      + + +
      +
      + diff --git a/sources/templates/show_edit_album_row.inc.php b/sources/templates/show_edit_album_row.inc.php new file mode 100644 index 0000000..8a60812 --- /dev/null +++ b/sources/templates/show_edit_album_row.inc.php @@ -0,0 +1,68 @@ + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + artist_count == '1') {*/ + show_artist_select('artist', $album->artist_id); + /*} else { + echo T_('Various'); + }*/ + ?> +
      + +
      + + +
      +
      diff --git a/sources/templates/show_edit_artist_row.inc.php b/sources/templates/show_edit_artist_row.inc.php new file mode 100644 index 0000000..e34e02c --- /dev/null +++ b/sources/templates/show_edit_artist_row.inc.php @@ -0,0 +1,46 @@ + +
      +
      + + + + + + + + + + + + + + + + + +
      + + +
      +
      diff --git a/sources/templates/show_edit_broadcast_row.inc.php b/sources/templates/show_edit_broadcast_row.inc.php new file mode 100644 index 0000000..e4c096e --- /dev/null +++ b/sources/templates/show_edit_broadcast_row.inc.php @@ -0,0 +1,46 @@ + +
      +
      + + + + + + + + + + + + + + + + + +
      is_private) ? 'checked' : ''; ?> />
      + + +
      +
      diff --git a/sources/templates/show_edit_catalog.inc.php b/sources/templates/show_edit_catalog.inc.php new file mode 100644 index 0000000..e46b5bf --- /dev/null +++ b/sources/templates/show_edit_catalog.inc.php @@ -0,0 +1,66 @@ +name . ' (' . $catalog->f_info . ')'), 'box box_edit_catalog'); +?> +
      + + + + + + + + + + + + + + + + + + +
      : + :
      + %A=
      + %a=
      + %c=
      + %T=
      + %t=
      + %y=
      + %o=
      +
      catalog_type)); ?>
      : + +
      + :
      +
      + +
      +
      + + + +
      +
      + diff --git a/sources/templates/show_edit_channel_row.inc.php b/sources/templates/show_edit_channel_row.inc.php new file mode 100644 index 0000000..9e3f44c --- /dev/null +++ b/sources/templates/show_edit_channel_row.inc.php @@ -0,0 +1,95 @@ + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      is_private) ? 'checked' : ''; ?> />
      random) ? 'checked' : ''; ?> />
      loop) ? 'checked' : ''; ?> />
      + + +
      +
      diff --git a/sources/templates/show_edit_live_stream_row.inc.php b/sources/templates/show_edit_live_stream_row.inc.php new file mode 100644 index 0000000..27f79ac --- /dev/null +++ b/sources/templates/show_edit_live_stream_row.inc.php @@ -0,0 +1,46 @@ + +
      +
      + + + + + + + + + + + + + + + + + +
      + + +
      +
      diff --git a/sources/templates/show_edit_playlist_row.inc.php b/sources/templates/show_edit_playlist_row.inc.php new file mode 100644 index 0000000..7fb9cf3 --- /dev/null +++ b/sources/templates/show_edit_playlist_row.inc.php @@ -0,0 +1,45 @@ + +
      +
      + + + + + + + + + +
      + type; ?> + + +
      + + +
      +
      diff --git a/sources/templates/show_edit_shout.inc.php b/sources/templates/show_edit_shout.inc.php new file mode 100644 index 0000000..a18a409 --- /dev/null +++ b/sources/templates/show_edit_shout.inc.php @@ -0,0 +1,46 @@ + + +
      + + + + + + + + + + + + + + + +
      f_link, $object->f_link); ?> +
      +
      sticky == "1") { echo "checked"; } ?>/>
      + +
      +
      + diff --git a/sources/templates/show_edit_smartplaylist_row.inc.php b/sources/templates/show_edit_smartplaylist_row.inc.php new file mode 100644 index 0000000..8f5df64 --- /dev/null +++ b/sources/templates/show_edit_smartplaylist_row.inc.php @@ -0,0 +1,45 @@ + +
      +
      + + + + + + + + + +
      + type; ?> + + +
      + + +
      +
      diff --git a/sources/templates/show_edit_song_row.inc.php b/sources/templates/show_edit_song_row.inc.php new file mode 100644 index 0000000..1432588 --- /dev/null +++ b/sources/templates/show_edit_song_row.inc.php @@ -0,0 +1,66 @@ + +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      + artist, true, $song->id); ?> +
      + id, 'change', 'check_inline_song_edit("artist", '.$song->id.')'); ?> +
      +
      + album, true, $song->id); ?> +
      + id, 'change', 'check_inline_song_edit("album", '.$song->id.')'); ?> +
      +
      + +
      + + +
      +
      diff --git a/sources/templates/show_edit_tag_row.inc.php b/sources/templates/show_edit_tag_row.inc.php new file mode 100644 index 0000000..90c4cd7 --- /dev/null +++ b/sources/templates/show_edit_tag_row.inc.php @@ -0,0 +1,34 @@ + +
      +
      + + + + + +
      + + +
      +
      diff --git a/sources/templates/show_edit_user.inc.php b/sources/templates/show_edit_user.inc.php new file mode 100644 index 0000000..45704ca --- /dev/null +++ b/sources/templates/show_edit_user.inc.php @@ -0,0 +1,146 @@ + + + +
      "> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + : + + + +
      : + +
      + : + + +
      + : + + +
      + : + + + +
      + : + + +
      + : + + access; ${$var_name} = 'selected="selected"'; ?> + +
      + + + + + +
      + + + + apikey; ?> +
      + +
      + +
      + +
      +
      + + + + +
      +
      + diff --git a/sources/templates/show_export.inc.php b/sources/templates/show_export.inc.php new file mode 100644 index 0000000..d271fb3 --- /dev/null +++ b/sources/templates/show_export.inc.php @@ -0,0 +1,64 @@ + +
      + + + + + + + + + +
      : + +
      : + +
      +
      + +
      +
      + diff --git a/sources/templates/show_gather_art.inc.php b/sources/templates/show_gather_art.inc.php new file mode 100644 index 0000000..043c981 --- /dev/null +++ b/sources/templates/show_gather_art.inc.php @@ -0,0 +1,28 @@ +" . T_('Starting Album Art Search') . ". . .
      \n"; +echo T_('Searched') . ": " . T_('None') . "
      "; +echo T_('Reading') . ":
      "; +echo "
      \n"; +UI::show_box_bottom(); diff --git a/sources/templates/show_get_albumart.inc.php b/sources/templates/show_get_albumart.inc.php new file mode 100644 index 0000000..295e1d8 --- /dev/null +++ b/sources/templates/show_get_albumart.inc.php @@ -0,0 +1,66 @@ + + +
      + + + + + + + + + + + + + + + + + +
      +   + + +
      +   + + +
      + + + +
      + + + +
      +
      + + + + +
      +
      + diff --git a/sources/templates/show_highest.inc.php b/sources/templates/show_highest.inc.php new file mode 100644 index 0000000..2fc4b30 --- /dev/null +++ b/sources/templates/show_highest.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_html5_player.inc.php b/sources/templates/show_html5_player.inc.php new file mode 100644 index 0000000..93a7494 --- /dev/null +++ b/sources/templates/show_html5_player.inc.php @@ -0,0 +1,708 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + + diff --git a/sources/templates/show_import_playlist.inc.php b/sources/templates/show_import_playlist.inc.php new file mode 100644 index 0000000..437b4a5 --- /dev/null +++ b/sources/templates/show_import_playlist.inc.php @@ -0,0 +1,38 @@ + + +
      + + + + + +
      + (): +
      +
      + + +
      +
      + diff --git a/sources/templates/show_index.inc.php b/sources/templates/show_index.inc.php new file mode 100644 index 0000000..66b439e --- /dev/null +++ b/sources/templates/show_index.inc.php @@ -0,0 +1,53 @@ + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + diff --git a/sources/templates/show_install.inc.php b/sources/templates/show_install.inc.php new file mode 100644 index 0000000..354e274 --- /dev/null +++ b/sources/templates/show_install.inc.php @@ -0,0 +1,136 @@ + +
      +

      +
      +
      + 33% +
      +
      +

      +
      +
      +
      +
        +
      • +
      • +
      +
      + +

      +
      " enctype="multipart/form-data"> +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      + + +
      + +
      +
      + +
      +
      + diff --git a/sources/templates/show_install_account.inc.php b/sources/templates/show_install_account.inc.php new file mode 100644 index 0000000..d97f2d6 --- /dev/null +++ b/sources/templates/show_install_account.inc.php @@ -0,0 +1,72 @@ + +
      +

      +
      +
      + 99% +
      +
      +
        +
      • +
      • +
      +

      +

      +
      +
      +
      + +

      +
      " enctype="multipart/form-data"> + +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      +
      + diff --git a/sources/templates/show_install_check.inc.php b/sources/templates/show_install_check.inc.php new file mode 100644 index 0000000..20a383b --- /dev/null +++ b/sources/templates/show_install_check.inc.php @@ -0,0 +1,66 @@ + + + +
      +

      + +

      +
        +
      • +
      • +
      +

      + +

      +
      + + + + + + + + + + + + + + + + + + + + + + +
      +
      " enctype="multipart/form-data" > + + +
      diff --git a/sources/templates/show_install_config.inc.php b/sources/templates/show_install_config.inc.php new file mode 100644 index 0000000..5ddcca0 --- /dev/null +++ b/sources/templates/show_install_config.inc.php @@ -0,0 +1,192 @@ + +
      +

      +
      +
      + 66% +
      +
      +

      +

      +
      +
      +
      +
        +
      • +
      +
      + + +

      +

      + +
      " enctype="multipart/form-data" > +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      +
      + +
      + +
      +
      + + + + +
      +

      +
      + +
      + . . +
      +
      +
      + +
      + + + + +
      +
      + +
      +

      + +
       
       
      +
      + +
      +
      + + +
      +
      +
      +
      +
      + +
       
       
      +
      + +
      +
      + + +
      +
      +
      +
      +
      + + +
       
       
      +
      + +
      +
      + + +
      +
      +
      +
      +
      +
       
       
      + +
      + +
      + +
      + [] +
      + +
      " + enctype="multipart/form-data" +> + +
      + + diff --git a/sources/templates/show_install_lang.inc.php b/sources/templates/show_install_lang.inc.php new file mode 100644 index 0000000..e28fedf --- /dev/null +++ b/sources/templates/show_install_lang.inc.php @@ -0,0 +1,52 @@ + + +
      +

      Ampache

      +
      + +

      +
      " enctype="multipart/form-data" > +
      + \n"; + + foreach ($languages as $lang=>$name) { + $var_name = $lang . "_lang"; + + echo "\t\n"; + } // end foreach + echo "\n"; + ?> +
      + +
      + diff --git a/sources/templates/show_ip_history.inc.php b/sources/templates/show_ip_history.inc.php new file mode 100644 index 0000000..8745736 --- /dev/null +++ b/sources/templates/show_ip_history.inc.php @@ -0,0 +1,64 @@ + +fullname)); ?> +
      +
        +
      • + + + + + + + +
      • +
      +
      +
      +
      + ++ + + + + + + + + + + + + + + + + + +
      + + + +
      + diff --git a/sources/templates/show_live_stream.inc.php b/sources/templates/show_live_stream.inc.php new file mode 100644 index 0000000..c1062fe --- /dev/null +++ b/sources/templates/show_live_stream.inc.php @@ -0,0 +1,31 @@ + + +
      +
        +
      • + +
      • +
      +
      + diff --git a/sources/templates/show_live_stream_row.inc.php b/sources/templates/show_live_stream_row.inc.php new file mode 100644 index 0000000..364dfbb --- /dev/null +++ b/sources/templates/show_live_stream_row.inc.php @@ -0,0 +1,48 @@ + + +   +
      + + id, 'play', T_('Play live stream'),'play_live_stream_' . $radio->id); ?> + +
      + +f_name_link; ?> + + + id,'add', T_('Add to temporary playlist'),'add_radio_' . $radio->id); ?> + + +f_url_link; ?> +codec; ?> + + + + + + + + id,'delete', T_('Delete'),'delete_live_stream_' . $radio->id); ?> + + diff --git a/sources/templates/show_live_streams.inc.php b/sources/templates/show_live_streams.inc.php new file mode 100644 index 0000000..26f0c21 --- /dev/null +++ b/sources/templates/show_live_streams.inc.php @@ -0,0 +1,65 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + + + +
      id . '&sort=name', T_('Name'),'live_stream_sort_name'); ?>id . '&sort=codec', T_('Codec'),'live_stream_codec'); ?>
      id . '&sort=name', T_('Name'),'live_stream_sort_name'); ?>id . '&sort=codec', T_('Codec'),'live_stream_codec_bottom'); ?>
      + +get_show_header()) require AmpConfig::Get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_localplay_add_instance.inc.php b/sources/templates/show_localplay_add_instance.inc.php new file mode 100644 index 0000000..8f0bf6a --- /dev/null +++ b/sources/templates/show_localplay_add_instance.inc.php @@ -0,0 +1,37 @@ + + +
      + +$field) { ?> + + + + + +
      +
      + +
      +
      + diff --git a/sources/templates/show_localplay_control.inc.php b/sources/templates/show_localplay_control.inc.php new file mode 100644 index 0000000..8477728 --- /dev/null +++ b/sources/templates/show_localplay_control.inc.php @@ -0,0 +1,29 @@ + +
      + + + + + +
      diff --git a/sources/templates/show_localplay_controllers.inc.php b/sources/templates/show_localplay_controllers.inc.php new file mode 100644 index 0000000..4f94417 --- /dev/null +++ b/sources/templates/show_localplay_controllers.inc.php @@ -0,0 +1,70 @@ + + + + + + + + + + + + + player_loaded()) { continue; } + $localplay->format(); + if (Localplay::is_enabled($controller)) { + $action = 'confirm_uninstall_localplay'; + $action_txt = T_('Disable'); + } else { + $action = 'install_localplay'; + $action_txt = T_('Activate'); + } + ?> + + + + + + + + + + + + + + + + + + + + +
      f_name); ?>f_description); ?>f_version); ?>
      +
      diff --git a/sources/templates/show_localplay_edit_instance.inc.php b/sources/templates/show_localplay_edit_instance.inc.php new file mode 100644 index 0000000..cca208a --- /dev/null +++ b/sources/templates/show_localplay_edit_instance.inc.php @@ -0,0 +1,37 @@ + + +
      + +$field) { ?> + + + + + +
      +
      + +
      +
      + diff --git a/sources/templates/show_localplay_instances.inc.php b/sources/templates/show_localplay_instances.inc.php new file mode 100644 index 0000000..47af03a --- /dev/null +++ b/sources/templates/show_localplay_instances.inc.php @@ -0,0 +1,45 @@ + + + + + $field) { ?> + + + + +$name) { + $instance = $localplay->get_instance($uid); +?> + + $field) { ?> + + + + + +
      + + +
      + diff --git a/sources/templates/show_localplay_playlist.inc.php b/sources/templates/show_localplay_playlist.inc.php new file mode 100644 index 0000000..0f741da --- /dev/null +++ b/sources/templates/show_localplay_playlist.inc.php @@ -0,0 +1,67 @@ +connect(); +$status = $localplay->status(); +?> +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + + + + + + + + + + + + + > + format_name($object['name'],$object['id']); ?> + + + + + + + + + + + + + + + + +
      + + + +
      +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_localplay_status.inc.php b/sources/templates/show_localplay_status.inc.php new file mode 100644 index 0000000..77ad9a3 --- /dev/null +++ b/sources/templates/show_localplay_status.inc.php @@ -0,0 +1,64 @@ +status(); +$now_playing = $status['track_title']; +if (!empty($status['track_album'])) { + $now_playing .= $status['track_album'] . ' - ' . $status['track_artist']; +} +?> + +type), 'box box_localplay_status'); ?> + +
      +
        +
      • + + + + :% +
      • +
      • + | + + +
      • +
      • + | + + +
      • +
      • + +
      • +
      +
      + +set_type('playlist_localplay'); + $browse->set_static_content(true); + $browse->show_objects($objects); + $browse->store(); +?> + + diff --git a/sources/templates/show_login_form.inc.php b/sources/templates/show_login_form.inc.php new file mode 100644 index 0000000..4a87c9c --- /dev/null +++ b/sources/templates/show_login_form.inc.php @@ -0,0 +1,118 @@ += AmpConfig::get('remember_length')) { + $remember_disabled = 'disabled="disabled"'; +} +$htmllang = str_replace("_","-",AmpConfig::get('lang')); +is_rtl(AmpConfig::get('lang')) ? $dir = 'rtl' : $dir = 'ltr'; + +?> + + + + + + + + <?php echo scrub_out(AmpConfig::get('site_title')); ?> + + + + + + +"> +
      + +
      +

      +
      + +
      + + +
      +
      + + +
      +
      /> +
      + + + + +
      + + + + + + + + +
      + +
      + +
      +
      + + diff --git a/sources/templates/show_lostpassword_form.inc.php b/sources/templates/show_lostpassword_form.inc.php new file mode 100644 index 0000000..99af682 --- /dev/null +++ b/sources/templates/show_lostpassword_form.inc.php @@ -0,0 +1,79 @@ += AmpConfig::get('remember_length')) { + $remember_disabled = 'disabled="disabled"'; +} +$htmllang = str_replace("_","-",AmpConfig::get('lang')); +is_rtl(AmpConfig::get('lang')) ? $dir = 'rtl' : $dir = 'ltr'; + +?> + + + + + + + +<?php echo scrub_out(AmpConfig::get('site_title')); ?> + + + + +
      + +
      +

      +
      + +
      + +
      +
      +
      +
      + + + diff --git a/sources/templates/show_lyrics.inc.php b/sources/templates/show_lyrics.inc.php new file mode 100644 index 0000000..f0a3c98 --- /dev/null +++ b/sources/templates/show_lyrics.inc.php @@ -0,0 +1,72 @@ +title); +$album = scrub_out($song->f_album_full); +$artist = scrub_out($song->f_artist_full); +?> +
      + name != T_('Unknown (Orphaned)')) { + $aa_url = $web_path . "/image.php?id=" . $song->album . "&sid=" . session_id(); + echo ""; + echo "\"".$song-f_album_full."\" alt=\"".$song->f_album_full."\" height=\"128\" width=\"128\" />"; + echo "\n"; + } + ?> +
      + +
      +
      + + + + +
      + +
      + + + + +
      + +
      + + + + +
      +
      +

      +
      +
      + +
      + +
      + + diff --git a/sources/templates/show_mail_users.inc.php b/sources/templates/show_mail_users.inc.php new file mode 100644 index 0000000..379d1f3 --- /dev/null +++ b/sources/templates/show_mail_users.inc.php @@ -0,0 +1,64 @@ + + + +
      + + + + + + + + + + + + + + + + + +
      : + +
      : + +
      : + +
      : + +
      +
      + +
      +
      + diff --git a/sources/templates/show_manage_catalogs.inc.php b/sources/templates/show_manage_catalogs.inc.php new file mode 100644 index 0000000..89d7644 --- /dev/null +++ b/sources/templates/show_manage_catalogs.inc.php @@ -0,0 +1,60 @@ + + +
      +
        +
      • +
      • +
      • +
      • +
      • +
      • +
      • +
      +
      +
      + + + + + + + + + + + + +
      /data/myNewMusic'); ?>
      /data/myUpdatedMusic'); ?>
      +
      +
      +set_type('catalog'); + $browse->set_static_content(true); + $browse->save_objects($catalog_ids); + $browse->show_objects($catalog_ids); + $browse->store(); +?> diff --git a/sources/templates/show_manage_democratic.inc.php b/sources/templates/show_manage_democratic.inc.php new file mode 100644 index 0000000..be15eb2 --- /dev/null +++ b/sources/templates/show_manage_democratic.inc.php @@ -0,0 +1,63 @@ + + + + + + + + + + + + format(); + $playlist = new Playlist($democratic->base_playlist); + $playlist->format(); + ?> + + + + + + + + + + + + + + +
      name); ?>f_name_link; ?>f_cooldown; ?>f_level; ?>f_primary; ?>count_items(); ?> + id,'all', T_('Play'),'play_democratic'); ?> + +
      +
      +
      + +
      + diff --git a/sources/templates/show_manage_shoutbox.inc.php b/sources/templates/show_manage_shoutbox.inc.php new file mode 100644 index 0000000..c1f4a47 --- /dev/null +++ b/sources/templates/show_manage_shoutbox.inc.php @@ -0,0 +1,58 @@ + + + + + + + + + + + format(); + $object = Shoutbox::get_object($shout->object_type,$shout->object_id); + $object->format(); + $client = new User($shout->user); + $client->format(); + + require AmpConfig::get('prefix') . '/templates/show_shout_row.inc.php'; + ?> + + + + + + + + + + + + + +
      diff --git a/sources/templates/show_missing_album.inc.php b/sources/templates/show_missing_album.inc.php new file mode 100644 index 0000000..0b14fac --- /dev/null +++ b/sources/templates/show_missing_album.inc.php @@ -0,0 +1,90 @@ +name) . ' (' . $walbum->year . ')'; +$title .= ' - ' . $walbum->f_artist_link; +?> + +
      +mbid, 'album'); +$options['artist'] = $artist->name; +$options['album_name'] = $walbum->name; +$options['keyword'] = $artist->name . " " . $walbum->name; +$images = $art->gather($options, '1'); + +if (count($images) > 0 && !empty($images[0]['url'])) { + $name = '[' . $artist->name . '] ' . scrub_out($walbum->name); + + $image = $images[0]['url']; + + echo ""; + echo "\"".$name."\""; + echo "\n"; +} +?> +
      +
      +

      :

      +
        + + +
      • + mbid,'play_preview', T_('Play'),'directplay_full_' . $walbum->mbid); ?> + mbid, T_('Play'),'directplay_full_text_' . $walbum->mbid); ?> +
      • + + +
      • + mbid . '&append=true','play_add_preview', T_('Play last'),'addplay_album_' . $walbum->mbid); ?> + mbid . '&append=true', T_('Play last'),'addplay_album_text_' . $walbum->mbid); ?> +
      • + +
      • + mbid,'add', T_('Add to temporary playlist'),'play_full_' . $walbum->mbid); ?> + mbid, T_('Add to temporary playlist'), 'play_full_text_' . $walbum->mbid); ?> +
      • + +
      • + : +
        + show_action_buttons(); ?> +
        +
      • +
      +
      + +
      +  +
      +
      +set_type('song_preview'); + $browse->set_static_content(true); + $browse->show_objects($walbum->songs); +?> +
      diff --git a/sources/templates/show_missing_albums.inc.php b/sources/templates/show_missing_albums.inc.php new file mode 100644 index 0000000..f458099 --- /dev/null +++ b/sources/templates/show_missing_albums.inc.php @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
      + diff --git a/sources/templates/show_missing_artist.inc.php b/sources/templates/show_missing_artist.inc.php new file mode 100644 index 0000000..8b2d092 --- /dev/null +++ b/sources/templates/show_missing_artist.inc.php @@ -0,0 +1,43 @@ + + + +
      + +
      + + + + +
      + diff --git a/sources/templates/show_newest.inc.php b/sources/templates/show_newest.inc.php new file mode 100644 index 0000000..ef57588 --- /dev/null +++ b/sources/templates/show_newest.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_now_playing.inc.php b/sources/templates/show_now_playing.inc.php new file mode 100644 index 0000000..56757c6 --- /dev/null +++ b/sources/templates/show_now_playing.inc.php @@ -0,0 +1,52 @@ + + +format(); + $agent = $item['agent']; + + /* If we've gotten a non-song object just skip this row */ + if (!is_object($media)) { continue; } + if (!$np_user->fullname) { $np_user->fullname = "Ampache User"; } +?> +
      + +
      + + + diff --git a/sources/templates/show_now_playing_row.inc.php b/sources/templates/show_now_playing_row.inc.php new file mode 100644 index 0000000..b957a33 --- /dev/null +++ b/sources/templates/show_now_playing_row.inc.php @@ -0,0 +1,103 @@ + + + +
      +
      + + f_link; ?> +
      +
      + + f_album_link; ?> +
      +
      + + f_artist_link; ?> +
      +
      + + f_tags; ?> +
      +
      + + +
      +
      + + <?php echo scrub_out($media->f_album_full); ?> + +
      +
      + + + +
      +
      +
      + +

      +
      +
      +
      +
      + +

      +
      +
      +
      + + + +
      + +
      + +
      + id,'song'); ?> +
      +
      +
      + +
      + id,'song'); ?> +
      +
      + +
      diff --git a/sources/templates/show_now_playing_similar.inc.php b/sources/templates/show_now_playing_similar.inc.php new file mode 100644 index 0000000..6fe3e2d --- /dev/null +++ b/sources/templates/show_now_playing_similar.inc.php @@ -0,0 +1,64 @@ + + + +
      +
      + + +
      + " . scrub_out($a['name']) . ""; + } else { + echo scrub_out($a['name']); + } + } else { + $artist = new Artist($a['id']); + $artist->format(); + echo $artist->f_name_link; + } + ?> +
      + +
      +
      + + + +
      +
      + + +
      + format(); + echo $song->f_link; + ?> +
      + +
      +
      + diff --git a/sources/templates/show_object_rating.inc.php b/sources/templates/show_object_rating.inc.php new file mode 100644 index 0000000..fde9c3f --- /dev/null +++ b/sources/templates/show_object_rating.inc.php @@ -0,0 +1,58 @@ +type . '&object_id=' . $rating->id; +$othering = false; +$rate = $rating->get_user_rating(); +if (!$rate) { + $rate = $rating->get_average_rating(); + $othering = true; +} +?> + +
      +
        + 20% per star) + $width = $rate * 20; + if ($width < 0) $width = 0; + + //set the current rating background + echo '
      • '; + echo T_('Current rating: '); + if ($rate <= 0) { + echo T_('not rated yet') . "
      • \n"; + } else printf(T_('%s of 5'), $rate); echo "\n"; + + for ($i = 1; $i < 6; $i++) { + ?> +
      • + id . '_' . $rating->type, '', 'star' . $i); ?> +
      • + +
      + id . '_' . $rating->type, '', 'star0'); ?> +
      diff --git a/sources/templates/show_object_row.inc.php b/sources/templates/show_object_row.inc.php new file mode 100644 index 0000000..d9edfd7 --- /dev/null +++ b/sources/templates/show_object_row.inc.php @@ -0,0 +1,31 @@ +uid) + * build TD's from $headers $key=>$header + */ + +?> +$header) { ?> + $key; ?> + diff --git a/sources/templates/show_object_userflag.inc.php b/sources/templates/show_object_userflag.inc.php new file mode 100644 index 0000000..4f35730 --- /dev/null +++ b/sources/templates/show_object_userflag.inc.php @@ -0,0 +1,38 @@ +type . '&object_id=' . $userflag->id; +$othering = false; +$flagged = $userflag->get_flag(); +?> + +
      +id . '_' . $userflag->type, '', 'userflag_true'); + } else { + echo Ajax::text($base_url . '&userflag=1', '', 'userflag_i_' . $userflag->id . '_' . $userflag->type, '', 'userflag_false'); + } +?> +
      diff --git a/sources/templates/show_objects.inc.php b/sources/templates/show_objects.inc.php new file mode 100644 index 0000000..3518e25 --- /dev/null +++ b/sources/templates/show_objects.inc.php @@ -0,0 +1,48 @@ + + + + + + + + format(); + ?> + + + + + + + + + +
      + +
      diff --git a/sources/templates/show_playlist.inc.php b/sources/templates/show_playlist.inc.php new file mode 100644 index 0000000..a5f4a0c --- /dev/null +++ b/sources/templates/show_playlist.inc.php @@ -0,0 +1,101 @@ + +id . '">' . $title . '
      ', 'info-box'); +?> +
      +
        +
      • + + +    + +
      • + +
      • + + +    + +
      • + + +
      • + id,'play', T_('Play all'),'directplay_full_' . $playlist->id); ?> + id, T_('Play all'),'directplay_full_text_' . $playlist->id); ?> +
      • + + +
      • + id . '&append=true','play_add', T_('Play all last'),'addplay_playlist_' . $playlist->id); ?> + id . '&append=true', T_('Play all last'),'addplay_playlist_text_' . $playlist->id); ?> +
      • + +
      • + id,'add', T_('Add all to temporary playlist'),'play_playlist'); ?> + id, T_('Add all to temporary playlist'),'play_playlist_text'); ?> +
      • +
      • + id,'random', T_('Random all to temporary playlist'),'play_playlist_random'); ?> + id, T_('Random all to temporary playlist'),'play_playlist_random_text'); ?> +
      • + has_access('50')) { ?> +
      • + + +    + +
      • + + has_access()) { ?> +
      • + + +    + +
      • + +
      +
      + +
      +set_type('playlist_song'); + $browse->add_supplemental_object('playlist', $playlist->id); + $browse->set_static_content(false); + $browse->show_objects($object_ids); + $browse->store(); +?> +
      diff --git a/sources/templates/show_playlist_row.inc.php b/sources/templates/show_playlist_row.inc.php new file mode 100644 index 0000000..feee6eb --- /dev/null +++ b/sources/templates/show_playlist_row.inc.php @@ -0,0 +1,62 @@ + + +   +
      + + id,'play', T_('Play'),'play_playlist_' . $playlist->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_playlist_' . $playlist->id); ?> + + +
      + +f_name_link; ?> + + + id,'add', T_('Add to temporary playlist'),'add_playlist_' . $playlist->id); ?> + id,'random', T_('Random to temporary playlist'),'random_playlist_' . $playlist->id); ?> + + + + + +f_type; ?> + +f_user); ?> + + + + + + + + + + has_access()) { ?> + + + + id, 'delete', T_('Delete'), 'delete_playlist_'.$playlist->id, '', '', T_('Do you really want to delete the playlist?')); ?> + + diff --git a/sources/templates/show_playlist_song_row.inc.php b/sources/templates/show_playlist_song_row.inc.php new file mode 100644 index 0000000..fc84d39 --- /dev/null +++ b/sources/templates/show_playlist_song_row.inc.php @@ -0,0 +1,68 @@ + + + '.$playlist_track.''; ?> +
      + + id, 'play', T_('Play'),'play_playlist_song_' . $song->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_song_' . $song->id); ?> + + +
      + +f_link; ?> + + + id,'add', T_('Add to temporary playlist'),'playlist_add_' . $song->id); ?> + + + + + +f_artist_link; ?> +f_album_link; ?> +f_tags; ?> +f_time; ?> + +id,'song'); ?> + + +id,'song'); ?> + + + + + + + + + + + has_access()) { ?> + id . '&track_id=' . $object['track_id'],'delete', T_('Delete'),'track_del_' . $object['track_id']); ?> + + + + + diff --git a/sources/templates/show_playlist_songs.inc.php b/sources/templates/show_playlist_songs.inc.php new file mode 100644 index 0000000..9f457b0 --- /dev/null +++ b/sources/templates/show_playlist_songs.inc.php @@ -0,0 +1,86 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> +
      + + + + + + + + + + + + + + + + + + + + + + format(); + $playlist_track = $object['track']; + ?> + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_playlist_title.inc.php b/sources/templates/show_playlist_title.inc.php new file mode 100644 index 0000000..aad0de6 --- /dev/null +++ b/sources/templates/show_playlist_title.inc.php @@ -0,0 +1,23 @@ +f_type, $playlist->name); diff --git a/sources/templates/show_playlists.inc.php b/sources/templates/show_playlists.inc.php new file mode 100644 index 0000000..cf0a7d3 --- /dev/null +++ b/sources/templates/show_playlists.inc.php @@ -0,0 +1,66 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> + + + + + + + + + + + + + + format(); + $count = $playlist->get_song_count(); + ?> + + + + + + + + + + + + + + + + + + + + + +
      id . '&type=playlist&sort=name', T_('Playlist Name'),'playlist_sort_name'); ?>id . '&type=playlist&sort=user', T_('Owner'),'playlist_sort_owner'); ?>
      id . '&type=playlist&sort=name', T_('Playlist Name'),'playlist_sort_name'); ?>id . '&type=playlist&sort=user', T_('Owner'),'playlist_sort_owner_bottom'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> diff --git a/sources/templates/show_playlists_dialog.inc.php b/sources/templates/show_playlists_dialog.inc.php new file mode 100644 index 0000000..eabf95a --- /dev/null +++ b/sources/templates/show_playlists_dialog.inc.php @@ -0,0 +1,43 @@ + + +
        +
      • + + + +
      • +id); + Playlist::build_cache($playlists); + foreach ($playlists as $playlist_id) { + $playlist = new Playlist($playlist_id); + $playlist->format(); +?> +
      • + + f_name; ?> + +
      • + +
      diff --git a/sources/templates/show_playtype_switch.inc.php b/sources/templates/show_playtype_switch.inc.php new file mode 100644 index 0000000..4ce9f08 --- /dev/null +++ b/sources/templates/show_playtype_switch.inc.php @@ -0,0 +1,50 @@ + +
      + +
      + + +
      + +
      diff --git a/sources/templates/show_plugins.inc.php b/sources/templates/show_plugins.inc.php new file mode 100644 index 0000000..4d95330 --- /dev/null +++ b/sources/templates/show_plugins.inc.php @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + _plugin->name); + if (! $installed_version) { + $action = "" . + T_('Activate') . ""; + } else { + $action = "" . + T_('Deactivate') . ""; + if ($installed_version < $plugin->_plugin->version) { + $action .= '  ' . T_('Upgrade') . ''; + } + } + ?> + + + + + + + + + + + + + + + + + + + + + + +
      _plugin->name); ?>_plugin->description); ?>_plugin->version); ?>
      +
      diff --git a/sources/templates/show_popular.inc.php b/sources/templates/show_popular.inc.php new file mode 100644 index 0000000..344a861 --- /dev/null +++ b/sources/templates/show_popular.inc.php @@ -0,0 +1,46 @@ +set_type('song', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Stats::get_top_sql('album'); +$browse = new Browse(); +$browse->set_type('album', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Stats::get_top_sql('artist'); +$browse = new Browse(); +$browse->set_type('artist', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +UI::show_box_bottom(); diff --git a/sources/templates/show_preference_admin.inc.php b/sources/templates/show_preference_admin.inc.php new file mode 100644 index 0000000..fafe8b8 --- /dev/null +++ b/sources/templates/show_preference_admin.inc.php @@ -0,0 +1,60 @@ + +
      + ++ + + + + + + + + + + + + + + + + +
      + + +
      +
      + + +
      +
      + diff --git a/sources/templates/show_preference_box.inc.php b/sources/templates/show_preference_box.inc.php new file mode 100644 index 0000000..9f6fc7e --- /dev/null +++ b/sources/templates/show_preference_box.inc.php @@ -0,0 +1,84 @@ + +

      + ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + +
      diff --git a/sources/templates/show_preferences.inc.php b/sources/templates/show_preferences.inc.php new file mode 100644 index 0000000..d8c1446 --- /dev/null +++ b/sources/templates/show_preferences.inc.php @@ -0,0 +1,52 @@ + + + + +
      + +
      + + + + + + + +
      + +
      + + diff --git a/sources/templates/show_random.inc.php b/sources/templates/show_random.inc.php new file mode 100644 index 0000000..f7ca1ca --- /dev/null +++ b/sources/templates/show_random.inc.php @@ -0,0 +1,117 @@ + + +
      + + + + + + + +
       
      + + + + + + + + + + + + + +
      + +
      + + +
      + +
      + + + +
      + +
      +
      + +
      +set_type('song'); + $browse->save_objects($object_ids); + $browse->show_objects(); + $browse->store(); + echo Ajax::observe('window','load',Ajax::action('?action=refresh_rightbar','playlist_refresh_load')); + } +?> +
      diff --git a/sources/templates/show_random_albums.inc.php b/sources/templates/show_random_albums.inc.php new file mode 100644 index 0000000..3a556e0 --- /dev/null +++ b/sources/templates/show_random_albums.inc.php @@ -0,0 +1,64 @@ + + +format(); + $name = '[' . $album->f_artist . '] ' . scrub_out($album->full_name); + ?> +
      + +
      + + get_http_album_query_ids('album_id'),'play', T_('Play'),'play_album_' . $album->id); ?> + + get_http_album_query_ids('album_id') . '&append=true','play_add', T_('Play last'),'addplay_album_' . $album->id); ?> + + + get_http_album_query_ids('id'),'add', T_('Add to temporary playlist'),'play_full_' . $album->id); ?> +
      + id . "_album\">"; + show_rating($album->id, 'album'); + echo "
      "; + } + ?> +
      + + + + diff --git a/sources/templates/show_recent.inc.php b/sources/templates/show_recent.inc.php new file mode 100644 index 0000000..0e815c2 --- /dev/null +++ b/sources/templates/show_recent.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_recently_played.inc.php b/sources/templates/show_recently_played.inc.php new file mode 100644 index 0000000..5e6b27b --- /dev/null +++ b/sources/templates/show_recently_played.inc.php @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + +id == $row_user->id; + if (!$is_allowed) { + $has_allowed_time = Preference::get_by_user($row_user->id, 'allow_personal_info_time'); + $has_allowed_agent = Preference::get_by_user($row_user->id, 'allow_personal_info_agent'); + } + + if ($is_allowed || $has_allowed_agent) { + $agent = $row['agent']; + } + + if ($is_allowed || $has_allowed_time) { + $interval = intval(time() - $row['date']); + + if ($interval < 60) { + $unit = ngettext('second ago', 'seconds ago', $interval); + } else if ($interval < 3600) { + $interval = floor($interval / 60); + $unit = ngettext('minute ago', 'minutes ago', $interval); + } else if ($interval < 86400) { + $interval = floor($interval / 3600); + $unit = ngettext('hour ago', 'hours ago', $interval); + } else if ($interval < 604800) { + $interval = floor($interval / 86400); + $unit = ngettext('day ago', 'days ago', $interval); + } else if ($interval < 2592000) { + $interval = floor($interval / 604800); + $unit = ngettext('week ago', 'weeks ago', $interval); + } else if ($interval < 31556926) { + $interval = floor($interval / 2592000); + $unit = ngettext('month ago', 'months ago', $interval); + } else if ($interval < 631138519) { + $interval = floor($interval / 31556926); + $unit = ngettext('year ago', 'years ago', $interval); + } else { + $interval = floor($interval / 315569260); + $unit = ngettext('decade ago', 'decades ago', $interval); + } + + $time_string = sprintf('%d ' . (T_ngettext($unit, $unit, $interval)), $interval); + } + $song->format(); +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +   +
      + + id,'play', T_('Play'),'play_song_' . $nb . '_' . $song->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_song_' . $nb . '_' . $song->id); ?> + + +
      +
      f_link; ?> + + id, 'add', T_('Add to temporary playlist'), 'add_' . $nb . '_'.$song->id); ?> + + + + + f_album_link; ?>f_artist_link; ?> + + fullname); ?> + + + +
      +
      + + +
      + + diff --git a/sources/templates/show_recommended_artists.inc.php b/sources/templates/show_recommended_artists.inc.php new file mode 100644 index 0000000..47192ff --- /dev/null +++ b/sources/templates/show_recommended_artists.inc.php @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + diff --git a/sources/templates/show_registration_confirmation.inc.php b/sources/templates/show_registration_confirmation.inc.php new file mode 100644 index 0000000..2dbaf86 --- /dev/null +++ b/sources/templates/show_registration_confirmation.inc.php @@ -0,0 +1,59 @@ + + + + + +<?php echo AmpConfig::get('site_title'); ?> - <?php echo T_('Registration'); ?> + + + + + + + + +
      + + +

      + +
      +
      +

      Ampache
      +For the love of Music.

      +
      + + diff --git a/sources/templates/show_rules.inc.php b/sources/templates/show_rules.inc.php new file mode 100644 index 0000000..4a423d9 --- /dev/null +++ b/sources/templates/show_rules.inc.php @@ -0,0 +1,71 @@ +logic_operator; +} else { + $logic_operator = $_REQUEST['operator']; +} +$logic_operator = strtolower($logic_operator); +?> + + + + + + + + + + + + + + +
      + +
      + + + + + +
      + + +to_js(); +} else { + $mysearch = new Search($_REQUEST['type']); + $mysearch->parse_rules(Search::clean_request($_REQUEST)); + $out = $mysearch->to_js(); +} +if ($out) { + echo $out; +} else { + echo ''; +} +?> diff --git a/sources/templates/show_run_add_catalog.inc.php b/sources/templates/show_run_add_catalog.inc.php new file mode 100644 index 0000000..a7311dc --- /dev/null +++ b/sources/templates/show_run_add_catalog.inc.php @@ -0,0 +1,26 @@ + +:
      +:
      diff --git a/sources/templates/show_search.inc.php b/sources/templates/show_search.inc.php new file mode 100644 index 0000000..95e0ae6 --- /dev/null +++ b/sources/templates/show_search.inc.php @@ -0,0 +1,60 @@ + + + diff --git a/sources/templates/show_search_bar.inc.php b/sources/templates/show_search_bar.inc.php new file mode 100644 index 0000000..bafa559 --- /dev/null +++ b/sources/templates/show_search_bar.inc.php @@ -0,0 +1,40 @@ + +
      +
      + + + + + + + +
      +
      diff --git a/sources/templates/show_search_descriptor.inc.php b/sources/templates/show_search_descriptor.inc.php new file mode 100644 index 0000000..879680a --- /dev/null +++ b/sources/templates/show_search_descriptor.inc.php @@ -0,0 +1,37 @@ +' . "\n"; +?> + + Ampache + + + + + + data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAAAABkZGRVLi4urywsLK8rKyuvKysrrywsLK9ERESQAAAABwAAAAcxMTGQLS0trysrK68rKyuvLS0tryMjI69KSkpVNDQ0uLS0tP+5ubn/uLi4/7i4uP+7u7v/VVVV/0hISBcQEBAXb29v/8HBwf+4uLj/uLi4/76+vv+UlJT/Q0NDuC4uLq+/v7//vr6+/729vf+9vb3/wMDA/1ZWVvg+Pj4WAAAAFnR0dPjIyMj/vb29/729vf/Dw8P/mZmZ/0BAQK8wMDCvv7+//7+/v/++vr7/vr6+/8HBwf9WVlb4Pz8/FgAAABZ1dXX4ycnJ/76+vv++vr7/xMTE/5mZmf9AQECvMDAwr8DAwP/BwcH/wMDA/8DAwP/ExMT/V1dX+D8/PxYAAAAWdXV1+MvLy//AwMD/wMDA/8bGxv+bm5v/Pz8/rzExMa/Dw8P/xMTE/8PDw//Dw8P/x8fH/1hYWPg/Pz8WAAAAFnd3d/jOzs7/w8PD/8PDw//Jycn/nZ2d/z8/P68xMTGvxsbG/8fHx//Gxsb/xsbG/8rKyv9YWFj4Pz8/FgAAABZ4eHj40dHR/8bGxv/Gxsb/zc3N/5+fn/8/Pz+vMjIyr8nJyf/Kysr/ycnJ/8nJyf/Nzc3/Wlpa+D8/PxYAAAAWenp6+NTU1P/Jycn/ycnJ/9DQ0P+ioqL/Pz8/rzIyMq/Ly8v/zc3N/8vLy//Ly8v/z8/P/1tbW/hCQkITAAAAE3x8fPjX19f/y8vL/8vLy//S0tL/pKSk/z8/P68zMzOvzs7O/9DQ0P/Ozs7/zs7O/9TU1P9TU1P4GBgYHwAAAB97e3v42tra/87Ozv/Ozs7/1dXV/6ampv9AQECvNDQ0r9HR0f/T09P/0dHR/9HR0f/X19f/WFhY/h4eHukhISHpjIyM/tra2v/R0dH/0dHR/9jY2P+pqan/QEBArzQ0NK/T09P/1dXV/9PT0//T09P/1NTU/9HR0f/Nzc3/zc3N/9PT0//U1NT/09PT/9PT0//a2tr/q6ur/0BAQK82Njaw1tbW/9jY2P/W1tb/1tbW/93d3f99fX39Pz8/ukVFRbqcnJz93t7e/9bW1v/W1tb/3d3d/62trf9AQECwKioqst7e3v/g4OD/3d3d/93d3f/j4+P/h4eH/kVFRd9ISEjfpqam/uXl5f/d3d3/3d3d/+Xl5f+3t7f/SEhItFJSUlR+fn7zu7u7/7y8vP+7u7v/u7u7/729vf+9vb3/vb29/729vf+7u7v/u7u7/729vf+9vb3/Wlpa1UlJSTuNjY0TR0dHgUZGRq9HR0evRkZGr0VFRa9JSUmvTU1Nr01NTa9ISEivRUVFr0ZGRq9HR0evTk5Orw8PD1UAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAA== + UTF-8 + UTF-8 + diff --git a/sources/templates/show_search_options.inc.php b/sources/templates/show_search_options.inc.php new file mode 100644 index 0000000..a86d8e0 --- /dev/null +++ b/sources/templates/show_search_options.inc.php @@ -0,0 +1,38 @@ + + +
      +
        +
      • + id,'add', T_('Add Search Results'),'add_search_results'); ?> + +
      • + +
      • + + +
      • + +
      +
      + diff --git a/sources/templates/show_share.inc.php b/sources/templates/show_share.inc.php new file mode 100644 index 0000000..a6837d4 --- /dev/null +++ b/sources/templates/show_share.inc.php @@ -0,0 +1,44 @@ +f_user . '
      '; + echo "" . $share->public_url . "
      "; + echo "

      "; + + if ($share->allow_download) { + echo "id . "&secret=" . $share->secret . "\">" . UI::get_icon('download', T_('Download')) . " "; + echo "id . "&secret=" . $share->secret . "\">" . T_('Download') . ""; + } +} + +$is_share = true; +$iframed = true; +$playlist = $share->create_fake_playlist(); +require AmpConfig::get('prefix') . '/templates/show_web_player.inc.php'; + +if (!empty($embed)) { + UI::show_box_bottom(); +} diff --git a/sources/templates/show_shared_object_row.inc.php b/sources/templates/show_shared_object_row.inc.php new file mode 100644 index 0000000..be73c5f --- /dev/null +++ b/sources/templates/show_shared_object_row.inc.php @@ -0,0 +1,41 @@ + + +f_object_link; ?> +object_type; ?> +f_user; ?> +f_creation_date; ?> +f_lastvisit_date; ?> +counter; ?> +max_counter; ?> +f_allow_stream; ?> +f_allow_download; ?> +expire_days; ?> +public_url; ?> + +
      + show_action_buttons(); + ?> +
      + diff --git a/sources/templates/show_shared_objects.inc.php b/sources/templates/show_shared_objects.inc.php new file mode 100644 index 0000000..6c6eb82 --- /dev/null +++ b/sources/templates/show_shared_objects.inc.php @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + format(); + ?> + + + + + +
      id . '&type=share&sort=object', T_('Object'),'sort_share_object'); ?>id . '&type=share&sort=object_type', T_('Object Type'),'sort_share_object_type'); ?>id . '&type=share&sort=user', T_('User'),'sort_share_user'); ?>id . '&type=share&sort=creation_date', T_('Creation Date'),'sort_share_creation_date'); ?>id . '&type=share&sort=lastvisit_date', T_('Last Visit'),'sort_share_lastvisit_date'); ?>id . '&type=share&sort=counter', T_('Counter'),'sort_share_counter'); ?>id . '&type=share&sort=max_counter', T_('Max Counter'),'sort_share_max_counter'); ?>id . '&type=share&sort=allow_stream', T_('Allow Stream'),'sort_share_allow_stream'); ?>id . '&type=share&sort=allow_download', T_('Allow Download'),'sort_share_allow_download'); ?>id . '&type=share&sort=expire', T_('Expire Days'),'sort_share_expire'); ?>
      + + diff --git a/sources/templates/show_shares.inc.php b/sources/templates/show_shares.inc.php new file mode 100644 index 0000000..b2e4123 --- /dev/null +++ b/sources/templates/show_shares.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_shout_row.inc.php b/sources/templates/show_shout_row.inc.php new file mode 100644 index 0000000..67efdbd --- /dev/null +++ b/sources/templates/show_shout_row.inc.php @@ -0,0 +1,37 @@ + + + f_link; ?> + f_link; ?> + sticky; ?> + text); ?> + date; ?> + + + + + + + + + diff --git a/sources/templates/show_shoutbox.inc.php b/sources/templates/show_shoutbox.inc.php new file mode 100644 index 0000000..b84d80f --- /dev/null +++ b/sources/templates/show_shoutbox.inc.php @@ -0,0 +1,34 @@ + + +
      + +
      + get_display(true, true); ?> +
      + +
      + diff --git a/sources/templates/show_smartplaylist.inc.php b/sources/templates/show_smartplaylist.inc.php new file mode 100644 index 0000000..0589369 --- /dev/null +++ b/sources/templates/show_smartplaylist.inc.php @@ -0,0 +1,60 @@ + +id . '">' . $title . '
      ' , 'box box_smartplaylist'); +?> +
      +
        + +
      • + + +
      • + +
      • + id,'add', T_('Add All'),'play_playlist'); ?> + +
      • + has_access()) { ?> +
      • + + + + +
      • + +
      +
      + +
      + +
      + +
      +
      + + diff --git a/sources/templates/show_smartplaylist_row.inc.php b/sources/templates/show_smartplaylist_row.inc.php new file mode 100644 index 0000000..95302ec --- /dev/null +++ b/sources/templates/show_smartplaylist_row.inc.php @@ -0,0 +1,57 @@ + + +   +
      + + id,'play', T_('Play'),'play_playlist_' . $playlist->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_playlist_' . $playlist->id); ?> + + +
      + +f_name_link; ?> + + + id,'add', T_('Add to temporary playlist'),'add_playlist_' . $playlist->id); ?> + + + + + +f_type; ?> +f_user); ?> + + + + + + + has_access()) { ?> + + + + id,'delete', T_('Delete'),'delete_playlist_' . $playlist->id); ?> + + diff --git a/sources/templates/show_smartplaylist_title.inc.php b/sources/templates/show_smartplaylist_title.inc.php new file mode 100644 index 0000000..65b0ad1 --- /dev/null +++ b/sources/templates/show_smartplaylist_title.inc.php @@ -0,0 +1,24 @@ + + +f_type, $playlist->name); ?> diff --git a/sources/templates/show_smartplaylists.inc.php b/sources/templates/show_smartplaylists.inc.php new file mode 100644 index 0000000..1d020b7 --- /dev/null +++ b/sources/templates/show_smartplaylists.inc.php @@ -0,0 +1,64 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + + + +
      id . '&type=smartplaylist&sort=name', T_('Playlist Name'),'playlist_sort_name'); ?>id . '&type=smartplaylist&sort=user', T_('Owner'),'playlist_sort_owner'); ?>
      id . '&type=playlist&sort=name', T_('Playlist Name'),'playlist_sort_name_bottom'); ?>id . '&type=playlist&sort=user', T_('Owner'),'playlist_sort_owner_bottom'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php' ?> diff --git a/sources/templates/show_song.inc.php b/sources/templates/show_song.inc.php new file mode 100644 index 0000000..b33600f --- /dev/null +++ b/sources/templates/show_song.inc.php @@ -0,0 +1,118 @@ +enabled ? 'disable' : 'enable'; +$button_flip_state_id = 'button_flip_state_' . $song->id; +?> +title . ' ' . T_('Details'), 'box box_song_details'); ?> +
      + + + +
      +
      +
      id,'song'); ?> +
      +
      + + + + +
      +
      +
      id,'song'); ?> +
      +
      + + + +
      +
      +
      + +
      +
      + + +
      +
      + + id, 'play', T_('Play'),'play_song_' . $song->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_song_' . $song->id); ?> + + show_custom_play_actions(); ?> + + id,'add', T_('Add to temporary playlist'),'add_song_' . $song->id); ?> + + + + + + + + + + + + + + + id,$icon, T_(ucfirst($icon)),'flip_song_' . $song->id); ?> + + +
      +title); + $songprops[gettext_noop('Artist')] = $song->f_artist_link; + $songprops[gettext_noop('Album')] = $song->f_album_link . ($song->year ? " (" . scrub_out($song->year). ")" : ""); + $songprops[gettext_noop('Genre')] = $song->f_genre_link; + $songprops[gettext_noop('Length')] = scrub_out($song->f_time); + $songprops[gettext_noop('Comment')] = scrub_out($song->comment); + $songprops[gettext_noop('Label')] = scrub_out($song->label); + $songprops[gettext_noop('Song Language')]= scrub_out($song->language); + $songprops[gettext_noop('Catalog Number')] = scrub_out($song->catalog_number); + $songprops[gettext_noop('Bitrate')] = scrub_out($song->f_bitrate); + if (Access::check('interface','75')) { + $songprops[gettext_noop('Filename')] = scrub_out($song->file) . " " . $song->f_size; + } + if ($song->update_time) { + $songprops[gettext_noop('Last Updated')] = date("d/m/Y H:i",$song->update_time); + } + $songprops[gettext_noop('Added')] = date("d/m/Y H:i",$song->addition_time); + if (AmpConfig::get('show_played_times')) { + $songprops[gettext_noop('# Played')] = scrub_out($song->object_cnt); + } + + if (AmpConfig::get('show_lyrics')) { + $songprops[gettext_noop('Lyrics')] = $song->f_lyrics; + } + + foreach ($songprops as $key => $value) { + if (trim($value)) { + $rowparity = UI::flip_class(); + echo "
      " . T_($key) . "
      " . $value . "
      "; + } + } +?> +
      + diff --git a/sources/templates/show_song_preview_row.inc.php b/sources/templates/show_song_preview_row.inc.php new file mode 100644 index 0000000..6377d54 --- /dev/null +++ b/sources/templates/show_song_preview_row.inc.php @@ -0,0 +1,49 @@ + + + + file)) { ?> + id,'play_preview', T_('Play'),'play_song_' . $song->id); ?> + + id . '&append=true','play_add_preview', T_('Play last'),'addplay_song_' . $song->id); ?> + + + + +title; ?> + + + + file)) { ?> + id,'add', T_('Add to temporary playlist'),'add_' . $song->id); ?> + + + + + + + +f_artist_link; ?> +f_album_link; ?> +track; ?> +disk; ?> diff --git a/sources/templates/show_song_previews.inc.php b/sources/templates/show_song_previews.inc.php new file mode 100644 index 0000000..ae500ed --- /dev/null +++ b/sources/templates/show_song_previews.inc.php @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
      + diff --git a/sources/templates/show_song_row.inc.php b/sources/templates/show_song_row.inc.php new file mode 100644 index 0000000..5576c45 --- /dev/null +++ b/sources/templates/show_song_row.inc.php @@ -0,0 +1,84 @@ + + + '.$song->f_track.''; } ?> +
      + + id, 'play', T_('Play'), 'play_song_' . $song->id); ?> + + id . '&append=true', 'play_add', T_('Play last'), 'addplay_song_' . $song->id); ?> + + +
      + +f_link; ?> + + + id,'add', T_('Add to temporary playlist'),'add_' . $song->id); ?> + + + + + + show_custom_play_actions(); ?> + + + +f_artist_link; ?> +f_album_link; ?> +f_tags; ?> +f_time; ?> + +id,'song'); ?> + + +id,'song'); ?> + + + + + + + + + + + + + + + + + + enabled ? 'disable' : 'enable'; ?> + + + id,$icon, T_(ucfirst($icon)),'flip_song_' . $song->id); ?> + + + + + + + + diff --git a/sources/templates/show_songs.inc.php b/sources/templates/show_songs.inc.php new file mode 100644 index 0000000..dd00abb --- /dev/null +++ b/sources/templates/show_songs.inc.php @@ -0,0 +1,95 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + + + + + + + + + + + + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      id . '&sort=title', T_('Song Title'), 'sort_song_title'.$browse->id); ?>id . '&sort=artist', T_('Artist'), 'sort_song_artist'.$browse->id); ?>id . '&sort=album', T_('Album'), 'sort_song_album'.$browse->id); ?>id . '&sort=time', T_('Time'), 'sort_song_time'.$browse->id); ?>
      id . '&sort=title', T_('Song Title'), 'sort_song_title'.$browse->id); ?>id . '&sort=artist', T_('Artist'), 'sort_song_artist'.$browse->id); ?>id . '&sort=album', T_('Album'), 'sort_song_album'.$browse->id); ?>id . '&sort=time', T_('Time'), 'sort_song_time'.$browse->id); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_stats.inc.php b/sources/templates/show_stats.inc.php new file mode 100644 index 0000000..b03d6e6 --- /dev/null +++ b/sources/templates/show_stats.inc.php @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + ++ + + + + + + + + + + + + + + + + + + +format(); + $stats = Catalog::get_stats($catalog_id); +?> + + + + + + + + + + + + +
      name; ?>f_path); ?>f_update); ?>f_add); ?>f_clean); ?>
      + diff --git a/sources/templates/show_stats_highest.inc.php b/sources/templates/show_stats_highest.inc.php new file mode 100644 index 0000000..0fbd11f --- /dev/null +++ b/sources/templates/show_stats_highest.inc.php @@ -0,0 +1,42 @@ +set_type('song', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Rating::get_highest_sql('album'); +$browse = new Browse(); +$browse->set_type('album', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Rating::get_highest_sql('artist'); +$browse = new Browse(); +$browse->set_type('artist', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); diff --git a/sources/templates/show_stats_newest.inc.php b/sources/templates/show_stats_newest.inc.php new file mode 100644 index 0000000..9d1e166 --- /dev/null +++ b/sources/templates/show_stats_newest.inc.php @@ -0,0 +1,35 @@ +set_type('album', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Stats::get_newest_sql('artist'); +$browse = new Browse(); +$browse->set_type('artist', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); diff --git a/sources/templates/show_stats_popular.inc.php b/sources/templates/show_stats_popular.inc.php new file mode 100644 index 0000000..cb0072a --- /dev/null +++ b/sources/templates/show_stats_popular.inc.php @@ -0,0 +1,38 @@ + +
      + T_('Most Popular Albums')); +UI::show_box_top('','info-box box_popular_albums'); +require AmpConfig::get('prefix') . '/templates/show_objects.inc.php'; +UI::show_box_bottom(); + +$objects = Stats::get_top('artist'); +$headers = array('f_name_link' => T_('Most Popular Artists')); +UI::show_box_top('','info-box box_popular_artists'); +require AmpConfig::get('prefix') . '/templates/show_objects.inc.php'; +UI::show_box_bottom(); + +?> +
      diff --git a/sources/templates/show_stats_recent.inc.php b/sources/templates/show_stats_recent.inc.php new file mode 100644 index 0000000..46f8651 --- /dev/null +++ b/sources/templates/show_stats_recent.inc.php @@ -0,0 +1,42 @@ +set_type('album', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Stats::get_recent_sql('artist', $user_id); +$browse = new Browse(); +$browse->set_type('artist', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Stats::get_recent_sql('song', $user_id); +$browse = new Browse(); +$browse->set_type('song', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); diff --git a/sources/templates/show_stats_share.inc.php b/sources/templates/show_stats_share.inc.php new file mode 100644 index 0000000..c2cdb0e --- /dev/null +++ b/sources/templates/show_stats_share.inc.php @@ -0,0 +1,29 @@ +set_type('share'); +$browse->set_static_content(true); +$browse->save_objects($object_ids); +$browse->show_objects($object_ids); +$browse->store(); diff --git a/sources/templates/show_stats_userflag.inc.php b/sources/templates/show_stats_userflag.inc.php new file mode 100644 index 0000000..af96c1a --- /dev/null +++ b/sources/templates/show_stats_userflag.inc.php @@ -0,0 +1,42 @@ +set_type('song', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Userflag::get_latest_sql('album'); +$browse = new Browse(); +$browse->set_type('album', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); + +$sql = Userflag::get_latest_sql('artist'); +$browse = new Browse(); +$browse->set_type('artist', $sql); +$browse->set_simple_browse(true); +$browse->show_objects(); +$browse->store(); diff --git a/sources/templates/show_stats_wanted.inc.php b/sources/templates/show_stats_wanted.inc.php new file mode 100644 index 0000000..1740b78 --- /dev/null +++ b/sources/templates/show_stats_wanted.inc.php @@ -0,0 +1,29 @@ +set_type('wanted'); +$browse->set_static_content(true); +$browse->save_objects($object_ids); +$browse->show_objects($object_ids); +$browse->store(); diff --git a/sources/templates/show_tagcloud.inc.php b/sources/templates/show_tagcloud.inc.php new file mode 100644 index 0000000..918a139 --- /dev/null +++ b/sources/templates/show_tagcloud.inc.php @@ -0,0 +1,50 @@ + + + +
      +
      + + id . '&tag_id=' . $data['id'], '')); ?> +
      + +
      + +
      + +
      + + + + + diff --git a/sources/templates/show_test.inc.php b/sources/templates/show_test.inc.php new file mode 100644 index 0000000..6c0308d --- /dev/null +++ b/sources/templates/show_test.inc.php @@ -0,0 +1,59 @@ + + + + + + Ampache -- Debug Page + + + + + + +
      + +
      +

      +
      +
      + + + + + + + +
      +
      + + diff --git a/sources/templates/show_test_config.inc.php b/sources/templates/show_test_config.inc.php new file mode 100644 index 0000000..498efa3 --- /dev/null +++ b/sources/templates/show_test_config.inc.php @@ -0,0 +1,81 @@ + + + + + +Ampache -- Config Debug Page + + + + + +
      +

      Ampache.cfg.php Parse Error

      +

      You've been redirected to this page because your /config/ampache.cfg.php was not parsable. +If you are upgrading from 3.3.x please see the directions below.

      + +

      Migrating from 3.3.x to >= 3.4.x

      +

      Ampache 3.4 uses a different config parser that is over 10x faster then the previous version. Unfortunately the new parser is +unable to read the old config files. From inside the Ampache root directory you must run php bin/migrate_config.inc from the command line to create your +new config file.

      + +

      The following settings will not be migrated by the migrate_config.inc script due to major changes between versions. The default +values from the ampache.cfg.php.dist file will be used.

      + +auth_methods (mysql)
      +This defines which auth methods Auth will attempt to use and in which order, if auto_create isn't enabled. +The user must exist locally as well
      +
      +tag_order (id3v2,id3v1,vorbiscomment,quicktime,ape,asf)
      +This determines the tag order for all cataloged music. If none of the listed tags are found then ampache will default to +the first tag format that was found.
      +
      +album_art_order (db,id3,folder,lastfm,amazon)
      +Simply arrange the following in the order you would like ampache to search if you want to disable one of the search +method simply comment it out valid values are
      +
      +amazon_base_urls (http://webservices.amazon.com)
      +An array of Amazon sites to search. NOTE: This will search each of these sites in turn so don't expect it +to be lightning fast! It is strongly recommended that only one of these is selected at any
      +
      +downsample_cmd
      +This variable no longer exists, all downsampling/transcoding is handled by the transcode_* please see config file for details. +
      +
      +
      +

      Ampache Debug.
      +For the love of Music.

      +
      + + diff --git a/sources/templates/show_test_table.inc.php b/sources/templates/show_test_table.inc.php new file mode 100644 index 0000000..8e10fcb --- /dev/null +++ b/sources/templates/show_test_table.inc.php @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +    "; + } else { + echo debug_result(false, "SKIPPED"); + } + + ?> + + + + + + diff --git a/sources/templates/show_update_items.inc.php b/sources/templates/show_update_items.inc.php new file mode 100644 index 0000000..d2fa992 --- /dev/null +++ b/sources/templates/show_update_items.inc.php @@ -0,0 +1,29 @@ + +
      +   + + diff --git a/sources/templates/show_user.inc.php b/sources/templates/show_user.inc.php new file mode 100644 index 0000000..0045cac --- /dev/null +++ b/sources/templates/show_user.inc.php @@ -0,0 +1,80 @@ +last_seen ? date("m\/d\/y - H:i",$client->last_seen) : T_('Never'); +$create_date = $client->create_date ? date("m\/d\/y - H:i",$client->create_date) : T_('Unknown'); +$client->format(); +?> +fullname); ?> +f_avatar) { + echo '
      ' . $client->f_avatar . '
      '; +} +?> +
      + +
      +
      fullname; ?>
      + +
      +
      + +
      +
      + +
      +
      f_useage; ?>
      + +
      +
      + is_logged_in() AND $client->is_online()) { ?> + + + + +
      +

      + + + + + + +
      + id)); + $object_ids = $tmp_playlist->get_items(); + foreach ($object_ids as $object_data) { + $type = array_shift($object_data); + $object = new $type(array_shift($object_data)); + $object->format(); + echo $object->f_link; ?> +
      + +

      + +id); + Song::build_cache(array_keys($data)); + $user_id = $client->id; + require AmpConfig::get('prefix') . '/templates/show_recently_played.inc.php'; +?> diff --git a/sources/templates/show_user_activate.inc.php b/sources/templates/show_user_activate.inc.php new file mode 100644 index 0000000..6dc4125 --- /dev/null +++ b/sources/templates/show_user_activate.inc.php @@ -0,0 +1,65 @@ + + + + + +<?php echo AmpConfig::get('site_title'); ?> - <?php echo T_('Registration'); ?> + + + + + + + + + + +
      + +

      +

      + ', ''); ?> +

      + +

      +

      + +
      +
      +

      Ampache
      +For the love of Music.

      +
      + + diff --git a/sources/templates/show_user_preferences.inc.php b/sources/templates/show_user_preferences.inc.php new file mode 100644 index 0000000..c91e5ae --- /dev/null +++ b/sources/templates/show_user_preferences.inc.php @@ -0,0 +1,61 @@ + +fullname),'box box_preferences'); ?> +
      + ++ + + + + + + + + + + + + + + + + +
      + +
      +
      + + + +
      +
       
      +
      + + diff --git a/sources/templates/show_user_registration.inc.php b/sources/templates/show_user_registration.inc.php new file mode 100644 index 0000000..20e598f --- /dev/null +++ b/sources/templates/show_user_registration.inc.php @@ -0,0 +1,120 @@ + + + + + +<?php echo AmpConfig::get('site_title'); ?> - <?php echo T_('Registration'); ?> + + + + + + + + +
      + + +
      +
      + +

      +
      +
      + +
      + +
      + + +
      +
      + +

      +
      + + + + +
      +
      + + + +
      + +
      + + + +
      +
      + + + +
      + +
      + + + +
      + +
      + + +
      + +
      + +
      + + + + + + +
      + + ' /> +
      +
      + diff --git a/sources/templates/show_user_row.inc.php b/sources/templates/show_user_row.inc.php new file mode 100644 index 0000000..de50683 --- /dev/null +++ b/sources/templates/show_user_row.inc.php @@ -0,0 +1,64 @@ + + + +f_avatar_mini) { + echo $client->f_avatar_mini; +} +?> + fullname; ?> (username; ?>) + + + + + f_useage; ?> + + + + ip_history; ?> + + + + + + + disabled == '1') { + echo "id\">" . UI::get_icon('enable', T_('Enable')) . ""; + } else { + echo "id\">" . UI::get_icon('disable', T_('Disable')) .""; + } + ?> + + + is_logged_in()) AND ($client->is_online())) { + echo "   "; + } elseif ($client->disabled == 1) { + echo "   "; + } else { + echo "   "; + } +?> diff --git a/sources/templates/show_userflag.inc.php b/sources/templates/show_userflag.inc.php new file mode 100644 index 0000000..3f24100 --- /dev/null +++ b/sources/templates/show_userflag.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_users.inc.php b/sources/templates/show_users.inc.php new file mode 100644 index 0000000..da9145a --- /dev/null +++ b/sources/templates/show_users.inc.php @@ -0,0 +1,80 @@ + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> + ++ + + + + + + + + + + + + + + + + + + + + + + + +format(); + $last_seen = $client->last_seen ? date("m\/d\/Y - H:i",$client->last_seen) : T_('Never'); + $create_date = $client->create_date ? date("m\/d\/Y - H:i",$client->create_date) : T_('Unknown'); +?> + + + + + + + + + + + + + + + + + + +
      id . '&type=user&sort=fullname', T_('Fullname'),'users_sort_fullname'); ?>( )id . '&type=user&sort=last_seen', T_('Last Seen'),'users_sort_lastseen'); ?>id . '&type=user&sort=create_date', T_('Registration Date'),'users_sort_createdate'); ?>
      id . '&type=user&sort=fullname', T_('Fullname'),'users_sort_fullname1'); ?>( )id . '&type=user&sort=last_seen', T_('Last Seen'),'users_sort_lastseen1'); ?>id . '&type=user&sort=create_date', T_('Registration Date'),'users_sort_createdate1'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_verify_catalog.inc.php b/sources/templates/show_verify_catalog.inc.php new file mode 100644 index 0000000..3bc7315 --- /dev/null +++ b/sources/templates/show_verify_catalog.inc.php @@ -0,0 +1,31 @@ +[ $this->name ]"); +echo "
      \n"; +printf(ngettext('%d item found checking tag information', '%d items found checking tag information', $number), $number); +echo "
      \n\n"; +echo T_('Verified') . ': ' . $catalog_verify_found . '
      '; +echo T_('Reading') . ': ' . $catalog_verify_directory . ''; +UI::show_box_bottom(); diff --git a/sources/templates/show_video_row.inc.php b/sources/templates/show_video_row.inc.php new file mode 100644 index 0000000..838de25 --- /dev/null +++ b/sources/templates/show_video_row.inc.php @@ -0,0 +1,48 @@ + + +   +
      + + id,'play', T_('Play'),'play_video_' . $video->id); ?> + + id . '&append=true','play_add', T_('Play last'),'addplay_video_' . $video->id); ?> + + +
      + +f_title; ?> + + + id,'add', T_('Add to temporary playlist'),'add_video_' . $video->id); ?> + + +f_codec; ?> +f_resolution; ?> +f_length; ?> +f_tags; ?> + + + + + diff --git a/sources/templates/show_videos.inc.php b/sources/templates/show_videos.inc.php new file mode 100644 index 0000000..f39aa15 --- /dev/null +++ b/sources/templates/show_videos.inc.php @@ -0,0 +1,70 @@ +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; +?> + + + + + + + + + + + + + + + format(); + ?> + + + + + + + + + + + + + + + + + + + + + + +
      id . '&type=video&sort=title', T_('Title'),'sort_video_title'); ?>id . '&type=video&sort=codec', T_('Codec'),'sort_video_codec'); ?>id . '&type=video&sort=resolution', T_('Resolution'),'sort_video_rez'); ?>id . '&type=video&sort=length', T_('Time'),'sort_video_length'); ?>
      id . '&type=video&sort=title', T_('Title'),'sort_video_title'); ?>id . '&type=video&sort=codec', T_('Codec'),'sort_video_codec'); ?>id . '&type=video&sort=resolution', T_('Resolution'),'sort_video_rez'); ?>id . '&type=video&sort=length', T_('Time'),'sort_video_length'); ?>
      + +get_show_header()) require AmpConfig::get('prefix') . '/templates/list_header.inc.php'; ?> diff --git a/sources/templates/show_wanted.inc.php b/sources/templates/show_wanted.inc.php new file mode 100644 index 0000000..f23b436 --- /dev/null +++ b/sources/templates/show_wanted.inc.php @@ -0,0 +1,25 @@ + + + + diff --git a/sources/templates/show_wanted_album_row.inc.php b/sources/templates/show_wanted_album_row.inc.php new file mode 100644 index 0000000..a1fee76 --- /dev/null +++ b/sources/templates/show_wanted_album_row.inc.php @@ -0,0 +1,34 @@ + + +f_name_link; ?> +f_artist_link; ?> +year; ?> +f_user; ?> + +
      + show_action_buttons(); + ?> +
      + diff --git a/sources/templates/show_wanted_albums.inc.php b/sources/templates/show_wanted_albums.inc.php new file mode 100644 index 0000000..9a1f674 --- /dev/null +++ b/sources/templates/show_wanted_albums.inc.php @@ -0,0 +1,46 @@ + + + + + + + + + + + + + format(); + ?> + + + + + +
      id . '&type=wanted&sort=name', T_('Album'),'sort_wanted_album'); ?>id . '&type=wanted&sort=artist', T_('Artist'),'sort_wanted_artist'); ?>id . '&type=wanted&sort=year', T_('Year'),'sort_wanted_year'); ?>id . '&type=wanted&sort=user', T_('User'),'sort_wanted_user'); ?>
      + + diff --git a/sources/templates/show_web_player.inc.php b/sources/templates/show_web_player.inc.php new file mode 100644 index 0000000..ab9140e --- /dev/null +++ b/sources/templates/show_web_player.inc.php @@ -0,0 +1,91 @@ + + + + +<?php echo AmpConfig::get('site_title'); ?> + + + +urls[0]; + } else { + $isVideo = WebPlayer::is_playlist_video($playlist); + } +} +require_once AmpConfig::get('prefix') . '/templates/show_html5_player.inc.php'; +?> diff --git a/sources/templates/sidebar.inc.php b/sources/templates/sidebar.inc.php new file mode 100644 index 0000000..5e664bc --- /dev/null +++ b/sources/templates/sidebar.inc.php @@ -0,0 +1,67 @@ +'home', 'title' => T_('Home'), 'icon'=>'home', 'access'=>5); +$sidebar_items[] = array('id'=>'localplay', 'title' => T_('Localplay'), 'icon'=>'volumeup', 'access'=>5); +$sidebar_items[] = array('id'=>'preferences', 'title' => T_('Preferences'), 'icon'=>'edit', 'access'=>5); +$sidebar_items[] = array('id'=>'modules','title' => T_('Modules'),'icon'=>'plugin','access'=>100); +$sidebar_items[] = array('id'=>'admin', 'title' => T_('Admin'), 'icon'=>'admin', 'access'=>100); + +$web_path = AmpConfig::get('web_path'); +?> + + diff --git a/sources/templates/sidebar_admin.inc.php b/sources/templates/sidebar_admin.inc.php new file mode 100644 index 0000000..4fe7df7 --- /dev/null +++ b/sources/templates/sidebar_admin.inc.php @@ -0,0 +1,66 @@ + +
        +
      • +
          +
        • +
        • +
        +
      • + +
      • +
          +
        • +
        • +
        +
      • +
      • +
          +
        • +
        • +
        +
      • +
      • +
          +
        • +
        • +
        • + +
        • + +
        +
      • + +
      • +
          + +
        • + +
        +
      • + +
      diff --git a/sources/templates/sidebar_home.inc.php b/sources/templates/sidebar_home.inc.php new file mode 100644 index 0000000..d4ad7aa --- /dev/null +++ b/sources/templates/sidebar_home.inc.php @@ -0,0 +1,108 @@ + +
        +
      • + +
          +
        • +
        • +
        • +
        • +
        • +
        • +
        • + +
        • + +
        • +
        • +
        +
      • + + +
      • +

        +
          +
        • + +
        • + + + current_instance(); + $class = $current_instance ? '' : ' class="active_instance"'; + ?> +
        • + +
        • +
        +
      • +
      • +

        +
          +
        • +
        • +
        • +
        • +
        • +
        +
      • +
      • +

        +
          +
        • +
        • + + +
        • + + +
        • + + +
        • + + +
        • + +
        • +
        +
      • +
      • +

        + +
      • +
      diff --git a/sources/templates/sidebar_localplay.inc.php b/sources/templates/sidebar_localplay.inc.php new file mode 100644 index 0000000..52cffb2 --- /dev/null +++ b/sources/templates/sidebar_localplay.inc.php @@ -0,0 +1,75 @@ + + +
        + +current_instance(); + $class = $current_instance ? '' : ' class="active_instance"'; +?> + +
      • +
          + +
        • +
        • + +
        • +
        +
      • + +
      • +
          +
        • >
        • + get_instances(); + foreach ($instances as $uid=>$name) { + $name = scrub_out($name); + $class = ''; + if ($uid == $current_instance) { + $class = ' class="active_instance"'; + } + ?> +
        • >
        • + +
        +
      • + +
      • + +
      • + +
      • + +
      • + + +
      diff --git a/sources/templates/sidebar_modules.inc.php b/sources/templates/sidebar_modules.inc.php new file mode 100644 index 0000000..1b91bb4 --- /dev/null +++ b/sources/templates/sidebar_modules.inc.php @@ -0,0 +1,46 @@ + +
        +
      • +
          +
        • +
        • +
        • +
        +
      • +
      • +
          +
        • +
        • +
        +
      • + +
      diff --git a/sources/templates/sidebar_preferences.inc.php b/sources/templates/sidebar_preferences.inc.php new file mode 100644 index 0000000..2c1a2a1 --- /dev/null +++ b/sources/templates/sidebar_preferences.inc.php @@ -0,0 +1,42 @@ + +
        +
      • +
          + +
        • + +
        • +
        +
      • +
      diff --git a/sources/templates/stylesheets.inc.php b/sources/templates/stylesheets.inc.php new file mode 100644 index 0000000..5fe881c --- /dev/null +++ b/sources/templates/stylesheets.inc.php @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/sources/templates/subnavbar.inc.php b/sources/templates/subnavbar.inc.php new file mode 100644 index 0000000..01446be --- /dev/null +++ b/sources/templates/subnavbar.inc.php @@ -0,0 +1,40 @@ + + diff --git a/sources/templates/uberviz.inc.php b/sources/templates/uberviz.inc.php new file mode 100644 index 0000000..8de6a9b --- /dev/null +++ b/sources/templates/uberviz.inc.php @@ -0,0 +1,47 @@ +
      +
      +
      + +
      +
      UberViz
      +
      +
      FPS
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      +
      +
      +
      +
      +
      +
      diff --git a/sources/test.php b/sources/test.php new file mode 100644 index 0000000..b06ccef --- /dev/null +++ b/sources/test.php @@ -0,0 +1,39 @@ +; +;;;;;;;;;;;;;;;;;; +; Copyright (c) 2001 - 2013 Ampache.org +; +; This program is free software; you can redistribute it and/or +; modify it under the terms of the GNU General Public License v2 +; as published by the Free Software Foundation. +; +; 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, write to the Free Software +; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +; +;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Classic Ampache Theme +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; Theme Name +; This is the actual name of the theme that +; will be displayed in the preferences screen +; DEFAULT: ampache-theme +name = "Classic Ampache" + +; Theme Author +; This is just a way of giving credit to the +; person who actually created this theme +; DEFAULT: N/A +author = "Ros" + +; Theme Maintainer +; This is just a way of listing who is responsible for +; maintaining this theme in case it's not working right +; please include an e-mail address so you can be contacted +; DEFAULT: N/A +maintainer = "Spocky" + +; Orientation +; This was added as of 3.3.2-Alpha4, this tells Ampache if this theme +; uses vertical or horizontal orientation of the menu, if this is a horizontal +; theme then it will not show the quick search and quick random play forms +orientation = "vertical" + +; Submenu +; If this is set to simple the sub menu's will only be shown when you're on one of the +; respective pages. If you want to make the menu's something like the classic theme +; comment this out +;submenu = "simple" diff --git a/sources/themes/fresh/ampache.psd b/sources/themes/fresh/ampache.psd new file mode 100644 index 0000000..fe20752 Binary files /dev/null and b/sources/themes/fresh/ampache.psd differ diff --git a/sources/themes/fresh/images/ajax-loader.gif b/sources/themes/fresh/images/ajax-loader.gif new file mode 100644 index 0000000..b20f505 Binary files /dev/null and b/sources/themes/fresh/images/ajax-loader.gif differ diff --git a/sources/themes/fresh/images/ajax-loader2.gif b/sources/themes/fresh/images/ajax-loader2.gif new file mode 100644 index 0000000..aaa180c Binary files /dev/null and b/sources/themes/fresh/images/ajax-loader2.gif differ diff --git a/sources/themes/fresh/images/ampache.png b/sources/themes/fresh/images/ampache.png new file mode 100644 index 0000000..bf502db Binary files /dev/null and b/sources/themes/fresh/images/ampache.png differ diff --git a/sources/themes/fresh/images/blank-pixel.gif b/sources/themes/fresh/images/blank-pixel.gif new file mode 100644 index 0000000..17d4390 Binary files /dev/null and b/sources/themes/fresh/images/blank-pixel.gif differ diff --git a/sources/themes/fresh/images/blankalbum.jpg b/sources/themes/fresh/images/blankalbum.jpg new file mode 100644 index 0000000..33e89a0 Binary files /dev/null and b/sources/themes/fresh/images/blankalbum.jpg differ diff --git a/sources/themes/fresh/images/icons/icon_add.png b/sources/themes/fresh/images/icons/icon_add.png new file mode 100755 index 0000000..da42e17 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_add.png differ diff --git a/sources/themes/fresh/images/icons/icon_add12.png b/sources/themes/fresh/images/icons/icon_add12.png new file mode 100755 index 0000000..6bbba51 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_add12.png differ diff --git a/sources/themes/fresh/images/icons/icon_add2.png b/sources/themes/fresh/images/icons/icon_add2.png new file mode 100755 index 0000000..1138739 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_add2.png differ diff --git a/sources/themes/fresh/images/icons/icon_add_user.png b/sources/themes/fresh/images/icons/icon_add_user.png new file mode 100755 index 0000000..9f6c0f5 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_add_user.png differ diff --git a/sources/themes/fresh/images/icons/icon_admin.png b/sources/themes/fresh/images/icons/icon_admin.png new file mode 100755 index 0000000..ee0c771 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_admin.png differ diff --git a/sources/themes/fresh/images/icons/icon_all.png b/sources/themes/fresh/images/icons/icon_all.png new file mode 100755 index 0000000..2dfaef5 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_all.png differ diff --git a/sources/themes/fresh/images/icons/icon_batch_download.png b/sources/themes/fresh/images/icons/icon_batch_download.png new file mode 100755 index 0000000..67a3d9a Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_batch_download.png differ diff --git a/sources/themes/fresh/images/icons/icon_delete.png b/sources/themes/fresh/images/icons/icon_delete.png new file mode 100755 index 0000000..6b9fa6d Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_delete.png differ diff --git a/sources/themes/fresh/images/icons/icon_disable.png b/sources/themes/fresh/images/icons/icon_disable.png new file mode 100755 index 0000000..7af3a51 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_disable.png differ diff --git a/sources/themes/fresh/images/icons/icon_edit.png b/sources/themes/fresh/images/icons/icon_edit.png new file mode 100755 index 0000000..0699492 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_edit.png differ diff --git a/sources/themes/fresh/images/icons/icon_edit2.png b/sources/themes/fresh/images/icons/icon_edit2.png new file mode 100755 index 0000000..7dc0d54 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_edit2.png differ diff --git a/sources/themes/fresh/images/icons/icon_enable.png b/sources/themes/fresh/images/icons/icon_enable.png new file mode 100755 index 0000000..210b1a6 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_enable.png differ diff --git a/sources/themes/fresh/images/icons/icon_feed.png b/sources/themes/fresh/images/icons/icon_feed.png new file mode 100755 index 0000000..cdf4e8f Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_feed.png differ diff --git a/sources/themes/fresh/images/icons/icon_home.png b/sources/themes/fresh/images/icons/icon_home.png new file mode 100755 index 0000000..622a2b7 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_home.png differ diff --git a/sources/themes/fresh/images/icons/icon_logout.png b/sources/themes/fresh/images/icons/icon_logout.png new file mode 100755 index 0000000..2bc51ac Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_logout.png differ diff --git a/sources/themes/fresh/images/icons/icon_next.png b/sources/themes/fresh/images/icons/icon_next.png new file mode 100755 index 0000000..7ae440a Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_next.png differ diff --git a/sources/themes/fresh/images/icons/icon_pause.png b/sources/themes/fresh/images/icons/icon_pause.png new file mode 100755 index 0000000..af57b25 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_pause.png differ diff --git a/sources/themes/fresh/images/icons/icon_play.png b/sources/themes/fresh/images/icons/icon_play.png new file mode 100755 index 0000000..2dfaef5 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_play.png differ diff --git a/sources/themes/fresh/images/icons/icon_playlist_add.png b/sources/themes/fresh/images/icons/icon_playlist_add.png new file mode 100755 index 0000000..df35ed6 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_playlist_add.png differ diff --git a/sources/themes/fresh/images/icons/icon_plugin.png b/sources/themes/fresh/images/icons/icon_plugin.png new file mode 100755 index 0000000..0f3736f Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_plugin.png differ diff --git a/sources/themes/fresh/images/icons/icon_prev.png b/sources/themes/fresh/images/icons/icon_prev.png new file mode 100755 index 0000000..a39522f Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_prev.png differ diff --git a/sources/themes/fresh/images/icons/icon_random.png b/sources/themes/fresh/images/icons/icon_random.png new file mode 100755 index 0000000..ab3dd30 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_random.png differ diff --git a/sources/themes/fresh/images/icons/icon_stop.png b/sources/themes/fresh/images/icons/icon_stop.png new file mode 100755 index 0000000..7c6af7f Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_stop.png differ diff --git a/sources/themes/fresh/images/icons/icon_view.png b/sources/themes/fresh/images/icons/icon_view.png new file mode 100755 index 0000000..b4b2312 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_view.png differ diff --git a/sources/themes/fresh/images/icons/icon_volumeup.png b/sources/themes/fresh/images/icons/icon_volumeup.png new file mode 100755 index 0000000..62fcfc6 Binary files /dev/null and b/sources/themes/fresh/images/icons/icon_volumeup.png differ diff --git a/sources/themes/fresh/images/ratings/star_rating.gif b/sources/themes/fresh/images/ratings/star_rating.gif new file mode 100644 index 0000000..55ac1f5 Binary files /dev/null and b/sources/themes/fresh/images/ratings/star_rating.gif differ diff --git a/sources/themes/fresh/images/ratings/star_rating.png b/sources/themes/fresh/images/ratings/star_rating.png new file mode 100644 index 0000000..6c75aed Binary files /dev/null and b/sources/themes/fresh/images/ratings/star_rating.png differ diff --git a/sources/themes/fresh/preview.png b/sources/themes/fresh/preview.png new file mode 100644 index 0000000..529c901 Binary files /dev/null and b/sources/themes/fresh/preview.png differ diff --git a/sources/themes/fresh/templates/default.css b/sources/themes/fresh/templates/default.css new file mode 100644 index 0000000..d7e1e40 --- /dev/null +++ b/sources/themes/fresh/templates/default.css @@ -0,0 +1,1255 @@ +/* vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */ +/** + * + * LICENSE: GNU General Public License, version 2 (GPLv2) + * Copyright 2001 - 2014 Ampache.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License v2 + * as published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +/*********************************************** + General style rules +***********************************************/ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td +{ + border:0; + outline:0; + font-size:100%; + vertical-align:baseline; + background:transparent; + margin:0; + padding:0; +} + +body +{ + line-height:1; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size:12px; + color:#111; + background:#e8e8e8; +} + +blockquote,q +{ + quotes:none; +} + +blockquote:before,blockquote:after,q:before,q:after +{ + content:none; +} + +:focus +{ + outline:0; +} + +del +{ + text-decoration:line-through; +} + +table +{ + border-collapse:collapse; + border-spacing:0; +} + +.error +{ + color:#c33; +} + +a +{ + color:#111; + text-decoration:none; +} + +h3 +{ + font-size:20px; + margin-bottom: 5px; +} + +hr { + border-top: 1px solid #bbb; + border-bottom: 1px solid #eee; + border-right: 0; + border-left: 0; +} + +/*********************************************** + Wrappers +***********************************************/ +#maincontainer +{ + width:100%; +} + +#sidebar +{ + width:140px; +} + +#content +{ + margin-top:32px; + margin-left:160px; + margin-right:230px; + width:auto; +} + +.box +{ + margin-top:5px; +} + +#rightbar +{ + width:180px; + margin-top:32px; + right:10px; + + /* Set position to fixed to 'pin' the playlist */ + /*position:fixed; + max-height: 85%; + overflow-x: hidden; + overflow-y: scroll;*/ + + position: absolute; + clear:both; +} + +#footer +{ + width: auto; + margin-top: 20px; + margin-bottom: 120px; + margin-right: 230px; +} + +/*********************************************** + Header +***********************************************/ +#header +{ + background:#222; + line-height:30px; + height:50px; + padding-top:7px; + padding-bottom:7px; + width: 100%; +} + +#headerbox +{ + margin-right: 7px; +} + +#header .box-inside +{ + width:auto; + text-align:right; + float:right; +} + +#header .clearfix +{ + clear:none; +} + +#header .box-inside input +{ + border:2px solid #666; + border-radius:2px; + background:#000; + font-weight:700; + color:#eee; + font-size:10px; + width:120px; + padding:4px; +} + +#header input.button +{ + width:80px; + background:#222; +} + +#header #advSearchBtn +{ + color:#aaa; + padding:4px; +} + +#header #loginInfo +{ + display: block; + color:#eee; +} + +#header #loginInfo a +{ + color:#ccc; +} + +/*********************************************** + Sidebar +***********************************************/ +#sidebar-page +{ + position:absolute; + padding-bottom:0.5em; + width:140px; + left:7px; +} + +#sidebar-tabs +{ + width:140px; + background:#e8e8e8; + float:left; + padding:7px; + margin-left: 8px; +} + +#sidebar-tabs li.sb1 +{ + float:left; + margin-right:5px; +} + +#sidebar-tabs .sb2 li +{ + background:#fff; + border-radius:4px; + margin-top:7px; + padding:7px; +} + +#sidebar-tabs .sb2 li h4 +{ + font-weight:700; + display:block; + border-radius:3px; + background:#ddd; + color:#222; + padding:5px; +} + +#sidebar-tabs .sb2 li ul li +{ + background:#fff; + border-radius:0; + margin-top:0; + padding:0; +} + +#sidebar-tabs .sb2 li ul li a +{ + font-weight:700; + display:block; + border-radius:3px; + padding:5px; +} + +#sidebar-tabs .sb2 li ul li a:hover +{ + background:#d2e6f9; +} + +#sidebar-tabs .sb2 #browse_filters li +{ + background:#f9f9f9; +} + +#sidebar-tabs .sb2 #browse_filters #multi_alpha_filterLabel +{ + margin:5px 3px 0; + display: block; +} + +#sidebar-tabs .sb2 #browse_filters #multi_alpha_filter +{ + border:1px solid #bbb; + border-radius:2px; + padding:4px; + width: 115px; +} + +#sidebar-tabs #catalog_select +{ + width:130px; +} + +/* Localplay */ +.active_instance { + border: 1px inset #99ccff; +} + +/*********************************************** + Rightbar +***********************************************/ +#rightbar +{ + background:#fff; + border-radius:4px; + padding:7px; +} + +#rightbar #rb_action +{ + padding:4px; +} + +#rightbar #rb_action li { + margin-right: 5px; +} + +#rightbar li#rb_add,#rightbar li#pl_add +{ + position:relative; + z-index:10; +} + +#rightbar li:hover .submenu +{ + display:block; +} + +#rightbar .submenu +{ + display:none; + position:absolute; + left:-50px; + top:14px; + background:#fff; + border:2px solid silver; + width:120px; + padding:0.6em; +} + +#rightbar #rb_action .submenu li { + margin: 0; +} + +* html #rightbar .submenu +{ + right:100px; +} + +/* IE6 fix */ +#rightbar .submenu a +{ + display:block; + border-bottom:1px dotted #ddd; + color:#5b5b5b; + text-decoration:none; + text-align:left; + padding:0.4em; +} + +#rightbar .submenu a:hover +{ + color:#333; +} + +#rightbar #rb_current_playlist li +{ + position:relative; + padding-right:20px; +} + +#rightbar #rb_current_playlist li a +{ + display:block; + padding:0.2em; + color:#5b5b5b; + border-bottom: 1px solid #f3f3f3; + line-height:16px; +} + +#rightbar #rb_current_playlist li a:hover +{ + color: #111; +} + +#rightbar #rb_current_playlist li.odd +{ + background: #f9f9f9; +} + +#rightbar .delitem +{ + position:absolute; + right:0; + top:0; +} + +/* Rightbar Localplay Controls */ +#rightbar #localplay-control +{ + padding-left:5px; +} + +#rightbar #localplay-control { + border: 1px solid #e3e3e3; + background: #f6f6f6; + padding: 5px; + text-align: center; + margin: 7px 0px; +} + +#rightbar #localplay-control img { + vertical-align: bottom; +} + +/*********************************************** + Content +***********************************************/ +#ajax-loading +{ + position:absolute; + background:#444; + text-align:center; + width:90px; + top:0; + color:#bbb; + margin:0 auto; + padding: 3px 1px 3px 10px; + border: 2px solid #333; + border-top: 0px; + border-left: 0px; + background: #444 url(../images/ajax-loader.gif) no-repeat 2px; + display: none; +} + +.box-content h3.box-title,.browse_content .box h3.box-title +{ + display:block; + background:#888; + color:#eee; + border-bottom:1px solid #335b0d; + border-top-right-radius:3px; + border-top-left-radius:3px; + font-weight:400; + padding:7px; + margin-bottom: 0px; +} + +.browse_content .box.browse_song h3.box-title +{ + background:#66B717; +} + +.browse_content .box.browse_album h3.box-title +{ + background:#09c; +} + +.browse_content .box.browse_artist h3.box-title +{ + background:#c60; +} + +.list-header +{ + border-left:1px solid #bbb; + border-right:1px solid #bbb; + border-bottom: 1px solid #bbb; + padding:7px; +} + +table.tabledata +{ + width:100%; + text-align:left; + color: #eee; + margin-bottom: 20px; +} + +table.tabledata th +{ + /*border:1px solid #bbb; + border-left:1px solid #ccc; + border-right:1px solid #ccc;*/ + background:#eee; + font-weight:700; + font-size:11px; + color:#444; + padding:7px 10px; +} + +table.tabledata tr +{ + height: 25px; +} + +.browse_content table.tabledata th, .browse_content table.tabledata +{ + border-top: 0px; +} + +table.tabledata tbody +{ + background:#fff; +} + +table .th-bottom { + border-top: 1px solid #ccc; +} + +table.tabledata tbody td +{ + vertical-align: middle; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 3px 10px 3px 0; + color: #686868; +} + +.browse_content table.tabledata tbody tr:hover +{ + background:#b3d6f7; +} + +table.tabledata tbody .odd +{ + background:#eee; +} + +table.tabledata tbody .cel_play { + max-width: 40px; + width: 40px !important; + text-align: right; +} + +table.tabledata tbody .cel_play_content { + display: block; +} + +table.tabledata tbody .cel_play_hover { + display: none; +} + +table.tabledata tbody tr:hover .cel_play_hover, table.tabledata tbody tr:focus .cel_play_hover { + display: block; +} +table.tabledata tbody tr:hover .cel_play_content, table.tabledata tbody tr:focus .cel_play_content { + display: none; +} + +table.tabledata tbody .cel_add { + max-width: 60px; + width: 60px !important; + text-align: right; +} + +table.tabledata tbody .cel_item_add { + display: none; +} + +table.tabledata tbody tr:hover .cel_item_add, table.tabledata tbody tr:focus .cel_item_add { + display: block; +} + +table.tabledata tbody .cel_time { + min-width: 40px; + width: 40px !important; +} + +table.tabledata tbody .cel_action { + width: 130px !important; + max-width: 100%; +} + +table.tabledata tbody td.cel_rating { + width: 100px !important; + max-width: 100px; +} + +table.tabledata tbody td.cel_userflag { + width: 40px !important; + max-width: 40px; +} + +table.tabledata tbody .cel_tags { + width: 150px !important; + max-width: 150px; +} + +table.tabledata tbody .cel_cover img +{ + border: 4px double #bbb; +} + +table.tabledata .cel_drag { + max-width: 16px; + width: 16px !important; +} + +table.tabledata .cel_drag img:hover { + cursor: pointer; +} + +table.tabledata .cel_agent { + text-align: right; +} + +table.tabledata .cel_agent img:hover { + cursor: help; +} + +.box_preferences h4 +{ + font-size:15px; + background:#555; + color:#eee; + font-weight:400; + border:1px solid #333; + padding:5px; +} + +span.page-nb { + font-weight: bold; + font-size: 1.3em; +} + +/*********************************************** + Content (info-box) +***********************************************/ +#content .info-box h3 +{ + margin-top:10px; +} + +#content .info-box .box-content div.star-rating +{ + width:150px; + max-width:150px; +} + + +/* Random album (homepage) */ +#random_selection .random_album +{ + float:left; + width:125px; + margin-bottom: 10px; + text-align: center; +} + +#random_selection .random_album div { + display: inline; +} + +#random_selection .random_album img { + border: 4px double #ccc; +} + +#random_selection .random_album .star-rating +{ + float: left; +} + +#random_selection .random_album .play_album +{ + float: left; + margin-left: 7px; +} + +#random_selection .random_album .play_album a img +{ + border: 0px; +} + +#random_selection .box-bottom +{ + clear:left; +} + +#random_selection .art_album img { + width: 80px; + height: 80px; +} + +/*********************************************** + Content (now playing) +***********************************************/ +#now_playing { + margin-bottom: 20px; + width: auto; +} + +#now_playing .np_group { + float: left; +} + +#now_playing .np_row { + display: table; + margin-bottom: 10px; +} + +#now_playing .np_cell { + line-height: 15px; +} + +#now_playing .cel_rating label { + display: none; +} + +#now_playing .cel_userflag label { + display: none; +} + +#now_playing .np_group label { + font-weight: bold; +} + +#now_playing .cel_username { + width: 140px; +} + +#now_playing .cel_song, #now_playing .cel_album, #now_playing .cel_artist { + width: 200px; +} + +#now_playing .cel_albumart { + float: left; + width: 90px; +} + +#now_playing .cel_albumart img { + border: 4px double #ccc; + float: left; +} + +#now_playing .cel_lyrics { + margin-top: 5px; +} + +#now_playing .cel_lyrics a:hover { + color: #0099CC; +} + +#now_playing .similars { + margin-right: 10px; + padding-right: 5px; + margin-left: 10px; + padding-left: 5px; +} + +/*********************************************** + Content (Tag cloud) +***********************************************/ +.box-content #tag_filter +{ + padding:5px 0px; +} + +.clearfix { + clear: left; +} + +.box-bottom { + clear: both; +} + +span.fatalerror { + color: #c60; + padding: 5px; + display: block; +} + +span.nodata { + padding: 5px; + display: block; + font-style: italic; +} + +.box-content #tag_filter span +{ + padding: 2px 6px; + border: 1px solid #bbb; + margin: 1px; + float: left; + background: #e8e8e8; +} + +.box-content #tag_filter span:hover +{ + background: #ddd; + color: #222; + border: 1px solid #888; +} + +/*********************************************** + Content (inline edit) +***********************************************/ +.inline-edit select { + max-width: 200px; +} + + +/*********************************************** + Content (information-actions) +***********************************************/ +#information_actions +{ + margin-top:10px; + width:300px; +} + +#information_actions h3 +{ + /* only showing up in album view, so strange..bug*/ +} + +#content .info-box .box-content .album_art +{ + float: right; + border: 4px double #ccc; +} + +#information_actions ul li +{ + border-bottom:1px solid #ccc; + padding:3px 0; +} + +#information_actions .star-rating li +{ + padding:0; +} + +#information_actions li img +{ + vertical-align:top; +} + +#information_actions li a +{ + vertical-align:bottom; +} + +#information_actions li a:hover +{ + color:#09c; +} + +.item_right_info { + float: right; + max-width: 60%; +} + +.external_links { + text-align: right; +} + +.external_links a { + margin: 0px 5px 0px 0px; + opacity: 0.3; +} + +.external_links a:hover { + opacity: 1; +} + +#artist_summary { + margin-right: 150px; +} + +/************************************************/ +/* Styles for the star ratings */ +/************************************************/ +.star-rating +{ + position:relative; +} +.dynamic-star-rating +{ + width:95px; +} +.star-rating ul, +.star-rating a:hover, +.star-rating .current-rating +{ + background: url(../images/ratings/star_rating.png) left -1000px repeat-x; +} +.star-rating ul +{ + position:relative; + width:80px; + height:15px; + overflow:hidden; + list-style:none; + margin:0; + padding:0; + background-position: left top; +} +.star-rating li +{ + display: inline; +} + +.star-rating a, .star-rating span, +.star-rating .current-rating +{ + position:absolute; + top:0; + left:0; + text-indent:-1000em; + height:15px; + line-height:15px; + outline:none; + overflow:hidden; + border:none; +} + +.star-rating .star1 { width:20%; z-index:6; } +.star-rating .star2 { width:40%; z-index:5; } +.star-rating .star3 { width:60%; z-index:4; } +.star-rating .star4 { width:80%; z-index:3; } +.star-rating .star5 { width:100%; z-index:2;} +.star-rating .current-rating { z-index:1; background-position: left bottom; } + +.star-rating a.star0 +{ + left:0px; + width:16px; + background: url(../../../images/ratings/x_off.gif) left top; +} + +.dynamic-star-rating a:hover +{ + background-position: left center; +} + +.dynamic-star-rating a:hover.star0 +{ + background: url(../../../images/ratings/x.gif) left top; +} +.dynamic-star-rating ul +{ + left:16px; +} + +/************************************************/ +/* Styles for user flags */ +/************************************************/ +.userflag +{ + position: relative; + width:16px; + height:16px; +} + +.userflag a { + position:absolute; + display: inline; +} + +.userflag a.userflag_true +{ + width:16px; + height: 16px; + background: url(../../../images/icon_flag.png) left top; +} + +.userflag a:hover.userflag_true +{ + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a.userflag_false +{ + width:16px; + height:16px; + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a:hover.userflag_false +{ + background: url(../../../images/icon_flag.png) left top; +} + +/*********************************************** + Content (Track view) +***********************************************/ +dl.song_details +{ + padding: 0.5em; +} + +dl.song_details dd +{ + margin: 0 0 0 95px; + padding: 0 0 0.5em 0; +} + +dl.song_details dt +{ + float: left; + clear: left; + width: 80px; + text-align: right; + font-weight: bold; + color: #c60; +} + +dl.song_details a:hover +{ + color: #09c; + text-decoration: underline; +} + + +/*********************************************** + Footer +***********************************************/ +#footer +{ + text-align:right; +} + +#footer a:hover { + color: #09c; + text-decoration: underline; +} + +/*********************************************** + Login +***********************************************/ +#loginPage #header +{ + padding-left: 0px; + border: 1px solid #000; +} + +#loginPage #maincontainer { + width: 470px; + margin: 40px auto 0; + background: #fff; + border: 1px solid #ddd; +} + +#loginPage #loginbox { + padding: 20px; +} + +#loginPage #loginbox h2 +{ + display: none; +} + +#loginPage #loginbox label +{ + float: left; + width: 110px; + font-weight: bold; + color: #333; + text-align: right; + vertical-align: middle; + line-height: 30px; + margin-right: 20px; +} + +#usernamefield, #passwordfield, #remembermefield { + clear: both; +} + +#loginPage #loginbox .loginfield input +{ + padding: 5px; + border: 1px solid #ccc; + width: 170px; + color: #444; + font-size: 12px; + outline: 0px; + vertical-align: middle; +} + +#loginPage #loginbox a.button +{ + color: #777; + cursor: pointer; + padding: 4px 10px; +} + +#loginPage #loginbox a:hover +{ + color: #111; +} + +#loginPage .loginfield +{ + width: 400px; +} + +#loginPage span.error +{ + display: block; + clear: left; + padding: 10px; + background: #ff9999; + border: 1px solid #cc3333; + font-weight: bold; + color: #990000; + text-align: center; + margin-bottom: 10px; +} + +#loginPage .formValidation +{ + clear: left; + background: #eee; + padding: 5px 10px; + border: 1px solid #ddd; + text-align: center; +} + +#loginPage #loginbox input.button +{ + padding: 4px 10px; + border: 1px solid #bbb; + font-weight: bold; + color: #333; + background: #e9e9e9; + cursor: pointer; +} + +#loginPage #loginbox .loginfield input#rememberme +{ + margin-top: 7px; + width: 15px; +} + +#loginPage input#lostpasswordbutton +{ + margin-left: 130px; +} + +#loginPage #loginbox input.button:hover +{ + background: #ddd; + border: 1px solid #aaa; +} + +#loginPage #footer +{ + width: 470px; + margin: 10px auto; + color: #888; +} + +#loginPage #footer a +{ + color: #555; +} + +/*********************************************** + Server settings +***********************************************/ +table.tabledata .cel_php_setting, table.tabledata .cel_configuration, .cel_preference +{ + width: 200px; +} + +/************************************************/ +/* Web Player */ +/************************************************/ +#web_player #playlist +{ + overflow-x: hidden; + overflow-y: scroll; + top: 85px; + left: 210px; + right: 5px; + bottom: 5px; +} + +/*********************************************** + Other +***********************************************/ +ol,ul,#rightbar ul +{ + list-style:none; +} + +ins,#rightbar a +{ + text-decoration:none; +} + +.clearfix,#content .info-box .box-content,#additional_information +{ + clear:both; +} + +#header #headerlogo +{ + float:left; + margin-left: 7px; +} + +#header .box-inside div,#header .box-inside form,#rightbar #rb_action li,.star-rating li +{ + display:inline; +} + +.list-header a:hover +{ + text-decoration:underline; +} + +#localplay-control span,.box-content #tag_filter span +{ + cursor:pointer; +} + +.browse-options { + float: right; +} + +.browse-options form { + display: inline; +} + +.browse-options input[type=text] { + width: 50px; +} + +.jscroll-next { + width:50%; + display:block; + border:1px solid #ccc; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius: 10px; + background-color:#eee; + color:#999; + font-weight:bold; + text-align:center; + padding:10px 0; + cursor:pointer; + margin: auto; +} + +.jscroll-next:hover { + color:#666; +} diff --git a/sources/themes/fresh/theme.cfg.php b/sources/themes/fresh/theme.cfg.php new file mode 100644 index 0000000..09769c8 --- /dev/null +++ b/sources/themes/fresh/theme.cfg.php @@ -0,0 +1,52 @@ +;;;;;;;;;;;;;;;;;; +;; +;;;;;;;;;;;;;;;;;; +; Copyright 2001 - 2013 Ampache.org +; +; This program is free software; you can redistribute it and/or +; modify it under the terms of the GNU General Public License v2 +; as published by the Free Software Foundation. +; +; 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, write to the Free Software +; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +; +;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Fresh Ampache Theme +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; Theme Name +; This is the actual name of the theme that +; will be displayed in the preferences screen +; DEFAULT: ampache-theme +name = "Fresh" + +; Theme Author +; This is just a way of giving credit to the +; person who actually created this theme +; DEFAULT: N/A +author = "eigan" + +; Theme Maintainer +; This is just a way of listing who is responsible for +; maintaining this theme in case it's not working right +; please include an e-mail address so you can be contacted +; DEFAULT: N/A +maintainer = "eigan - einargangso@gmail.com" + +; Orientation +; This was added as of 3.3.2-Alpha4, this tells Ampache if this theme +; uses vertical or horizontal orientation of the menu, if this is a horizontal +; theme then it will not show the quick search and quick random play forms +orientation = "vertical" + +; Submenu +; If this is set to simple the sub menu's will only be shown when you're on one of the +; respective pages. If you want to make the menu's something like the classic theme +; comment this out +;submenu = "simple" diff --git a/sources/themes/greysme/images/ajax-loader.gif b/sources/themes/greysme/images/ajax-loader.gif new file mode 100644 index 0000000..30fdb5d Binary files /dev/null and b/sources/themes/greysme/images/ajax-loader.gif differ diff --git a/sources/themes/greysme/images/ampache.png b/sources/themes/greysme/images/ampache.png new file mode 100644 index 0000000..0486507 Binary files /dev/null and b/sources/themes/greysme/images/ampache.png differ diff --git a/sources/themes/greysme/images/ampache_back.gif b/sources/themes/greysme/images/ampache_back.gif new file mode 100644 index 0000000..9de458e Binary files /dev/null and b/sources/themes/greysme/images/ampache_back.gif differ diff --git a/sources/themes/greysme/images/ampache_menu.gif b/sources/themes/greysme/images/ampache_menu.gif new file mode 100644 index 0000000..136d124 Binary files /dev/null and b/sources/themes/greysme/images/ampache_menu.gif differ diff --git a/sources/themes/greysme/images/back-box.gif b/sources/themes/greysme/images/back-box.gif new file mode 100644 index 0000000..47879ac Binary files /dev/null and b/sources/themes/greysme/images/back-box.gif differ diff --git a/sources/themes/greysme/images/background.jpg b/sources/themes/greysme/images/background.jpg new file mode 100644 index 0000000..3339fe0 Binary files /dev/null and b/sources/themes/greysme/images/background.jpg differ diff --git a/sources/themes/greysme/images/blankalbum.gif b/sources/themes/greysme/images/blankalbum.gif new file mode 100644 index 0000000..8413d5e Binary files /dev/null and b/sources/themes/greysme/images/blankalbum.gif differ diff --git a/sources/themes/greysme/images/blankalbum.jpg b/sources/themes/greysme/images/blankalbum.jpg new file mode 100644 index 0000000..33e89a0 Binary files /dev/null and b/sources/themes/greysme/images/blankalbum.jpg differ diff --git a/sources/themes/greysme/images/box_bottom.png b/sources/themes/greysme/images/box_bottom.png new file mode 100644 index 0000000..13e379f Binary files /dev/null and b/sources/themes/greysme/images/box_bottom.png differ diff --git a/sources/themes/greysme/images/box_top.png b/sources/themes/greysme/images/box_top.png new file mode 100644 index 0000000..9d60b94 Binary files /dev/null and b/sources/themes/greysme/images/box_top.png differ diff --git a/sources/themes/greysme/images/button_back.png b/sources/themes/greysme/images/button_back.png new file mode 100644 index 0000000..deca200 Binary files /dev/null and b/sources/themes/greysme/images/button_back.png differ diff --git a/sources/themes/greysme/images/button_back2.png b/sources/themes/greysme/images/button_back2.png new file mode 100644 index 0000000..95e2ef9 Binary files /dev/null and b/sources/themes/greysme/images/button_back2.png differ diff --git a/sources/themes/greysme/images/curl.gif b/sources/themes/greysme/images/curl.gif new file mode 100644 index 0000000..c2c57eb Binary files /dev/null and b/sources/themes/greysme/images/curl.gif differ diff --git a/sources/themes/greysme/images/icons/icon_add.png b/sources/themes/greysme/images/icons/icon_add.png new file mode 100644 index 0000000..2bb4ee4 Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_add.png differ diff --git a/sources/themes/greysme/images/icons/icon_admin.png b/sources/themes/greysme/images/icons/icon_admin.png new file mode 100644 index 0000000..e587c9e Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_admin.png differ diff --git a/sources/themes/greysme/images/icons/icon_all.png b/sources/themes/greysme/images/icons/icon_all.png new file mode 100644 index 0000000..63f0301 Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_all.png differ diff --git a/sources/themes/greysme/images/icons/icon_batch_download.png b/sources/themes/greysme/images/icons/icon_batch_download.png new file mode 100644 index 0000000..f30c96b Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_batch_download.png differ diff --git a/sources/themes/greysme/images/icons/icon_browse.png b/sources/themes/greysme/images/icons/icon_browse.png new file mode 100644 index 0000000..798f6fc Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_browse.png differ diff --git a/sources/themes/greysme/images/icons/icon_delete.png b/sources/themes/greysme/images/icons/icon_delete.png new file mode 100644 index 0000000..934a0ea Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_delete.png differ diff --git a/sources/themes/greysme/images/icons/icon_download.png b/sources/themes/greysme/images/icons/icon_download.png new file mode 100644 index 0000000..f30c96b Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_download.png differ diff --git a/sources/themes/greysme/images/icons/icon_edit.png b/sources/themes/greysme/images/icons/icon_edit.png new file mode 100644 index 0000000..0b8271b Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_edit.png differ diff --git a/sources/themes/greysme/images/icons/icon_home.png b/sources/themes/greysme/images/icons/icon_home.png new file mode 100644 index 0000000..b2476cd Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_home.png differ diff --git a/sources/themes/greysme/images/icons/icon_logout.png b/sources/themes/greysme/images/icons/icon_logout.png new file mode 100644 index 0000000..95b6e31 Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_logout.png differ diff --git a/sources/themes/greysme/images/icons/icon_playlist_add.png b/sources/themes/greysme/images/icons/icon_playlist_add.png new file mode 100644 index 0000000..bc0a051 Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_playlist_add.png differ diff --git a/sources/themes/greysme/images/icons/icon_random.png b/sources/themes/greysme/images/icons/icon_random.png new file mode 100644 index 0000000..144955a Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_random.png differ diff --git a/sources/themes/greysme/images/icons/icon_volumeup.png b/sources/themes/greysme/images/icons/icon_volumeup.png new file mode 100644 index 0000000..a725832 Binary files /dev/null and b/sources/themes/greysme/images/icons/icon_volumeup.png differ diff --git a/sources/themes/greysme/images/list_back-old.png b/sources/themes/greysme/images/list_back-old.png new file mode 100644 index 0000000..60b8250 Binary files /dev/null and b/sources/themes/greysme/images/list_back-old.png differ diff --git a/sources/themes/greysme/images/list_back.png b/sources/themes/greysme/images/list_back.png new file mode 100644 index 0000000..84b26e1 Binary files /dev/null and b/sources/themes/greysme/images/list_back.png differ diff --git a/sources/themes/greysme/images/overlay.png b/sources/themes/greysme/images/overlay.png new file mode 100644 index 0000000..69cd1c4 Binary files /dev/null and b/sources/themes/greysme/images/overlay.png differ diff --git a/sources/themes/greysme/images/puce.gif b/sources/themes/greysme/images/puce.gif new file mode 100644 index 0000000..4f19e04 Binary files /dev/null and b/sources/themes/greysme/images/puce.gif differ diff --git a/sources/themes/greysme/images/punaise-bl.gif b/sources/themes/greysme/images/punaise-bl.gif new file mode 100644 index 0000000..18bd716 Binary files /dev/null and b/sources/themes/greysme/images/punaise-bl.gif differ diff --git a/sources/themes/greysme/images/punaise-br.gif b/sources/themes/greysme/images/punaise-br.gif new file mode 100644 index 0000000..e86e1c1 Binary files /dev/null and b/sources/themes/greysme/images/punaise-br.gif differ diff --git a/sources/themes/greysme/images/punaise-tl.gif b/sources/themes/greysme/images/punaise-tl.gif new file mode 100644 index 0000000..4cc0d7a Binary files /dev/null and b/sources/themes/greysme/images/punaise-tl.gif differ diff --git a/sources/themes/greysme/images/ratings/star_rating.gif b/sources/themes/greysme/images/ratings/star_rating.gif new file mode 100644 index 0000000..f1d3e32 Binary files /dev/null and b/sources/themes/greysme/images/ratings/star_rating.gif differ diff --git a/sources/themes/greysme/images/ratings/x.gif b/sources/themes/greysme/images/ratings/x.gif new file mode 100644 index 0000000..ab5cc17 Binary files /dev/null and b/sources/themes/greysme/images/ratings/x.gif differ diff --git a/sources/themes/greysme/images/ratings/x_off.gif b/sources/themes/greysme/images/ratings/x_off.gif new file mode 100644 index 0000000..9847f01 Binary files /dev/null and b/sources/themes/greysme/images/ratings/x_off.gif differ diff --git a/sources/themes/greysme/images/sort_off.gif b/sources/themes/greysme/images/sort_off.gif new file mode 100644 index 0000000..2f50467 Binary files /dev/null and b/sources/themes/greysme/images/sort_off.gif differ diff --git a/sources/themes/greysme/images/sort_on.gif b/sources/themes/greysme/images/sort_on.gif new file mode 100644 index 0000000..c6848c3 Binary files /dev/null and b/sources/themes/greysme/images/sort_on.gif differ diff --git a/sources/themes/greysme/preview.png b/sources/themes/greysme/preview.png new file mode 100644 index 0000000..567bfeb Binary files /dev/null and b/sources/themes/greysme/preview.png differ diff --git a/sources/themes/greysme/templates/default.css b/sources/themes/greysme/templates/default.css new file mode 100644 index 0000000..49645c0 --- /dev/null +++ b/sources/themes/greysme/templates/default.css @@ -0,0 +1,879 @@ +/* vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */ +/** + * + * LICENSE: GNU General Public License, version 2 (GPLv2) + * Copyright 2001 - 2014 Ampache.org + * Copyright 2001 - 2008 Mickael Despesse + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License v2 + * as published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +/* + + Ampache Theme "Greysme" + by Mickael Despesse (Spocky) v1.03 (2008/04/18) + + Feel free to modify/reuse, just mention my name _somewhere_ + +*/ +/* Theme colors : */ +/* ---------------*/ +/* Red : #8b3e38 (#5a211c was too dark) */ +/* Light blue : #74718a (#5b596d was too dark) */ +/* Dark blue : #2b293d */ +/* Black : #050505 */ +/* Dark grey : #111 */ +/* Orange: #e9ad51 */ + + + + +/************************************************/ +/* Unify default browsers style rules */ +/************************************************/ +h1, h2, h3, h4, h5, h6, pre, code { font-size: 1em; line-height: 1em; } /* avoid browser default inconsistent font-sizes */ +ol, ul { list-style: none; } +table { border-collapse: separate; border-spacing: 0; } +caption, th, td { text-align: left; font-weight: normal; } +* { margin: 0; padding: 0; border:0; } /* White space reset */ +a img, :link img, :visited img { border: 0; } /* no blue linked image borders */ + +/************************************************/ +/* General style rules */ +/************************************************/ +html{ font-size: 62.5%; } + +body{ + font-size:1.1em; + font-family: Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif; + background: #2b293d url(../images/background.jpg) 0 0 repeat-x fixed; + /*min-width:90em;*/ + color:#e9ad51; +} + +ol { list-style-type: decimal-leading-zero; } +p { color: #e9ad51; } +a { color: #74718a; text-decoration: none; } + +td { padding: 0 8px; color: #e9ad51; } +th { font-weight:bold; padding: 0 .3em;} + +input, select { + font-size:1em; + font-family: Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif; + color: #e9ad51; + background-color: #111; + border: 1px solid #8b3e38; + margin:0 0 0px 0; +} + +input{ padding:0 2px; } +input:focus, select:focus { border-style: dotted; } +textarea { background-color: #111; color: #e9ad51; } + +/***************************************************/ +/* IE6 behaviors */ +/* - Whatever:hover: :hover support on any element */ +/***************************************************/ +body { behavior:url("modules/whatever_hover/csshover3.htc"); } + +/************************************************/ +/* Float Clearer */ +/************************************************/ +/* float clearing for IE6 */ +* html .clearfix{ height: 1%; overflow: visible; } +/* float clearing for IE7 */ +/**+html .clearfix{ min-height: 1%; }*/ +/* float clearing for everyone else */ +.clearfix:after{ clear: both; content: "."; display: block; height: 0; visibility: hidden; } + + +/************************************************/ +/* Main Container */ +/************************************************/ +#maincontainer{ margin-top:18px;} + +/************************************************/ +/* Header */ +/************************************************/ +#header { padding: 0 0; } +#headerbox { font-size: 0.9em; text-align: right; color: #e9ad51; position: absolute; top: 3px; right: 0; padding: 3px; } +#headerbox b { font-weight: normal } +#headerbox a { color: #e9ad51;} +#headerlogo { text-align: center; background: url(../images/ampache_back.gif) 0 0 repeat-x;} +#headerlogo a { } +#headerbox .box-content {background:transparent;border:none;} +#headerbox .box-top{display:none;} +#headerbox .box-bottom{display:none;} + +#play_type_switch { + float:left; + margin-top:2px; +} + +/************************************************/ +/* Content block */ +/************************************************/ +#content { + margin:10px 14em 10px 13.5em; +} + +/************************************************/ +/* Footer */ +/************************************************/ +#footer { + clear:both; + text-align:center; + font-size:.8em; + padding:3px 0; + background:#2c2b39; + border-top:5px solid #21212a; + margin-top:20px; +} + +/************************************************/ +/* Buttons */ +/************************************************/ + +.button, input[type=button], input[type=submit] { + background:#8b3e38 url(../images/button_back2.png) 0 100% repeat-x !important; + background:#8b3e38; + color:#e9ad51; + padding:0px 0.5em; + margin:4px 0 0 0; + border:0; + cursor:pointer; +} +.button:hover, input[type=button]:hover, input[type=submit]:hover { + background:#74718a url(../images/button_back2.png) 0 100% repeat-x !important; + background:#74718a; +} + +a.button { padding:1px .5em; } +input[type=checkbox] { border:0;background:none; } + +/************************************************/ +/* Sidebar */ +/************************************************/ +#sidebar { + float:left: + position:relative; + width: 13em; + text-align: left; + font-size: 0.8em; + padding-top:29px; + background:#2b293d url(../images/ampache_menu.gif) 50% 0 no-repeat; + z-index:20; +} + +/* For supporting browsers *cough*... I mean not IE6... *cough*, fix sidebar position on the left */ +*>div#sidebar{ position: fixed; } + +#sidebar select { width: 95%; } + +/* For sidebar tabs */ +/********************/ +#sidebar-tabs, #rightbar #rb_action{ + border-top:1px solid #000; + background:#111; +} + +#sidebar-tabs li.sb1, #rightbar #rb_action li { + float: left; + padding:1px; + background: #111 ; + border-top:0.5em solid #8b3e38; + border-right:1px solid #000;width:16px; +} +#sidebar-tabs li.active { + border-top-color: #e9ad51; +} +#sidebar-tabs li:hover.sb1, #rightbar #rb_action li:hover +{ + background:#000; + border-top-color: #e9ad51; +} +/* Tabs content */ +/****************/ +#sidebar-page { + position:absolute; + left:0; + top:52px; + background: #111; + padding-bottom:0.5em; + width:13em; + color:#8b3e38; +} +#sidebar-page ul.sb2 { +} +#sidebar-page ul.sb2 li{ + margin:1em auto; + padding-bottom: 0.5em; +} +#sidebar-page ul.sb2 h4{ + padding:.2em .5em .5em 0; + font-style:italic; + font-weight:normal; + font-size:1em; + letter-spacing:.2em; + text-align:right; + border-bottom:1px dotted #e9ad51; + text-decoration: overline; + background: url(../images/puce.gif) -8px -8px no-repeat; + color:#e9ad51; +} +#sidebar-page ul.sb2 li:hover h4{ + background-color:#000; +} + +#sidebar-page ul.sb3, #sidebar-page div.sb3 { + color:#8b3e38; +} +#sidebar-page .sb3 a{ color:#8b3e38; } +#sidebar-page ul.sb3 li{ + margin:0; + padding:0; + border:none; + font-weight:normal; + background: #111 url(../images/button_back.png) 0 100% repeat-x; + border-bottom: 1px solid #000; +} +* html #sidebar-page ul.sb3 li{display:inline;background:#111;} /* fix ie6 */ +#sidebar-page .sb3 a, #sidebar-page .sb3 div{ + padding:.2em .6em; + border-left: .5em solid #8b3e38; +} +#sidebar-page .sb3 a:hover{ + border-left-color: #e9ad51; +} + +#sidebar-page a{ + display:block; +} +#sidebar-page a:hover{ + background:#000; + color:#e9ad51; +} + +/* SIDEBAR : Home */ +/******************/ + +/* SIDEBAR : Browse */ +/********************/ +#multi_alpha_filter { + width:40px; + margin-bottom:4px; +} + +/* SIDEBAR : Localplay */ +/***********************/ +.active_instance { +} + +/* SIDEBAR : Preferences */ +/*************************/ + +/* SIDEBAR : Admin */ +/*******************/ +#sb_admin_catalogs li.sb_admin_catalogs_ctrls img {margin:0;} +#sb_admin_catalogs li.sb_admin_catalogs_ctrls a{ + display:inline; + padding:0; + border:none; +} +/************************************************/ +/* XSPF Player */ +/************************************************/ +#xspf_player { + width:410px; + position: relative; + float: left; + font-family: Verdana,Helvetica,sans-serif; +} + + +/************************************************/ +/* Rightbar */ +/************************************************/ +#rightbar { + width:13em; + background:#000; + float:right; + z-index:21; +} +#rightbar a { text-decoration:none; } + + +/* Rightbar Menu */ +#rightbar #rb_action { } +#rightbar #rb_action li { } +#rightbar li#rb_add, #rightbar li#pl_add { position:relative; z-index:10; } +/* Rightbar AddItems SubMenu */ +#rightbar li:hover .submenu { display:block;} +#rightbar .submenu { + display:none; + position:absolute; + right:0px; + top:15px; + background: #050505 url(../images/button_back.png) 0 100% repeat-x !important; + background: #050505; + border:2px solid #e9ad51; + width:15em; + padding:0.3em; + font-size:0.8em; + +} +* html #rightbar .submenu {right:100px;} /* IE6 fix */ + +#rightbar #rb_action .submenu li {float:none; width:auto; border:none;} +#rightbar .submenu a { + display:block; + padding:0.1em; + color:#8b3e38; + text-decoration:none; + text-align:right; +} +#rightbar .submenu a:hover, +#rightbar #rb_current_playlist a:hover { color:#e9ad51; } + +/* Rightbar playlist */ +#rightbar #rb_current_playlist { + background: #111; + padding-bottom:0.5em; + clear:both; +} +#rightbar #rb_current_playlist li { + position:relative; + font-size:0.9em; + line-height:14px; + color:#5b5b5b; + padding-right:20px; + background: #111 url(../images/button_back.png) 0 100% repeat-x; + border-bottom: 1px solid #000; +} +#rightbar #rb_current_playlist li a { display:block; padding:0.2em;} +#rightbar .delitem { position:absolute;right:0;top:0; } + + +/************************************************/ +/* Styles for the star ratings */ +/************************************************/ +.star-rating { + position:relative; +} +.dynamic-star-rating{ + width:96px; +} + +.star-rating ul, +.star-rating a:hover, +.star-rating .current-rating{ + background: url(../images/ratings/star_rating.gif) left -1000px repeat-x; +} +.star-rating ul{ + position:relative; + width:80px; + height:15px; + overflow:hidden; + list-style:none; + margin:0; + padding:0; + background-position: left top; + /*float:left;*/ +} +.star-rating li{ + display: inline; +} +.star-rating a, .star-rating span, +.star-rating .current-rating{ + position:absolute; + top:0; + left:0; + text-indent:-1000em; + height:15px; + line-height:15px; + outline:none; + overflow:hidden; + border: none; +} +.star-rating .star1 { width:20%; z-index:6; } +.star-rating .star2 { width:40%; z-index:5; } +.star-rating .star3 { width:60%; z-index:4; } +.star-rating .star4 { width:80%; z-index:3; } +.star-rating .star5 { width:100%; z-index:2;} +.star-rating .current-rating { z-index:1; background-position: left bottom; } + +.star-rating-reset {height:16px;} +.star-rating a.star0 { + left:0px; + height:16px; + width:16px; + background: url(../images/ratings/x_off.gif) left top; +} +/* hovering effect only for dynamic star rating */ +#content .dynamic-star-rating a:hover{ + background-position: left center; + background-color:transparent; +} +.dynamic-star-rating a:hover.star0 { + background: url(../images/ratings/x.gif) left top; +} +.dynamic-star-rating ul { + left:16px; +} + +/************************************************/ +/* Styles for user flags */ +/************************************************/ +.userflag +{ + position: relative; + width:16px; + height:16px; +} + +.userflag a { + position:absolute; + display: inline; +} + +.userflag a.userflag_true +{ + width:16px; + height: 16px; + background: url(../../../images/icon_flag.png) left top; +} + +.userflag a:hover.userflag_true +{ + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a.userflag_false +{ + width:16px; + height:16px; + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a:hover.userflag_false +{ + background: url(../../../images/icon_flag.png) left top; +} + +/************************************************/ +/* Box Related Styles */ +/************************************************/ +.box-title { + display:block; + color:#8b3e38; + padding:3px 13px 0 28px; + background: #000 url(../images/puce.gif) 10px 50% no-repeat; + font-size: 1.1em; + font-variant:small-caps; + border-bottom:1px solid #8b3e38; + letter-spacing:0.1em; +} +.box-title:first-letter{font-style:italic;} + +.box-list { + padding-right: 10px; +} + +/* Enclosing Boxes Styles */ +.box, .info-box { + margin-top: 7px; + margin-right: 11px; + /*background: url(../images/back-box.gif) 0 0 no-repeat;*/ + font-size : 0.9em; + float:left; + clear:left; + height:1%; /* IE6 : Holly Hack comes to rescue once again */ +} +/* Hovering effects on links */ +.box a:hover, .info-box a:hover { /*background-color: #8b3e38;*/ color: #e9ad51;} + +.box-inside { +/* background: url(../images/right.gif) top right repeat-y; */ +} +.box-content { + padding:12px 12px; + background:#000; +} + +.box-top { + position:relative; + background:transparent url(../images/box_top.png) 0 100% repeat-x !important; + background:#000; +} +.box-left-top { + /*background: url(../images/punaise-tl.gif) no-repeat;*/ + height:15px; + width:17px; + position:relative;left:/*-8px*/ 30%;top:-3px; +} +.box-right-top { + /*background: url(../images/curl.gif) no-repeat;*/ + background: url(../images/punaise-tl.gif) no-repeat; + height:15px; + width:17px; + position:absolute;left:30%;top:-3px; +} +* html .box-right-top {right: expression(-this.parentNode.offsetWidth%2+"px");} /* Fixes an IE6 rounding error */ +.box-bottom { + position:relative;clear:both; + background:transparent url(../images/box_bottom.png) 0 0 repeat-x !important; + background:#000; +} +.box-left-bottom { + background: url(../images/punaise-bl.gif) no-repeat; + height:15px; + width:17px; + position:relative;left:-7px;top:-3px; +} +.box-right-bottom { + background: url(../images/punaise-br.gif) no-repeat; + height:15px; + width:17px; + position:absolute;right:-7px;top:-3px; +} +* html .box-right-bottom {right: expression(-this.parentNode.offsetWidth%2+"px");} /* Fixes an IE6 rounding error */ + + +/* Specific to Info Boxes */ +.info-box .album_art {float:left;margin-right:10px;} +.info-box th {color:#8b3e38;} +#information_actions { } +#information_actions h3 { color:#8b3e38; font-size:1.2em; margin:0.2em; } + +.item_right_info { + float: right; + max-width: 60%; +} + +.external_links { + text-align: right; +} + +.external_links a { + margin: 0px 5px 0px 0px; + opacity: 0.3; +} + +.external_links a:hover { + opacity: 1; +} + +#artist_summary { + margin-right: 150px; +} + +/* Specific boxes */ +.box_newest_albums {} +.box_newest_artists {clear:none;} +.box_newest_genres {clear:none;} +.box_popular_album {} +.box_popular_artists {clear:none;} +.box_popular_genres {clear:none;} +.box_preferences h4 {color: #8b3e38;font-size: 1.1em;text-align:center;font-weight: bold;border-bottom:1px solid #8b3e38;padding:1em;} + +/************************************************/ +/* Tables (songs lists...) */ +/************************************************/ +.tabledata .th-top, .tabledata .th-bottom { + background: #111; + vertical-align: top; + font-size:1em; +} +.tabledata th { + color:#8b3e38; + font-variant:small-caps; + font-weight:normal; + border-right:3px solid #000; + text-align:center; + line-height:2em; +} +.tabledata th a { + color:#8b3e38; + padding-right:10px; + background: url(../images/sort_off.gif) 100% 50% no-repeat; + display:block; +} +.tabledata th a:hover { + color:#8b3e38; + background-color:transparent; + background-image:url(../images/sort_on.gif); +} + +.tableform select { + width: 150px; +} + +/* table rows */ +.tabledata .odd, .tabledata .even, .row-highlight { background: url(../images/list_back.png) 0 50% repeat-x !important; background-image: none;} +.tabledata .odd { background-color: #111 !important;} +.tabledata .even { } +.tabledata .odd:hover, +.tabledata .even:hover { background-color: #2b293d !important;} +.row-highlight:hover { background-color: #cc3333 !important;} + +/* Misc */ +.border { background: #000; } +.tabledata input[type=text], .tabledata select{ margin:1px 0; } +.discnb { font-style: italic; font-size:0.8em; } + +/* specific cells */ +td.cel_cover{padding:6px;} +.cel_select, .cel_action, .cel_date, .cel_applytoall, .cel_level {text-align:center;} +td.cel_track {text-align:right;} +/* specific cells : users login state */ +.user_online{background:#0f0;} +.user_offline{background:#7f0000;} +.user_disabled{background:#ccc;} +/* specific cells : enlarge links */ +.tabledata td a{display:block;} +.tabledata td.cel_add a, .tabledata td.cel_action a{display:inline;} +/* specific cells : image links */ +.odd td a img, .even td a img {opacity:0.7;} +.odd td a img:hover, .even td a img:hover {opacity:1;} + +/* specific tables */ +#recently_played .th-bottom {display:none;} +.box_preferences .th-bottom {display:none;} + +/* Inline Editing Tables */ +.inline-edit input, .inline-edit select { + font-size: 0.8em; +} + +/************************************************/ +/* Song details */ +/************************************************/ +dl.song_details{font-size:1em;} +.song_details dt { + float:left; + clear:both; + width:20%; + min-width:20%; /*Ie bugfix*/ + font-weight:bold; +} +.song_details dd { + float:left; + width:79%; + min-width:79%; /*Ie bugfix*/ + margin:0 0 0.2em .3em; + padding-left:.2em; +} +dt, dt + dd {background: url(../images/list_back.png) 0 50% repeat-x !important; background-image: none;} +dt, dt + dd { background-color: #111 !important;} +dt:hover, dt:hover + dd {background-color: #2b293d !important;} + + +/************************************************/ +/* Albums of the moment */ +/************************************************/ +.random_album{ + position:relative; + float:left; + padding:8px; +} + +.random_album .play_album{ + position:absolute; + top:10px; + right:0; +} + +#random_selection .box-content{ + float:left; +} + +#random_selection .art_album img { + width: 80px; + height: 80px; +} + +/************************************************/ +/* Now Playing */ +/************************************************/ +#now_playing{ +} + +.np_row { + padding: 3px; + float:left; + clear:both; +} +.np_cell { + padding-left:5px; + margin-left:5px; +} + +.np_row label { + display:block; + font-weight:bold; + margin-left:-5px; +} + +.np_group { + float:left; + padding-left:10px; +} + +#now_playing .box-content{ +background:#000; + float:left; +} + +/************************************************/ +/* Shoutbox */ +/************************************************/ + + +#shoutbox { + font-size:1em; + position:relative; +} + +#shoutbox div.shout { + /*float:left;*/ + padding:1em 85px 0 30px; +} + +#shoutbox span.information { +/* float:left; + clear:left;*/ +} +#shoutbox .shouttext{display:block;} +img.shoutboximage { + margin-right:3px; + width:25px; + height:25px; + position:absolute;margin-left:-30px; +} +#shoutbox .odd img.shoutboximage {/*float:right;*/} +#shoutbox .even img.shoutboximage {/*float:right;*/} + +div.shout:hover img.shoutboximage{width:75px;height:75px;position:absolute;top:50%;right:0;margin-top:-38px} + +/************************************************/ +/* List Header */ +/************************************************/ +.list-header{margin:7px 0; padding:0 4em; text-align:center; font-size: 0.9em;position:relative;} +.list-header .prev{position:absolute; top:0; left:0;} +.list-header .next{position:absolute; top:0; right:0;} +.list-header .selected{background:#e9ad51;color:#111;} +.list-header .page-nb{padding:1px;border: 1px solid #111;} +.list-header a:hover{background: transparent; border-color:#e9ad51;} + +/************************************************/ +/* Errors */ +/************************************************/ +.error { + color: #990033; +} + +.fatalerror .nodata { + padding: 3px; + display: table-cell; + color: #990033; + font-weight:bold; + border:2px solid #990033; +} + +/************************************************/ +/* LocalPlay */ +/************************************************/ +.lp_box_ctrl, .lp_box_vol { + text-align: center; /*for compatibility, may be controlled by themers now*/ +} + +td.lp_current a { + font-weight:bold; + text-decoration:none; +} + +/************************************************/ +/* Styles for Login template */ +/************************************************/ +#loginPage #maincontainer { + margin: 5% auto 0px auto; + text-align:center; +} +#loginPage h2{ + color:#111; + font-size:0.8em; + font-style:italic; + font-weight:normal; + margin: 0 0 2em 0; +} +#loginPage #loginbox{ +} +.loginfield{ + text-align:right; + margin: 1px 0; + width:15em; + margin:auto; +} +.loginfield input.text_input{ + width:8em; + border:1px solid #74718a; +} + +#loginPage div.fatalerror { + padding:5px; + margin:10px; +} + +#motd { + margin:0 auto 0 auto; + width: 437px; +} + + +/************************************************/ +/* Misc */ +/************************************************/ + +.formValidation{ + margin-top:1em; + text-align:center; +} + +.text-box, .confirmation-box { + display: table-cell; + padding:5px; + margin:0 0 10px 0; + background-color: #111; +} + +#ajax-loading { + position: absolute; + top:106px; + left:42%; + width:265px; + height:50px; + z-index: 100; + background: url(../images/ajax-loader.gif) no-repeat; + text-indent:-9999em; + display: none; +} + +.information,.information a { + font-size: 0.9em; + font-style: italic; + color: #c0c0c0; +} + diff --git a/sources/themes/greysme/theme.cfg.php b/sources/themes/greysme/theme.cfg.php new file mode 100644 index 0000000..dc92eb0 --- /dev/null +++ b/sources/themes/greysme/theme.cfg.php @@ -0,0 +1,53 @@ +;;;;;;;;;;;;;;;;;; +;; +;;;;;;;;;;;;;;;;;; +; Copyright 2001 - 2013 Ampache.org +; 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 v2 +; as published by the Free Software Foundation. +; +; 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, write to the Free Software +; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +; +;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Alternate Ampache Theme +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; Theme Name +; This is the actual name of the theme that +; will be displayed in the preferences screen +; DEFAULT: ampache-theme +name = "Greysme" + +; Theme Author +; This is just a way of giving credit to the +; person who actually created this theme +; DEFAULT: N/A +author = "Mickael Despesse (Spocky)" + +; Theme Maintainer +; This is just a way of listing who is responsible for +; maintaining this theme in case it's not working right +; please include an e-mail address so you can be contacted +; DEFAULT: N/A +maintainer = "Spocky" + +; Orientation +; This was added as of 3.3.2-Alpha4, this tells Ampache if this theme +; uses vertical or horizontal orientation of the menu, if this is a horizontal +; theme then it will not show the quick search and quick random play forms +orientation = "vertical" + +; Submenu +; If this is set to simple the sub menu's will only be shown when you're on one of the +; respective pages. If you want to make the menu's something like the classic theme +; comment this out +;submenu = "simple" diff --git a/sources/themes/penguin/images/ajax-loader.gif b/sources/themes/penguin/images/ajax-loader.gif new file mode 100644 index 0000000..bc92f77 Binary files /dev/null and b/sources/themes/penguin/images/ajax-loader.gif differ diff --git a/sources/themes/penguin/images/ampache.png b/sources/themes/penguin/images/ampache.png new file mode 100644 index 0000000..b053dd8 Binary files /dev/null and b/sources/themes/penguin/images/ampache.png differ diff --git a/sources/themes/penguin/images/background.gif b/sources/themes/penguin/images/background.gif new file mode 100644 index 0000000..857578c Binary files /dev/null and b/sources/themes/penguin/images/background.gif differ diff --git a/sources/themes/penguin/images/bg_login.jpg b/sources/themes/penguin/images/bg_login.jpg new file mode 100644 index 0000000..b2eef20 Binary files /dev/null and b/sources/themes/penguin/images/bg_login.jpg differ diff --git a/sources/themes/penguin/images/blank-pixel.gif b/sources/themes/penguin/images/blank-pixel.gif new file mode 100644 index 0000000..17d4390 Binary files /dev/null and b/sources/themes/penguin/images/blank-pixel.gif differ diff --git a/sources/themes/penguin/images/blankalbum.gif b/sources/themes/penguin/images/blankalbum.gif new file mode 100644 index 0000000..58f4562 Binary files /dev/null and b/sources/themes/penguin/images/blankalbum.gif differ diff --git a/sources/themes/penguin/images/blankalbum.jpg b/sources/themes/penguin/images/blankalbum.jpg new file mode 100644 index 0000000..9a05cad Binary files /dev/null and b/sources/themes/penguin/images/blankalbum.jpg differ diff --git a/sources/themes/penguin/images/bottom.gif b/sources/themes/penguin/images/bottom.gif new file mode 100644 index 0000000..75e83a9 Binary files /dev/null and b/sources/themes/penguin/images/bottom.gif differ diff --git a/sources/themes/penguin/images/bottomright.gif b/sources/themes/penguin/images/bottomright.gif new file mode 100644 index 0000000..7ef339d Binary files /dev/null and b/sources/themes/penguin/images/bottomright.gif differ diff --git a/sources/themes/penguin/images/icons/icon_add.png b/sources/themes/penguin/images/icons/icon_add.png new file mode 100644 index 0000000..2a6bf09 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_add.png differ diff --git a/sources/themes/penguin/images/icons/icon_add_key.png b/sources/themes/penguin/images/icons/icon_add_key.png new file mode 100644 index 0000000..f51387a Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_add_key.png differ diff --git a/sources/themes/penguin/images/icons/icon_add_user.png b/sources/themes/penguin/images/icons/icon_add_user.png new file mode 100644 index 0000000..b1af086 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_add_user.png differ diff --git a/sources/themes/penguin/images/icons/icon_admin.png b/sources/themes/penguin/images/icons/icon_admin.png new file mode 100644 index 0000000..277161d Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_admin.png differ diff --git a/sources/themes/penguin/images/icons/icon_all.png b/sources/themes/penguin/images/icons/icon_all.png new file mode 100644 index 0000000..8a24abd Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_all.png differ diff --git a/sources/themes/penguin/images/icons/icon_batch_download.png b/sources/themes/penguin/images/icons/icon_batch_download.png new file mode 100644 index 0000000..2aefc9a Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_batch_download.png differ diff --git a/sources/themes/penguin/images/icons/icon_browse.png b/sources/themes/penguin/images/icons/icon_browse.png new file mode 100644 index 0000000..5c0dbaa Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_browse.png differ diff --git a/sources/themes/penguin/images/icons/icon_cog.png b/sources/themes/penguin/images/icons/icon_cog.png new file mode 100644 index 0000000..80a78be Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_cog.png differ diff --git a/sources/themes/penguin/images/icons/icon_delete.png b/sources/themes/penguin/images/icons/icon_delete.png new file mode 100644 index 0000000..c4e0ec1 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_delete.png differ diff --git a/sources/themes/penguin/images/icons/icon_disable.png b/sources/themes/penguin/images/icons/icon_disable.png new file mode 100644 index 0000000..0ae07f3 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_disable.png differ diff --git a/sources/themes/penguin/images/icons/icon_download.png b/sources/themes/penguin/images/icons/icon_download.png new file mode 100644 index 0000000..ad67c76 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_download.png differ diff --git a/sources/themes/penguin/images/icons/icon_edit.png b/sources/themes/penguin/images/icons/icon_edit.png new file mode 100644 index 0000000..04dd5d6 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_edit.png differ diff --git a/sources/themes/penguin/images/icons/icon_enable.png b/sources/themes/penguin/images/icons/icon_enable.png new file mode 100644 index 0000000..41b5ee3 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_enable.png differ diff --git a/sources/themes/penguin/images/icons/icon_feed.png b/sources/themes/penguin/images/icons/icon_feed.png new file mode 100644 index 0000000..4cb31ce Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_feed.png differ diff --git a/sources/themes/penguin/images/icons/icon_home.png b/sources/themes/penguin/images/icons/icon_home.png new file mode 100644 index 0000000..8938c20 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_home.png differ diff --git a/sources/themes/penguin/images/icons/icon_link.png b/sources/themes/penguin/images/icons/icon_link.png new file mode 100644 index 0000000..cbea21a Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_link.png differ diff --git a/sources/themes/penguin/images/icons/icon_logout.png b/sources/themes/penguin/images/icons/icon_logout.png new file mode 100644 index 0000000..6ab0ef6 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_logout.png differ diff --git a/sources/themes/penguin/images/icons/icon_next.png b/sources/themes/penguin/images/icons/icon_next.png new file mode 100644 index 0000000..7e03946 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_next.png differ diff --git a/sources/themes/penguin/images/icons/icon_pause.png b/sources/themes/penguin/images/icons/icon_pause.png new file mode 100644 index 0000000..2914803 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_pause.png differ diff --git a/sources/themes/penguin/images/icons/icon_play.png b/sources/themes/penguin/images/icons/icon_play.png new file mode 100644 index 0000000..8e09f54 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_play.png differ diff --git a/sources/themes/penguin/images/icons/icon_playlist_add.png b/sources/themes/penguin/images/icons/icon_playlist_add.png new file mode 100644 index 0000000..dba78cf Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_playlist_add.png differ diff --git a/sources/themes/penguin/images/icons/icon_plugin.png b/sources/themes/penguin/images/icons/icon_plugin.png new file mode 100644 index 0000000..5789e7e Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_plugin.png differ diff --git a/sources/themes/penguin/images/icons/icon_preferences.png b/sources/themes/penguin/images/icons/icon_preferences.png new file mode 100644 index 0000000..f2897bf Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_preferences.png differ diff --git a/sources/themes/penguin/images/icons/icon_prev.png b/sources/themes/penguin/images/icons/icon_prev.png new file mode 100644 index 0000000..3ca8d5d Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_prev.png differ diff --git a/sources/themes/penguin/images/icons/icon_random.png b/sources/themes/penguin/images/icons/icon_random.png new file mode 100644 index 0000000..107e7ab Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_random.png differ diff --git a/sources/themes/penguin/images/icons/icon_server_lightning.png b/sources/themes/penguin/images/icons/icon_server_lightning.png new file mode 100644 index 0000000..8734256 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_server_lightning.png differ diff --git a/sources/themes/penguin/images/icons/icon_stop.png b/sources/themes/penguin/images/icons/icon_stop.png new file mode 100644 index 0000000..2b4f394 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_stop.png differ diff --git a/sources/themes/penguin/images/icons/icon_view.png b/sources/themes/penguin/images/icons/icon_view.png new file mode 100644 index 0000000..c51f7a9 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_view.png differ diff --git a/sources/themes/penguin/images/icons/icon_volumedn.png b/sources/themes/penguin/images/icons/icon_volumedn.png new file mode 100644 index 0000000..159b3be Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_volumedn.png differ diff --git a/sources/themes/penguin/images/icons/icon_volumemute.png b/sources/themes/penguin/images/icons/icon_volumemute.png new file mode 100644 index 0000000..6c009ef Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_volumemute.png differ diff --git a/sources/themes/penguin/images/icons/icon_volumeup.png b/sources/themes/penguin/images/icons/icon_volumeup.png new file mode 100644 index 0000000..c85b003 Binary files /dev/null and b/sources/themes/penguin/images/icons/icon_volumeup.png differ diff --git a/sources/themes/penguin/images/ratings/star_rating.gif b/sources/themes/penguin/images/ratings/star_rating.gif new file mode 100644 index 0000000..06d5e4b Binary files /dev/null and b/sources/themes/penguin/images/ratings/star_rating.gif differ diff --git a/sources/themes/penguin/images/ratings/x.gif b/sources/themes/penguin/images/ratings/x.gif new file mode 100644 index 0000000..5417ddc Binary files /dev/null and b/sources/themes/penguin/images/ratings/x.gif differ diff --git a/sources/themes/penguin/images/ratings/x_off.gif b/sources/themes/penguin/images/ratings/x_off.gif new file mode 100644 index 0000000..7e756ea Binary files /dev/null and b/sources/themes/penguin/images/ratings/x_off.gif differ diff --git a/sources/themes/penguin/images/rightbar_top.jpg b/sources/themes/penguin/images/rightbar_top.jpg new file mode 100644 index 0000000..0b119b8 Binary files /dev/null and b/sources/themes/penguin/images/rightbar_top.jpg differ diff --git a/sources/themes/penguin/preview.png b/sources/themes/penguin/preview.png new file mode 100644 index 0000000..e19981a Binary files /dev/null and b/sources/themes/penguin/preview.png differ diff --git a/sources/themes/penguin/templates/default.css b/sources/themes/penguin/templates/default.css new file mode 100644 index 0000000..c1c53b2 --- /dev/null +++ b/sources/themes/penguin/templates/default.css @@ -0,0 +1,1107 @@ +/* vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */ +/** + * + * LICENSE: GNU General Public License, version 2 (GPLv2) + * Copyright 2001 - 2014 Ampache.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License v2 + * as published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +/************************************************/ +/* Unify default browsers style rules */ +/************************************************/ +h1, h2, h3, h4, h5, h6, pre, code { + font-family:Verdana, Geneva, sans-serif; + font-size: 10px; +} /* avoid browser default inconsistent font-sizes */ +ol, ul { + list-style: none; +} +table { + border-collapse: separate; + border-spacing: 0; +} +caption, th, td { + text-align: left; + font-weight: normal; +} +* { + margin: 0; + padding: 0; +} /* White space reset */ +a img, :link img, :visited img { + border: 0; +} /* no blue linked image borders */ +/************************************************/ +/* General style rules */ +/************************************************/ +body { + background:#222 url(../images/background.gif) repeat; + font-family:Arial, Helvetica, Sans-Serif; + min-width:900px; +} +p { + color: #fc0; + margin:1em 0; +} +a, a:visited, a:active { + color: #fff; + font-family: Verdana, Helvetica, sans-serif; + text-decoration:none; +} +td { + padding-left: 5px; + padding-right: 10px; + color: #fff; + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + text-transform:small-caps; +} +th { + padding-right: 10px; + font-family: Verdana, Helvetica, sans-serif; + font-size:10px; + font-weight:bold; + text-transform:uppercase; + background: #333; + padding:2px; +} +input, textarea { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + background: #222; + color: #fff; + border: 1px solid #666; + margin: 2px; +} +input { + font-weight: bold; + padding: 1px; +} +select { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + background:#222; + color: #fff; + margin:3px; + border: 1px solid #666; +} +input[type=checkbox] { + border:0 +} +/***************************************************/ +/* IE6 behaviors */ +/* - Whatever:hover: :hover support on any element */ +/***************************************************/ +body { + behavior:url("modules/whatever_hover/csshover3.htc"); +} +/************************************************/ +/* Float Clearer */ +/************************************************/ +/* float clearing for IE6 */ +* html .clearfix { + height: 1%; + overflow: visible; +} +/* float clearing for IE7 */ +/**+html .clearfix{ min-height: 1%; }*/ +/* float clearing for everyone else */ +.clearfix:after { + clear: both; + content: "."; + display: block; + height: 0; + visibility: hidden; +} +/************************************************/ +/* XSPF Player */ +/************************************************/ +.xspf_player { + right: 20px; + position: absolute; +} +/************************************************/ +/* Main Container */ +/************************************************/ +#maincontainer { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + text-transform:uppercase; +} +/************************************************/ +/* Header */ +/************************************************/ +#header { + height: 40px; + padding: 0px 0 0 10px; +} +#headerbox { + position:absolute; + top:0px; + left:264px; + font-size: 10px; + padding-top: 5px; + padding-bottom: 5px; + /*background: #fff;*/ + height: 30px; + width: 504px; +} +#headerlogo, #headerlogo a { + position: absolute; + left: 0px; + top: 0px; +} +.box_headerbox { + padding-left: 100px; + display:table; + color: #333; +} +.box_headerbox #loginInfo { + position:absolute; + top:-200px; + left:-200px; + font-size:0px; +} +#play_type_switch { + position:absolute; + top:10px; + left:-10px; + text-transform:lowercase; +} +/************************************************/ +/* Content block */ +/************************************************/ +#content { + margin: 8px 50px 20px 300px; +} +/************************************************/ +/* Footer */ +/************************************************/ +#footer { + color:FC0; + font-size:9px; + font-weight:bold; + position:absolute; + top:5px; + left: 815px; + width:350px; + text-align:left; + z-index:250; + text-transform:uppercase; + line-height: 10px; +} +#footer a:link { + color:#222; +} +/************************************************/ +/* Buttons */ +/************************************************/ +.button, input[type=button], input[type=submit] { + border: 1px solid #fff; + border-style: none; + background: #fff; + color: #333; + font-weight:bold; + text-decoration:none; + cursor: pointer; + font-family:Verdana, Geneva, sans-serif; + font-size:9px; + text-transform: uppercase; + vertical-align:middle; +} +a.button { + border: 1px solid #fff; + padding-left:5px; + padding-right:5px; + padding-top:2px; + padding-bottom:2px; + vertical-align:middle; + color: #222; +} +/************************************************/ +/* Sidebar */ +/************************************************/ +#sidebar { + position:absolute; + top:0px; + left:0px; + width:200px; + height: 40px; + padding-top:0px; + background:#fff; +} +#sidebar select { + width: 95%; +} +#sidebar input { + vertical-align:middle; + background:#fff; + color:#000; +} +#sidebar ul { + list-style:none; +} +#sidebar a { + text-decoration:none; +} +/* For sidebar tabs */ +/********************/ +#sidebar-tabs { + padding-left: 5px; + padding-top: 12px; + padding-bottom: 0px; +} +#sidebar-tabs li.sb1 { + float: left; + padding-left:0px; + padding-right:11px; + background: #fff; +} +#sidebar-tabs li.active { + background: #fff; + margin-top:0px; +} +#sidebar-tabs li.active img { + margin-top:0px; + position:relative; + z-index:2; +} +/* Tabs content */ +/****************/ +#sidebar-page { + position:absolute; + left:0; + top:41px; + background: #fff url(../images/bottom.gif) 0 100% repeat-x; + padding-bottom:10px; + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + text-transform:uppercase; + width:130px; +} +#sidebar-page ul.sb2 { + padding-top:2px; + padding-left:5px; + padding-right:10px; +} +#sidebar-page ul.sb2 li { + font-weight:bold; + margin:10px auto; + padding-bottom: 10px; +} +#sidebar-page ul.sb2 h4 { + padding-bottom: 5px; +} +#sidebar-page ul.sb3, #sidebar-page div.sb3 { + font-size:10px; + margin-left:0px; + font-weight:normal; + text-transform:capitalize; + color:#666; +} +#sidebar-page div.sb3 input[type=radio] { + margin-left:0px; + border:none; +} +#sidebar-page ul.sb3 li { + margin:0; + padding:0; + border:none; + font-weight:normal; +} +* html #sidebar-page ul.sb3 li { + display:inline; +} /* fix ie6 */ +#sidebar-page .sb3 a { + padding:1px; + border-bottom:1px dotted #ccc; + color: #666; +} +#sidebar-page a { + display:block; +} +#sidebar-page a:hover { + background:#FC0; + color:#000; +} +/* SIDEBAR : Home */ +/******************/ + +/* SIDEBAR : Browse */ +/********************/ +.alphabet { + background:transparent; /* fix ie bug */ + font-size:0.95em; + font-weight:normal; + margin: 0.3em auto; + color:#5b5b5b; +} +.alphabet span.link { + cursor: pointer; + margin: 0; + padding:0 5px; + font-family: monospace, Courier, Georgia; +} +.alphabet span.active { + background:#5b5b5b; + color:#fff; +} +.alphabet span.link:hover { + background: #99ccff; + color:#fff; +} +#multi_alpha_filter { + width:40px; + margin-bottom:4px; +} +/* SIDEBAR : Localplay */ +/***********************/ +.active_instance { + background:#0C0; +} +/* SIDEBAR : Preferences */ +/*************************/ + +/* SIDEBAR : Admin */ +/*******************/ +#sb_admin_catalogs li.sb_admin_catalogs_ctrls img { + margin:0; +} +#sb_admin_catalogs li.sb_admin_catalogs_ctrls a { + display:inline; + padding:0; + border:none; +} +/************************************************/ +/* XSPF Player */ +/************************************************/ +#xspf_player { + width:400px; + float: left; + background:#fff; + font-family: Verdana, Helvetica, sans-serif; +} +/************************************************/ +/* Rightbar */ +/************************************************/ +#rightbar { + position:absolute; + left: 131px; + top: 0px; + width:150px; + padding-top:12px; + background:url(../images/rightbar_top.jpg) 0px 41px no-repeat; + font-family: Verdana, Helvetica, sans-serif; +} +#rightbar ul { + list-style:none; +} +#rightbar a { + text-decoration:none; +} +/* Rightbar Menu */ +#rightbar #rb_action { + padding-bottom:11px; + padding-left:3px; +} +#rightbar #rb_action li { + display:inline; + margin-right:11px; +} +#rightbar li#rb_add, #rightbar li#pl_add { + position:relative; + z-index:10; +} +#rightbar li#rb_add:hover, #rightbar li#pl_add:hover { +} +/* Rightbar AddItems SubMenu */ +#rightbar li:hover .submenu { + display:block; + color:#000; +} +#rightbar .submenu { + display:none; + position:absolute; + left:-15px; + top:12px; + background:#FC0; + border:5px solid #222; + width:120px; + font-size:10px; + text-transform: capitalize; + padding: 5px 5px 5px 5px; +} +* html #rightbar .submenu { + right:100px; +} /* IE6 fix */ +#rightbar .submenu a { + display: block; + padding: 1px; + border-bottom:1px dotted #222; + color:#222; + text-decoration:none; + text-align:left; +} +#rightbar .submenu a:hover, #rightbar #rb_current_playlist a:hover { + background:#222; + color:#fff; +} +/* Rightbar playlist */ +#rightbar #rb_current_playlist { + background: #666 url(../images/bottomright.gif) 0 100% repeat-x; + margin-top: 36px; + padding-bottom:10px; + padding-left: 6px; + padding-right: 0px; + text-transform: capitalize; +} +#rightbar #rb_current_playlist li { + position:relative; + font-size:10px; + line-height:10px; + color:#fff; + padding-right:20px; +} +#rightbar #rb_current_playlist li a { + display:block; + padding:1px; + color:#FC0; +} +#rightbar #rb_current_playlist li a:hover { + background: #FC0; + color:#000; +} +#rightbar .delitem { + position:absolute; + right:5px; + top:0px; +} +/* Rightbar Localplay Controls */ +#rightbar #localplay-control { + position:fixed; + display: block; + width: 15px; + height: 50%; + bottom:0px; + right:0px; + list-style:none; + background:#inherit; + font-size:0px; + z-index:200; +} +#localplay-control span { + cursor: pointer; +} +/************************************************/ +/* Styles for the star ratings */ +/************************************************/ +.star-rating { + position:relative; +} +.dynamic-star-rating { + width:90px; +} +.star-rating ul, .star-rating a:hover, .star-rating .current-rating { + background: url(../images/ratings/star_rating.gif) left -1000px repeat-x; +} +.star-rating ul { + position:relative; + width:80px; + height:15px; + overflow:hidden; + list-style:none; + margin:0; + padding:0; + background-position: left top; +} +.star-rating li { + display: inline; +} +.star-rating a, .star-rating span, .star-rating .current-rating { + position:absolute; + top:0; + left:0; + text-indent:-1000em; + height:15px; + line-height:15px; + outline:none; + overflow:hidden; + border:none; +} +.star-rating .star1 { + width:20%; + z-index:6; +} +.star-rating .star2 { + width:40%; + z-index:5; +} +.star-rating .star3 { + width:60%; + z-index:4; +} +.star-rating .star4 { + width:80%; + z-index:3; +} +.star-rating .star5 { + width:100%; + z-index:2; +} +.star-rating .current-rating { + z-index:1; + background-position: left bottom; +} +.star-rating a.star0 { + left:0px; + width:16px; + background: url(../images/ratings/x_off.gif) left top; +} +/* hovering effect only for dynamic star rating */ +.dynamic-star-rating a:hover { + background-position: left center; +} +.dynamic-star-rating a:hover.star0 { + background: url(../images/ratings/x.gif) left top; +} +.dynamic-star-rating ul { + left:16px; +} + +/************************************************/ +/* Styles for user flags */ +/************************************************/ +.userflag +{ + position: relative; + width:16px; + height:16px; +} + +.userflag a { + position:absolute; + display: inline; +} + +.userflag a.userflag_true +{ + width:16px; + height: 16px; + background: url(../../../images/icon_flag.png) left top; +} + +.userflag a:hover.userflag_true +{ + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a.userflag_false +{ + width:16px; + height:16px; + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a:hover.userflag_false +{ + background: url(../../../images/icon_flag.png) left top; +} + +/************************************************/ +/* Box Related Styles */ +/************************************************/ + +.box-title { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + font-weight: bold; + text-transform:uppercase; + padding-bottom: 10px; + color:#FFF; + width:500px; +} +.box-list { +} +/* Enclosing Boxes Styles */ + +.box, .info-box { + background:inherit; + color:#FFF; + float:left; + clear:left; + height:1%; /* IE6 : Holly Hack comes to rescue once again */ +} +.box-inside { +} +.box-content { +} +.box-top { + position:relative; +} +.box-left-top { + height:5px; + width:5px; + position:relative; + left:0; + top:0; +} +.box-right-top { + height:5px; + width:5px; + position:absolute; + right:0; + top:0; +} +* html .box-right-top { +right: expression(-this.parentNode.offsetWidth%2+"px"); +} /* Fixes an IE6 rounding error */ +.box-bottom { + position:relative; + clear:both; +} +.box-left-bottom { + height:5px; + width:5px; + position:relative; + left:0; + top:0; +} +.box-right-bottom { + height:5px; + width:5px; + position:absolute; + right:0; + top:0; +} +* html .box-right-bottom { +right: expression(-this.parentNode.offsetWidth%2+"px"); +} /* Fixes an IE6 rounding error */ +/* Specific to Info Boxes */ +.info-box { + float:left; + margin-right:10px; +} +.album_art { + float:left; + margin-right:10px; +} +#information_actions { + padding: 0px 0px 10px 0px; + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + line-height: 15px; +} +#information_actions h3 { + float:none; + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + height: 0px; + margin: 0px 0px 20px 1px; +} + +.item_right_info { + float: right; + max-width: 60%; +} + +.external_links { + text-align: right; +} + +.external_links a { + margin: 0px 5px 0px 0px; + opacity: 0.3; +} + +.external_links a:hover { + opacity: 1; +} + +#artist_summary { + margin-right: 150px; +} + +/* Specific boxes */ +.box_newest_albums { +} +.box_newest_artists { + clear:none; +} +.box_newest_genres { + clear:none; +} +.box_popular_album { +} +.box_popular_artists { + clear:none; +} +.box_popular_genres { + clear:none; +} +.box_preferences h4 { + color:#fff; + font-family: Verdana, Geneva, sans-serif; + font-size:10px; + font-weight:bold; + text-transform:uppercase; + padding:5px 0px 10px 0px; + color: #fc0; +} +/************************************************/ +/* Tables (songs lists...) */ +/************************************************/ +.tabledata .th-top, .tabledata .th-bottom { + vertical-align: center; + text-align:center; +} +.tableform select { + width: 100px; +} +/* table rows */ +.tabledata .odd { +} +.tabledata .odd td { +} +.tabledata .even { +} +.tabledata .even td { +} +.row-highlight { +} +.tabledata .even:hover, .tabledata .odd:hover { + background:#666; +} +.row-highlight:hover { + background:#333; +} +/* Misc */ +.border { + background: #000; +} +.tabledata input, .tabledata select { + font-weight:normal; + background:#222; + color: #FC0; + margin:2px 0px 0px 0px; + border:1px solid #666; +} +/* specific cells */ +td.cel_cover { +} +.cel_select, .cel_date, .cel_applytoall, .cel_level { + text-align:right; +} +.cel_action { + text-align:right; + padding-right:0px; +} +/* specific cells : users login state */ +.user_online { + background:#222; +} +.user_offline { + background:#C00; +} +.user_disabled { + background:#ccc; +} +/* specific tables */ +#recently_played .th-bottom { + display:none; +} +.box_preferences .th-bottom { + display:none; +} +/* Inline Editing Tables */ +.inline-edit input, .inline-edit select { + font-size: 10px; +} +/************************************************/ +/* Song details */ +/************************************************/ +dl.song_details { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + width: 300px; +} +.song_details dt { + text-transform:uppercase; + float:left; + clear:both; + width:20%; + min-width:20%; /*Ie bugfix*/ + font-weight:bold; + padding: 2px 0px 0px 0px; + border-right: 1px dotted #666666; + border-bottom: 1px dotted #666666; +} +.song_details dd { + float:left; + width:79%; + min-width:79%; /*Ie bugfix*/ + padding: 2px 0px 0px 2px; +} +dt + dd { + border-bottom:1px dotted #666; +} +dt:hover, dt:hover + dd { + background:#666; + color: #fff; +} +/************************************************/ +/* Albums of the moment */ +/************************************************/ +.random_album { + position:relative; + float:left; + padding-right:10px; + width:80px; +} +.random_album .play_album { + display:none; +} + +#random_selection .art_album img { + width: 80px; + height: 80px; +} + +/************************************************/ +/* Now Playing */ +/************************************************/ +#now_playing { +} +.np_row { + padding: 3px; + float:left; + font-size:10px; + display:block; +} +.np_cell { + padding-left:5px; + margin-left:5px; +} +.np_row label { + display:block; + font-weight:bold; + margin:2px 0 0 -5px; +} +.np_group { + float:left; + padding-right:15px; +} +.np_row a { + font-size:10px; +} +/************************************************/ +/* Shoutbox */ +/************************************************/ + +#shoutbox { + font-size:1em; +} +#shoutbox div.shout { + padding-top:0.5em; + margin:10px 5px 0 0; + border-top:1px dotted #c0c0c0; +} +#shoutbox div.shout:hover { + border-top:1px solid #9cf; +} +#shoutbox span.information { +} +#shoutbox .shouttext { + display:block; + font-size:.9em; + margin-top:.5em; +} +img.shoutboximage { + margin:0 3px; +} +#shoutbox div.odd { + margin-right:20%; + text-align:left; +} +#shoutbox div.even { + margin-left:20%; + text-align:right; +} +#shoutbox .odd img.shoutboximage { + float:left; +} +#shoutbox .even img.shoutboximage { + float:right; +} +/************************************************/ +/* List Header */ +/************************************************/ +.list-header { + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + text-transform:uppercase; + color:#000; + margin-top: 10px; + margin-bottom: 10px; + text-align:center; + position:relative; +} +.list-header .prev { + position:absolute; + top:0; + left:0; + text-transform:uppercase; + font-size:10px; + font-weight:bold; + color: #FC0; +} +.list-header .next { + position:absolute; + top:0; + right:0; + text-transform:uppercase; + font-size:10px; + font-weight:bold; + color: #FC0; + padding-right:0px; +} +.list-header .selected { + background: #fff; +} +.list-header .page-nb { + padding: 2px 5px 2px 5px; + border: 1px dotted #ccc; + text-decoration: none; +} +.list-header .page-nb:hover { + background: #FC0; + color:#000 +} +/************************************************/ +/* Errors */ +/************************************************/ +.error { + display:block; + font-family:Verdana, Geneva, sans-serif; + font-size: 10px; + color:#C00; +} +.fatalerror { + display:table-cell; + padding:3px; + color:#c00; + font-weight:bold; + font-size:10px; +} +/************************************************/ +/* LocalPlay */ +/************************************************/ +.lp_box_ctrl { +} +.lp_box_vol { + text-align: center; /*for compatibility, may be controlled by themers now*/ +} +td.lp_current a { + font-weight:bold; + text-decoration:none; +} +/************************************************/ +/* Styles for Login template */ +/************************************************/ +#loginPage #maincontainer { + margin:100px auto 0 auto; + width:360px; + font-size:10px; + text-align:center; +} +#loginPage #header { + padding:0; +} +#loginPage #loginbox { + background:url(../images/bg_login.jpg) no-repeat; + height:230px; +} +#loginPage h2 { + color:#fff; + padding-top:60px; + font-weight: normal; +} +.loginfield { + text-align:right; + padding-top:10px; + padding-right:50px; +} +.loginfield input.text_input { + vertical-align:middle; + width:180px; + border:5px solid #222; +} +.loginfield label { + color: #222; + font-weight:bold; +} +.loginfield #rememberme { + vertical-align:middle; + text-align:left; + margin-left: auto; + margin-right: auto; + background:none; +} +#loginPage div.fatalerror { + padding:5px; + margin:10px; +} +#motd { + background: #fff; + color: #222; + margin:0 auto 0 auto; + width: 360px; +} +/************************************************/ +/* Misc */ +/************************************************/ +.formValidation { + margin-top:10px; + text-align:center; +} +.text-box, .confirmation-box { + display:table-cell; + padding:5px 5px 0 5px; + margin-bottom:10px; + background:#bbb; + border:2px solid #000; +} +#ajax-loading { + position: absolute; + top:25px; + left:265px; + width:48px; + height:48px; + z-index:-1; + background: url(../images/ajax-loader.gif) no-repeat; + display: none; + text-indent:-9999em; +} +.information, .information a { + position:relative; + font-family:Verdana, Geneva, sans-serif; + font-size:10px; + font-weight:bold; + text-transform:uppercase; + color: #FC0; +} +.format-specifier { + text-transform: none; +} diff --git a/sources/themes/penguin/theme.cfg.php b/sources/themes/penguin/theme.cfg.php new file mode 100644 index 0000000..aef3f91 --- /dev/null +++ b/sources/themes/penguin/theme.cfg.php @@ -0,0 +1,53 @@ +;;;;;;;;;;;;;;;;;; +;; +;;;;;;;;;;;;;;;;;; +; Copyright 2001 - 2013 Ampache.org +; +; This program is free software; you can redistribute it and/or +; modify it under the terms of the GNU General Public License v2 +; as published by the Free Software Foundation. +; +; 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, write to the Free Software +; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +; +;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Penguin Ampache Theme +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; Theme Name +; This is the actual name of the theme that +; will be displayed in the preferences screen +; DEFAULT: ampache-theme +name = "Penguin" + +; Theme Author +; This is just a way of giving credit to the +; person who actually created this theme +; DEFAULT: N/A +author = "Jeroen Doppenberg" +; Updated by harrysand + +; Theme Maintainer +; This is just a way of listing who is responsible for +; maintaining this theme in case it's not working right +; please include an e-mail address so you can be contacted +; DEFAULT: N/A +maintainer = "info@dopdop.nl" + +; Orientation +; This was added as of 3.3.2-Alpha4, this tells Ampache if this theme +; uses vertical or horizontal orientation of the menu, if this is a horizontal +; theme then it will not show the quick search and quick random play forms +orientation = "vertical" + +; Submenu +; If this is set to simple the sub menu's will only be shown when you're on one of the +; respective pages. If you want to make the menu's something like the classic theme +; comment this out +;submenu = "simple" diff --git a/sources/themes/reborn/images/ajax-loader.gif b/sources/themes/reborn/images/ajax-loader.gif new file mode 100644 index 0000000..b20f505 Binary files /dev/null and b/sources/themes/reborn/images/ajax-loader.gif differ diff --git a/sources/themes/reborn/images/ajax-loader2.gif b/sources/themes/reborn/images/ajax-loader2.gif new file mode 100644 index 0000000..aaa180c Binary files /dev/null and b/sources/themes/reborn/images/ajax-loader2.gif differ diff --git a/sources/themes/reborn/images/ampache-reborn.png b/sources/themes/reborn/images/ampache-reborn.png new file mode 100644 index 0000000..b81d474 Binary files /dev/null and b/sources/themes/reborn/images/ampache-reborn.png differ diff --git a/sources/themes/reborn/images/ampache.png b/sources/themes/reborn/images/ampache.png new file mode 100644 index 0000000..ed29e84 Binary files /dev/null and b/sources/themes/reborn/images/ampache.png differ diff --git a/sources/themes/reborn/images/background.png b/sources/themes/reborn/images/background.png new file mode 100644 index 0000000..e85c4fd Binary files /dev/null and b/sources/themes/reborn/images/background.png differ diff --git a/sources/themes/reborn/images/blank-pixel.gif b/sources/themes/reborn/images/blank-pixel.gif new file mode 100644 index 0000000..17d4390 Binary files /dev/null and b/sources/themes/reborn/images/blank-pixel.gif differ diff --git a/sources/themes/reborn/images/blankalbum.jpg b/sources/themes/reborn/images/blankalbum.jpg new file mode 100644 index 0000000..33e89a0 Binary files /dev/null and b/sources/themes/reborn/images/blankalbum.jpg differ diff --git a/sources/themes/reborn/images/icons.sprite.png b/sources/themes/reborn/images/icons.sprite.png new file mode 100644 index 0000000..ca9cc25 Binary files /dev/null and b/sources/themes/reborn/images/icons.sprite.png differ diff --git a/sources/themes/reborn/images/icons/icon_add.png b/sources/themes/reborn/images/icons/icon_add.png new file mode 100644 index 0000000..da42e17 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_add.png differ diff --git a/sources/themes/reborn/images/icons/icon_add12.png b/sources/themes/reborn/images/icons/icon_add12.png new file mode 100644 index 0000000..6bbba51 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_add12.png differ diff --git a/sources/themes/reborn/images/icons/icon_add2.png b/sources/themes/reborn/images/icons/icon_add2.png new file mode 100644 index 0000000..1138739 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_add2.png differ diff --git a/sources/themes/reborn/images/icons/icon_add_user.png b/sources/themes/reborn/images/icons/icon_add_user.png new file mode 100644 index 0000000..9f6c0f5 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_add_user.png differ diff --git a/sources/themes/reborn/images/icons/icon_admin.png b/sources/themes/reborn/images/icons/icon_admin.png new file mode 100644 index 0000000..ee0c771 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_admin.png differ diff --git a/sources/themes/reborn/images/icons/icon_all.png b/sources/themes/reborn/images/icons/icon_all.png new file mode 100644 index 0000000..2dfaef5 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_all.png differ diff --git a/sources/themes/reborn/images/icons/icon_delete.png b/sources/themes/reborn/images/icons/icon_delete.png new file mode 100644 index 0000000..6b9fa6d Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_delete.png differ diff --git a/sources/themes/reborn/images/icons/icon_disable.png b/sources/themes/reborn/images/icons/icon_disable.png new file mode 100644 index 0000000..7af3a51 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_disable.png differ diff --git a/sources/themes/reborn/images/icons/icon_edit.png b/sources/themes/reborn/images/icons/icon_edit.png new file mode 100644 index 0000000..0699492 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_edit.png differ diff --git a/sources/themes/reborn/images/icons/icon_edit2.png b/sources/themes/reborn/images/icons/icon_edit2.png new file mode 100644 index 0000000..7dc0d54 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_edit2.png differ diff --git a/sources/themes/reborn/images/icons/icon_enable.png b/sources/themes/reborn/images/icons/icon_enable.png new file mode 100644 index 0000000..210b1a6 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_enable.png differ diff --git a/sources/themes/reborn/images/icons/icon_feed.png b/sources/themes/reborn/images/icons/icon_feed.png new file mode 100644 index 0000000..cdf4e8f Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_feed.png differ diff --git a/sources/themes/reborn/images/icons/icon_home.png b/sources/themes/reborn/images/icons/icon_home.png new file mode 100644 index 0000000..09bad7b Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_home.png differ diff --git a/sources/themes/reborn/images/icons/icon_logout.png b/sources/themes/reborn/images/icons/icon_logout.png new file mode 100644 index 0000000..2bc51ac Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_logout.png differ diff --git a/sources/themes/reborn/images/icons/icon_next.png b/sources/themes/reborn/images/icons/icon_next.png new file mode 100644 index 0000000..7ae440a Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_next.png differ diff --git a/sources/themes/reborn/images/icons/icon_pause.png b/sources/themes/reborn/images/icons/icon_pause.png new file mode 100644 index 0000000..af57b25 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_pause.png differ diff --git a/sources/themes/reborn/images/icons/icon_play.png b/sources/themes/reborn/images/icons/icon_play.png new file mode 100644 index 0000000..2dfaef5 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_play.png differ diff --git a/sources/themes/reborn/images/icons/icon_play_add.png b/sources/themes/reborn/images/icons/icon_play_add.png new file mode 100644 index 0000000..5a30a7c Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_play_add.png differ diff --git a/sources/themes/reborn/images/icons/icon_playlist_add.png b/sources/themes/reborn/images/icons/icon_playlist_add.png new file mode 100644 index 0000000..df35ed6 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_playlist_add.png differ diff --git a/sources/themes/reborn/images/icons/icon_plugin.png b/sources/themes/reborn/images/icons/icon_plugin.png new file mode 100644 index 0000000..0f3736f Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_plugin.png differ diff --git a/sources/themes/reborn/images/icons/icon_prev.png b/sources/themes/reborn/images/icons/icon_prev.png new file mode 100644 index 0000000..a39522f Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_prev.png differ diff --git a/sources/themes/reborn/images/icons/icon_random.png b/sources/themes/reborn/images/icons/icon_random.png new file mode 100644 index 0000000..ab3dd30 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_random.png differ diff --git a/sources/themes/reborn/images/icons/icon_stop.png b/sources/themes/reborn/images/icons/icon_stop.png new file mode 100644 index 0000000..7c6af7f Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_stop.png differ diff --git a/sources/themes/reborn/images/icons/icon_view.png b/sources/themes/reborn/images/icons/icon_view.png new file mode 100644 index 0000000..b4b2312 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_view.png differ diff --git a/sources/themes/reborn/images/icons/icon_volumeup.png b/sources/themes/reborn/images/icons/icon_volumeup.png new file mode 100644 index 0000000..62fcfc6 Binary files /dev/null and b/sources/themes/reborn/images/icons/icon_volumeup.png differ diff --git a/sources/themes/reborn/images/missing.png b/sources/themes/reborn/images/missing.png new file mode 100644 index 0000000..3819cd5 Binary files /dev/null and b/sources/themes/reborn/images/missing.png differ diff --git a/sources/themes/reborn/images/ratings/star_rating.gif b/sources/themes/reborn/images/ratings/star_rating.gif new file mode 100644 index 0000000..55ac1f5 Binary files /dev/null and b/sources/themes/reborn/images/ratings/star_rating.gif differ diff --git a/sources/themes/reborn/images/ratings/star_rating.png b/sources/themes/reborn/images/ratings/star_rating.png new file mode 100644 index 0000000..6c75aed Binary files /dev/null and b/sources/themes/reborn/images/ratings/star_rating.png differ diff --git a/sources/themes/reborn/preview.png b/sources/themes/reborn/preview.png new file mode 100644 index 0000000..6d67de2 Binary files /dev/null and b/sources/themes/reborn/preview.png differ diff --git a/sources/themes/reborn/templates/default.css b/sources/themes/reborn/templates/default.css new file mode 100644 index 0000000..5782de9 --- /dev/null +++ b/sources/themes/reborn/templates/default.css @@ -0,0 +1,1548 @@ +/* vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */ +/** + * + * LICENSE: GNU General Public License, version 2 (GPLv2) + * Copyright 2001 - 2014 Ampache.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License v2 + * as published by the Free Software Foundation. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +@font-face { + font-family: 'DejaVuSansCondensed'; + src: url('fonts/dejavusanscondensed.eot'); + src: url('fonts/dejavusanscondensed.eot') format('embedded-opentype'), + url('fonts/dejavusanscondensed.woff') format('woff'), + url('fonts/dejavusanscondensed.ttf') format('truetype'), + url('fonts/dejavusanscondensed.svg#DejaVuSansCondensed') format('svg'); +} + +/*********************************************** + General style rules +***********************************************/ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td { + border: 0; + outline:0 ; + vertical-align: baseline; + background: transparent; + margin:0; + padding:0; +} + +html { + font-size: 100%; + -ms-text-size-adjust: 100%; + height: 100%; +} + +body { + background-image: url('../images/background.png'); + -webkit-tap-highlight-color: rgba(0,0,0,0); + margin: 0; + font-family: "DejaVuSansCondensed",Helvetica,Arial,sans-serif; + font-weight: normal; + font-size: 16px; + line-height: 1.5em; + color: #fff; + background-color: #222; + height: 90%; +} + +blockquote,q { + quotes:none; +} + +blockquote:before,blockquote:after,q:before,q:after { + content:none; +} + +:focus { + outline:0; +} + +del { + text-decoration:line-through; +} + +table { + border-collapse:collapse; + border-spacing:0; +} + +.error { + color:#c33; +} + +a { + color: #ff9d00; + text-decoration: none; + cursor: pointer; +} + +a:hover { + color:#ffc466; +} + +h3 { + font-size:20px; + margin-bottom: 5px; +} + +hr { + border-top: 1px solid #bbb; + border-bottom: 1px solid #eee; + border-right: 0; + border-left: 0; +} + +input, button, textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +input[type=password], input[type=text] { + line-height: normal; + height: 23px; +} + +input[type=password], input[type=text], textarea { + padding: 4px 6px; + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top,#eee,#fff); + background-image: -ms-linear-gradient(top,#eee,#fff); + background-image: -webkit-gradient(linear,0 0,0 100%,from(#eee),to(#fff)); + background-image: -webkit-linear-gradient(top,#eee,#fff); + background-image: -o-linear-gradient(top,#eee,#fff); + background-image: linear-gradient(top,#eee,#fff); + background-repeat: repeat-x; + border: 2px solid #fff; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +input[type=button], input[type=submit] { + background-image: -moz-linear-gradient(top,#ff9d00,#cc6200); + background-image: -ms-linear-gradient(top,#ff9d00,#cc6200); + background-image: -webkit-gradient(linear,0 0,0 100%,from(#ff9d00),to(#cc6200)); + background-image: -webkit-linear-gradient(top,#ff9d00,#cc6200); + background-image: -o-linear-gradient(top,#ff9d00,#cc6200); + background-image: linear-gradient(top,#ff9d00,#cc6200); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + position: relative; + padding: 5px 12px 4px; + font-size: 18px; + line-height: normal; + border: 0; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.2); + -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.2); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.2); + cursor: pointer; + color: #fff; +} + +input[type=button]:hover, input[type=button]:focus, input[type=submit]:hover, input[type=submit]:focus { + background-position: 0 -10px; + background-color: #cc6200; + -webkit-transition: background-position .1s linear; + -moz-transition: background-position .1s linear; + -ms-transition: background-position .1s linear; + -o-transition: background-position .1s linear; + transition: background-position .1s linear; + text-shadow: 0 -1px 0 rgba(0,0,0,0.5); + text-decoration: none; +} + +input[type=button]:focus:active, input[type=submit]:focus:active { + -webkit-box-shadow:inset 0 0 3px #000,0 1px 0 rgba(255,255,255,0.1); + -moz-box-shadow:inset 0 0 3px #000,0 1px 0 rgba(255,255,255,0.1); + box-shadow:inset 0 0 3px #000,0 1px 0 rgba(255,255,255,0.1); + -webkit-transition: background-position .1s linear; + -moz-transition: background-position .1s linear; + -ms-transition: background-position .1s linear; + -o-transition: background-position .1s linear; + transition: background-position .1s linear; +} + +.box-title { + font-size: 18px; + margin-bottom: 10px; +} + +/*********************************************** + Main +***********************************************/ +#maincontainer { + width:100%; + position: relative; + z-index: 1; + padding-bottom: 40px; +} + +#footer { + width: auto; + z-index: 5; + margin: 15px 270px 0px 0px; +} + +#ajax-loading { + position: fixed; + top: 63px; + left: 6px; + font-size: 12px; + text-align: center; + width: 120px; + color: #ff9d00; + background: url(../images/ajax-loader.gif) no-repeat 2px; + display: none; +} + +/*********************************************** + Header +***********************************************/ +#header { + z-index: 5; + height: 64px; + padding: 0 32px; + background-color: #000; + border-bottom: 2px solid #2d2d2d; + -webkit-box-shadow: 0 18px 18px rgba(30,30,30,0.7); + -moz-box-shadow: 0 18px 18px rgba(30,30,30,0.7); + box-shadow: 0 18px 18px rgba(30,30,30,0.7); + font-size: 15px; +} + +.header-float { +} + +.header-fixed { + position: fixed; + top: 0px; + left: 0px; + width: 97%; +} + +#header .box-top, #header .box-bottom { + display: none; +} + +#header #headerlogo, #header #headerbox , #header .box_headerbox, #header .box-inside, #header .box-content { + height: 100%; +} + +#header #headerlogo { + float:left; + width: 20%; +} + +#header #headerlogo img { + height: 64px; + width: 64px; +} + +#header #headerbox { + float:left; + width: 80%; +} + +#header .box-inside { + text-align:right; + float:right; + width: 100%; +} + +#sb_Subsearch , #play_type_switch { + display: inline-block; + margin: 15px auto auto 50px; +} + +#loginInfo { + position: absolute; + left: 150px; + top: 20px; +} + +#updateInfo { + float: left; +} + +#sb_Subsearch input[type=submit] { + min-width: 100px; + font-size: 15px; +} + +#sb_Subsearch input[type=text] { + width: 175px; + height: 30px; + padding: 0 25px; + margin: 0; + font-size: 15px; + border-color: #444; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +#sb_Subsearch input[type=text]:focus { + color: #555; + background: #fff; + background: rgba(255,255,255,0.9); + border-color:#ff9d00; +} + +#headerbox select { + min-width: 150px; + color: #555; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + font-size: 16px; + font-weight: normal; + line-height: normal; +} + +#sb_Subsearch a { + font-size: 11px; + vertical-align: bottom; +} + +/*********************************************** + Login +***********************************************/ +#loginPage #maincontainer { + padding: 20px; + background-color: #222; + background-color: rgba(0,0,0,0.15); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + border-color: #1d1d1d; + border: 2px solid rgba(0,0,0,0.15); + -webkit-box-shadow: 0 0 5px rgba(255,255,255,0.05); + -moz-box-shadow: 0 0 5px rgba(255,255,255,0.05); + box-shadow: 0 0 5px rgba(255,255,255,0.05); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + display: block; + width: 800px; + height: 580px; + margin: 50px auto 0 auto; +} + +#loginPage #header { + background-color: transparent; + height: 254px; + width: 254px; + margin: auto; + border: none; + box-shadow: none; +} + +#loginPage #headerlogo { + width: 256px; + height: 256px; + background: url('../images/ampache-reborn.png') no-repeat center; + background-size: contain; + margin: auto; +} + +#loginPage #headerlogo a { + display: none; +} + +#loginPage #loginbox { + width: 500px; + margin: auto auto 20px auto; + color: #999; +} + +#loginPage #loginbox h2{ + display: none; +} + +#loginPage #loginbox div { + font-size: 18px; + line-height: 1.5em; + text-shadow: 0 1px 0 #000; + text-rendering: auto; +} + +#loginPage #loginbox #usernamefield input, +#loginPage #loginbox #emailfield input, +#loginPage #loginbox #passwordfield input { + float: none; + margin-left: 0; + width: 100%; + min-height: 28px; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + border-radius: 7px; + height: 32px; +} + +#loginPage #loginbox #usernamefield input:focus, +#loginPage #loginbox #emailfield input:focus, +#loginPage #loginbox #passwordfield input:focus { + float: none; + outline: none; + border-color: #f1b720; + box-shadow: 0 0 10px #9ecaed; +} + +#loginPage #loginbox #usernamefield, +#loginPage #loginbox #passwordfield, +#loginPage #loginbox #emailfield, +#loginPage #loginbox #remembermefield { + margin-top: 20px; +} + +#loginPage #loginbox #remembermefield { + color: #eee; + font-size: 16px; +} + +#loginPage #loginbox #remembermefield input { + cursor: pointer; + width: 40px; +} + +#lostpasswordbutton { + margin-top: 10px; +} + +.formValidation { + margin: 0; + padding: 0; + float: right !important; +} + +.formValidation input { + color: #fff; + border: none; + min-width: 150px; +} + +.formValidation a { + font-size: 15px; + vertical-align: bottom; + margin-right: 20px; +} + +#loginPage span.error { + display: block; + clear: left; + padding: 10px; + background: #ff9999; + border: 1px solid #cc3333; + /*font-weight: bold;*/ + color: #990000; + text-align: center; + margin-bottom: 10px; +} + +#loginPage #footer { + width: 800px; + margin: 10px auto; + color: #888; +} + +#loginPage #footer a { + color: #555; +} + +/*********************************************** + Sidebar +***********************************************/ +#sidebar { + width: 150px; + /*z-index: 8;*/ + margin: 20px auto auto 5px; + padding: 10px; + border-radius: 2px; + border-color: #1d1d1d; + border: 2px solid rgba(0,0,0,0.15); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -webkit-box-shadow: 0 0 5px rgba(255,255,255,0.05); + -moz-box-shadow: 0 0 5px rgba(255,255,255,0.05); + box-shadow: 0 0 5px rgba(255,255,255,0.05); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + font-size: 12px; + background-color: #1a1a1a; + border-right: 2px solid #000; + color: #999; +} + +.sidebar-float { + float: left; +} + +.sidebar-fixed { + position: fixed; + top: 66px; + left: 5px; + height: 90%; + overflow: hidden; +} + +/* For sidebar tabs */ +#sidebar-tabs { + padding: 2px; + height: 25px; +} + +#sidebar-tabs li.sb1 { + float: left; + margin-right: 3px; + height: 25px; +} + +#sidebar-tabs li.active { +} +#sidebar-tabs li.active img{ +} + +/* Tabs content */ +#sidebar-page { + width: 150px; + margin: 3px auto 5px 5px; + background-color: rgba(0,0,0,0.15); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + border-color: #1d1d1d; + border: 2px solid rgba(0,0,0,0.15); + -webkit-box-shadow: 0 0 5px rgba(255,255,255,0.05); + -moz-box-shadow: 0 0 5px rgba(255,255,255,0.05); + box-shadow: 0 0 5px rgba(255,255,255,0.05); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +.sidebar-page-float { + position: absolute; + left: 0px; +} + +.sidebar-page-fixed { + position: absolute; + left: -7px; + overflow-x: hidden; + overflow-y: auto; + height: 90%; +} + +#sidebar-tabs .sb2 li h4 { + color: #fff; + height: 23px; + margin: 10px 0 5px; + padding: 0 10px 0 20px; + background-color: #262626; + border-bottom: 1px solid #0f0f0f; + -webkit-box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; + -moz-box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; + box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; +} + +#sidebar-tabs .sb2 li ul li a { + border-radius: 4px; + margin-top: 3px; + padding: 7px; + background: none!important; + -webkit-transition: color .1s; + -moz-transition: color .1s; + -ms-transition: color .1s; + -o-transition: color .1s; + transition: color .1s; + color: #999; +} + +#sidebar-tabs .sb2 li ul li a:hover { + color: #fff; +} + +#sidebar-tabs .sb2 #browse_filters li { + margin: 3px auto 5px 5px; +} + +#sidebar-tabs .sb2 #browse_filters #multi_alpha_filterLabel { + margin: 5px 3px 0; + display: block; +} + +#sidebar-tabs .sb2 #browse_filters #multi_alpha_filter { + border: 1px solid #bbb; + border-radius: 2px; + padding: 4px; + width: 130px; +} + +#sidebar-tabs #catalog_select { + width: 130px; +} + +/* Localplay */ +.active_instance { + border: 1px solid #fff; +} + +/*********************************************** + Rightbar +***********************************************/ +#rightbar { + position: fixed; + right: 10px; + top: 66px; + width: 200px; + margin: 20px 20px auto auto; + max-height: 85%; + padding: 10px; + background-color: rgba(0,0,0,0.15); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + border-color: #1d1d1d; + border: 2px solid rgba(0,0,0,0.15); + -webkit-box-shadow: 0 0 5px rgba(255,255,255,0.05); + -moz-box-shadow: 0 0 5px rgba(255,255,255,0.05); + box-shadow: 0 0 5px rgba(255,255,255,0.05); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + color: #fff; + z-index: 0; + overflow-y: auto; +} + +/* For jQuery scrolling feature */ +.fixedrightbar { + position: fixed !important; + top: -20px !important; + right: 10px !important; +} + +.fixedrightbarsubmenu { + position: fixed !important; + top: 33px !important; + right: 70px !important; +} +/******************************/ + +#rightbar #rb_action { + list-style-type: none; + padding: 4px; +} + +#rightbar #rb_action li { + margin-right: 5px; + display: inline; +} + +#rightbar li #rb_add, #rightbar li #pl_add { + position: relative; +} + +#rightbar #rb_current_playlist li { + min-height: 29px; + font-size: 14px; + color: #fff; + width: 100%; + display: table; +} + +#rightbar #rb_current_playlist li a { + width: 100%; + padding: 4px 7px 3px; + color:#eee; + display: table-cell; +} + +#rightbar #rb_current_playlist li a:hover, #rightbar #rb_current_playlist li a:focus { + color:#fff; + text-shadow:none; + background-color:#c85a00; + background-image:-moz-linear-gradient(top,#cc6200,#c24d00); + background-image:-ms-linear-gradient(top,#cc6200,#c24d00); + background-image:-webkit-gradient(linear,0 0,0 100%,from(#cc6200),to(#c24d00)); + background-image:-webkit-linear-gradient(top,#cc6200,#c24d00); + background-image:-o-linear-gradient(top,#cc6200,#c24d00); + background-image:linear-gradient(top,#cc6200,#c24d00); + background-repeat:repeat-x; +} + +#rightbar #rb_current_playlist li.odd { + background-color: rgba(255,255,255,0.1); +} + +#rightbar li:hover .submenu { + display:block; +} + +#rightbar .submenu { + display: none; + position: fixed; + top: 120px; + right: 70px; + background-color: #222; + border: 2px solid silver; + width: 120px; + padding: 0.6em; + font-size: 12px; + z-index: 99; + overflow: hidden; +} + +#rightbar #rb_action .submenu li { + margin: 0; +} + +#rightbar .submenu a { + display: block; + border-bottom: 1px dotted #ddd; + color: #eee; + text-decoration: none; + text-align: left; + padding: 0.4em; + width: 100%; +} + +#rightbar .submenu a:hover { + color: #ff9d00; + cursor: pointer; +} + +#rightbar #localplay-control { + padding-left: 5px; +} + +#rightbar #localplay-control { + padding: 5px; + text-align: center; + margin: 7px 0px; +} + +#rightbar #localplay-control img { + vertical-align: bottom; +} + +/*********************************************** + Content +***********************************************/ +#content { + z-index: 3; + -webkit-transition: top .5s; + -moz-transition: top .5s; + -ms-transition: top .5s; + -o-transition: top .5s; + transition: top .5s; + width: auto; + max-height: 85%; + padding: 10px; + background-color: rgba(0,0,0,0.15); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + border-color: #1d1d1d; + border: 2px solid rgba(0,0,0,0.15); + -webkit-box-shadow: 0 0 5px rgba(255,255,255,0.05); + -moz-box-shadow: 0 0 5px rgba(255,255,255,0.05); + box-shadow: 0 0 5px rgba(255,255,255,0.05); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + color: #fff; + padding: 20px; +} + +.content-float { + margin: 20px 250px 0px 180px; +} + +.content-wild { + margin-right: 20px !important; +} + +.content-fixed { + margin: 86px 250px 0px 180px; +} + +.browse_content { + margin-bottom: 20px; +} + +.list-header, .list-header a { + margin: 10px 0 10px 0; + color: #999; + font-size: 13px; +} + +.list-header a:hover, .list-header a:focus { + color: #fff; + text-decoration: none; +} + +span.page-nb { + color: #ff9d00; + } + +table.tabledata { + width:100%; + /*table-layout: fixed;*/ + text-align:left; + /*border:1px solid #bbb;*/ + font-size: 14px; + color: #eee; + margin-bottom: 20px; +} + +table.tabledata a { + color:#eee; +} + +table.tabledata a:hover, table.tabledata a:focus { + color: #ffc466; +} + +table.tabledata thead .th-top, table.tabledata tfoot .th-bottom { + font-size: 12px; + background-color: #262626; + -webkit-box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; + -moz-box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; + box-shadow: 0 1px 0 #2a2a2a,inset 0 1px 1px #000; +} + +table.tabledata thead .th-top { + border-bottom: 1px solid #0f0f0f; +} + +table.tabledata tfoot .th-bottom { + border-top: 1px solid #0f0f0f; + display: none; +} + +table.tabledata td { + vertical-align: middle; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 3px 10px 3px 0; +} + +table.tabledata th a { + color:#ff9d00; +} + +table.tabledata tr:hover, table.tabledata tr:focus { + text-shadow:none; + background-color:#c85a00; + background-image:-moz-linear-gradient(top,#cc6200,#c24d00); + background-image:-ms-linear-gradient(top,#cc6200,#c24d00); + background-image:-webkit-gradient(linear,0 0,0 100%,from(#cc6200),to(#c24d00)); + background-image:-webkit-linear-gradient(top,#cc6200,#c24d00); + background-image:-o-linear-gradient(top,#cc6200,#c24d00); + background-image:linear-gradient(top,#cc6200,#c24d00); + background-repeat:repeat-x; +} + +table.tabledata .th-top:hover, +table.tabledata .th-bottom:hover, +table.tabledata .th-top:focus, +table.tabledata .th-bottom:focus { + color: #fff; + background-color: #262626; + background-image: none; + background-repeat: no-repeat; +} + +table.tabledata tbody .odd { + background-color: rgba(255,255,255,0.1); +} + +table.tabledata tbody .cel_play { + max-width: 40px; + width: 40px !important; + text-align: right; +} + +table.tabledata tbody .cel_play_content { + display: block; +} + +table.tabledata tbody .cel_play_hover { + display: none; +} + +table.tabledata tbody tr:hover .cel_play_hover, table.tabledata tbody tr:focus .cel_play_hover { + display: block; +} +table.tabledata tbody tr:hover .cel_play_content, table.tabledata tbody tr:focus .cel_play_content { + display: none; +} + +table.tabledata tbody .cel_add { + max-width: 60px; + width: 60px !important; + text-align: right; +} + +table.tabledata tbody .cel_item_add { + display: none; +} + +table.tabledata tbody tr:hover .cel_item_add, table.tabledata tbody tr:focus .cel_item_add { + display: block; +} + +table.tabledata tbody .cel_time { + min-width: 40px; + width: 40px !important; +} + +table.tabledata tbody .cel_action { + width: 130px !important; + max-width: 130px; +} + +table.tabledata tbody .cel_action_text { + max-width: 100% !important; +} + +table.tabledata tbody td.cel_rating { + width: 100px !important; + max-width: 100px; +} + +table.tabledata tbody td.cel_userflag { + width: 40px !important; + max-width: 40px; +} + +table.tabledata tbody .cel_tags { + width: 150px !important; + max-width: 150px; +} + +table.tabledata tbody .cel_cover img { + vertical-align: middle; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); + -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); + box-shadow: 0 0 10px rgba(0,0,0,0.75); + margin: 2px; +} + +table.tabledata tbody .cel_cover img:hover { + border: 2px solid #ff9d00; + margin: 0px; +} + +table.tabledata .cel_drag { + max-width: 16px; + width: 16px !important; +} + +table.tabledata .cel_drag img:hover { + cursor: pointer; +} + +table.tabledata .cel_agent { + text-align: right; +} + +table.tabledata .cel_agent img:hover { + cursor: help; +} + +.box_preferences h4 { + font-size: 15px; + margin-bottom: 10px; +} + +div.box.box_current_configuration { + margin-top: 20px; +} + +/*********************************************** + Content (info-box) +***********************************************/ +#content .info-box h3 { + margin-top:10px; +} + +#content .info-box .box-content div.star-rating { + width: 150px; + max-width: 150px; +} + +/* Random album (homepage) */ +#random_selection { + margin-bottom: 20px; +} + +#random_selection .random_album { + float: left; + width: 125px; + margin-bottom: 10px; + text-align: center; +} + +#random_selection .art_album img { + vertical-align: middle; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); + -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); + box-shadow: 0 0 10px rgba(0,0,0,0.75); + width: 80px; + height: 80px; + margin: 2px; +} + +#random_selection .art_album img:hover { + border: 2px solid #ff9d00; + margin: 0px; +} + +#random_selection .random_album .star-rating { + margin: auto; +} + +#random_selection .random_album .play_album { + margin-left: 7px; +} + +#random_selection .random_album .play_album img { + border: 0; + box-shadow: 0 0 0 0; +} + +#random_selection .box-bottom { + clear:left; +} + +tr#search_item_count td, tr#search_length td, tr#search_size_limit td, tr#search_max_results td { + width: 150px; + overflow: hidden; + display: inline-block; + white-space: nowrap +} + +div.box.box_rules { + margin: 30px 0 30px 0; +} + +#content .missing { + background-image: url('../images/missing.png'); + background-repeat: no-repeat; + background-position: center; +} + +.cel_song, .cel_album, .cel_artist { + max-width: 350px; +} + +/*********************************************** + Content (now playing) +***********************************************/ +#now_playing { + margin-bottom: 20px; + width: auto; +} + +#now_playing .np_group { + float: left; +} + +#now_playing .np_row { + display: table; + margin-bottom: 10px; +} + +#now_playing .np_cell { + line-height: 15px; + font-size: 13px; +} + +#now_playing .cel_rating label { + display: none; +} + +#now_playing .cel_userflag label { + display: none; +} + +#now_playing .np_group label { + font-weight: bold; +} + +#now_playing .cel_username { + width: 140px; +} + +#now_playing .cel_song, #now_playing .cel_album, #now_playing .cel_artist { + width: 200px; +} + +#now_playing .cel_albumart { + float: left; + width: 90px; +} + +#now_playing .cel_albumart img { + vertical-align: middle; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); + -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); + box-shadow: 0 0 10px rgba(0,0,0,0.75); + float: left; + margin: 2px; +} + +#now_playing .cel_albumart img:hover { + border: 2px solid #ff9d00; + margin: 0px; +} + +#now_playing .cel_lyrics { + margin-top: 5px; +} + +#now_playing .cel_lyrics a:hover { + color: #0099CC; +} + +#now_playing .similars { + margin-right: 10px; + padding-right: 5px; + margin-left: 10px; + padding-left: 5px; +} + +#recent_more { + text-align: right; + width: 100%; +} + +.album_group_disks_title { + float: left; + margin-right: 30px; +} + +.album_group_disks_actions { + +} + +/*********************************************** + Content (Tag cloud) +***********************************************/ +.box_tag_cloud { + margin-bottom: 15px; +} + +.clearfix { + clear: left; +} + +.box-bottom { + clear: both; +} + +span.fatalerror { + color: #c60; + padding: 5px; + display: block; +} + +.box-content #tag_filter div { + float: left; + height: 32px; +} + +.box-content #tag_filter .tag_container { + margin: 20px 0 0 15px; +} + +.box-content #tag_filter .tag_button { + margin: 20px 0 0 15px; + font-size: 14px; + vertical-align: middle; + color: #fff; + text-align: center; +} + +.box-content #tag_filter .tag_actions { + margin: 15px 0 0 -8px; +} + +.box-content #tag_filter .tag_button span { + padding: 8px 10px 8px; + cursor: pointer; + background-color: #5f5f5f; + background-image: -moz-linear-gradient(top,#6d6d6d,#4a4a4a); + background-image: -ms-linear-gradient(top,#6d6d6d,#4a4a4a); + background-image: -webkit-gradient(linear,0 0,0 100%,from(#6d6d6d),to(#4a4a4a)); + background-image: -webkit-linear-gradient(top,#6d6d6d,#4a4a4a); + background-image: -o-linear-gradient(top,#6d6d6d,#4a4a4a); + background-image: linear-gradient(top,#6d6d6d,#4a4a4a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-border-top-left-radius: 2px; + -moz-border-radius-topleft: 2px; + border-top-left-radius: 2px; + -webkit-border-bottom-left-radius: 2px; + -moz-border-radius-bottomleft: 2px; + border-bottom-left-radius: 2px; + -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),inset -1px 0 0 rgba(0,0,0,0.2); + -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),inset -1px 0 0 rgba(0,0,0,0.2); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),inset -1px 0 0 rgba(0,0,0,0.2); +} + +.box-content #tag_filter .tag_button span:hover { + background-color:#c85a00; + background-image:-moz-linear-gradient(top,#cc6200,#c24d00); + background-image:-ms-linear-gradient(top,#cc6200,#c24d00); + background-image:-webkit-gradient(linear,0 0,0 100%,from(#cc6200),to(#c24d00)); + background-image:-webkit-linear-gradient(top,#cc6200,#c24d00); + background-image:-o-linear-gradient(top,#cc6200,#c24d00); + background-image:linear-gradient(top,#cc6200,#c24d00); + background-repeat:repeat-x; +} + +.box-content #tag_filter li{ + height: 16px; +} + +/*********************************************** + Content (inline edit) +***********************************************/ +.inline-edit select { + max-width: 200px; +} + +/*********************************************** + Content (information-actions) +***********************************************/ +#information_actions { + margin: 20px 0 10px 0; + width: 300px; + font-size: 12px; +} + +#content .info-box { + margin-bottom: 30px; +} + +#content .info-box .box-content .album_art { + float: right; +} + +#content .info-box .box-content .album_art img { + vertical-align: middle; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.75); + -moz-box-shadow: 0 0 10px rgba(0,0,0,0.75); + box-shadow: 0 0 10px rgba(0,0,0,0.75); + float: left; + margin: 2px; +} + +#content .info-box .box-content .album_art img:hover { + border: 2px solid #ff9d00; + margin: 0px; +} + +#information_actions ul li{ + /*border-bottom:1px solid #ccc;*/ + color: #999; + border-radius: 4px; + -webkit-transition: color .1s; + -moz-transition: color .1s; + -ms-transition: color .1s; + -o-transition: color .1s; + transition: color .1s; +} + +#information_actions h3 { + font-size: 13px; +} + +#information_actions a { + margin-right: 5px; + color: #999; +} + +#information_actions input { + margin: 0px 5px 0 2px; +} + +#information_actions a:hover, #information_actions li:hover { + color: #fff;/*#ffc466;*/ +} + +#information_actions .star-rating li { + padding: 0; +} + +#information_actions li img { + vertical-align:top; +} + +#information_actions li a { + vertical-align:bottom; +} + +.item_right_info { + float: right; + max-width: 60%; +} + +.external_links { + text-align: right; +} + +.external_links a { + margin: 0px 5px 0px 0px; + opacity: 0.3; +} + +.external_links a:hover { + opacity: 1; +} + +#artist_summary { + margin-right: 150px; +} + +/************************************************/ +/* Styles for the star ratings */ +/************************************************/ +.star-rating { + position:relative; +} +.dynamic-star-rating { + width:95px; +} +.star-rating ul, +.star-rating a:hover, +.star-rating .current-rating { + background: url(../images/ratings/star_rating.png) left -1000px repeat-x; +} +.star-rating ul { + position:relative; + width:80px; + height:15px; + overflow:hidden; + list-style:none; + margin:0; + padding:0; + background-position: left top; +} +.star-rating li { + display: inline; +} + +.star-rating a, .star-rating span, +.star-rating .current-rating { + position:absolute; + top:0; + left:0; + text-indent:-1000em; + height:15px; + line-height:15px; + outline:none; + overflow:hidden; + border:none; +} + +.star-rating .star1 { width:20%; z-index:6; } +.star-rating .star2 { width:40%; z-index:5; } +.star-rating .star3 { width:60%; z-index:4; } +.star-rating .star4 { width:80%; z-index:3; } +.star-rating .star5 { width:100%; z-index:2;} +.star-rating .current-rating { z-index:1; background-position: left bottom; } + +.star-rating a.star0 { + left:0px; + width:16px; + background: url(../../../images/ratings/x_off.gif) left top; +} + +.dynamic-star-rating a:hover { + background-position: left center; +} + +.dynamic-star-rating a:hover.star0 { + background: url(../../../images/ratings/x.gif) left top; +} +.dynamic-star-rating ul { + left:16px; +} + +/************************************************/ +/* Styles for user flags */ +/************************************************/ +.userflag +{ + position: relative; + width:16px; + height:16px; +} + +.userflag a { + position:absolute; + display: inline; +} + +.userflag a.userflag_true +{ + width:16px; + height: 16px; + background: url(../../../images/icon_flag.png) left top; +} + +.userflag a:hover.userflag_true +{ + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a.userflag_false +{ + width:16px; + height:16px; + background: url(../../../images/icon_flag_off.png) left top; +} + +.userflag a:hover.userflag_false +{ + background: url(../../../images/icon_flag.png) left top; +} + +/*********************************************** + Content (Track view) +***********************************************/ +.song_details { + margin-top: 20px; +} + +dl.song_details dt { + float: left; + clear: left; + width: 200px; + text-align: right; + color: #c60; +} + +dl.song_details dd { + margin: 10px 0 10px 250px; + height: 24px; +} + +/*********************************************** + Footer +***********************************************/ +#footer { + text-align:right; +} + +#footer a:hover { + color: #09c; + text-decoration: underline; +} + +/*********************************************** + Other +***********************************************/ +ol, ul, #rightbar ul { + list-style:none; +} + +.browse-options { + float: right; +} + +.browse-options form { + display: inline; +} + +.browse-options input[type=text] { + width: 50px; +} + +.browse-options-content { + display: none; +} + +.browse-options-content span { + margin-right: 5px; +} + +.jscroll-next { + width:50%; + display:block; + border:1px solid #ccc; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius: 10px; + background-color:#eee; + color:#999; + font-weight:bold; + text-align:center; + padding:10px 0; + cursor:pointer; + margin: auto; +} + +.jscroll-next:hover { + color:#666; +} + +.missing_album { + text-decoration: none; + border-bottom:1px dotted; + color: #bbb !important; +} + +.user_avatar { + float: right; +} diff --git a/sources/themes/reborn/templates/fonts/dejavusanscondensed.css b/sources/themes/reborn/templates/fonts/dejavusanscondensed.css new file mode 100644 index 0000000..868e1bf --- /dev/null +++ b/sources/themes/reborn/templates/fonts/dejavusanscondensed.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'DejaVuSansCondensed'; + src: url('dejavusanscondensed.eot'); + src: url('dejavusanscondensed.eot') format('embedded-opentype'), + url('dejavusanscondensed.woff') format('woff'), + url('dejavusanscondensed.ttf') format('truetype'), + url('dejavusanscondensed.svg#DejaVuSansCondensed') format('svg'); +} diff --git a/sources/themes/reborn/templates/fonts/dejavusanscondensed.eot b/sources/themes/reborn/templates/fonts/dejavusanscondensed.eot new file mode 100644 index 0000000..5f78039 Binary files /dev/null and b/sources/themes/reborn/templates/fonts/dejavusanscondensed.eot differ diff --git a/sources/themes/reborn/templates/fonts/dejavusanscondensed.svg b/sources/themes/reborn/templates/fonts/dejavusanscondensed.svg new file mode 100644 index 0000000..7838a55 --- /dev/null +++ b/sources/themes/reborn/templates/fonts/dejavusanscondensed.svg @@ -0,0 +1,17652 @@ + + + + +Created by FontForge 20110222 at Mon Nov 11 06:44:27 2013 + By Orthosie Webhosting +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. +DejaVu changes are in public domain + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/themes/reborn/templates/fonts/dejavusanscondensed.ttf b/sources/themes/reborn/templates/fonts/dejavusanscondensed.ttf new file mode 100644 index 0000000..3379e4b Binary files /dev/null and b/sources/themes/reborn/templates/fonts/dejavusanscondensed.ttf differ diff --git a/sources/themes/reborn/templates/fonts/dejavusanscondensed.woff b/sources/themes/reborn/templates/fonts/dejavusanscondensed.woff new file mode 100644 index 0000000..ae33322 Binary files /dev/null and b/sources/themes/reborn/templates/fonts/dejavusanscondensed.woff differ diff --git a/sources/themes/reborn/templates/icons.sprite.css b/sources/themes/reborn/templates/icons.sprite.css new file mode 100644 index 0000000..f38c5e2 --- /dev/null +++ b/sources/themes/reborn/templates/icons.sprite.css @@ -0,0 +1,71 @@ +.sprite { background: url(../images/icons.sprite.png) no-repeat top left; display: inline-block; } +.sprite-icon_add{ background-position: 0 0; width: 16px; height: 16px; } +.sprite-icon_add12{ background-position: 0 -32px; width: 16px; height: 16px; } +.sprite-icon_add2{ background-position: 0 -64px; width: 16px; height: 16px; } +.sprite-icon_add_key{ background-position: 0 -96px; width: 16px; height: 16px; } +.sprite-icon_add_tag{ background-position: 0 -128px; width: 16px; height: 16px; } +.sprite-icon_add_user{ background-position: 0 -160px; width: 16px; height: 16px; } +.sprite-icon_add_wanted{ background-position: 0 -192px; width: 16px; height: 16px; } +.sprite-icon_admin{ background-position: 0 -224px; width: 16px; height: 16px; } +.sprite-icon_all{ background-position: 0 -256px; width: 16px; height: 16px; } +.sprite-icon_ampache{ background-position: 0 -288px; width: 16px; height: 16px; } +.sprite-icon_batch_download{ background-position: 0 -320px; width: 16px; height: 16px; } +.sprite-icon_broadcast{ background-position: 0 -352px; width: 16px; height: 16px; } +.sprite-icon_cancel{ background-position: 0 -384px; width: 16px; height: 16px; } +.sprite-icon_cog{ background-position: 0 -416px; width: 16px; height: 16px; } +.sprite-icon_comment{ background-position: 0 -448px; width: 16px; height: 16px; } +.sprite-icon_delete{ background-position: 0 -480px; width: 16px; height: 16px; } +.sprite-icon_disable{ background-position: 0 -512px; width: 16px; height: 16px; } +.sprite-icon_download{ background-position: 0 -544px; width: 16px; height: 16px; } +.sprite-icon_drag{ background-position: 0 -576px; width: 16px; height: 16px; } +.sprite-icon_edit{ background-position: 0 -608px; width: 16px; height: 16px; } +.sprite-icon_edit2{ background-position: 0 -640px; width: 16px; height: 16px; } +.sprite-icon_enable{ background-position: 0 -672px; width: 16px; height: 16px; } +.sprite-icon_equalizer{ background-position: 0 -704px; width: 16px; height: 16px; } +.sprite-icon_feed{ background-position: 0 -736px; width: 16px; height: 16px; } +.sprite-icon_flag{ background-position: 0 -768px; width: 16px; height: 16px; } +.sprite-icon_flag_off{ background-position: 0 -800px; width: 16px; height: 16px; } +.sprite-icon_flow{ background-position: 0 -832px; width: 16px; height: 16px; } +.sprite-icon_fullscreen{ background-position: 0 -864px; width: 16px; height: 16px; } +.sprite-icon_google{ background-position: 0 -896px; width: 16px; height: 16px; } +.sprite-icon_home{ background-position: 0 -928px; width: 16px; height: 16px; } +.sprite-icon_image{ background-position: 0 -960px; width: 16px; height: 16px; } +.sprite-icon_info{ background-position: 0 -992px; width: 16px; height: 16px; } +.sprite-icon_lastfm{ background-position: 0 -1024px; width: 16px; height: 16px; } +.sprite-icon_link{ background-position: 0 -1056px; width: 16px; height: 16px; } +.sprite-icon_lock{ background-position: 0 -1088px; width: 16px; height: 16px; } +.sprite-icon_logout{ background-position: 0 -1120px; width: 16px; height: 16px; } +.sprite-icon_microphone{ background-position: 0 -1152px; width: 16px; height: 16px; } +.sprite-icon_money{ background-position: 0 -1184px; width: 16px; height: 16px; } +.sprite-icon_next{ background-position: 0 -1216px; width: 16px; height: 16px; } +.sprite-icon_next_hover{ background-position: 0 -1248px; width: 16px; height: 16px; } +.sprite-icon_pause{ background-position: 0 -1280px; width: 16px; height: 16px; } +.sprite-icon_pause_hover{ background-position: 0 -1312px; width: 16px; height: 16px; } +.sprite-icon_play{ background-position: 0 -1344px; width: 16px; height: 16px; } +.sprite-icon_play_add{ background-position: 0 -1376px; width: 16px; height: 16px; } +.sprite-icon_play_add_preview{ background-position: 0 -1408px; width: 16px; height: 16px; } +.sprite-icon_play_hover{ background-position: 0 -1440px; width: 16px; height: 16px; } +.sprite-icon_play_preview{ background-position: 0 -1472px; width: 16px; height: 16px; } +.sprite-icon_playlist_add{ background-position: 0 -1504px; width: 16px; height: 16px; } +.sprite-icon_plugin{ background-position: 0 -1536px; width: 16px; height: 16px; } +.sprite-icon_preferences{ background-position: 0 -1568px; width: 16px; height: 16px; } +.sprite-icon_prev{ background-position: 0 -1600px; width: 16px; height: 16px; } +.sprite-icon_prev_hover{ background-position: 0 -1632px; width: 16px; height: 16px; } +.sprite-icon_random{ background-position: 0 -1664px; width: 16px; height: 16px; } +.sprite-icon_run{ background-position: 0 -1696px; width: 16px; height: 16px; } +.sprite-icon_save{ background-position: 0 -1728px; width: 16px; height: 16px; } +.sprite-icon_server_lightning{ background-position: 0 -1760px; width: 16px; height: 16px; } +.sprite-icon_share{ background-position: 0 -1792px; width: 16px; height: 16px; } +.sprite-icon_statistics{ background-position: 0 -1824px; width: 16px; height: 16px; } +.sprite-icon_stop{ background-position: 0 -1856px; width: 16px; height: 16px; } +.sprite-icon_stop_hover{ background-position: 0 -1888px; width: 16px; height: 16px; } +.sprite-icon_tick{ background-position: 0 -1920px; width: 16px; height: 16px; } +.sprite-icon_view{ background-position: 0 -1952px; width: 16px; height: 16px; } +.sprite-icon_visualizer{ background-position: -32px 0; width: 16px; height: 16px; } +.sprite-icon_volumedn{ background-position: -32px -32px; width: 16px; height: 16px; } +.sprite-icon_volumemute{ background-position: -32px -64px; width: 16px; height: 16px; } +.sprite-icon_volumeup{ background-position: -32px -96px; width: 16px; height: 16px; } +.sprite-icon_wand{ background-position: -32px -128px; width: 16px; height: 16px; } +.sprite-icon_wikipedia{ background-position: -32px -160px; width: 16px; height: 16px; } +.sprite-icon_world_link{ background-position: -32px -192px; width: 16px; height: 16px; } + diff --git a/sources/themes/reborn/theme.cfg.php b/sources/themes/reborn/theme.cfg.php new file mode 100644 index 0000000..022d408 --- /dev/null +++ b/sources/themes/reborn/theme.cfg.php @@ -0,0 +1,52 @@ +;;;;;;;;;;;;;;;;;; +;; +;;;;;;;;;;;;;;;;;; +; Copyright 2001 - 2013 Ampache.org +; +; This program is free software; you can redistribute it and/or +; modify it under the terms of the GNU General Public License v2 +; as published by the Free Software Foundation. +; +; 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, write to the Free Software +; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +; +;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Reborn Ampache Theme +;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; Theme Name +; This is the actual name of the theme that +; will be displayed in the preferences screen +; DEFAULT: ampache-theme +name = "Reborn" + +; Theme Author +; This is just a way of giving credit to the +; person who actually created this theme +; DEFAULT: N/A +author = "SUTJael" + +; Theme Maintainer +; This is just a way of listing who is responsible for +; maintaining this theme in case it's not working right +; please include an e-mail address so you can be contacted +; DEFAULT: N/A +maintainer = "SUTJael - orco@netcourrier.com" + +; Orientation +; This was added as of 3.3.2-Alpha4, this tells Ampache if this theme +; uses vertical or horizontal orientation of the menu, if this is a horizontal +; theme then it will not show the quick search and quick random play forms +orientation = "vertical" + +; Submenu +; If this is set to simple the sub menu's will only be shown when you're on one of the +; respective pages. If you want to make the menu's something like the classic theme +; comment this out +;submenu = "simple" diff --git a/sources/update.php b/sources/update.php new file mode 100644 index 0000000..fa68733 --- /dev/null +++ b/sources/update.php @@ -0,0 +1,81 @@ + + + + + + + Ampache :: For the love of Music - Update + + + + + + +
      + +
      +

      3.3.3.5. According to your database your current version is: %s.'), Update::format_version($version)); ?>

      +

      +
      + +
      + +
      + +
      + +
      + +
      + + diff --git a/sources/util.php b/sources/util.php new file mode 100644 index 0000000..fee9294 --- /dev/null +++ b/sources/util.php @@ -0,0 +1,40 @@ +