Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2012-09-03 10:32:09 +02:00
18 changed files with 395 additions and 102 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
*.pyc
/data/
/_source/
/_source/
.project
.pydevproject

View File

@@ -305,7 +305,7 @@ class SourceUpdater(BaseUpdater):
if not os.path.isdir(dirname):
self.makeDir(dirname)
os.rename(fromfile, tofile)
shutil.move(fromfile, tofile)
try:
existing_files.remove(tofile)
except ValueError:

View File

@@ -1,3 +1,4 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
@@ -48,7 +49,12 @@ class Downloader(Plugin):
return is_correct
def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{40})', magnet_link)[0]
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
# Convert base 32 to hex
if len(torrent_hash) == 32:
torrent_hash = b16encode(b32decode(torrent_hash))
url = 'http://torrage.com/torrent/%s.torrent' % torrent_hash
try:

View File

@@ -11,7 +11,9 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent', 'torrent_magnet']
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
if self.isDisabled(manual) or \
(not self.isCorrectType(data.get('type')) or \
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
return
directory = self.conf('directory')

View File

@@ -92,10 +92,9 @@ class Sabnzbd(Downloader):
return False
for slot in history['queue']['slots']:
if slot['cat'] == self.conf('category'):
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
# Go through history items
params = {
@@ -119,44 +118,44 @@ class Sabnzbd(Downloader):
return
for slot in history['history']['slots']:
if slot['category'] == self.conf('category'):
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
if slot['status'] == 'Failed' or slot['fail_message'].strip():
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
# Delete failed download
if self.conf('delete_failed', default = True):
# Delete failed download
if self.conf('delete_failed', default = True):
log.info('%s failed downloading, deleting...', slot['name'])
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
log.info('%s failed downloading, deleting...', slot['name'])
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
return 'failed'
else:
return slot['status'].lower()
return 'failed'
else:
return slot['status'].lower()
return 'not_found'

View File

@@ -2,6 +2,7 @@ from couchpotato.core.logger import CPLog
from string import ascii_letters, digits
from urllib import quote_plus
import re
import traceback
import unicodedata
log = CPLog(__name__)
@@ -30,8 +31,8 @@ def toUnicode(original, *args):
return ek(original, *args)
except:
raise
except UnicodeDecodeError:
log.error('Unable to decode value: %s... ', repr(original)[:20])
except:
log.error('Unable to decode value "%s..." : %s ', (repr(original)[:20], traceback.format_exc()))
ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text)

View File

@@ -314,6 +314,7 @@ class Renamer(Plugin):
break
# Remove files
delete_folders = []
for src in remove_files:
if isinstance(src, File):
@@ -329,13 +330,17 @@ class Renamer(Plugin):
os.remove(src)
parent_dir = os.path.normpath(os.path.dirname(src))
if os.path.isdir(parent_dir) and destination != parent_dir:
self.deleteEmptyFolder(parent_dir, show_error = False)
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir:
delete_folders.append(parent_dir)
except:
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
self.tagDir(group, 'failed_remove')
# Delete leftover folder from older releases
for delete_folder in delete_folders:
self.deleteEmptyFolder(delete_folder, show_error = False)
# Rename all files marked
group['renamed_files'] = []
for src in rename_files:

View File

@@ -23,7 +23,7 @@ class Searcher(Plugin):
in_progress = False
def __init__(self):
addEvent('searcher.all', self.all_movies)
addEvent('searcher.all', self.allMovies)
addEvent('searcher.single', self.single)
addEvent('searcher.correct_movie', self.correctMovie)
addEvent('searcher.download', self.download)
@@ -37,11 +37,11 @@ class Searcher(Plugin):
})
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
fireEvent('schedule.interval', 'searcher.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
def all_movies(self):
def allMovies(self):
if self.in_progress:
log.info('Search already in progress')
@@ -328,10 +328,6 @@ class Searcher(Plugin):
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
return True
# Get the nfo and see if it contains the proper imdb url
if self.checkNFO(nzb['name'], movie['library']['identifier']):
return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False
@@ -406,19 +402,6 @@ class Searcher(Plugin):
return False
def checkNFO(self, check_name, imdb_id):
cache_key = 'srrdb.com %s' % simplifyString(check_name)
nfo = self.getCache(cache_key)
if not nfo:
try:
nfo = self.urlopen('http://www.srrdb.com/showfile.php?release=%s' % check_name, show_error = False)
self.setCache(cache_key, nfo)
except:
pass
return nfo and getImdb(nfo) == imdb_id
def couldBeReleased(self, wanted_quality, dates, pre_releases):
now = int(time.time())
@@ -453,6 +436,8 @@ class Searcher(Plugin):
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
done_status = fireEvent('status.get', 'done', single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id'))
@@ -470,6 +455,13 @@ class Searcher(Plugin):
log.debug('Checking snatched movie: %s' , default_title)
# Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
db.commit()
continue
item = {}
for info in rel.info:
item[info.identifier] = info.value
@@ -520,8 +512,6 @@ class Searcher(Plugin):
ignored_status = fireEvent('status.get', 'ignored', single = True)
try:
movie_dict = fireEvent('movie.get', movie_id, single = True)
db = get_session()
rels = db.query(Release).filter_by(
status_id = snatched_status.get('id'),
@@ -532,6 +522,7 @@ class Searcher(Plugin):
rel.status_id = ignored_status.get('id')
db.commit()
movie_dict = fireEvent('movie.get', movie_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
fireEvent('searcher.single', movie_dict)

View File

@@ -15,7 +15,7 @@ log = CPLog(__name__)
class Userscript(Plugin):
version = 2
version = 3
def __init__(self):
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)

View File

@@ -1,6 +1,7 @@
// ==UserScript==
// @name CouchPotato UserScript
// @description Add movies like a real CouchPotato
// @grant none
// @version {{version}}
// @match {{host}}*
@@ -44,21 +45,19 @@ function create() {
return A;
}
if (typeof GM_addStyle == 'undefined'){
GM_addStyle = function(css) {
var head = document.getElementsByTagName('head')[0],
style = document.createElement('style');
if (!head)
return;
var addStyle = function(css) {
var head = document.getElementsByTagName('head')[0],
style = document.createElement('style');
if (!head)
return;
style.type = 'text/css';
style.textContent = css;
head.appendChild(style);
}
style.type = 'text/css';
style.textContent = css;
head.appendChild(style);
}
// Styles
GM_addStyle('\
addStyle('\
#cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \
#cp_popup.opened { width: 492px; } \
#cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \

View File

@@ -31,7 +31,7 @@ class MovieResultModifier(Plugin):
order.append(imdb)
if item.get('via_imdb'):
if order.index(imdb):
if order.count(imdb):
order.remove(imdb)
order.insert(0, imdb)

View File

@@ -27,7 +27,7 @@ class IMDBAPI(MovieProvider):
name_year = fireEvent('scanner.name_year', q, single = True)
if not q or not name_year.get('name'):
if not q or not name_year or (name_year and not name_year.get('name')):
return []
cache_key = 'imdbapi.cache.%s' % q

View File

@@ -0,0 +1,36 @@
from main import PassThePopcorn
def start():
return PassThePopcorn()
config = [{
'name': 'passthepopcorn',
'groups': [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'PassThePopcorn',
'description': 'See <a href="http://passthepopcorn.me">PassThePopcorn.me</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).',
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
}
],
}]
}]

View File

@@ -0,0 +1,254 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
from dateutil.parser import parse
import cookielib
import htmlentitydefs
import json
import re
import time
import traceback
import urllib2
log = CPLog(__name__)
class PassThePopcorn(TorrentProvider):
urls = {
'domain': 'https://tls.passthepopcorn.me',
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/login.php',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
}
quality_search_params = {
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'},
'720p': {'resolution': '720p'},
'brrip': {'media': 'Blu-ray'},
'dvdr': {'resolution': 'anysd'},
'dvdrip': {'media': 'DVD'},
'scr': {'media': 'DVD-Screener'},
'r5': {'media': 'R5'},
'tc': {'media': 'TC'},
'ts': {'media': 'TS'},
'cam': {'media': 'CAM'}
}
post_search_filters = {
'bd50': {'Codec': ['BD50']},
'1080p': {'Resolution': ['1080p']},
'720p': {'Resolution': ['720p']},
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
'dvdr': {'Codec': ['DVD5', 'DVD9']},
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
'scr': {'Source': ['DVD-Screener']},
'r5': {'Source': ['R5']},
'tc': {'Source': ['TC']},
'ts': {'Source': ['TS']},
'cam': {'Source': ['CAM']}
}
class NotLoggedInHTTPError(urllib2.HTTPError):
def __init__(self, url, code, msg, headers, fp):
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
log.debug("302 detected; redirected to %s" % headers['Location'])
if (headers['Location'] != 'login.php'):
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
else:
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
def search(self, movie, quality):
results = []
if self.isDisabled():
return results
movie_title = getTitle(movie['library'])
quality_id = quality['identifier']
log.info('Searching for %s at quality %s' % (movie_title, quality_id))
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
'order_by': 'relevance',
'order_way': 'descending',
'searchstr': movie['library']['identifier']
})
# Do login for the cookies
if not self.login_opener and not self.login():
return results
try:
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
txt = self.urlopen(url, opener = self.login_opener)
res = json.loads(txt)
except:
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)' % params)
return []
try:
if not 'Movies' in res:
log.info("PTP search returned nothing for '%s' at quality '%s' with search parameters %s" % (movie_title, quality_id, params))
return []
authkey = res['AuthKey']
passkey = res['PassKey']
for ptpmovie in res['Movies']:
if not 'Torrents' in ptpmovie:
log.debug('Movie %s (%s) has NO torrents' % (ptpmovie['Title'], ptpmovie['Year']))
continue
log.debug('Movie %s (%s) has %d torrents' % (ptpmovie['Title'], ptpmovie['Year'], len(ptpmovie['Torrents'])))
for torrent in ptpmovie['Torrents']:
torrent_id = tryInt(torrent['Id'])
torrentdesc = '%s %s %s' % (torrent['Resolution'], torrent['Source'], torrent['Codec'])
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
torrentdesc += ' HQ'
if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene'
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
# eliminate odd characters...
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
torrentdesc += ' (%s)' % quality_id
torrent_name = re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) - %s' % (movie_title, ptpmovie['Year'], torrentdesc))
def extra_check(item):
return self.torrentMeetsQualitySpec(item, type)
def extra_score(item):
return 50 if torrent['GoldenPopcorn'] else 0
new = {
'id': torrent_id,
'type': 'torrent',
'provider': self.getName(),
'name': torrent_name,
'description': '',
'url': '%s?action=download&id=%d&authkey=%s&torrent_pass=%s' % (self.urls['torrent'], torrent_id, authkey, passkey),
'detail_url': self.urls['detail'] % torrent_id,
'date': tryInt(time.mktime(parse(torrent['UploadTime']).timetuple())),
'size': tryInt(torrent['Size']) / (1024 * 1024),
'provider': self.getName(),
'seeders': tryInt(torrent['Seeders']),
'leechers': tryInt(torrent['Leechers']),
'extra_score': extra_score,
'extra_check': extra_check,
'download': self.loginDownload,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
if fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality):
results.append(new)
self.found(new)
return results
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
return []
def login(self):
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
opener.addheaders = [
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-gb,en;q=0.5'),
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
('Keep-Alive', '115'),
('Connection', 'keep-alive'),
('Cache-Control', 'max-age=0'),
]
try:
response = opener.open(self.urls['login'], self.getLoginParams())
except urllib2.URLError as e:
log.error('Login to PassThePopcorn failed: %s' % e)
return False
if response.getcode() == 200:
log.debug('Login HTTP status 200; seems successful')
self.login_opener = opener
return True
else:
log.error('Login to PassThePopcorn failed: returned code %d' % response.getcode())
return False
def torrentMeetsQualitySpec(self, torrent, quality):
if not quality in self.post_search_filters:
return True
for field, specs in self.post_search_filters[quality].items():
matches_one = False
seen_one = False
if not field in torrent:
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"' % (torrent['Id'], field, quality))
continue
for spec in specs:
if len(spec) > 0 and spec[0] == '!':
# a negative rule; if the field matches, return False
if torrent[field] == spec[1:]:
return False
else:
# a positive rule; if any of the possible positive values match the field, return True
seen_one = True
if torrent[field] == spec:
matches_one = True
if seen_one and not matches_one:
return False
return True
def htmlToUnicode(self, text):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
# character reference
try:
if text[:3] == "&#x":
return unichr(int(text[3:-1], 16))
else:
return unichr(int(text[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
return re.sub("&#?\w+;", fixup, u'%s' % text)
def unicodeToASCII(self, text):
import unicodedata
return ''.join(c for c in unicodedata.normalize('NFKD', text) if unicodedata.category(c) != 'Mn')
def htmlToASCII(self, text):
return self.unicodeToASCII(self.htmlToUnicode(text))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'keeplogged': '1',
'login': 'Login'
})

View File

@@ -37,6 +37,8 @@ class ThePirateBay(TorrentProvider):
'https://piratereverse.info',
'https://tpb.pirateparty.org.uk',
'https://argumentomteemigreren.nl',
'https://livepirate.com/',
'https://www.getpirate.com/',
]
def __init__(self):

