diff --git a/server/djrandom.init b/server/djrandom.init new file mode 100755 index 0000000000000000000000000000000000000000..53dbd31730c9b70ece759157ee8b474ef01c9c89 --- /dev/null +++ b/server/djrandom.init @@ -0,0 +1,88 @@ +#!/bin/bash +# +# Init script for djrandom frontend and backend workers. +# +### BEGIN INIT INFO +# Provides: djrandom +# Required-Start: $local_fs $remote_fs $network $syslog $named +# Required-Stop: $local_fs $remote_fs $network $syslog $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# X-Interactive: true +# Short-Description: Start/stop DJRandom daemons +### END INIT INFO + +# Usually tools will be installed in /usr/local/bin. +PATH=/usr/local/bin:$PATH + +RUN_DIR=/var/run/djrandom +USER=djrandom +DAEMONS="receiver scanner fingerprinter frontend" +COMMON_OPTIONS="--debug --user $USER" + +. /lib/lsb/init-functions + +[ -r /etc/default/djrandom ] && . /etc/default/djrandom + + +start_daemons() { + ret=0 + [ -d ${RUN_DIR} ] || mkdir -p ${RUN_DIR} + [ -n "${USER}" ] && chown ${USER} ${RUN_DIR} + for daemon in ${DAEMONS} ; do + program_name="djrandom-${daemon}" + pidfile="${RUN_DIR}/${program_name}.pid" + + log_daemon_msg "Starting DJRandom ${daemon}" "${program_name}" + + upcase_prog=$(echo ${program_name} | tr a-z A-Z) + program_opts=$(eval echo \${$(echo ${upcasevar})_OPTIONS}) + options="${COMMON_OPTIONS} ${program_opts}" + if start-stop-daemon --start --quiet --pidfile ${pidfile} \ + --exec ${program_name} -- --pidfile ${pidfile} --user ${USER} \ + ${options} ; then + log_end_msg 0 + else + log_end_msg 1 + ret=1 + fi + done + return ${ret} +} + +stop_daemons() { + for daemon in ${DAEMONS} ; do + program_name="djrandom-${daemon}" + pidfile="${RUN_DIR}/${program_name}.pid" + + log_daemon_msg "Stopping DJRandom ${daemon}" "${program_name}" + start-stop-daemon --stop --oknodo --quiet --pidfile ${pidfile} + log_end_msg 0 + done +} + + +err=0 + +case "$1" in + start) + start_daemons + err=$? + ;; + + stop) + stop_daemons + ;; + + restart) + stop_daemons + start_daemons + err=$? + ;; + + *) + echo "Usage: $0 {start|stop|restart}" + ;; +esac + +exit $err diff --git a/server/djrandom/fingerprint/dedup.py b/server/djrandom/fingerprint/dedup.py index 3d98b2f1535b75a02e69bc64487431f0595f54e1..652369f955b14c00af61480bb0d1ec305942768a 100644 --- a/server/djrandom/fingerprint/dedup.py +++ b/server/djrandom/fingerprint/dedup.py @@ -1,3 +1,4 @@ +import eyeD3 import fp import os import optparse @@ -11,9 +12,17 @@ from djrandom.database import Session, init_db log = logging.getLogger(__name__) +# Monkey-patch eyeD3 so that it does not look at file extensions to +# figure out if something is an MP3 or not. +eyeD3.tag.isMp3File = lambda x: True + class DeDuper(object): + def __init__(self): + self.songs_to_remove = {} + self.dupes_cache = set() + def _generate_code_json(self, jsondata, track_id): """Parse the JSON string output of echoprint-codegen, and return a structure that fp.ingest() can deal with. @@ -42,9 +51,11 @@ class DeDuper(object): if title: data["track"] = title return data - def dedupe(self): + def dedupe(self, dry_run=True): self._ingest() self._scan_for_dupes() + if not dry_run: + self._cleanup() def _ingest(self): self.codes = {} @@ -58,7 +69,7 @@ class DeDuper(object): mp3s = MP3.query.filter( (MP3.artist == u'bonobo') & (MP3.state == MP3.READY) - & (MP3.echoprint_fp != None)) + & (MP3.has_fingerprint == True)) for mp3 in mp3s: code = self._generate_code_json(mp3.echoprint_fp, mp3.sha1) if not code: @@ -72,13 +83,17 @@ class DeDuper(object): # Now dedupe by going through all our codes over again. log.debug('de-duping fingerprint database...') start = time.time() + dup_count = 0 for sha1, code in self.codes.iteritems(): results = fp.query_fp(code, local=True).results if len(results) < 2: continue - self._dedupe_song(sha1, code, results) + if self._dedupe_song(sha1, code, results): + dup_count += 1 elapsed = time.time() - start log.debug('de-duped fingerprint database in %g seconds' % elapsed) + log.debug('found %d duplicates' % dup_count) + return dup_count def _dedupe_song(self, sha1, code_string, results): """Find fingerprint matches and eventually de-duplicate a song. @@ -135,34 +150,87 @@ class DeDuper(object): # Actually de-duplicate the songs we've found. self._resolve_dupes([x[0] for x in dupes]) + return True + + def _get_song_score(self, mp3): + af = eyeD3.Mp3AudioFile(mp3.path) + + # Get encoding parameters. + bitrate = af.getBitRate()[1] + duration = 30 * (int(af.getPlayTime()) / 30) # round to 30 secs + + # Count metadata tags. + tag = af.getTag() + has_album = not (not tag.getAlbum()) + has_artist = not (not tag.getArtist()) + has_title = not (not tag.getTitle()) + has_genre = not (not tag.getGenre()) + has_year = not (not tag.getYear()) + has_tracknum = (tag.getTrackNum()[0] is not None) + has_images = not (not tag.getImages()) + num_meta = (4 * int(has_images) + + 2 * sum(map(int, (has_album, has_artist, has_title))) + + sum(map(int, (has_genre, has_year, has_tracknum)))) + + return (bitrate, duration, num_meta) def _resolve_dupes(self, hashes): """Perform best duplicate selection and remove dupes from db.""" - log.debug('remove_dupes(%s)' % ','.join(hashes)) + hashes_key = ','.join(sorted(hashes)) + log.debug('remove_dupes(%s)' % hashes_key) + if hashes_key in self.dupes_cache: + return self.dupes_cache[hashes_key] + + def _compare_score(a, b): + a_bitrate, a_duration, a_nmeta = a[0] + b_bitrate, b_duration, b_nmeta = b[0] + res = cmp(a_bitrate, b_bitrate) + if res == 0: + res = cmp(a_duration, b_duration) + if res == 0: + res = cmp(a_nmeta, b_nmeta) + return res # Compute 'score' for each song and sort them. scores = [] - mp3s = MP3.query.filter(MP3.sha1 in hashes) + mp3s = MP3.query.filter(MP3.sha1.in_(hashes)) for mp3 in mp3s: scores.append((self._get_song_score(mp3), mp3.sha1)) - scores.sort(key=lambda x: x[0]) + scores.sort(cmp=_compare_score, reverse=True) best_song = scores[0][1] log.debug('remove_dupes: best song is %s' % best_song) + log.debug('remove_dupes: score dump:') + for score, sha1 in scores: + bitrate, duration, nmeta = score + log.debug(' * (bitrate=%s, duration=%s, nmeta=%s) %s' % ( + bitrate, duration, nmeta, sha1)) # Remove all the other songs. - songs_to_remove = [x for x in hashes if x != best_song] - - - -def run_deduper(db_url): + songs_to_remove = dict((x, best_song) for x in hashes if x != best_song) + log.info('remove_dupes: songs to remove: %s' % str(songs_to_remove)) + self.songs_to_remove.update(songs_to_remove) + self.dupes_cache[hashes_key] = best_song + return best_song + + def _cleanup(self): + for sha1, duplicate_of in self.songs_to_remove.iteritems(): + # Mark the MP3 as duplicate, remove the associated file. + mp3 = MP3.query.get(sha1) + mp3.mark_as_duplicate(duplicate_of) + Session.add(mp3) + Session.commit() + + +def run_deduper(db_url, dry_run): init_db(db_url) dup = DeDuper() - dup.dedupe() + dup.dedupe(dry_run) def main(): parser = optparse.OptionParser() parser.add_option('--db_url') + parser.add_option('--apply', action='store_true') daemonize.add_standard_options(parser) utils.read_config_defaults( parser, os.getenv('DJRANDOM_CONF', '/etc/djrandom.conf')) @@ -173,7 +241,7 @@ def main(): parser.error('Too many arguments') daemonize.daemonize(opts, run_deduper, - (opts.db_url,)) + (opts.db_url, not opts.apply)) if __name__ == '__main__': diff --git a/server/djrandom/fingerprint/fingerprint.py b/server/djrandom/fingerprint/fingerprint.py index 8249395dfe6ff86d47e89b18e4279ef6e9064317..c1fd67c859edb892bf11addfde1ef5828a57474b 100644 --- a/server/djrandom/fingerprint/fingerprint.py +++ b/server/djrandom/fingerprint/fingerprint.py @@ -26,20 +26,17 @@ class Fingerprinter(object): if fp_json: # Remove the square brackets that make fp_json an array. # (Ugly Hack!) - mp3.echoprint_fp = fp_json[2:-2] + mp3.set_fingerprint(fp_json[2:-2]) def compute_fingerprints(self, run_once): """Compute fingerprints of new files.""" while True: - mp3 = MP3.query.filter((MP3.state == MP3.READY) - & (MP3.echoprint_fp == None) - ).limit(1).first() + mp3 = MP3.get_with_no_fingerprint().limit(1).first() if not mp3: if run_once: break Session.remove() - self.idx.commit() - time.sleep(60) + time.sleep(300) continue log.info('fingerprinting %s' % mp3.sha1) try: diff --git a/server/djrandom/frontend/api_views.py b/server/djrandom/frontend/api_views.py index 8cae5d75bf0a8bae791bc125d7dfb041695e057b..7b19685aacda620f0357838abf76f1a4203cbdb1 100644 --- a/server/djrandom/frontend/api_views.py +++ b/server/djrandom/frontend/api_views.py @@ -80,6 +80,7 @@ def get_album_image(artist, album): def save_playlist(): hashes = request.form.get('h', '') uuid = request.form.get('uuid') + title = request.form.get('title') if uuid == 'null': uuid = None playlist = None @@ -89,6 +90,7 @@ def save_playlist(): playlist = Playlist(uuid=uuid, userid=g.userid) playlist.modified_at = datetime.now() playlist.contents = hashes + playlist.title = title Session.add(playlist) Session.commit() return jsonify(uuid=playlist.uuid, status=True) @@ -120,6 +122,23 @@ def playlist_info_json(uuid): return jsonify(playlist.to_dict()) +@app.route('/json/playlist/by_title/<uuid>/<title>') +@require_auth +def playlist_info_by_title_json(uuid, title): + title = urllib.unquote(title) + playlist = Playlist.get_by_title(uuid, title) + if not playlist: + abort(404) + return jsonify(playlist.to_dict()) + + +@app.route('/json/playlist/list/<uuid>') +@require_auth +def playlist_list_by_uuid_json(uuid): + playlists = [x.to_dict() for x in Playlist.get_all_by_user(uuid)] + return jsonify(results=playlists) + + @app.route('/json/search') @require_auth def search_json(): @@ -141,9 +160,10 @@ def search_json(): @require_auth def more_like_these_json(): hashes = request.form.get('h', '').split(',') + n = int(request.form.get('n', 5)) results = [] if hashes: - results = svcs['searcher'].more_like_these(hashes) + results = svcs['searcher'].more_like_these(hashes, n) return jsonify(results=results) @@ -156,6 +176,14 @@ def most_played_json(): return jsonify(results=most_played) +@app.route('/json/never_played', methods=['GET']) +@require_auth +def never_played_json(): + n = int(request.args.get('n', 20)) + never_played = MP3.never_played(n) + return jsonify(results=never_played) + + @app.route('/json/last_uploaded', methods=['GET']) @require_auth def last_uploaded_json(): @@ -169,8 +197,8 @@ def last_uploaded_json(): def markov_json(): n = int(request.form.get('n', 10)) hashes = request.form.get('h', '').split(',') - last_song = hashes[-1] - sequence = svcs['markov'].generate_sequence(last_song, 2, n) + last_songs = hashes[-2:] + sequence = svcs['markov'].generate_sequence(last_songs, 2, n) return jsonify(results=sequence) diff --git a/server/djrandom/frontend/frontend.py b/server/djrandom/frontend/frontend.py index c0ed117cb9330a5c696a9af812b41df488fc06c4..8eca9b368081cb42e99f270dd04f83757f59a09b 100644 --- a/server/djrandom/frontend/frontend.py +++ b/server/djrandom/frontend/frontend.py @@ -19,7 +19,7 @@ log = logging.getLogger(__name__) def run_frontend(port, solr_url, db_url, lastfm_api_key, album_art_dir, - email_sender, markov_data_file): + email_sender, markov_data_file, do_profile): init_db(db_url) svcs['searcher'] = Searcher(solr_url) @@ -32,6 +32,18 @@ def run_frontend(port, solr_url, db_url, lastfm_api_key, album_art_dir, log.error('Could not read Markov data from %s: %s' % ( markov_data_file, str(e))) + # Start the WSGI profiling middleware, if requested. + if do_profile: + from repoze.profile.profiler import AccumulatingProfileMiddleware + app.wsgi_app = AccumulatingProfileMiddleware( + app.wsgi_app, + log_filename='/var/tmp/djrandom-profile.log', + cachegrind_filename='/var/tmp/djrandom-profile.cachegrind', + discard_first_request=True, + flush_at_shutdown=True, + path='/__profile__' + ) + http_server = WSGIServer(('0.0.0.0', port), app) http_server.serve_forever() @@ -46,6 +58,7 @@ def main(): parser.add_option('--album_art_dir', default='/var/tmp/album-image-cache') parser.add_option('--markov_data', default='/var/lib/djrandom/djrandom-markov.dat') + parser.add_option('--profile', action='store_true') daemonize.add_standard_options(parser) utils.read_config_defaults( parser, os.getenv('DJRANDOM_CONF', '/etc/djrandom.conf')) @@ -58,7 +71,7 @@ def main(): daemonize.daemonize(opts, run_frontend, (opts.port, opts.solr_url, opts.db_url, opts.lastfm_api_key, opts.album_art_dir, - opts.email_sender, opts.markov_data), + opts.email_sender, opts.markov_data, opts.profile), support_gevent=True) diff --git a/server/djrandom/frontend/search.py b/server/djrandom/frontend/search.py index 720b631c185a21fcbdc57148edfd4637800eb3e2..9f96651b332fc103de39c6f522eb0ac57aaadba2 100644 --- a/server/djrandom/frontend/search.py +++ b/server/djrandom/frontend/search.py @@ -14,11 +14,11 @@ class Searcher(object): for doc in results.results: yield doc['score'], doc['id'] - def more_like_these(self, hashes): + def more_like_these(self, hashes, n=5): """Return a set of hashes that are related to the ones provided.""" query_str = ' OR '.join('(id:%s)' % x for x in hashes) results = self.solr.select( - q=query_str, fields='score,id', + q=query_str, fields='score,id', rows=n, mlt='true', mlt_fl='text', mlt_mindf=1, mlt_mintf=1) # Parse and uniq-ify the results. diff --git a/server/djrandom/frontend/static/circle.skin/bgr.jpg b/server/djrandom/frontend/static/circle.skin/bgr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3cf5998fae6ae8911f52d3ebb3141c51b8b03c02 Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/bgr.jpg differ diff --git a/server/djrandom/frontend/static/circle.skin/bgr.png b/server/djrandom/frontend/static/circle.skin/bgr.png new file mode 100644 index 0000000000000000000000000000000000000000..61d52e5edc80378aa9f3038ba2f81f11d74d9922 Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/bgr.png differ diff --git a/server/djrandom/frontend/static/circle.skin/buffer.png b/server/djrandom/frontend/static/circle.skin/buffer.png new file mode 100644 index 0000000000000000000000000000000000000000..e36c65ae7ac1cd4c61d30343681fba5b804e4e6d Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/buffer.png differ diff --git a/server/djrandom/frontend/static/circle.skin/circle.player.css b/server/djrandom/frontend/static/circle.skin/circle.player.css new file mode 100644 index 0000000000000000000000000000000000000000..d8be510925a53d12616e4c5173adc892fa7d11d2 --- /dev/null +++ b/server/djrandom/frontend/static/circle.skin/circle.player.css @@ -0,0 +1,132 @@ +/* + * Project: CirclePlayer + * http://www.jplayer.org + * + * Copyright (c) 2011 Happyworm Ltd + * + * Author: Silvia Benvenuti + * Edited by: Mark J Panaghiston + * Date: 6th May 2011 + * Artwork inspired by: http://forrst.com/posts/Untitled-CJz + */ + +.cp-container { + position: relative; + width: 104px; /* 200 - (2 * 48) */ + height: 104px; + background: url("bgr.png") 0 0 no-repeat; + padding: 48px; + -webkit-tap-highlight-color:rgba(0,0,0,0); +} + +.cp-container :focus { + border:none; + outline:0; +} + +.cp-buffer-1, +.cp-buffer-2, +.cp-progress-1, +.cp-progress-2 { + position: absolute; + top: 0; + left: 0; + width: 104px; + height: 104px; + clip:rect(0px,52px,104px,0px); + + -moz-border-radius:52px; + -webkit-border-radius:52px; + border-radius:52px; +} + +.cp-buffer-1, +.cp-buffer-2 { + background: url("buffer.png") 0 0 no-repeat; +} + + +/* FALLBACK for .progress + * (24 steps starting from 1hr filled progress, Decrease second value by 104px for next step) + * (It needs the container selector to work. Or use div) + */ + +.cp-container .cp-fallback { + background: url("progress_sprite.jpg") no-repeat; + background-position: 0 104px; +} + +.cp-progress-1, +.cp-progress-2 { + background: url("progress.png") 0 0 no-repeat; +} + +.cp-buffer-holder, +.cp-progress-holder, +.cp-circle-control { + position:absolute; + width:104px; + height:104px; +} + +.cp-circle-control { + cursor:pointer; +} + +.cp-buffer-holder, +.cp-progress-holder { + clip:rect(0px,104px,104px,52px); + display:none; +} + + +/* This is needed when progress is greater than 50% or for fallback */ + +.cp-buffer-holder.cp-gt50, +.cp-progress-holder.cp-gt50, +.cp-progress.cp-fallback{ + clip:rect(auto, auto, auto, auto); +} + +.cp-controls { + margin:0; + padding: 26px; +} + +.cp-controls li{ + list-style-type:none; + display: block; + + /*IE Fix*/ + position:absolute; +} + +.cp-controls li a{ + position: relative; + display: block; + width:50px; + height:50px; + text-indent:-9999px; + z-index:1; +} + +.cp-controls .cp-play { + background: url("controls.jpg") 0 0 no-repeat; +} + +.cp-controls .cp-play:hover { + background: url("controls.jpg") -50px 0 no-repeat; +} + +.cp-controls .cp-pause { + background: url("controls.jpg") 0 -50px no-repeat; +} + +.cp-controls .cp-pause:hover { + background: url("controls.jpg") -50px -50px no-repeat; +} + +.cp-jplayer { + width: 0; + height: 0; +} diff --git a/server/djrandom/frontend/static/circle.skin/controls.jpg b/server/djrandom/frontend/static/circle.skin/controls.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be3798f97992df3bc740202539bf380ffefbd63a Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/controls.jpg differ diff --git a/server/djrandom/frontend/static/circle.skin/progress.png b/server/djrandom/frontend/static/circle.skin/progress.png new file mode 100644 index 0000000000000000000000000000000000000000..d839da4df26f14a8fc609dd7986a2a418a186cc2 Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/progress.png differ diff --git a/server/djrandom/frontend/static/circle.skin/progress_sprite.jpg b/server/djrandom/frontend/static/circle.skin/progress_sprite.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5e55393db43d764d45831a2af28613b08b42a2d8 Binary files /dev/null and b/server/djrandom/frontend/static/circle.skin/progress_sprite.jpg differ diff --git a/server/djrandom/frontend/static/css/style.css b/server/djrandom/frontend/static/css/style.css index c052cd69c7c8df9a5f9e79b5d63bd43ec0f0ee5d..8ae4ecff686c61f03c2288eb79fddbf2b9373848 100644 --- a/server/djrandom/frontend/static/css/style.css +++ b/server/djrandom/frontend/static/css/style.css @@ -12,13 +12,22 @@ body { } #rbox { - float: right; - width: 500px; + float: right; + width: 500px; + padding-top: 45px; } #lbox { - padding-top: 40px; - margin-right: 501px; + padding-top: 5px; + margin-right: 501px; +} + +#loaderImgDiv { + float: left; + width: 20px; + margin: 0; + margin-left: -24px; + padding-top: 7px; } #tophdr { @@ -29,26 +38,23 @@ body { } #queryField { - width:75%; - font-size: 1.1em; - border:1px solid red; + width:75%; + font-size: 1.1em; + border:1px solid red; } -a#playlistLast25,a#playlistRandom,a#playlistMost { - font-size:0.8em; +.buttonbox a { + font-size:0.8em; + cursor: pointer; } -a#playlistExtendBtn,a#playlistClearBtn,a#playlistStreamBtn { - font-size:0.8em; -} -h1 { - font-size: 66px; - letter-spacing: -4px; - margin-bottom: 10px; - padding-top: 10px; - padding-left: 140px; - background: url(/static/img/pirate_tape_small.png) top left no-repeat; - min-height: 90px; +h1.title { + font-size: 32px; + letter-spacing: -3px; + margin: 0; + padding-left: 50px; + background: url(/static/img/pirate_tape_tiny.png) 0px 4px no-repeat; + min-height: 36px; } #playerintf { @@ -128,6 +134,7 @@ h1 { cursor: pointer; outline: none; text-decoration: none; + margin-left: 5px; } .ctlbox .ctl_remove { @@ -205,10 +212,13 @@ table.summary { background: #ff8; } - - #debug { - margin-top: 30px; - font-size: 75%; - color: #999; + margin-top: 30px; + font-size: 75%; + color: #999; +} + +#debug p { + margin: 0; + padding: 0; } diff --git a/server/djrandom/frontend/static/img/loader.gif b/server/djrandom/frontend/static/img/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..599f03bdf26b8b2c3f1ee20aeb03a8ad65f615ce Binary files /dev/null and b/server/djrandom/frontend/static/img/loader.gif differ diff --git a/server/djrandom/frontend/static/img/pirate_tape_tiny.png b/server/djrandom/frontend/static/img/pirate_tape_tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..0998239842f129f28baf73cf95e8fb5197424456 Binary files /dev/null and b/server/djrandom/frontend/static/img/pirate_tape_tiny.png differ diff --git a/server/djrandom/frontend/static/js/circle.player.js b/server/djrandom/frontend/static/js/circle.player.js new file mode 100644 index 0000000000000000000000000000000000000000..02c408dab66da615f1ea92d397cb74af86b01b2b --- /dev/null +++ b/server/djrandom/frontend/static/js/circle.player.js @@ -0,0 +1,243 @@ +/* + * CirclePlayer for the jPlayer Plugin (jQuery) + * http://www.jplayer.org + * + * Copyright (c) 2009 - 2011 Happyworm Ltd + * Dual licensed under the MIT and GPL licenses. + * - http://www.opensource.org/licenses/mit-license.php + * - http://www.gnu.org/copyleft/gpl.html + * + * Version: 1.0.1 (jPlayer 2.0.9) + * Date: 30th May 2011 + * + * Author: Mark J Panaghiston @thepag + * + * CirclePlayer prototype developed by: + * Mark Boas @maboa + * Silvia Benvenuti @aulentina + * Jussi Kalliokoski @quinirill + * + * Inspired by : + * Neway @imneway http://imneway.net/ http://forrst.com/posts/Untitled-CPt + * and + * Liam McKay @liammckay http://dribbble.com/shots/50882-Purple-Play-Pause + * + * Standing on the shoulders of : + * John Resig @jresig + * Mark Panaghiston @thepag + * Louis-Rémi Babé @Louis_Remi + */ + + +var CirclePlayer = function(jPlayerSelector, media, options) { + var self = this, + + defaults = { + // solution: "flash, html", // For testing Flash with CSS3 + supplied: "m4a, oga", + // Android 2.3 corrupts media element if preload:"none" is used. + // preload: "none", // No point preloading metadata since no times are displayed. It helps keep the buffer state correct too. + cssSelectorAncestor: "#cp_container_1", + cssSelector: { + play: ".cp-play", + pause: ".cp-pause" + } + }, + + cssSelector = { + bufferHolder: ".cp-buffer-holder", + buffer1: ".cp-buffer-1", + buffer2: ".cp-buffer-2", + progressHolder: ".cp-progress-holder", + progress1: ".cp-progress-1", + progress2: ".cp-progress-2", + circleControl: ".cp-circle-control" + }; + + this.cssClass = { + gt50: "cp-gt50", + fallback: "cp-fallback" + }; + + this.spritePitch = 104; + this.spriteRatio = 0.24; // Number of steps / 100 + + this.player = $(jPlayerSelector); + this.media = $.extend({}, media); + this.options = $.extend(true, {}, defaults, options); // Deep copy + + this.cssTransforms = Modernizr.csstransforms; + this.audio = {}; + this.dragging = false; // Indicates if the progressbar is being 'dragged'. + + this.eventNamespace = ".CirclePlayer"; // So the events can easily be removed in destroy. + + this.jq = {}; + $.each(cssSelector, function(entity, cssSel) { + self.jq[entity] = $(self.options.cssSelectorAncestor + " " + cssSel); + }); + + this._initSolution(); + this._initPlayer(); +}; + +CirclePlayer.prototype = { + _createHtml: function() { + }, + _initPlayer: function() { + var self = this; + this.player.jPlayer(this.options); + + this.player.bind($.jPlayer.event.ready + this.eventNamespace, function(event) { + if(event.jPlayer.html.used && event.jPlayer.html.audio.available) { + self.audio = $(this).data("jPlayer").htmlElement.audio; + } + $(this).jPlayer("setMedia", self.media); + self._initCircleControl(); + }); + + this.player.bind($.jPlayer.event.play + this.eventNamespace, function(event) { + $(this).jPlayer("pauseOthers"); + }); + + // This event fired as play time increments + this.player.bind($.jPlayer.event.timeupdate + this.eventNamespace, function(event) { + if (!self.dragging) { + self._timeupdate(event.jPlayer.status.currentPercentAbsolute); + } + }); + + // This event fired as buffered time increments + this.player.bind($.jPlayer.event.progress + this.eventNamespace, function(event) { + var percent = 0; + if((typeof self.audio.buffered === "object") && (self.audio.buffered.length > 0)) { + if(self.audio.duration > 0) { + var bufferTime = 0; + for(var i = 0; i < self.audio.buffered.length; i++) { + bufferTime += self.audio.buffered.end(i) - self.audio.buffered.start(i); + // console.log(i + " | start = " + self.audio.buffered.start(i) + " | end = " + self.audio.buffered.end(i) + " | bufferTime = " + bufferTime + " | duration = " + self.audio.duration); + } + percent = 100 * bufferTime / self.audio.duration; + } // else the Metadata has not been read yet. + // console.log("percent = " + percent); + } else { // Fallback if buffered not supported + // percent = event.jPlayer.status.seekPercent; + percent = 0; // Cleans up the inital conditions on all browsers, since seekPercent defaults to 100 when object is undefined. + } + self._progress(percent); // Problem here at initial condition. Due to the Opera clause above of buffered.length > 0 above... Removing it means Opera's white buffer ring never shows like with polyfill. + // Firefox 4 does not always give the final progress event when buffered = 100% + }); + + this.player.bind($.jPlayer.event.ended + this.eventNamespace, function(event) { + self._resetSolution(); + }); + }, + _initSolution: function() { + if (this.cssTransforms) { + this.jq.progressHolder.show(); + this.jq.bufferHolder.show(); + } + else { + this.jq.progressHolder.addClass(this.cssClass.gt50).show(); + this.jq.progress1.addClass(this.cssClass.fallback); + this.jq.progress2.hide(); + this.jq.bufferHolder.hide(); + } + this._resetSolution(); + }, + _resetSolution: function() { + if (this.cssTransforms) { + this.jq.progressHolder.removeClass(this.cssClass.gt50); + this.jq.progress1.css({'transform': 'rotate(0deg)'}); + this.jq.progress2.css({'transform': 'rotate(0deg)'}).hide(); + } + else { + this.jq.progress1.css('background-position', '0 ' + this.spritePitch + 'px'); + } + }, + _initCircleControl: function() { + var self = this; + this.jq.circleControl.grab({ + onstart: function(){ + self.dragging = true; + }, onmove: function(event){ + var pc = self._getArcPercent(event.position.x, event.position.y); + self.player.jPlayer("playHead", pc).jPlayer("play"); + self._timeupdate(pc); + }, onfinish: function(event){ + self.dragging = false; + var pc = self._getArcPercent(event.position.x, event.position.y); + self.player.jPlayer("playHead", pc).jPlayer("play"); + } + }); + }, + _timeupdate: function(percent) { + var degs = percent * 3.6+"deg"; + + var spriteOffset = (Math.floor((Math.round(percent))*this.spriteRatio)-1)*-this.spritePitch; + + if (percent <= 50) { + if (this.cssTransforms) { + this.jq.progressHolder.removeClass(this.cssClass.gt50); + this.jq.progress1.css({'transform': 'rotate(' + degs + ')'}); + this.jq.progress2.hide(); + } else { // fall back + this.jq.progress1.css('background-position', '0 '+spriteOffset+'px'); + } + } else if (percent <= 100) { + if (this.cssTransforms) { + this.jq.progressHolder.addClass(this.cssClass.gt50); + this.jq.progress1.css({'transform': 'rotate(180deg)'}); + this.jq.progress2.css({'transform': 'rotate(' + degs + ')'}); + this.jq.progress2.show(); + } else { // fall back + this.jq.progress1.css('background-position', '0 '+spriteOffset+'px'); + } + } + }, + _progress: function(percent) { + var degs = percent * 3.6+"deg"; + + if (this.cssTransforms) { + if (percent <= 50) { + this.jq.bufferHolder.removeClass(this.cssClass.gt50); + this.jq.buffer1.css({'transform': 'rotate(' + degs + ')'}); + this.jq.buffer2.hide(); + } else if (percent <= 100) { + this.jq.bufferHolder.addClass(this.cssClass.gt50); + this.jq.buffer1.css({'transform': 'rotate(180deg)'}); + this.jq.buffer2.show(); + this.jq.buffer2.css({'transform': 'rotate(' + degs + ')'}); + } + } + }, + _getArcPercent: function(pageX, pageY) { + var offset = this.jq.circleControl.offset(), + x = pageX - offset.left - this.jq.circleControl.width()/2, + y = pageY - offset.top - this.jq.circleControl.height()/2, + theta = Math.atan2(y,x); + + if (theta > -1 * Math.PI && theta < -0.5 * Math.PI) { + theta = 2 * Math.PI + theta; + } + + // theta is now value between -0.5PI and 1.5PI + // ready to be normalized and applied + + return (theta + Math.PI / 2) / 2 * Math.PI * 10; + }, + setMedia: function(media) { + this.media = $.extend({}, media); + this.player.jPlayer("setMedia", this.media); + }, + play: function(time) { + this.player.jPlayer("play", time); + }, + pause: function(time) { + this.player.jPlayer("pause", time); + }, + destroy: function() { + this.player.unbind(this.eventNamespace); + this.player.jPlayer("destroy"); + } +}; diff --git a/server/djrandom/frontend/static/js/djr.min.js b/server/djrandom/frontend/static/js/djr.min.js index 81fe21552500ec84ba759e3e1175f4d940dbe9cf..3353fb35fa092fc9cf97d67b8632ebced2c871d8 100644 --- a/server/djrandom/frontend/static/js/djr.min.js +++ b/server/djrandom/frontend/static/js/djr.min.js @@ -1,22 +1,28 @@ -djr={};var CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");djr.generateRandomId=function(){var a=[],b=CHARS,c,d=b.length;for(c=0;c<40;c++)a[c]=b[0|Math.random()*d];return a.join("")};djr.Backend=function(){};djr.Backend.prototype.search=function(a,b,c){$.ajax({url:"/json/search",data:{q:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.moreLikeThese=function(a,b,c){$.ajax({url:"/json/morelikethese",data:{h:a.join(",")},dataType:"json",type:"POST",context:c||this,success:function(a){b(a.results)}})}; -djr.Backend.prototype.getPlaylist=function(a,b,c){$.ajax({url:"/json/playlist/get/"+a,dataType:"json",type:"GET",context:c||this,success:function(c){b(new djr.PlaylistChunk(c.songs,a))}})};djr.Backend.prototype.savePlaylist=function(a,b){$.ajax({url:"/json/playlist/save",data:{uuid:a,h:b.join(",")},type:"POST"})};djr.Backend.prototype.streamPlaylist=function(a,b,c,d){$.ajax({url:"/json/playlist/stream",data:{uuid:a,stream:b?"y":"n"},dataType:"json",type:"POST",context:d||this,success:function(a){c(a)}})}; -djr.Backend.prototype.getHtmlForSongs=function(a,b,c){$.ajax({url:"/fragment/songs",data:{h:a.join(",")},dataType:"html",type:"POST",context:c||this,success:b})};djr.Backend.prototype.nowPlaying=function(a,b){$.ajax({url:"/json/playing",data:{cur:a,prev:b.join(",")},type:"POST"})};djr.Backend.prototype.mostPlayedPlaylist=function(a,b,c){$.ajax({url:"/json/most_played",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})}; -djr.Backend.prototype.lastPlaylist=function(a,b,c){$.ajax({url:"/json/last_uploaded",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.markovPlaylist=function(a,b,c,d){$.ajax({url:"/json/markov",data:{h:b.join(","),n:a},dataType:"json",type:"POST",context:d||this,success:function(a){c(a.results)}})};djr.Backend.prototype.randomPlaylist=function(a,b,c){$.ajax({url:"/json/random",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.PlaylistChunk=function(a,b){this.songs=a||[];this.title=b};djr.PlaylistChunk.prototype.hasSong=function(a){return this.songs.indexOf(a)>=0};djr.PlaylistChunk.prototype.removeSong=function(a){this.songs=$.grep(this.songs,function(b){return b!=a})};djr.controlButtons=function(a){return'<div class="ctlbox" style="display:none"><a id="'+a+'_remove" class="ctl_btn ctl_remove"> </a></div>'}; -djr.PlaylistChunk.prototype.wrapHtml=function(a,b){return'<div id="chunk_'+a+'" class="chunk"><div class="chunk_ctl_wrap">'+djr.controlButtons("chunk_ctl_"+a)+'<a class="chunk_title">'+this.title+'</a></div><div class="chunk_inner">'+(b||"")+"</div></div>"};djr.Playlist=function(a){this.uuid=a||djr.generateRandomId();this.chunks=[];this.song_map={};this.chunk_map={};this.next_chunk_id=0}; +djr={loading:function(a){var b=$("#loaderImg");a?b.show():b.hide()}};var CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");djr.generateRandomId=function(){var a=[],b=CHARS,c,d=b.length;for(c=0;c<40;c++)a[c]=b[0|Math.random()*d];return a.join("")};djr.Backend=function(){};var wrap_with_loader=function(a){djr.loading(!0);var b=a.success;a.success=function(a,d,e){b(a,d,e);djr.loading(!1)};a.error=function(){djr.loading(!1)};return $.ajax(a)};djr.Backend.prototype.search=function(a,b,c){wrap_with_loader({url:"/json/search",data:{q:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results);djr.loading(!1)}})}; +djr.Backend.prototype.moreLikeThese=function(a,b,c){wrap_with_loader({url:"/json/morelikethese",data:{h:a.join(",")},dataType:"json",type:"POST",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.getPlList=function(a,b,c){$.ajax({url:"/json/playlist/list/"+a,dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.getPlaylist=function(a,b,c){$.ajax({url:"/json/playlist/get/"+a,dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})}; +djr.Backend.prototype.savePlaylist=function(a,b,c){$.ajax({url:"/json/playlist/save",data:{uuid:a,title:b,h:c.join(",")},type:"POST"})};djr.Backend.prototype.streamPlaylist=function(a,b,c,d){$.ajax({url:"/json/playlist/stream",data:{uuid:a,stream:b?"y":"n"},dataType:"json",type:"POST",context:d||this,success:function(a){c(a)}})};djr.Backend.prototype.getHtmlForSongs=function(a,b,c){wrap_with_loader({url:"/fragment/songs",data:{h:a.join(",")},dataType:"html",type:"POST",context:c||this,success:b})}; +djr.Backend.prototype.nowPlaying=function(a,b){$.ajax({url:"/json/playing",data:{cur:a,prev:b.join(",")},type:"POST"})};djr.Backend.prototype.neverPlayedPlaylist=function(a,b,c){wrap_with_loader({url:"/json/never_played",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.mostPlayedPlaylist=function(a,b,c){wrap_with_loader({url:"/json/most_played",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})}; +djr.Backend.prototype.lastPlaylist=function(a,b,c){wrap_with_loader({url:"/json/last_uploaded",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.markovPlaylist=function(a,b,c,d){wrap_with_loader({url:"/json/markov",data:{h:b.join(","),n:a},dataType:"json",type:"POST",context:d||this,success:function(a){c(a.results)}})}; +djr.Backend.prototype.randomPlaylist=function(a,b,c){wrap_with_loader({url:"/json/random",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.PlaylistChunk=function(a,b){this.songs=a||[];this.title=b};djr.PlaylistChunk.prototype.hasSong=function(a){return this.songs.indexOf(a)>=0};djr.PlaylistChunk.prototype.removeSong=function(a){this.songs=$.grep(this.songs,function(b){return b!=a})};djr.controlButtons=function(a){return'<div class="ctlbox" style="display:none"><a id="'+a+'_remove" class="ctl_btn ctl_remove"> </a></div>'}; +djr.PlaylistChunk.prototype.wrapHtml=function(a,b){return'<div id="chunk_'+a+'" class="chunk"><div class="chunk_ctl_wrap">'+djr.controlButtons("chunk_ctl_"+a)+'<a class="chunk_title">'+this.title+'</a></div><div class="chunk_inner">'+(b||"")+"</div></div>"};djr.Playlist=function(a){this.uuid=a||djr.generateRandomId();this.chunks=[];this.song_map={};this.chunk_map={};this.title="";this.next_chunk_id=0}; djr.Playlist.prototype.allSongs=function(){var a=[],b,c;for(b=0;b<this.chunks.length;b++)for(c=0;c<this.chunk_map[this.chunks[b]].songs.length;c++)a.push(this.chunk_map[this.chunks[b]].songs[c]);return a};djr.Playlist.prototype.createUniqueChunk=function(a,b){var c=[],d;for(d=0;d<a.length;d++)this.song_map[a[d]]==null&&c.push(a[d]);return c.length>0?new djr.PlaylistChunk(c,b):null}; djr.Playlist.prototype.addChunk=function(a){djr.debug("adding chunk to playlist "+this.uuid);var b,c=this.next_chunk_id++;for(b=0;b<a.songs.length;b++)this.song_map[a.songs[b]]=c;this.chunk_map[c]=a;this.chunks.push(c);return c};djr.Playlist.prototype.getChunkSongs=function(a){return this.chunk_map[a].songs}; djr.Playlist.prototype.removeChunk=function(a){djr.debug("removing chunk "+a);var b=this.chunk_map[a].songs,c;for(c=0;c<b.length;c++)delete this.song_map[b[c]];delete this.chunk_map[a];this.chunks=$.grep(this.chunks,function(b){return b!=a})};djr.Playlist.prototype.removeSong=function(a){djr.debug("removing song "+a);var b=this.song_map[a];this.chunk_map[b].removeSong(a);delete this.song_map[a];return this.chunk_map[b].songs.length==0?(this.removeChunk(b),b):-1}; -djr.Playlist.prototype.merge=function(){var a=[],b;for(b=0;b<this.chunks.length;b++)a.push(this.chunk_map[this.chunks[b]].title);a=a.join(" + ");b=new djr.Playlist;b.uuid=this.uuid;b.addChunk(new djr.PlaylistChunk(this.allSongs(),a));return b};djr.Playlist.prototype.getNextSong=function(a){var b=this.song_map[a],c=this.chunk_map[b].songs,b=this.chunks.indexOf(b),a=c.indexOf(a)+1;a>=c.length&&(a=0,b++,b>=this.chunks.length&&(b=0));return this.chunk_map[this.chunks[b]].songs[a]};djr.Player=function(a,b){this.backend=a;this.player=$(b);this.playlist=new djr.Playlist;this.old_songs=[];this.cur_song=null;this.player.jPlayer({swfPath:"/static/js",ready:function(){djr.debug("player ready")}});this.player.bind($.jPlayer.event.ended+".djr",function(){djr.state.player.nextSong()});this.player.bind($.jPlayer.event.error+".djr",function(){djr.state.player.reportError()})};djr.Player.prototype.hideAllChunks=function(){$(".chunk .chunk_inner").hide()}; -djr.Player.prototype.removeChunk=function(a){this.playlist.removeChunk(a);this.savePlaylist();$("#chunk_"+a).remove()};djr.Player.prototype.removeSong=function(a){$("#song_"+a).remove();a=this.playlist.removeSong(a);this.savePlaylist();a>0&&$("#chunk_"+a).remove()};djr.Player.prototype.savePlaylist=function(){this.backend.savePlaylist(this.playlist.uuid,this.playlist.allSongs())};djr.Player.prototype.clearPlaylist=function(){this.playlist=new djr.Playlist;$("#playlistDiv").empty()}; -djr.Player.prototype.mergePlaylistChunks=function(){this.playlist=this.playlist.merge();var a=[];$(".chunk .chunk_inner").each(function(){a.push($(this).html())});$("#playlistDiv").empty();var b=this.playlist.chunks[0];this.setChunkHtml(this.playlist.chunk_map[b],b,a.join(""))};djr.Player.prototype.search=function(a){var b=this;this.backend.search(a,function(c){var d=[];$.each(c,function(a,b){d.push(b.sha1)});d.length==0?djr.debug("No results found."):b.createChunk(d,a)})}; -djr.Player.prototype.lastPlaylist=function(a){var b=this,c="Last "+a+" Songs Uploaded";this.backend.lastPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};djr.Player.prototype.randomPlaylist=function(a){var b=this,c=""+a+" Random Songs ";this.backend.randomPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})}; +djr.Playlist.prototype.merge=function(){var a=[],b;for(b=0;b<this.chunks.length;b++)a.push(this.chunk_map[this.chunks[b]].title);a=a.join(" + ");b=new djr.Playlist;b.uuid=this.uuid;b.addChunk(new djr.PlaylistChunk(this.allSongs(),a));return b};djr.Playlist.prototype.getNextSong=function(a){var b=this.song_map[a],c=this.chunk_map[b].songs,b=this.chunks.indexOf(b),a=c.indexOf(a)+1;a>=c.length&&(a=0,b++,b>=this.chunks.length&&(b=0));return this.chunk_map[this.chunks[b]].songs[a]}; +djr.Playlist.prototype.isLastSong=function(a){var b=this.song_map[a],c=this.chunk_map[b].songs,b=this.chunks.indexOf(b),a=c.indexOf(a),c=c.length-1;return b==this.chunks.length-1&&a==c?!0:!1};djr.Player=function(a,b){this.backend=a;this.playlist=new djr.Playlist;this.old_songs=[];this.cur_song=null;this.auto_extend=!0;this.circleplayer=new CirclePlayer(b,{},{supplied:"mp3",swfPath:"/static/js",cssSelectorAncestor:"#cp_container",error:function(a){djr.state.player.reportError(a)}});this.player=this.circleplayer.player;this.player.bind($.jPlayer.event.ended+".djr",function(){djr.state.player.nextSong()})};djr.Player.prototype.hideAllChunks=function(){$(".chunk .chunk_inner").hide()}; +djr.Player.prototype.removeChunk=function(a){this.playlist.removeChunk(a);this.savePlaylist();$("#chunk_"+a).remove()};djr.Player.prototype.removeSong=function(a){$("#song_"+a).remove();a=this.playlist.removeSong(a);this.savePlaylist();a>0&&$("#chunk_"+a).remove()};djr.Player.prototype.savePlaylist=function(){this.backend.savePlaylist(this.playlist.uuid,"",this.playlist.allSongs())};djr.Player.prototype.saveNewPlaylist=function(a){this.backend.savePlaylist(djr.generateRandomId(),a,this.playlist.allSongs())}; +djr.Player.prototype.clearPlaylist=function(){this.playlist=new djr.Playlist;$("#playlistDiv").empty()};djr.Player.prototype.mergePlaylistChunks=function(){this.playlist=this.playlist.merge();var a=[];$(".chunk .chunk_inner").each(function(){a.push($(this).html())});$("#playlistDiv").empty();var b=this.playlist.chunks[0];this.setChunkHtml(this.playlist.chunk_map[b],b,a.join(""))}; +djr.Player.prototype.search=function(a){var b=this;this.backend.search(a,function(c){var d=[];$.each(c,function(a,b){d.push(b.sha1)});d.length==0?djr.debug("No results found."):b.createChunk(d,a)})};djr.Player.prototype.lastPlaylist=function(a){var b=this,c="Last "+a+" Songs Uploaded";this.backend.lastPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})}; +djr.Player.prototype.randomPlaylist=function(a){var b=this,c=""+a+" Random Songs ";this.backend.randomPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};djr.Player.prototype.neverPlayedPlaylist=function(a){var b=this,c=""+a+" Never Played Songs";this.backend.neverPlayedPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})}; djr.Player.prototype.mostPlayedPlaylist=function(a){var b=this,c=""+a+" Most Played Songs";this.backend.mostPlayedPlaylist(a,function(a){var e=[];$.each(a,function(a,b){e.push(b.sha1)});e.length==0?djr.debug("No results found."):b.createChunk(e,c)})};djr.Player.prototype.extendCurrentPlaylist=function(){var a=this;this.backend.moreLikeThese(this.playlist.allSongs(),function(b){a.createChunk(b,"suggestions")})}; -djr.Player.prototype.createChunk=function(a,b){var c=this.playlist.createUniqueChunk(a,b);if(c){this.playlist.chunks.length>1&&this.mergePlaylistChunks();var d=this.playlist.addChunk(c);this.savePlaylist();this.backend.getHtmlForSongs(a,function(a){this.hideAllChunks();this.setChunkHtml(c,d,a)},this)}else djr.debug("All the results are already in the playlist")}; +djr.Player.prototype.autoExtendCurrentPlaylist=function(a){var b=this;this.backend.markovPlaylist(10,this.playlist.allSongs(),function(c){b.createChunk(c,"more...");a()})};djr.Player.prototype.createChunk=function(a,b){var c=this.playlist.createUniqueChunk(a,b);if(c){this.playlist.chunks.length>1&&this.mergePlaylistChunks();var d=this.playlist.addChunk(c);this.savePlaylist();var e=this;this.backend.getHtmlForSongs(a,function(a){e.hideAllChunks();e.setChunkHtml(c,d,a)})}else djr.debug("All the results are already in the playlist")}; djr.Player.prototype.setChunkHtml=function(a,b,c){a=a.wrapHtml(b,c);$("#playlistDiv").append(a);var d=this,e=$("#chunk_"+b);e.find(".song_a").click(function(){d.play($(this).attr("id").substr(5))});e.find(".album_a").click(function(){d.search('(album:"'+$(this).text()+'")')});e.find(".chunk_title").click(function(){e.find(".chunk_inner").toggle()});e.hover(function(){$(this).find(".chunk_ctl_wrap .ctlbox").show()},function(){$(this).find(".chunk_ctl_wrap .ctlbox").hide()});e.find(".chunk_ctl_wrap .ctlbox .ctl_remove").click(function(){djr.debug("removing chunk "+ b);d.removeChunk(b)});e.find(".chunk_inner .song").hover(function(){$(this).find(".ctlbox").show()},function(){$(this).find(".ctlbox").hide()});e.find(".chunk_inner .ctlbox .ctl_remove").click(function(){var a=$(this).parent().parent().attr("id").substr(5);d.removeSong(a)})}; -djr.Player.prototype.play=function(a){djr.debug("play "+a);this.cur_song&&(this.old_songs.push(this.cur_song),this.old_songs.length>5&&this.old_songs.shift());this.cur_song=a;$(".song").removeClass("playing");$("#song_"+a).addClass("playing");this.player.jPlayer("setMedia",{mp3:"/dl/"+a}).jPlayer("play");var b=$("#song_"+a+" .artist").text(),c=$("#song_"+a+" .album").text();$("#jp_playlist_1").html($("#song_"+a+" .title").text()+"<br>"+b+"<br><small>"+c+"</small>");b="/album_image/"+escape(b)+"/"+ -escape(c);$("#albumart_fs").attr("src",b);$("#albumart_fs").fullBg();$("#albumart_fs").show();this.backend.nowPlaying(a,this.old_songs)};djr.Player.prototype.nextSong=function(){this.play(this.playlist.getNextSong(this.cur_song))};djr.Player.prototype.streamCurrentPlaylist=function(){};djr.state={backend:null,player:null}; -djr.init=function(){djr.state.backend=new djr.Backend;djr.state.player=new djr.Player(djr.state.backend,"#djr_player");$("#playlistClearBtn").click(function(){djr.state.player.clearPlaylist()});$("#playlistStreamBtn").click(function(){djr.state.player.streamCurrentPlaylist()});$("#playlistExtendBtn").click(function(){djr.state.player.extendCurrentPlaylist()});$("#playlistLast25").click(function(){djr.state.player.lastPlaylist(25)});$("#playlistRandom").click(function(){djr.state.player.randomPlaylist(25)}); -$("#playlistMost").click(function(){djr.state.player.mostPlayedPlaylist(25)});$("#wikibtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .artist").text();$("#wikipedia").is(":visible")==!1?a!=""&&(a=a.split(" ").join("+"),$("#wikipedia").show("slow"),$("#wikipedia").attr("src","http://en.m.wikipedia.org/w/index.php?search="+a)):$("#wikipedia").hide("slow")});$("#lastfmbtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .title").text(),b=$("#song_"+djr.state.player.cur_song+ -" .artist").text();$("#lastfm").is(":visible")==!1?a!=""&&(a=a.split(" ").join("+"),b=b.split(" ").join("+"),$("#lastfm").show("slow"),$("#lastfm").attr("src","http://m.last.fm/search?q="+a+b)):$("#lastfm").hide("slow")});$("#lyricsbtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .title").text();$("#lyrics").is(":visible")==!1?a!=""&&(a=a.split(" ").join("+"),$("#lyrics").show("slow"),$("#lyrics").attr("src","http://lyrics.wikia.com/index.php?search="+a+"&fulltext=0")):$("#lyrics").hide("slow")}); -$("#albumart_fs").load(function(){$(this).fullBg();$(this).show()})};djr.player=function(){return djr.state.player};djr.debug=function(a){$("#debug").append(a+"<br>")}; +djr.Player.prototype.play=function(a){djr.debug("play "+a);this.cur_song&&(this.old_songs.push(this.cur_song),this.old_songs.length>5&&this.old_songs.shift());this.cur_song=a;$(".song").removeClass("playing");$("#song_"+a).addClass("playing");this.player.jPlayer("setMedia",{mp3:"/dl/"+a}).jPlayer("play");var b=$("#song_"+a+" .artist").text(),c=$("#song_"+a+" .album").text();$("#songInfoDiv").html($("#song_"+a+" .title").text()+"<br>"+b+"<br><small>"+c+"</small>");b="/album_image/"+escape(b)+"/"+escape(c); +$("#albumart_fs").attr("src",b);$("#albumart_fs").fullBg();$("#albumart_fs").show();this.backend.nowPlaying(a,this.old_songs)};djr.Player.prototype.nextSong=function(){if(this.auto_extend&&this.playlist.isLastSong(this.cur_song)){var a=this;this.autoExtendCurrentPlaylist(function(){a.nextSong()})}else this.play(this.playlist.getNextSong(this.cur_song))};djr.Player.prototype.streamCurrentPlaylist=function(){};djr.state={backend:null,player:null}; +djr.init=function(){djr.state.backend=new djr.Backend;djr.state.player=new djr.Player(djr.state.backend,"#djr_player");$("#playlistClearBtn").click(function(){djr.state.player.clearPlaylist()});$("#playlistStreamBtn").click(function(){djr.state.player.streamCurrentPlaylist()});$("#playlistExtendBtn").click(function(){djr.state.player.extendCurrentPlaylist()});$("#playlistLast5").click(function(){djr.state.player.lastPlaylist(5)});$("#playlistLast25").click(function(){djr.state.player.lastPlaylist(25)}); +$("#playlistRandom5").click(function(){djr.state.player.randomPlaylist(5)});$("#playlistRandom25").click(function(){djr.state.player.randomPlaylist(25)});$("#playlistMost5").click(function(){djr.state.player.mostPlayedPlaylist(5)});$("#playlistMost25").click(function(){djr.state.player.mostPlayedPlaylist(25)});$("#playlistNever5").click(function(){djr.state.player.neverPlayedPlaylist(5)});$("#playlistNever25").click(function(){djr.state.player.neverPlayedPlaylist(25)});$("#playlistSave").click(function(){$("#saveForm").is(":visible")== +!1?($("#savetext").val(djr.playlist.title),$("#saveForm").show("slow")):($("#savetext").val(""),$("#saveForm").hide("slow"))});$("#playlistSaveBtn").click(function(){$("#savetext").val()!=""&&djr.player.saveNewPlaylist($("#savetext").val())});$("#wikibtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .artist").text();$("#wikipedia").is(":visible")==!1?a!=""&&(a=a.split(" ").join("+"),$("#wikipedia").show("slow"),$("#wikipedia").attr("src","http://en.m.wikipedia.org/w/index.php?search="+ +a)):$("#wikipedia").hide("slow")});$("#lastfmbtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .title").text(),b=$("#song_"+djr.state.player.cur_song+" .artist").text();$("#lastfm").is(":visible")==!1?a!=""&&(a=a.split(" ").join("+"),b=b.split(" ").join("+"),$("#lastfm").show("slow"),$("#lastfm").attr("src","http://m.last.fm/search?q="+a+b)):$("#lastfm").hide("slow")});$("#lyricsbtn").click(function(){var a=$("#song_"+djr.state.player.cur_song+" .title").text();$("#lyrics").is(":visible")== +!1?a!=""&&(a=a.split(" ").join("+"),$("#lyrics").show("slow"),$("#lyrics").attr("src","http://lyrics.wikia.com/index.php?search="+a+"&fulltext=0")):$("#lyrics").hide("slow")});$("#pllistbtn").click(function(){$("#pllist").is(":visible")==!1?($("#pllist").show("slow"),this.backend.getPlList(uid,function(a){if(a.length==0)djr.debug("No results found.");else{for(var b="<ul>",c;c<a.lenght;c++)b+="<il><a id='"+a[c].uuid+"' onclick='djm.player.showPlaylist(\""+a[c].uuid+"\"' >"+a[c].title+"</a></il>";b+= +"</ul>";$("#pllist").html(b)}})):$("#pllist").hide("slow")});$("#albumart_fs").load(function(){$(this).fullBg();$(this).show()})};djr.player=function(){return djr.state.player};djr.debug=function(a){$("#debug").append(a+"<br>")};djr.Player.prototype.reportError=function(a){console.log(a.jPlayer.error);console.log(a.jPlayer.error.type);switch(a.jPlayer.error.type){case $.jPlayer.error.URL:djr.debug("Error downloading song, skipping...");this.nextSong();break;case $.jPlayer.error.NO_SOLUTION:djr.debug("Error: unexpected error!")}}; diff --git a/server/djrandom/frontend/static/js/djr/Makefile b/server/djrandom/frontend/static/js/djr/Makefile index 094d7250a7d6013277f8d83f5bb85f7c03be0ba7..7d483dbc1cde3db997329792888b3caf693ae2ba 100644 --- a/server/djrandom/frontend/static/js/djr/Makefile +++ b/server/djrandom/frontend/static/js/djr/Makefile @@ -1,5 +1,5 @@ -JSCOMPILER_JAR = /usr/bin/compiler.jar +JSCOMPILER_JAR = /usr/lib/jscompiler/compiler.jar JSCOMPILER = java -jar $(JSCOMPILER_JAR) SOURCES = \ diff --git a/server/djrandom/frontend/static/js/djr/backend.js b/server/djrandom/frontend/static/js/djr/backend.js index 1921dc102228286e3ec43b720160cb30fdcc1854..21caba4d3d937e613380bd67933018cfae1d4c2d 100644 --- a/server/djrandom/frontend/static/js/djr/backend.js +++ b/server/djrandom/frontend/static/js/djr/backend.js @@ -18,6 +18,22 @@ djr.Backend = function() { // No state yet. }; +// Wrap an AJAX call with some UI feedback. +var wrap_with_loader = function(ajaxData) { + djr.loading(true); + var old_success_handler = ajaxData.success; + ajaxData.success = function(data, status, jqxhr) { + old_success_handler(data, status, jqxhr); + djr.loading(false); + }; + var old_error_handler = ajaxData.error; + ajaxData.error = function(jqXHR, textStatus, errorThrown) { + djr.loading(false); + }; + return $.ajax(ajaxData); +}; + + /** * Search. * @@ -29,15 +45,17 @@ djr.Backend = function() { * @param {Object} ctx Callback context. */ djr.Backend.prototype.search = function(query, callback, ctx) { - $.ajax({url: '/json/search', - data: {'q': query}, - dataType: 'json', - type: 'GET', - context: ctx || this, - success: function(data, status, jqxhr) { - callback(data.results); - } - }); + wrap_with_loader( + {url: '/json/search', + data: {'q': query}, + dataType: 'json', + type: 'GET', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + djr.loading(false); + } + }); }; /** @@ -47,11 +65,54 @@ djr.Backend.prototype.search = function(query, callback, ctx) { * @param {function} callback Callback function. * @param {Object} ctx Callback context. */ -djr.Backend.prototype.moreLikeThese = function(uuids, callback, ctx) { - $.ajax({url: '/json/morelikethese', - data: {'h': uuids.join(',')}, +djr.Backend.prototype.moreLikeThese = function(n, uuids, callback, ctx) { + wrap_with_loader( + {url: '/json/morelikethese', + data: {'h': uuids.join(','), 'n': n}, + dataType: 'json', + type: 'POST', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); +}; + +/** + * Autocomplete. + * + * Autocompletion callback suitable for use with the jQuery UI + * autocomplete plugin. + * + * @param {Object} request request object (contains term). + * @param {function} callback Callback function. + */ +djr.Backend.prototype.autocomplete = function(request, callback) { + $.ajax({url: '/autocomplete', + data: {'term': request.term}, dataType: 'json', - type: 'POST', + success: function(data) { + callback(data.results); + }, + error: function(xhr, status, errorThrown) { + callback(); + }}); +}; + +/** + * Get the playlist list for a specified user. + * + * It will invoke callback(songs) where 'playlist' is an array of uuid + * song hashes. + * + * @param {string} uuid Playlist identifier. + * @param {function} callback Callback function. + * @param {Object} ctx Callback context. + */ +djr.Backend.prototype.getPlList = function(uid, callback, ctx) { + $.ajax({url: '/json/playlist/list/' + uid, + dataType: 'json', + type: 'GET', context: ctx || this, success: function(data, status, jqxhr) { callback(data.results); @@ -75,7 +136,7 @@ djr.Backend.prototype.getPlaylist = function(uuid, callback, ctx) { type: 'GET', context: ctx || this, success: function(data, status, jqxhr) { - callback(new djr.PlaylistChunk(data.songs, uuid)); + callback(data.results); } }); }; @@ -89,9 +150,10 @@ djr.Backend.prototype.getPlaylist = function(uuid, callback, ctx) { * @param {string} uuid Playlist identifier. * @param {Array[string]} songs SHA1 hashes of the songs. */ -djr.Backend.prototype.savePlaylist = function(uuid, songs) { +djr.Backend.prototype.savePlaylist = function(uuid, title, songs) { $.ajax({url: '/json/playlist/save', data: {uuid: uuid, + title: title, h: songs.join(',')}, type: 'POST' }); @@ -134,13 +196,14 @@ djr.Backend.prototype.streamPlaylist = function(uuid, streaming, callback, ctx) * @param {Object} ctx Callback context. */ djr.Backend.prototype.getHtmlForSongs = function(songs, callback, ctx) { - $.ajax({url: '/fragment/songs', - data: {'h': songs.join(',')}, - dataType: 'html', - type: 'POST', - context: ctx || this, - success: callback - }); + wrap_with_loader( + {url: '/fragment/songs', + data: {'h': songs.join(',')}, + dataType: 'html', + type: 'POST', + context: ctx || this, + success: callback + }); }; /** @@ -162,6 +225,25 @@ djr.Backend.prototype.nowPlaying = function(song, old_songs) { }); }; +/** + * Request N never played songs + * + * @param {integer} n Number of songs requested + * + */ +djr.Backend.prototype.neverPlayedPlaylist = function(num, callback ,ctx) { + wrap_with_loader( + {url: '/json/never_played', + data: {n: num }, + dataType: 'json', + type: 'GET', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); +}; + /** * Request N most played songs * @@ -169,15 +251,16 @@ djr.Backend.prototype.nowPlaying = function(song, old_songs) { * */ djr.Backend.prototype.mostPlayedPlaylist = function(num, callback ,ctx) { - $.ajax({url: '/json/most_played', - data: {n: num }, - dataType: 'json', - type: 'GET', - context: ctx || this, - success: function(data, status, jqxhr) { - callback(data.results); - } - }); + wrap_with_loader( + {url: '/json/most_played', + data: {n: num }, + dataType: 'json', + type: 'GET', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); }; /** @@ -187,15 +270,16 @@ djr.Backend.prototype.mostPlayedPlaylist = function(num, callback ,ctx) { * */ djr.Backend.prototype.lastPlaylist = function(num, callback ,ctx) { - $.ajax({url: '/json/last_uploaded', - data: {n: num }, - dataType: 'json', - type: 'GET', - context: ctx || this, - success: function(data, status, jqxhr) { - callback(data.results); - } - }); + wrap_with_loader( + {url: '/json/last_uploaded', + data: {n: num }, + dataType: 'json', + type: 'GET', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); }; /** @@ -206,16 +290,17 @@ djr.Backend.prototype.lastPlaylist = function(num, callback ,ctx) { * */ djr.Backend.prototype.markovPlaylist = function(num, uuids, callback ,ctx) { - $.ajax({url: '/json/markov', - data: {'h': uuids.join(','), - 'n': num }, - dataType: 'json', - type: 'POST', - context: ctx || this, - success: function(data, status, jqxhr) { - callback(data.results); - } - }); + wrap_with_loader( + {url: '/json/markov', + data: {'h': uuids.join(','), + 'n': num }, + dataType: 'json', + type: 'POST', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); }; /** @@ -225,14 +310,15 @@ djr.Backend.prototype.markovPlaylist = function(num, uuids, callback ,ctx) { * */ djr.Backend.prototype.randomPlaylist = function(num, callback ,ctx) { - $.ajax({url: '/json/random', - data: {'n': num }, - dataType: 'json', - type: 'GET', - context: ctx || this, - success: function(data, status, jqxhr) { - callback(data.results); - } - }); + wrap_with_loader( + {url: '/json/random', + data: {'n': num }, + dataType: 'json', + type: 'GET', + context: ctx || this, + success: function(data, status, jqxhr) { + callback(data.results); + } + }); }; diff --git a/server/djrandom/frontend/static/js/djr/djr.js b/server/djrandom/frontend/static/js/djr/djr.js index b7e70d636698f99f1efbb3b9d59ca2c567caf898..d12da4ad2525e275870552b4ce4bf6ba29dc9740 100644 --- a/server/djrandom/frontend/static/js/djr/djr.js +++ b/server/djrandom/frontend/static/js/djr/djr.js @@ -6,6 +6,174 @@ // Namespace. djr = {}; +// Global state (with Player instance). +djr.state = { + backend: null, + player: null +}; + +// Initialize the Player and add all our onclick handlers. +djr.init = function () { + djr.state.backend = new djr.Backend(); + djr.state.player = new djr.Player(djr.state.backend, '#djr_player'); + + // Set autocompletion and search handlers. + $('#queryField').autocomplete({ + source: djr.state.backend.autocomplete, + autoFocus: true, + delay: 200, + minLength: 1 + }); + $('#queryField').focus(); + $('#searchForm').submit(function() { + var query_string = $('#queryField').val(); + djr.state.player.search(query_string); + return false; + }); + + // Add onclick hooks to the playlist controls. + $('#playlistClearBtn').click(function() { + djr.state.player.clearPlaylist(); + }); + $('#playlistStreamBtn').click(function() { + djr.state.player.streamCurrentPlaylist(); + }); + $('#playlistExtendBtn').click(function() { + djr.state.player.extendCurrentPlaylist(); + }); + $('#playlistLast5').click(function() { + djr.state.player.lastPlaylist(5); + }); + $('#playlistLast25').click(function() { + djr.state.player.lastPlaylist(25); + }); + $('#playlistRandom5').click(function() { + djr.state.player.randomPlaylist(5); + }); + $('#playlistRandom25').click(function() { + djr.state.player.randomPlaylist(25); + }); + $('#playlistMost5').click(function() { + djr.state.player.mostPlayedPlaylist(5); + }); + $('#playlistMost25').click(function() { + djr.state.player.mostPlayedPlaylist(25); + }); + $('#playlistNever5').click(function() { + djr.state.player.neverPlayedPlaylist(5); + }); + $('#playlistNever25').click(function() { + djr.state.player.neverPlayedPlaylist(25); + }); + $('#playlistSave').click(function() { + if ( $('#saveForm').is(':visible') == false ) { + $('#savetext').val(djr.playlist.title); + $('#saveForm').show('slow'); + } else { + $('#savetext').val(''); + $('#saveForm').hide('slow'); + } + }); + $('#playlistSaveBtn').click(function() { + if ( $('#savetext').val() != '' ) { + djr.player.saveNewPlaylist($('#savetext').val()); + } + }); + $('#wikibtn').click(function () { + var stitle = $('#song_' + djr.state.player.cur_song + ' .artist').text(); + if ( $('#wikipedia').is(':visible') == false ) { + if ( stitle != "" ) { + stitle = stitle.split(' ').join('+'); + $('#wikipedia').show('slow'); + //$('#wikipedia').attr("src", "/ext?url=" + escape("http://en.wikipedia.org/wiki/Special:Search?search=" + stitle + "&go=Go")); + $('#wikipedia').attr("src", "http://en.m.wikipedia.org/w/index.php?search=" + stitle); + } + } else { + $('#wikipedia').hide('slow') + } + }); + $('#lastfmbtn').click(function () { + var stitle = $('#song_' + djr.state.player.cur_song + ' .title').text(); + var sartist = $('#song_' + djr.state.player.cur_song + ' .artist').text(); + if ( $('#lastfm').is(':visible') == false ) { + if ( stitle != "" ) { + stitle = stitle.split(' ').join('+'); + sartist = sartist.split(' ').join('+'); + $('#lastfm').show('slow'); + //$('#lastfm').attr("src", "/ext?url=" + escape("http://www.lastfm.com/#&q=" + stitle + sartist)); + $('#lastfm').attr("src", "http://m.last.fm/search?q=" + stitle + sartist ); + } + } else { + $('#lastfm').hide('slow') + } + }); + $('#lyricsbtn').click(function () { + var stitle = $('#song_' + djr.state.player.cur_song + ' .title').text(); + if ( $('#lyrics').is(':visible') == false ) { + if ( stitle != "" ) { + stitle = stitle.split(' ').join('+'); + $('#lyrics').show('slow'); + //$('#lyrics').attr("src", "/ext?url=" + escape("http://www.lyrics.com/#&q=" + stitle)); + $('#lyrics').attr("src", "http://lyrics.wikia.com/index.php?search=" + stitle + "&fulltext=0" ); + } + } else { + $('#lyrics').hide('slow') + } + }); + $('#pllistbtn').click(function () { + if ( $('#pllist').is(':visible') == false ) { + $('#pllist').show('slow'); + this.backend.getPlList(uid, function(results) { + var pllist = results; + if (pllist.length == 0) { + djr.debug('No results found.'); + return; + } + var output = "<ul>"; + for (var i; i < pllist.lenght; i++) { + output += "<il><a id='" + pllist[i].uuid + "' onclick='djm.player.showPlaylist(\"" + pllist[i].uuid + "\"' >" + pllist[i].title + "</a></il>"; + //output += "<il><a id='" + pllist[i].uuid + "' onclick='djr.Playlist.saveNewPlaylist();djr.player.createChunk(djr.Playlist.getPlaylist(\"" + pllist[i].uuid + "\"), \"" + pllist[i].title + "\");' >" + pllist[i].title + "</a></il>"; + } + output += "</ul>"; + $('#pllist').html(output); + }); + } else { + $('#pllist').hide('slow') + } + }); + + // Set the album art image to auto-fullscreen on load. + $('#albumart_fs').load(function() { + $(this).fullBg(); + $(this).show(); + }); + + djr.debug('initialization done.'); +}; + +// Export the player for quick onclick access +djr.player = function() { + return djr.state.player; +}; + +// Show/hide the 'loader' animated GIF. +djr.loading = function(active) { + var loader = $('#loaderImg'); + if (active) { + loader.show(); + } else { + loader.hide(); + } +}; + +// Debugging. +djr.debug = function(msg) { + var n_logs = $('#debug p').length; + if (n_logs > 7) { + $('#debug p:first').remove(); + } + $('#debug').append('<p>' + msg + '</p>'); +}; // Utility functions. var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); @@ -15,3 +183,4 @@ djr.generateRandomId = function() { for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; return uuid.join(''); }; + diff --git a/server/djrandom/frontend/static/js/djr/player.js b/server/djrandom/frontend/static/js/djr/player.js index c6643c33919a6943aa402a2a715f0f097746183b..11996e3a1a5f859680203ab1604585fc9400ee23 100644 --- a/server/djrandom/frontend/static/js/djr/player.js +++ b/server/djrandom/frontend/static/js/djr/player.js @@ -5,22 +5,24 @@ djr.Player = function(backend, selector) { this.backend = backend; - this.player = $(selector); this.playlist = new djr.Playlist(); this.old_songs = []; this.cur_song = null; + this.auto_extend = true; // Setup the jPlayer interface. - this.player.jPlayer({ - swfPath: '/static/js', - ready: function() { - djr.debug('player ready'); + this.circleplayer = new CirclePlayer(selector, {}, { + supplied: "mp3", + swfPath: "/static/js", + cssSelectorAncestor: "#cp_container", + error: function(e) { + djr.state.player.reportError(e); } }); + this.player = this.circleplayer.player; + this.player.bind($.jPlayer.event.ended + '.djr', function () {djr.state.player.nextSong(); }); - this.player.bind($.jPlayer.event.error + '.djr', - function() {djr.state.player.reportError(); }); }; djr.Player.prototype.hideAllChunks = function() { @@ -46,7 +48,12 @@ djr.Player.prototype.removeSong = function(song) { // Save the current playlist. djr.Player.prototype.savePlaylist = function() { - this.backend.savePlaylist(this.playlist.uuid, this.playlist.allSongs()); + this.backend.savePlaylist(this.playlist.uuid, '', this.playlist.allSongs()); +}; + +// Save the current playlist. +djr.Player.prototype.saveNewPlaylist = function(title) { + this.backend.savePlaylist(djr.generateRandomId(), title, this.playlist.allSongs()); }; // Completely clear out the current playlist, and replace it with @@ -112,8 +119,6 @@ djr.Player.prototype.lastPlaylist = function(num) { // Create a chunk of unique new songs. player.createChunk(songs, title); }); - - }; djr.Player.prototype.randomPlaylist = function(num) { @@ -131,6 +136,22 @@ djr.Player.prototype.randomPlaylist = function(num) { }; +djr.Player.prototype.neverPlayedPlaylist = function(num) { + var player = this; + var title = "" + num + " Never Played Songs"; + this.backend.neverPlayedPlaylist(num, function(results) { + var songs = results; + if (songs.length == 0) { + djr.debug('No results found.'); + return; + } + // Create a chunk of unique new songs. + player.createChunk(songs, title); + }); + +}; + + djr.Player.prototype.mostPlayedPlaylist = function(num) { var player = this; var title = "" + num + " Most Played Songs"; @@ -154,11 +175,34 @@ djr.Player.prototype.extendCurrentPlaylist = function() { var player = this; var cur_songs = this.playlist.allSongs(); - this.backend.moreLikeThese(cur_songs, function(results) { + this.backend.moreLikeThese(5, cur_songs, function(results) { player.createChunk(results, 'suggestions'); }); }; +// Automatically extend the current playlist using Markov-based +// suggestions. Invokes callback when done. +djr.Player.prototype.autoExtendCurrentPlaylist = function(callback) { + var player = this; + var cur_songs = this.playlist.allSongs(); + + this.backend.markovPlaylist(10, cur_songs, function(results) { + player.createChunk(results, 'more...'); + callback(); + }); +}; + +// Extend the current playlist with the results of a MoreLikeThis +// search based on a single selected song. +djr.Player.prototype.moreLikeThis = function(sha1) { + var player = this; + var songs = [sha1]; + + this.backend.moreLikeThese(10, songs, function(results) { + player.createChunk(results, 'like that'); + }); +}; + djr.Player.prototype.createChunk = function(songs, chunk_title) { // Create the new chunk, with unique songs. var chunk = this.playlist.createUniqueChunk(songs, chunk_title); @@ -181,11 +225,12 @@ djr.Player.prototype.createChunk = function(songs, chunk_title) { // Load the HTML fragment for the chunk and display it. Also, // minimize all the other chunks. + var player = this; this.backend.getHtmlForSongs(songs, function(songs_html) { var chunk_div = 'chunk_' + chunk_id; - this.hideAllChunks(); - this.setChunkHtml(chunk, chunk_id, songs_html); - }, this); + player.hideAllChunks(); + player.setChunkHtml(chunk, chunk_id, songs_html); + }); }; // Set the HTML contents of a chunk object, and the related event hooks. @@ -229,6 +274,10 @@ djr.Player.prototype.setChunkHtml = function(chunk, chunk_id, songs_html) { var sha1 = $(this).parent().parent().attr('id').substr(5); player.removeSong(sha1); }); + chunk_div.find('.chunk_inner .ctlbox .ctl_love').click(function() { + var sha1 = $(this).parent().parent().attr('id').substr(5); + player.moreLikeThis(sha1); + }); }; // Start playing a specific song. @@ -254,7 +303,7 @@ djr.Player.prototype.play = function(song) { // from the existing <div> instead than from the server. var artist = $('#song_' + song + ' .artist').text(); var album = $('#song_' + song + ' .album').text(); - $('#jp_playlist_1').html( + $('#songInfoDiv').html( $('#song_' + song + ' .title').text() + '<br>' + artist + '<br>' + '<small>' + album + '</small>' @@ -272,7 +321,15 @@ djr.Player.prototype.play = function(song) { // Start playing the next song. djr.Player.prototype.nextSong = function() { - this.play(this.playlist.getNextSong(this.cur_song)); + if (this.auto_extend && this.playlist.isLastSong(this.cur_song)) { + var player = this; + // auto-extend! + this.autoExtendCurrentPlaylist(function() { + player.nextSong(); + }); + } else { + this.play(this.playlist.getNextSong(this.cur_song)); + } }; // Activate streaming of the current playlist. @@ -280,114 +337,21 @@ djr.Player.prototype.streamCurrentPlaylist = function(enable) { // Nothing for now. }; - -djr.state = { - backend: null, - player: null -}; - -djr.init = function () { - djr.state.backend = new djr.Backend(); - djr.state.player = new djr.Player(djr.state.backend, '#djr_player'); - - // Add onclick hooks to the playlist controls. - $('#playlistClearBtn').click(function() { - djr.state.player.clearPlaylist(); - }); - $('#playlistStreamBtn').click(function() { - djr.state.player.streamCurrentPlaylist(); - }); - $('#playlistExtendBtn').click(function() { - djr.state.player.extendCurrentPlaylist(); - }); - $('#playlistLast25').click(function() { - djr.state.player.lastPlaylist(25); - }); - $('#playlistRandom').click(function() { - djr.state.player.randomPlaylist(25); - }); - $('#playlistMost').click(function() { - djr.state.player.mostPlayedPlaylist(25); - }); - - $('#wikibtn').click(function () { - var stitle = $('#song_' + djr.state.player.cur_song + ' .artist').text(); - if ( $('#wikipedia').is(':visible') == false ) { - if ( stitle != "" ) { - stitle = stitle.split(' ').join('+'); - $('#wikipedia').show('slow'); - //$('#wikipedia').attr("src", "/ext?url=" + escape("http://en.wikipedia.org/wiki/Special:Search?search=" + stitle + "&go=Go")); - $('#wikipedia').attr("src", "http://en.m.wikipedia.org/w/index.php?search=" + stitle); - } - } else { - $('#wikipedia').hide('slow') - } - }); - $('#lastfmbtn').click(function () { - var stitle = $('#song_' + djr.state.player.cur_song + ' .title').text(); - var sartist = $('#song_' + djr.state.player.cur_song + ' .artist').text(); - if ( $('#lastfm').is(':visible') == false ) { - if ( stitle != "" ) { - stitle = stitle.split(' ').join('+'); - sartist = sartist.split(' ').join('+'); - $('#lastfm').show('slow'); - //$('#lastfm').attr("src", "/ext?url=" + escape("http://www.lastfm.com/#&q=" + stitle + sartist)); - $('#lastfm').attr("src", "http://m.last.fm/search?q=" + stitle + sartist ); - } - } else { - $('#lastfm').hide('slow') - } - }); - $('#lyricsbtn').click(function () { - var stitle = $('#song_' + djr.state.player.cur_song + ' .title').text(); - if ( $('#lyrics').is(':visible') == false ) { - if ( stitle != "" ) { - stitle = stitle.split(' ').join('+'); - $('#lyrics').show('slow'); - //$('#lyrics').attr("src", "/ext?url=" + escape("http://www.lyrics.com/#&q=" + stitle)); - $('#lyrics').attr("src", "http://lyrics.wikia.com/index.php?search=" + stitle + "&fulltext=0" ); - } - } else { - $('#lyrics').hide('slow') - } - }); - - - // Set the album art image to auto-fullscreen on load. - $('#albumart_fs').load(function() { - $(this).fullBg(); - $(this).show(); - }); -}; - -// Export the player for quick onclick access -djr.player = function() { - return djr.state.player; -}; - -// Debugging. -djr.debug = function(msg) { - $('#debug').append(msg + '<br>'); -}; - - - -// ------ OLD -------- - -/** - -// An error has occurred in the player. +// An error has occurred in the player. Report it on the javascript +// console. djr.Player.prototype.reportError = function(event) { + console.log(event.jPlayer.error); + console.log(event.jPlayer.error.type); switch(event.jPlayer.error.type) { case $.jPlayer.error.URL: - djr.debug('error downloading song, moving on'); + djr.debug('Error downloading song, skipping...') this.nextSong(); break; case $.jPlayer.error.NO_SOLUTION: - djr.debug('unexpected error!'); + djr.debug('Error: unexpected error!'); break; } }; -**/ + // Fine diff --git a/server/djrandom/frontend/static/js/djr/playlist.js b/server/djrandom/frontend/static/js/djr/playlist.js index cc23bd39b78ed5d48689ca92555a465ea9fd1c77..96b029f5b1d582a16600006bf522ede91aa52332 100644 --- a/server/djrandom/frontend/static/js/djr/playlist.js +++ b/server/djrandom/frontend/static/js/djr/playlist.js @@ -74,6 +74,7 @@ djr.Playlist = function(uuid) { this.chunks = []; this.song_map = {}; this.chunk_map = {}; + this.title = ''; this.next_chunk_id = 0; }; @@ -109,10 +110,14 @@ djr.Playlist.prototype.allSongs = function() { djr.Playlist.prototype.createUniqueChunk = function(songs, title) { var unique = [], i; for (i = 0; i < songs.length; i++) { - if (this.song_map[songs[i]] != null) { + var sha1 = songs[i]; + if (!sha1) { continue; } - unique.push(songs[i]); + if (this.song_map[sha1] != null) { + continue; + } + unique.push(sha1); } if (unique.length > 0) { return new djr.PlaylistChunk(unique, title); @@ -234,3 +239,18 @@ djr.Playlist.prototype.getNextSong = function(song) { return this.chunk_map[this.chunks[chunk_idx]].songs[idx]; }; + +djr.Playlist.prototype.isLastSong = function(song) { + var cur_chunk = this.song_map[song]; + var chunk_songs = this.chunk_map[cur_chunk].songs; + var chunk_idx = this.chunks.indexOf(cur_chunk); + var song_idx = chunk_songs.indexOf(song); + var last_chunk = this.chunks.length - 1; + var last_song = chunk_songs.length - 1; + if (chunk_idx == last_chunk && song_idx == last_song) { + return true; + } else { + return false; + } +}; + diff --git a/server/djrandom/frontend/static/js/jquery.grab.js b/server/djrandom/frontend/static/js/jquery.grab.js new file mode 100644 index 0000000000000000000000000000000000000000..d73189912e079e2fe85392ee129791d2adf37e60 --- /dev/null +++ b/server/djrandom/frontend/static/js/jquery.grab.js @@ -0,0 +1,201 @@ +/* +jQuery grab +https://github.com/jussi-kalliokoski/jQuery.grab +Ported from Jin.js::gestures +https://github.com/jussi-kalliokoski/jin.js/ +Created by Jussi Kalliokoski +Licensed under MIT License. + +Includes fix for IE +*/ + + +(function($){ + var extend = $.extend, + mousedown = 'mousedown', + mousemove = 'mousemove', + mouseup = 'mouseup', + touchstart = 'touchstart', + touchmove = 'touchmove', + touchend = 'touchend', + touchcancel = 'touchcancel'; + + function unbind(elem, type, func){ + if (type.substr(0,5) !== 'touch'){ // A temporary fix for IE8 data passing problem in Jin. + return $(elem).unbind(type, func); + } + var fnc, i; + for (i=0; i<bind._binds.length; i++){ + if (bind._binds[i].elem === elem && bind._binds[i].type === type && bind._binds[i].func === func){ + if (document.addEventListener){ + elem.removeEventListener(type, bind._binds[i].fnc, false); + } else { + elem.detachEvent('on'+type, bind._binds[i].fnc); + } + bind._binds.splice(i--, 1); + } + } + } + + function bind(elem, type, func, pass){ + if (type.substr(0,5) !== 'touch'){ // A temporary fix for IE8 data passing problem in Jin. + return $(elem).bind(type, pass, func); + } + var fnc, i; + if (bind[type]){ + return bind[type].bind(elem, type, func, pass); + } + fnc = function(e){ + if (!e){ // Fix some ie bugs... + e = window.event; + } + if (!e.stopPropagation){ + e.stopPropagation = function(){ this.cancelBubble = true; }; + } + e.data = pass; + func.call(elem, e); + }; + if (document.addEventListener){ + elem.addEventListener(type, fnc, false); + } else { + elem.attachEvent('on' + type, fnc); + } + bind._binds.push({elem: elem, type: type, func: func, fnc: fnc}); + } + + function grab(elem, options) + { + var data = { + move: {x: 0, y: 0}, + offset: {x: 0, y: 0}, + position: {x: 0, y: 0}, + start: {x: 0, y: 0}, + affects: document.documentElement, + stopPropagation: false, + preventDefault: true, + touch: true // Implementation unfinished, and doesn't support multitouch + }; + extend(data, options); + data.element = elem; + bind(elem, mousedown, mouseDown, data); + if (data.touch){ + bind(elem, touchstart, touchStart, data); + } + } + function ungrab(elem){ + unbind(elem, mousedown, mousedown); + } + function mouseDown(e){ + e.data.position.x = e.pageX; + e.data.position.y = e.pageY; + e.data.start.x = e.pageX; + e.data.start.y = e.pageY; + e.data.event = e; + if (e.data.onstart && e.data.onstart.call(e.data.element, e.data)){ + return; + } + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.stopPropagation){ + e.stopPropagation(); + } + bind(e.data.affects, mousemove, mouseMove, e.data); + bind(e.data.affects, mouseup, mouseUp, e.data); + } + function mouseMove(e){ + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.preventDefault){ + e.stopPropagation(); + } + e.data.move.x = e.pageX - e.data.position.x; + e.data.move.y = e.pageY - e.data.position.y; + e.data.position.x = e.pageX; + e.data.position.y = e.pageY; + e.data.offset.x = e.pageX - e.data.start.x; + e.data.offset.y = e.pageY - e.data.start.y; + e.data.event = e; + if (e.data.onmove){ + e.data.onmove.call(e.data.element, e.data); + } + } + function mouseUp(e){ + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.stopPropagation){ + e.stopPropagation(); + } + unbind(e.data.affects, mousemove, mouseMove); + unbind(e.data.affects, mouseup, mouseUp); + e.data.event = e; + if (e.data.onfinish){ + e.data.onfinish.call(e.data.element, e.data); + } + } + function touchStart(e){ + e.data.position.x = e.touches[0].pageX; + e.data.position.y = e.touches[0].pageY; + e.data.start.x = e.touches[0].pageX; + e.data.start.y = e.touches[0].pageY; + e.data.event = e; + if (e.data.onstart && e.data.onstart.call(e.data.element, e.data)){ + return; + } + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.stopPropagation){ + e.stopPropagation(); + } + bind(e.data.affects, touchmove, touchMove, e.data); + bind(e.data.affects, touchend, touchEnd, e.data); + } + function touchMove(e){ + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.stopPropagation){ + e.stopPropagation(); + } + e.data.move.x = e.touches[0].pageX - e.data.position.x; + e.data.move.y = e.touches[0].pageY - e.data.position.y; + e.data.position.x = e.touches[0].pageX; + e.data.position.y = e.touches[0].pageY; + e.data.offset.x = e.touches[0].pageX - e.data.start.x; + e.data.offset.y = e.touches[0].pageY - e.data.start.y; + e.data.event = e; + if (e.data.onmove){ + e.data.onmove.call(e.data.elem, e.data); + } + } + function touchEnd(e){ + if (e.preventDefault && e.data.preventDefault){ + e.preventDefault(); + } + if (e.stopPropagation && e.data.stopPropagation){ + e.stopPropagation(); + } + unbind(e.data.affects, touchmove, touchMove); + unbind(e.data.affects, touchend, touchEnd); + e.data.event = e; + if (e.data.onfinish){ + e.data.onfinish.call(e.data.element, e.data); + } + } + + bind._binds = []; + + $.fn.grab = function(a, b){ + return this.each(function(){ + return grab(this, a, b); + }); + }; + $.fn.ungrab = function(a){ + return this.each(function(){ + return ungrab(this, a); + }); + }; +})(jQuery); \ No newline at end of file diff --git a/server/djrandom/frontend/static/js/jquery.transform.js b/server/djrandom/frontend/static/js/jquery.transform.js new file mode 100644 index 0000000000000000000000000000000000000000..db58781289b2f676769889527a2bd16fab6acdad --- /dev/null +++ b/server/djrandom/frontend/static/js/jquery.transform.js @@ -0,0 +1,532 @@ +/* + * transform: A jQuery cssHooks adding cross-browser 2d transform capabilities to $.fn.css() and $.fn.animate() + * + * limitations: + * - requires jQuery 1.4.3+ + * - Should you use the *translate* property, then your elements need to be absolutely positionned in a relatively positionned wrapper **or it will fail in IE678**. + * - transformOrigin is not accessible + * + * latest version and complete README available on Github: + * https://github.com/louisremi/jquery.transform.js + * + * Copyright 2011 @louis_remi + * Licensed under the MIT license. + * + * This saved you an hour of work? + * Send me music http://www.amazon.co.uk/wishlist/HNTU0468LQON + * + */ +(function( $ ) { + +/* + * Feature tests and global variables + */ +var div = document.createElement('div'), + divStyle = div.style, + propertyName = 'transform', + suffix = 'Transform', + testProperties = [ + 'O' + suffix, + 'ms' + suffix, + 'Webkit' + suffix, + 'Moz' + suffix, + // prefix-less property + propertyName + ], + i = testProperties.length, + supportProperty, + supportMatrixFilter, + propertyHook, + propertyGet, + rMatrix = /Matrix([^)]*)/; + +// test different vendor prefixes of this property +while ( i-- ) { + if ( testProperties[i] in divStyle ) { + $.support[propertyName] = supportProperty = testProperties[i]; + continue; + } +} +// IE678 alternative +if ( !supportProperty ) { + $.support.matrixFilter = supportMatrixFilter = divStyle.filter === ''; +} +// prevent IE memory leak +div = divStyle = null; + +// px isn't the default unit of this property +$.cssNumber[propertyName] = true; + +/* + * fn.css() hooks + */ +if ( supportProperty && supportProperty != propertyName ) { + // Modern browsers can use jQuery.cssProps as a basic hook + $.cssProps[propertyName] = supportProperty; + + // Firefox needs a complete hook because it stuffs matrix with 'px' + if ( supportProperty == 'Moz' + suffix ) { + propertyHook = { + get: function( elem, computed ) { + return (computed ? + // remove 'px' from the computed matrix + $.css( elem, supportProperty ).split('px').join(''): + elem.style[supportProperty] + ) + }, + set: function( elem, value ) { + // remove 'px' from matrices + elem.style[supportProperty] = /matrix[^)p]*\)/.test(value) ? + value.replace(/matrix((?:[^,]*,){4})([^,]*),([^)]*)/, 'matrix$1$2px,$3px'): + value; + } + } + /* Fix two jQuery bugs still present in 1.5.1 + * - rupper is incompatible with IE9, see http://jqbug.com/8346 + * - jQuery.css is not really jQuery.cssProps aware, see http://jqbug.com/8402 + */ + } else if ( /^1\.[0-5](?:\.|$)/.test($.fn.jquery) ) { + propertyHook = { + get: function( elem, computed ) { + return (computed ? + $.css( elem, supportProperty.replace(/^ms/, 'Ms') ): + elem.style[supportProperty] + ) + } + } + } + /* TODO: leverage hardware acceleration of 3d transform in Webkit only + else if ( supportProperty == 'Webkit' + suffix && support3dTransform ) { + propertyHook = { + set: function( elem, value ) { + elem.style[supportProperty] = + value.replace(); + } + } + }*/ + +} else if ( supportMatrixFilter ) { + propertyHook = { + get: function( elem, computed ) { + var elemStyle = ( computed && elem.currentStyle ? elem.currentStyle : elem.style ), + matrix; + + if ( elemStyle && rMatrix.test( elemStyle.filter ) ) { + matrix = RegExp.$1.split(','); + matrix = [ + matrix[0].split('=')[1], + matrix[2].split('=')[1], + matrix[1].split('=')[1], + matrix[3].split('=')[1] + ]; + } else { + matrix = [1,0,0,1]; + } + matrix[4] = elemStyle ? elemStyle.left : 0; + matrix[5] = elemStyle ? elemStyle.top : 0; + return "matrix(" + matrix + ")"; + }, + set: function( elem, value, animate ) { + var elemStyle = elem.style, + currentStyle, + Matrix, + filter; + + if ( !animate ) { + elemStyle.zoom = 1; + } + + value = matrix(value); + + // rotate, scale and skew + if ( !animate || animate.M ) { + Matrix = [ + "Matrix("+ + "M11="+value[0], + "M12="+value[2], + "M21="+value[1], + "M22="+value[3], + "SizingMethod='auto expand'" + ].join(); + filter = ( currentStyle = elem.currentStyle ) && currentStyle.filter || elemStyle.filter || ""; + + elemStyle.filter = rMatrix.test(filter) ? + filter.replace(rMatrix, Matrix) : + filter + " progid:DXImageTransform.Microsoft." + Matrix + ")"; + + // center the transform origin, from pbakaus's Transformie http://github.com/pbakaus/transformie + if ( (centerOrigin = $.transform.centerOrigin) ) { + elemStyle[centerOrigin == 'margin' ? 'marginLeft' : 'left'] = -(elem.offsetWidth/2) + (elem.clientWidth/2) + 'px'; + elemStyle[centerOrigin == 'margin' ? 'marginTop' : 'top'] = -(elem.offsetHeight/2) + (elem.clientHeight/2) + 'px'; + } + } + + // translate + if ( !animate || animate.T ) { + // We assume that the elements are absolute positionned inside a relative positionned wrapper + elemStyle.left = value[4] + 'px'; + elemStyle.top = value[5] + 'px'; + } + } + } +} +// populate jQuery.cssHooks with the appropriate hook if necessary +if ( propertyHook ) { + $.cssHooks[propertyName] = propertyHook; +} +// we need a unique setter for the animation logic +propertyGet = propertyHook && propertyHook.get || $.css; + +/* + * fn.animate() hooks + */ +$.fx.step.transform = function( fx ) { + var elem = fx.elem, + start = fx.start, + end = fx.end, + split, + pos = fx.pos, + transform, + translate, + rotate, + scale, + skew, + T = false, + M = false, + prop; + translate = rotate = scale = skew = ''; + + // fx.end and fx.start need to be converted to their translate/rotate/scale/skew components + // so that we can interpolate them + if ( !start || typeof start === "string" ) { + // the following block can be commented out with jQuery 1.5.1+, see #7912 + if (!start) { + start = propertyGet( elem, supportProperty ); + } + + // force layout only once per animation + if ( supportMatrixFilter ) { + elem.style.zoom = 1; + } + + // if the start computed matrix is in end, we are doing a relative animation + split = end.split(start); + if ( split.length == 2 ) { + // remove the start computed matrix to make animations more accurate + end = split.join(''); + fx.origin = start; + start = 'none'; + } + + // start is either 'none' or a matrix(...) that has to be parsed + fx.start = start = start == 'none'? + { + translate: [0,0], + rotate: 0, + scale: [1,1], + skew: [0,0] + }: + unmatrix( toArray(start) ); + + // fx.end has to be parsed and decomposed + fx.end = end = ~end.indexOf('matrix')? + // bullet-proof parser + unmatrix(matrix(end)): + // faster and more precise parser + components(end); + + // get rid of properties that do not change + for ( prop in start) { + if ( prop == 'rotate' ? + start[prop] == end[prop]: + start[prop][0] == end[prop][0] && start[prop][1] == end[prop][1] + ) { + delete start[prop]; + } + } + } + + /* + * We want a fast interpolation algorithm. + * This implies avoiding function calls and sacrifying DRY principle: + * - avoid $.each(function(){}) + * - round values using bitewise hacks, see http://jsperf.com/math-round-vs-hack/3 + */ + if ( start.translate ) { + // round translate to the closest pixel + translate = ' translate('+ + ((start.translate[0] + (end.translate[0] - start.translate[0]) * pos + .5) | 0) +'px,'+ + ((start.translate[1] + (end.translate[1] - start.translate[1]) * pos + .5) | 0) +'px'+ + ')'; + T = true; + } + if ( start.rotate != undefined ) { + rotate = ' rotate('+ (start.rotate + (end.rotate - start.rotate) * pos) +'rad)'; + M = true; + } + if ( start.scale ) { + scale = ' scale('+ + (start.scale[0] + (end.scale[0] - start.scale[0]) * pos) +','+ + (start.scale[1] + (end.scale[1] - start.scale[1]) * pos) + + ')'; + M = true; + } + if ( start.skew ) { + skew = ' skew('+ + (start.skew[0] + (end.skew[0] - start.skew[0]) * pos) +'rad,'+ + (start.skew[1] + (end.skew[1] - start.skew[1]) * pos) +'rad'+ + ')'; + M = true; + } + + // In case of relative animation, restore the origin computed matrix here. + transform = fx.origin ? + fx.origin + translate + skew + scale + rotate: + translate + rotate + scale + skew; + + propertyHook && propertyHook.set ? + propertyHook.set( elem, transform, {M: M, T: T} ): + elem.style[supportProperty] = transform; +}; + +/* + * Utility functions + */ + +// turns a transform string into its 'matrix(A,B,C,D,X,Y)' form (as an array, though) +function matrix( transform ) { + transform = transform.split(')'); + var + trim = $.trim + // last element of the array is an empty string, get rid of it + , i = transform.length -1 + , split, prop, val + , A = 1 + , B = 0 + , C = 0 + , D = 1 + , A_, B_, C_, D_ + , tmp1, tmp2 + , X = 0 + , Y = 0 + ; + // Loop through the transform properties, parse and multiply them + while (i--) { + split = transform[i].split('('); + prop = trim(split[0]); + val = split[1]; + A_ = B_ = C_ = D_ = 0; + + switch (prop) { + case 'translateX': + X += parseInt(val, 10); + continue; + + case 'translateY': + Y += parseInt(val, 10); + continue; + + case 'translate': + val = val.split(','); + X += parseInt(val[0], 10); + Y += parseInt(val[1] || 0, 10); + continue; + + case 'rotate': + val = toRadian(val); + A_ = Math.cos(val); + B_ = Math.sin(val); + C_ = -Math.sin(val); + D_ = Math.cos(val); + break; + + case 'scaleX': + A_ = val; + D_ = 1; + break; + + case 'scaleY': + A_ = 1; + D_ = val; + break; + + case 'scale': + val = val.split(','); + A_ = val[0]; + D_ = val.length>1 ? val[1] : val[0]; + break; + + case 'skewX': + A_ = D_ = 1; + C_ = Math.tan(toRadian(val)); + break; + + case 'skewY': + A_ = D_ = 1; + B_ = Math.tan(toRadian(val)); + break; + + case 'skew': + A_ = D_ = 1; + val = val.split(','); + C_ = Math.tan(toRadian(val[0])); + B_ = Math.tan(toRadian(val[1] || 0)); + break; + + case 'matrix': + val = val.split(','); + A_ = +val[0]; + B_ = +val[1]; + C_ = +val[2]; + D_ = +val[3]; + X += parseInt(val[4], 10); + Y += parseInt(val[5], 10); + } + // Matrix product + tmp1 = A * A_ + B * C_; + B = A * B_ + B * D_; + tmp2 = C * A_ + D * C_; + D = C * B_ + D * D_; + A = tmp1; + C = tmp2; + } + return [A,B,C,D,X,Y]; +} + +// turns a matrix into its rotate, scale and skew components +// algorithm from http://hg.mozilla.org/mozilla-central/file/7cb3e9795d04/layout/style/nsStyleAnimation.cpp +function unmatrix(matrix) { + var + scaleX + , scaleY + , skew + , A = matrix[0] + , B = matrix[1] + , C = matrix[2] + , D = matrix[3] + ; + + // Make sure matrix is not singular + if ( A * D - B * C ) { + // step (3) + scaleX = Math.sqrt( A * A + B * B ); + A /= scaleX; + B /= scaleX; + // step (4) + skew = A * C + B * D; + C -= A * skew; + D -= B * skew; + // step (5) + scaleY = Math.sqrt( C * C + D * D ); + C /= scaleY; + D /= scaleY; + skew /= scaleY; + // step (6) + if ( A * D < B * C ) { + //scaleY = -scaleY; + //skew = -skew; + A = -A; + B = -B; + skew = -skew; + scaleX = -scaleX; + } + + // matrix is singular and cannot be interpolated + } else { + rotate = scaleX = scaleY = skew = 0; + } + + return { + translate: [+matrix[4], +matrix[5]], + rotate: Math.atan2(B, A), + scale: [scaleX, scaleY], + skew: [skew, 0] + } +} + +// parse tranform components of a transform string not containing 'matrix(...)' +function components( transform ) { + // split the != transforms + transform = transform.split(')'); + + var translate = [0,0], + rotate = 0, + scale = [1,1], + skew = [0,0], + i = transform.length -1, + trim = $.trim, + split, name, value; + + // add components + while ( i-- ) { + split = transform[i].split('('); + name = trim(split[0]); + value = split[1]; + + if (name == 'translateX') { + translate[0] += parseInt(value, 10); + + } else if (name == 'translateY') { + translate[1] += parseInt(value, 10); + + } else if (name == 'translate') { + value = value.split(','); + translate[0] += parseInt(value[0], 10); + translate[1] += parseInt(value[1] || 0, 10); + + } else if (name == 'rotate') { + rotate += toRadian(value); + + } else if (name == 'scaleX') { + scale[0] *= value; + + } else if (name == 'scaleY') { + scale[1] *= value; + + } else if (name == 'scale') { + value = value.split(','); + scale[0] *= value[0]; + scale[1] *= (value.length>1? value[1] : value[0]); + + } else if (name == 'skewX') { + skew[0] += toRadian(value); + + } else if (name == 'skewY') { + skew[1] += toRadian(value); + + } else if (name == 'skew') { + value = value.split(','); + skew[0] += toRadian(value[0]); + skew[1] += toRadian(value[1] || '0'); + } + } + + return { + translate: translate, + rotate: rotate, + scale: scale, + skew: skew + }; +} + +// converts an angle string in any unit to a radian Float +function toRadian(value) { + return ~value.indexOf('deg') ? + parseInt(value,10) * (Math.PI * 2 / 360): + ~value.indexOf('grad') ? + parseInt(value,10) * (Math.PI/200): + parseFloat(value); +} + +// Converts 'matrix(A,B,C,D,X,Y)' to [A,B,C,D,X,Y] +function toArray(matrix) { + // Fremove the unit of X and Y for Firefox + matrix = /\(([^,]*),([^,]*),([^,]*),([^,]*),([^,p]*)(?:px)?,([^)p]*)(?:px)?/.exec(matrix); + return [matrix[1], matrix[2], matrix[3], matrix[4], matrix[5], matrix[6]]; +} + +$.transform = { + centerOrigin: 'margin' +}; + +})( jQuery ); \ No newline at end of file diff --git a/server/djrandom/frontend/static/js/mod.csstransforms.min.js b/server/djrandom/frontend/static/js/mod.csstransforms.min.js new file mode 100644 index 0000000000000000000000000000000000000000..6386d5bf31fd184569d1ed0d44d8a67010529af5 --- /dev/null +++ b/server/djrandom/frontend/static/js/mod.csstransforms.min.js @@ -0,0 +1,2 @@ +/* Modernizr custom build of 1.7pre: csstransforms */ +window.Modernizr=function(a,b,c){function G(){}function F(a,b){var c=a.charAt(0).toUpperCase()+a.substr(1),d=(a+" "+p.join(c+" ")+c).split(" ");return!!E(d,b)}function E(a,b){for(var d in a)if(k[a[d]]!==c&&(!b||b(a[d],j)))return!0}function D(a,b){return(""+a).indexOf(b)!==-1}function C(a,b){return typeof a===b}function B(a,b){return A(o.join(a+";")+(b||""))}function A(a){k.cssText=a}var d="1.7pre",e={},f=!0,g=b.documentElement,h=b.head||b.getElementsByTagName("head")[0],i="modernizr",j=b.createElement(i),k=j.style,l=b.createElement("input"),m=":)",n=Object.prototype.toString,o=" -webkit- -moz- -o- -ms- -khtml- ".split(" "),p="Webkit Moz O ms Khtml".split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v,w=function(a){var c=b.createElement("style"),d=b.createElement("div"),e;c.textContent=a+"{#modernizr{height:3px}}",h.appendChild(c),d.id="modernizr",g.appendChild(d),e=d.offsetHeight===3,c.parentNode.removeChild(c),d.parentNode.removeChild(d);return!!e},x=function(){function d(d,e){e=e||b.createElement(a[d]||"div");var f=(d="on"+d)in e;f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=C(e[d],"function"),C(e[d],c)||(e[d]=c),e.removeAttribute(d))),e=null;return f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),y=({}).hasOwnProperty,z;C(y,c)||C(y.call,c)?z=function(a,b){return b in a&&C(a.constructor.prototype[b],c)}:z=function(a,b){return y.call(a,b)},r.csstransforms=function(){return!!E(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"])};for(var H in r)z(r,H)&&(v=H.toLowerCase(),e[v]=r[H](),u.push((e[v]?"":"no-")+v));e.input||G(),e.crosswindowmessaging=e.postmessage,e.historymanagement=e.history,e.addTest=function(a,b){a=a.toLowerCase();if(!e[a]){b=!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b;return e}},A(""),j=l=null,e._enableHTML5=f,e._version=d,g.className=g.className.replace(/\bno-js\b/,"")+" js "+u.join(" ");return e}(this,this.document) \ No newline at end of file diff --git a/server/djrandom/frontend/templates/_base.html b/server/djrandom/frontend/templates/_base.html index d5002416b6deaa75c59c1addf32decb5045f49a0..dafc8c07a6d9856f9dc5205eb0393f31517ebf20 100644 --- a/server/djrandom/frontend/templates/_base.html +++ b/server/djrandom/frontend/templates/_base.html @@ -7,15 +7,23 @@ <link rel="stylesheet" type="text/css" href="/static/css/blitzer/jquery-ui-1.8.13.custom.css"> <link rel="stylesheet" type="text/css" - href="/static/jplayer-bluemonday/jplayer.blue.monday.css"> + href="/static/circle.skin/circle.player.css"> <script type="text/javascript" src="/static/js/jquery-1.5.1.min.js"></script> <script type="text/javascript" src="/static/js/jquery-ui-1.8.13.custom.min.js"></script> + <script type="text/javascript" + src="/static/js/jquery.fullbg.min.js"></script> + <script type="text/javascript" + src="/static/js/jquery.transform.js"></script> + <script type="text/javascript" + src="/static/js/jquery.grab.js"></script> + <script type="text/javascript" + src="/static/js/mod.csstransforms.min.js"></script> <script type="text/javascript" src="/static/js/jquery.jplayer.min.js"></script> <script type="text/javascript" - src="/static/js/jquery.fullbg.min.js"></script> + src="/static/js/circle.player.js"></script> <script type="text/javascript" src="/static/js/djr.min.js"></script> {% block head %}{% endblock %} diff --git a/server/djrandom/frontend/templates/_macros.html b/server/djrandom/frontend/templates/_macros.html index 1c25e24ed85c51e3482619c22824cbc0ff19c5c4..3557ad5a8cf461de64954bdc5b37a25fa0b8a421 100644 --- a/server/djrandom/frontend/templates/_macros.html +++ b/server/djrandom/frontend/templates/_macros.html @@ -1,6 +1,7 @@ {% macro render_song(mp3) -%} <div class="song" id="song_{{ mp3.sha1 }}"> <div class="ctlbox" style="display:none;"> + <a id="song_{{ mp3.sha1 }}_mlt" class="ctl_btn ctl_love"></a> <a id="song_{{ mp3.sha1 }}_remove" class="ctl_btn ctl_remove"></a> </div> <a id="play_{{ mp3.sha1 }}" class="song_a"> diff --git a/server/djrandom/frontend/templates/about.html b/server/djrandom/frontend/templates/about.html index aedc42e9eca58f0f9c1b254169ec5298eedadc17..df3049d9bcdaf2a9ce569aa1ebdbd0bec3114b78 100644 --- a/server/djrandom/frontend/templates/about.html +++ b/server/djrandom/frontend/templates/about.html @@ -11,5 +11,10 @@ <b>{{ num_songs }}</b> songs (<b>{{ used_gb }}</b> Gb). </p> + {% if dup_songs %} + <p> + <b>{{ dup_songs }}</b> duplicates found. + </p> + {% endif %} {% endblock %} diff --git a/server/djrandom/frontend/templates/index.html b/server/djrandom/frontend/templates/index.html index e81f857ad16b17069112e52d3d066d364a79b371..a40880a5f41e494cace56d93fda80b521f48968d 100644 --- a/server/djrandom/frontend/templates/index.html +++ b/server/djrandom/frontend/templates/index.html @@ -2,36 +2,8 @@ {% block head %} <script type="text/javascript"> -do_autocomplete = function(request, response) { - $.ajax({url: '/autocomplete', - data: {'term': request.term}, - dataType: 'json', - success: function(data) { - var items = data.results; - response(data.results); - }, - error: function(xhr, status, errorThrown) { - response(); - }}); -}; - -do_search = function() { - var player = djr.player(); - player.search($('#queryField').val()); - return false; -}; - $(document).ready(function() { - $('#queryField').autocomplete({ - source: do_autocomplete, - autoFocus: true, - delay: 200, - minLength: 1, - }); - $('#queryField').focus(); - $('#searchForm').submit(do_search); - - // Initialize DJRd. + // Initialize DJR. djr.init(); }); </script> @@ -44,74 +16,82 @@ DJ:Random {% block body %} <div id="rbox"> - <h1>{{ self.title() }}</h1> - - <div id="djr_player" class="jp-jplayer"></div> - <div id="playerintf" class="jp-audio"> - <div class="jp-type-single"> - <div id="jp_interface_1" class="jp-interface"> - <ul class="jp-controls"> - <li><a href="#" class="jp-play" tabindex="1">play</a></li> - <li><a href="#" class="jp-pause" tabindex="1">pause</a></li> - <li><a href="#" class="jp-stop" tabindex="1">stop</a></li> - <li><a href="#" class="jp-mute" tabindex="1">mute</a></li> - <li><a href="#" class="jp-unmute" tabindex="1">unmute</a></li> - </ul> - <div class="jp-progress"> - <div class="jp-seek-bar"> - <div class="jp-play-bar"></div> - </div> - </div> - <div class="jp-volume-bar"> - <div class="jp-volume-bar-value"></div> - </div> - <div class="jp-current-time"></div> - <div class="jp-duration"></div> - </div> - <div id="jp_playlist_1" class="jp-playlist"> - <ul> - <li></li> - </ul> - </div> - <div id="lyricsDiv" > - <a id="lyricsbtn">Lyrics</a> - <iframe id="lyrics" width="100%" height="300" style="display: none" > - <p>Your browser does not support iframes.</p> - </iframe> - </div> - <div id="wikiDiv" > - <a id="wikibtn">Wikipedia Author</a> - <iframe id="wikipedia" width="100%" height="300" style="display: none" > - <p>Your browser does not support iframes.</p> - </iframe> - </div> - <div id="lastfmDiv" > - <a id="lastfmbtn">LastFM Song Info</a> - <iframe id="lastfm" width="100%" height="300" style="display: none" > - <p>Your browser does not support iframes.</p> - </iframe> - </div> + <div id="djr_player" class="cp-jplayer"></div> + <div id="cp_container" class="cp-container"> + <div class="cp-buffer-holder"> + <div class="cp-buffer-1"></div> + <div class="cp-buffer-2"></div> + </div> + <div class="cp-progress-holder"> + <div class="cp-progress-1"></div> + <div class="cp-progress-2"></div> </div> + <div class="cp-circle-control"></div> + <ul class="cp-controls"> + <li><a href="#" class="cp-play" tabindex="1">play</a></li> + <li><a href="#" class="cp-pause" style="display:none;" tabindex="1">pause</a></li> + </ul> + </div> + + <div id="songInfoDiv"> + </div> + + <div id="pllistDiv" > + <a id="pllistbtn">Playlists</a> + <div id="pllist" style="display: none"></div> + </div> + + <div id="lyricsDiv" > + <a id="lyricsbtn">Lyrics</a> + <iframe id="lyrics" width="100%" height="300" style="display: none"> + <p>Your browser does not support iframes.</p> + </iframe> + </div> + + <div id="wikiDiv" > + <a id="wikibtn">Wikipedia Author</a> + <iframe id="wikipedia" width="100%" height="300" style="display: none"> + <p>Your browser does not support iframes.</p> + </iframe> + </div> + + <div id="lastfmDiv" > + <a id="lastfmbtn">LastFM Song Info</a> + <iframe id="lastfm" width="100%" height="300" style="display: none"> + <p>Your browser does not support iframes.</p> + </iframe> </div> </div> <div id="lbox"> - <form id="searchForm" action="/" method="get"> - <p id="searchBox"> - <input type="text" name="q" size="40" id="queryField"> - <input type="submit" value="->"> - </p> - </form> + <div id="titleDiv"> + <h1 class="title">DJ:Random</h1> + </div> + + <div id="searchDiv"> + <form id="searchForm" action="/" method="get"> + <div id="loaderImgDiv"> + <img id="loaderImg" src="/static/img/loader.gif" style="display:none"> + </div> + <div id="searchBoxDiv"> + <input type="text" name="q" size="40" id="queryField"> + <input type="submit" value="->"> + </div> + </form> + </div> - <div id="playlistButtons" > - <a id="playlistLast25">Last 25</a> - | <a id="playlistRandom">Random</a> - | <a id="playlistMost">Most Played</a> + <div id="playlistButtons" class="buttonbox"> + <a id="playlistLast5">Last 5</a>, <a id="playlistLast25">(25)</a> + | <a id="playlistRandom5">Random 5</a>, <a id="playlistRandom25">(25)</a> + | <a id="playlistMost5">Most 5</a>, <a id="playlistMost25">(25)</a> + | <a id="playlistNever5">Never 5</a>, <a id="playlistNever25">(25)</a> + | <a id="playlistSave">Save as</a><div id="saveForm" style="display:none;" > + <input type="text" name="s" size="15" id="savetext" ><a id="playlistSaveBtn" >Save</a></div> </div> <div id="playlistDiv"> </div> - <div id="playlistControls"> + <div id="playlistControls" class="buttonbox"> <a id="playlistExtendBtn">Extend</a> | <a id="playlistClearBtn">Clear</a> | <a id="playlistStreamBtn">Stream</a> diff --git a/server/djrandom/frontend/templates/user_details.html b/server/djrandom/frontend/templates/user_details.html index d12373a88a078e867b82b68865973f6415176f70..bd840d29a94caa927973bc2f973082e0d16bf964 100644 --- a/server/djrandom/frontend/templates/user_details.html +++ b/server/djrandom/frontend/templates/user_details.html @@ -39,9 +39,11 @@ </form> </p> + {% if user.invites_left > 0 %} <p> <a href="/user/invite"><b>Invite someone</b></a> </p> + {% endif %} </div> diff --git a/server/djrandom/frontend/user_views.py b/server/djrandom/frontend/user_views.py index c62695926b91af96d797707175cddaf531e8b5d5..d12025bca3d608e64e030d1cf130dc11ea04ad87 100644 --- a/server/djrandom/frontend/user_views.py +++ b/server/djrandom/frontend/user_views.py @@ -50,6 +50,7 @@ def login(): user = User.query.filter_by(email=email).first() if user and user.check_password(form.password.data): session['userid'] = user.id + session.permanent = True return redirect('/') log.info('failed login for %s' % email) login_failed = True @@ -103,6 +104,10 @@ def user_revoke_api_key(keyid): @app.route('/user/invite', methods=['GET', 'POST']) @require_auth def user_send_invite(): + user = User.query.get(g.userid) + if not user.invites_left: + abort(403) + form = InviteForm() if form.validate_on_submit(): user = User.query.get(g.userid) @@ -113,6 +118,8 @@ def user_send_invite(): svcs['mailer'].send(email, 'Invitation', new_user.get_activation_email(username)) + user.invites_left -= 1 + Session.add(user) Session.add(new_user) Session.commit() flash('invitation sent to %s' % email) diff --git a/server/djrandom/frontend/views.py b/server/djrandom/frontend/views.py index ec792dd7e8b7defdc867fa880aefc95ea0d5c09c..ca83889bb1055d5657700decf075d98c4db4a416 100644 --- a/server/djrandom/frontend/views.py +++ b/server/djrandom/frontend/views.py @@ -38,11 +38,13 @@ def songs_fragment(): @app.route('/about') @require_auth def about(): - num_songs = MP3.query.filter_by(error=False).count() + num_songs = MP3.query.filter_by(state=MP3.READY).count() + dup_songs = MP3.query.filter_by(state=MP3.DUPLICATE).count() used_gb = int(Session.query(func.sum(MP3.size)).first()[0] / (2 << 29)) num_users = User.query.filter_by(active=True).count() return render_template('about.html', num_users=num_users, - num_songs=num_songs, used_gb=used_gb) + num_songs=num_songs, used_gb=used_gb, + dup_songs=dup_songs) @app.route('/') diff --git a/server/djrandom/metadata_fixer/__init__.py b/server/djrandom/metadata_fixer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/djrandom/metadata_fixer/metadata_fixer.py b/server/djrandom/metadata_fixer/metadata_fixer.py new file mode 100644 index 0000000000000000000000000000000000000000..c89ed8fed76251b858ad7facef3649fc4ed10da1 --- /dev/null +++ b/server/djrandom/metadata_fixer/metadata_fixer.py @@ -0,0 +1,134 @@ +import json +import os +import optparse +import logging +import socket +import time +import traceback +import urllib2 +from djrandom import daemonize +from djrandom import utils +from djrandom.model.mp3 import MP3 +from djrandom.database import Session, init_db +from djrandom.scanner import metadata +from djrandom.scanner import indexer + +log = logging.getLogger(__name__) + + +class NoMetadataError(Exception): + pass + + +class MetadataFixer(object): + + ECHONEST_API_URL = 'http://developer.echonest.com/api/v4/song/identify' + + def __init__(self, solr_url, echonest_api_key): + self.api_key = echonest_api_key + self.idx = indexer.Indexer(solr_url) + + def identify_song(self, mp3): + json_fp = mp3.get_fingerprint() + while True: + + req = urllib2.Request( + '%s?api_key=%s' % (self.ECHONEST_API_URL, self.api_key), + data=json_fp, + headers={'Content-Type': 'application/octet-stream'}) + + try: + result = json.loads(urllib2.urlopen(req).read()) + response = result['response'] + logging.debug('response:\n%s' % str(response)) + if response['status']['code'] != 0: + log.error('EchoNest API replied with code %d: %s' % ( + response['status']['code'], + response['status']['message'])) + raise NoMetadataError('API Error') + if not response['songs']: + log.info('no information found for %s' % mp3.sha1) + raise NoMetadataError('Not found') + return response['songs'][0] + + except urllib2.HTTPError, e: + # HTTPErrors are fatal only in case of 4xx codes. + if e.code >= 400 and e.code < 500: + raise NoMetadataError('HTTP Error %d' % e.code) + + log.debug('retrying...') + + def process(self, mp3): + info = self.identify_song(mp3) + mp3.title = metadata.normalize_string(info['title']) + mp3.artist = metadata.normalize_string(info['artist_name']) + + def scan(self): + """Scan the database for new files.""" + n_bad = n_ok = n_err = 0 + for mp3 in MP3.get_with_bad_metadata(): + n_bad += 1 + log.info('searching metadata for %s' % mp3.sha1) + try: + self.process(mp3) + mp3.state = MP3.READY + self.idx.add_mp3(mp3) + log.info('found: %s / %s' % (mp3.artist, mp3.title)) + n_ok += 1 + except NoMetadataError: + mp3.state = MP3.ERROR + n_err += 1 + except Exception, e: + log.error(traceback.format_exc()) + n_err += 1 + mp3.state = MP3.ERROR + Session.add(mp3) + Session.commit() + self.idx.commit() + log.debug('total: %d songs, found: %d' % (n_bad, n_ok)) + + def run(self, run_once): + while True: + self.scan() + if run_once: + break + Session.remove() + time.sleep(600) + + +def run_fixer(solr_url, echonest_api_key, db_url, run_once): + socket.setdefaulttimeout(300) + + init_db(db_url) + fixer = MetadataFixer(solr_url, echonest_api_key) + fixer.run(run_once) + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--once', action='store_true') + parser.add_option('--solr_url', default='http://localhost:8080/solr') + parser.add_option('--echonest_api_key') + parser.add_option('--db_url') + daemonize.add_standard_options(parser) + utils.read_config_defaults( + parser, os.getenv('DJRANDOM_CONF', '/etc/djrandom.conf')) + opts, args = parser.parse_args() + if not opts.db_url: + parser.error('Must provide --db_url') + if not opts.echonest_api_key: + parser.error('Must provide --echonest_api_key') + if args: + parser.error('Too many arguments') + + if opts.once: + opts.foreground = True + + daemonize.daemonize(opts, run_fixer, + (opts.solr_url, opts.echonest_api_key, + opts.db_url, opts.once)) + + +if __name__ == '__main__': + main() + diff --git a/server/djrandom/model/markov.py b/server/djrandom/model/markov.py index 11350ea8c193e92ab9af7cb3af7f0d5bb328d797..fa8dfecc6bc35a19c7109320298c8dd01a9cf646 100644 --- a/server/djrandom/model/markov.py +++ b/server/djrandom/model/markov.py @@ -72,14 +72,13 @@ class MarkovModel(object): def suggest(self, prev): prev_n = tuple(self._to_i(x) for x in prev) - if prev_n not in self._map: - return None - r = self.random.random() - for off, value in self._norm_map[prev_n]: - if off > r: - return self._i2hash[value] + if prev_n in self._map: + r = self.random.random() + for off, value in self._norm_map[prev_n]: + if off > r: + return self._i2hash[value] # Can't find anything, get a random song instead. - return random.choice(self._ishash()) + return random.choice(self._i2hash) def generate_sequence(self, prev, n, count): if len(prev) < n: diff --git a/server/djrandom/model/mp3.py b/server/djrandom/model/mp3.py index 10bdfc819b2a80119fe6a9dd332a02216c9d757d..3f70280c30549a664682ad58eb00f017b1d78c26 100644 --- a/server/djrandom/model/mp3.py +++ b/server/djrandom/model/mp3.py @@ -1,9 +1,22 @@ +import os import random -from sqlalchemy.orm import deferred +import shutil +from sqlalchemy.orm import deferred, relationship from sqlalchemy import * from datetime import datetime, timedelta from djrandom.database import Base, Session +# Stage duplicate files to this directory, pending cleanup. +DUPLICATE_DIR = '/var/tmp/djrandom-duplicates' + + +class Fingerprint(Base): + + __tablename__ = 'fingerprints' + + sha1 = Column(String(40), primary_key=True) + echoprint_fp = Column(Text()) + class MP3(Base): """A single MP3. @@ -17,6 +30,7 @@ class MP3(Base): READY = 'R' ERROR = 'E' DUPLICATE = 'D' + BAD_METADATA = 'M' sha1 = Column(String(40), primary_key=True) state = Column(String(1), default=INCOMING, index=True) @@ -25,12 +39,18 @@ class MP3(Base): artist = Column(Unicode(256)) title = Column(Unicode(256)) album = Column(Unicode(256)) + track_num = Column(Integer(3)) genre = Column(Unicode(64)) uploaded_at = Column(DateTime()) play_count = Column(Integer(), default=0) - echoprint_fp = deferred(Column(Text())) duplicate_of = Column(String(40)) + has_fingerprint = Column(Boolean, default=False) + echoprint_fp = relationship(Fingerprint, + primaryjoin=sha1 == Fingerprint.sha1, + foreign_keys=Fingerprint.sha1, + uselist=False) + def __init__(self, **kw): for k, v in kw.items(): setattr(self, k, v) @@ -45,8 +65,46 @@ class MP3(Base): 'uploaded_at': self.uploaded_at} if self.duplicate: data['duplicate_of'] = self.duplicate_of + if self.track_num: + data['track_num'] = self.track_num return data + def mark_as_duplicate(self, duplicate_of): + self.state = self.DUPLICATE + self.duplicate_of = duplicate_of + try: + if not os.path.isdir(DUPLICATE_DIR): + os.makedirs(DUPLICATE_DIR) + shutil.move(self.path, + os.path.join(DUPLICATE_DIR, self.sha1)) + except: + pass + + def get_fingerprint(self): + if self.has_fingerprint: + return self.echoprint_fp.echoprint_fp + + def set_fingerprint(self, fp): + fpobj = Fingerprint.query.get(self.sha1) + if not fpobj: + fpobj = Fingerprint() + fpobj.sha1 = self.sha1 + self.echoprint_fp = fpobj + fpobj.echoprint_fp = fp + self.has_fingerprint = True + Session.add(fpobj) + + @classmethod + def get_with_no_fingerprint(cls): + return cls.query.filter(((cls.state == cls.READY) + | (cls.state == cls.BAD_METADATA)) + & (cls.has_fingerprint == 0)) + + @classmethod + def get_with_bad_metadata(cls): + return cls.query.filter_by(state=cls.BAD_METADATA, + has_fingerprint=1) + @classmethod def last_uploaded(cls, n=10): """Return the N last uploaded songs.""" @@ -54,13 +112,16 @@ class MP3(Base): desc(cls.uploaded_at)).limit(n) @classmethod - def get_random_songs(cls, n=10): + def get_random_songs(cls, n=10, where_clause=None): """Return N completely random songs.""" results = [] - num_songs = cls.query.filter_by(state=cls.READY).count() + if where_clause is None: + where_clause = (cls.state == cls.READY) + num_songs = cls.query.filter(where_clause).count() fraction = float(n) / num_songs + where_clause = where_clause & (func.rand() < fraction) while len(results) < n: - tmprows = Session.query(cls.sha1).filter(func.rand() < fraction).limit(n) + tmprows = Session.query(cls.sha1).filter(where_clause).limit(n) for row in tmprows: results.append(row[0]) return results @@ -70,6 +131,11 @@ class MP3(Base): return cls.query.filter_by( state=cls.READY, artist=artist, album=album) + @classmethod + def never_played(cls, n=10): + """Return N random songs that were never played.""" + return cls.get_random_songs(n, where_clause=(cls.play_count == 0)) + class PlayLog(Base): diff --git a/server/djrandom/model/playlist.py b/server/djrandom/model/playlist.py index 93e1bd4dff6ff87b5652895a35f049c298082303..e4fabd98750d3f9f80cee35d8bbdf8cbabfad619 100644 --- a/server/djrandom/model/playlist.py +++ b/server/djrandom/model/playlist.py @@ -10,6 +10,7 @@ class Playlist(Base): __tablename__ = 'playlists' uuid = Column(String(40), primary_key=True) + title = Column(Unicode(128), index=True) userid = Column(String(40), index=True) modified_at = Column(DateTime()) play_count = Column(Integer(), default=0) @@ -24,7 +25,19 @@ class Playlist(Base): def to_dict(self): return {'uuid': self.uuid, + 'title': self.title, 'songs': self.contents.split(',')} def stream_url(self): return 'http://djrandom.incal.net:8000/%s.mp3' % self.uuid + + @classmethod + def get_all_by_user(cls, userid, public=False): + where = (cls.userid == userid) + if public: + where = where & (cls.title != None) + return cls.query.filter(where).order_by(asc(cls.modified_at)) + + @classmethod + def get_by_title(cls, userid, title): + return cls.query.filter_by(userid=userid, title=title) diff --git a/server/djrandom/model/user.py b/server/djrandom/model/user.py index 27ba32ea2a647a15addb8c05121d988ae3993dd2..3605a3f6cb2a5e8c00454afbcbe7704ac0d50ded 100644 --- a/server/djrandom/model/user.py +++ b/server/djrandom/model/user.py @@ -49,6 +49,7 @@ class User(Base): password = Column(String(128)) created_at = Column(DateTime()) invited_by = Column(Integer()) + invites_left = Column(Integer(3)) active = Column(Boolean()) activation_token = Column(String(40)) @@ -59,6 +60,7 @@ class User(Base): self.activation_token = utils.random_token() self.created_at = datetime.now() self.invited_by = invited_by + self.invites_left = 3 def set_password(self, password): self.password = crypt.crypt(password, _salt()) diff --git a/server/djrandom/scanner/metadata.py b/server/djrandom/scanner/metadata.py index 4895b39049db2c0342af748253f65f2e29534bf9..2ef62620d1e0be0dfc64cf0250e813d289082372 100644 --- a/server/djrandom/scanner/metadata.py +++ b/server/djrandom/scanner/metadata.py @@ -1,9 +1,19 @@ import eyeD3 +import unicodedata import re +# Compiling and including 'unicodedata' apparently makes it possible +# to correctly interpret accented characters in unicode tag metadata. +_nonalpha_pattern = re.compile(r'\W+', re.UNICODE) +_spaces_pattern = re.compile(r'\s+', re.UNICODE) + def normalize_string(s): - return re.sub(r'\W+', ' ', s, re.UNICODE).lower() + s = s.replace('_', ' ') + s = _nonalpha_pattern.sub(' ', s) + s = _spaces_pattern.sub(' ', s) + s = s.lower().strip() + return s def analyze_mp3(path): @@ -16,4 +26,5 @@ def analyze_mp3(path): return {'artist': normalize_string(tag.getArtist()), 'album': normalize_string(tag.getAlbum()), 'title': normalize_string(tag.getTitle()), + 'track_num': tag.getTrackNum()[0], 'genre': unicode(genre)} diff --git a/server/djrandom/scanner/scanner.py b/server/djrandom/scanner/scanner.py index 9fdfddd927bf37541b4fbdf76908513708ad24c1..6617a59b831d78b243951acc29495a05349a9168 100644 --- a/server/djrandom/scanner/scanner.py +++ b/server/djrandom/scanner/scanner.py @@ -13,6 +13,10 @@ from djrandom.scanner import indexer log = logging.getLogger(__name__) +class BadMetadataError(Exception): + pass + + class Scanner(object): def __init__(self, solr_url): @@ -20,6 +24,8 @@ class Scanner(object): def process(self, mp3): mp3_info = metadata.analyze_mp3(mp3.path) + if not mp3_info['artist'] or not mp3_info['title']: + raise BadMetadataError() for key, value in mp3_info.iteritems(): setattr(mp3, key, value) self.idx.add_mp3(mp3) @@ -40,6 +46,9 @@ class Scanner(object): try: self.process(mp3) mp3.state = MP3.READY + except BadMetadataError: + log.info('bad metadata for %s' % mp3.sha1) + mp3.state = MP3.BAD_METADATA except Exception, e: log.error(traceback.format_exc()) mp3.state = MP3.ERROR diff --git a/server/setup.py b/server/setup.py index 4f8a61aa48a58b5e608dfbf7d297bfa9cdfa7e6d..22f935ba5e49ffc9aab03fadb78a55f8d91e405d 100644 --- a/server/setup.py +++ b/server/setup.py @@ -22,6 +22,7 @@ setup( "djrandom-streamer = djrandom.stream.stream:main", "djrandom-frontend = djrandom.frontend.frontend:main", "djrandom-update-markov = djrandom.model.markov:main", + "djrandom-metadata-fixer = djrandom.metadata_fixer.metadata_fixer:main", ], }, )