View File

@@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits
from string import digits, ascii_letters
import re
log = CPLog(__name__)
@@ -46,7 +46,7 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library'])
url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name}))
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try:
@@ -100,7 +100,7 @@ class HDTrailers(TrailerProvider):
return results
def movieUrlName(self, string):
safe_chars = letters + digits + ' '
safe_chars = ascii_letters + digits + ' '
r = ''.join([char if char in safe_chars else ' ' for char in string])
name = re.sub('\s+' , '-', r).lower()

View File

@@ -24,39 +24,36 @@
}
.page.settings .tabs a {
display: block;
padding: 11px 15px;
padding: 7px 15px;
font-weight: normal;
transition: all 0.1s ease-in-out;
transition: all 0.3s ease-in-out;
color: rgba(255, 255, 255, 0.8);
text-shadow: none;
}
.page.settings .tabs a:hover, .page.settings .tabs .active a {
.page.settings .tabs a:hover,
.page.settings .tabs .active a {
background: rgb(78, 89, 105);
font-weight: bold;
font-size: 25px;
color: #fff;
}
.page.settings .tabs > li {
border-bottom: 1px solid rgb(78, 89, 105);
}
.page.settings .tabs .subtabs {
list-style: none;
padding: 0;
overflow: hidden;
transition: all 1s ease-in-out;
max-height: 0;
margin: -5px 0 10px;
}
.page.settings .tabs > .active .subtabs {
max-height: 300px;
}
.page.settings .tabs .subtabs a {
font-size: 15px;
padding: 1px 15px;
font-size: 13px;
padding: 0 15px;
font-weight: normal;
color: rgba(255, 255, 255, 0.8);
background: rgba(78, 89, 105, 0.4);
transition: all .3s ease-in-out;
color: rgba(255, 255, 255, 0.7);
}
.page.settings .tabs .subtabs .active a {
font-weight: bold;
color: #fff;
background: rgb(78, 89, 105);
}

View File

@@ -43,7 +43,7 @@
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
<script type="text/javascript">
@@ -51,7 +51,6 @@
new Uniform();
Api.setup({
'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }},
'url': {{ url_for('api.index')|tojson|safe }},
'path_sep': {{ sep|tojson|safe }},
'is_remote': false