This commit is contained in:
Ruud Burger committed 2011-08-17 14:00:56 +02:00
commit a1a814c153
33 files changed
+708 -365

No files matched your search

+2 -1
View File
@@ -48,6 +48,7 @@ def cmd_couchpotato(base_path, args):
Env.get('settings').setFile(os.path.join(options.data_dir, 'settings.conf')) Env.get('settings').setFile(os.path.join(options.data_dir, 'settings.conf'))
Env.set('app_dir', base_path) Env.set('app_dir', base_path)
Env.set('data_dir', options.data_dir) Env.set('data_dir', options.data_dir)
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db')) Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db'))
Env.set('cache_dir', os.path.join(options.data_dir, 'cache')) Env.set('cache_dir', os.path.join(options.data_dir, 'cache'))
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python'))) Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
@@ -73,7 +74,7 @@ def cmd_couchpotato(base_path, args):
logger.addHandler(hdlr) logger.addHandler(hdlr)
# To file # To file
hdlr2 = handlers.RotatingFileHandler(os.path.join(log_dir, 'CouchPotato.log'), 'a', 5000000, 4) hdlr2 = handlers.RotatingFileHandler(Env.get('log_path'), 'a', 500000, 10)
hdlr2.setFormatter(formatter) hdlr2.setFormatter(formatter)
logger.addHandler(hdlr2) logger.addHandler(hdlr2)
@@ -13,7 +13,7 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent'] type = ['nzb', 'torrent']
def download(self, data = {}): def download(self, data = {}, movie = {}):
if self.isDisabled() or not self.isCorrectType(data.get('type')): if self.isDisabled() or not self.isCorrectType(data.get('type')):
return return
@@ -23,7 +23,8 @@ class Blackhole(Downloader):
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.' % data.get('type')) log.error('No directory set for blackhole %s download.' % data.get('type'))
else: else:
fullPath = os.path.join(directory, toSafeString(data.get('name')) + '.' + data.get('type')) cp_tag = '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
fullPath = os.path.join(directory, '%s%s.%s' % (toSafeString(data.get('name')), cp_tag , data.get('type')))
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
@@ -26,6 +26,11 @@ config = [{
'label': 'Api Key', 'label': 'Api Key',
'description': 'Used for all calls to Sabnzbd.', 'description': 'Used for all calls to Sabnzbd.',
}, },
{
'name': 'category',
'label': 'Category',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
},
{ {
'advanced': True, 'advanced': True,
'name': 'pp_directory', 'name': 'pp_directory',
+8 -5
View File
@@ -14,7 +14,7 @@ class Sabnzbd(Downloader):
type = ['nzb'] type = ['nzb']
def download(self, data = {}): def download(self, data = {}, movie = {}):
if self.isDisabled() or not self.isCorrectType(data.get('type')): if self.isDisabled() or not self.isCorrectType(data.get('type')):
return return
@@ -34,11 +34,14 @@ class Sabnzbd(Downloader):
else: else:
pp = False pp = False
cp_tag = '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
params = { params = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'name': data.get('url') 'name': data.get('url'),
'nzbname': '%s%s' % (data.get('name'), cp_tag),
} }
# sabNzbd complains about "invalid archive file" for newzbin urls # sabNzbd complains about "invalid archive file" for newzbin urls
@@ -53,9 +56,9 @@ class Sabnzbd(Downloader):
log.info("URL: " + url) log.info("URL: " + url)
try: try:
r = urllib2.urlopen(url, timeout = 30) r = urllib2.urlopen(url)
except: except Exception, e:
log.error("Unable to connect to SAB.") log.error("Unable to connect to SAB: %s" % e)
return False return False
result = r.read().strip() result = r.read().strip()
+1 -1
View File
@@ -44,7 +44,7 @@ def fireEvent(name, *args, **kwargs):
if single and not merge: if single and not merge:
results = None results = None
if result[0][0] == True and result[0][1]: if result[0][0] is True and result[0][1] is not None:
results = result[0][1] results = result[0][1]
elif result[0][1]: elif result[0][1]:
errorHandler(result[0][1]) errorHandler(result[0][1])
+8 -6
View File
@@ -13,9 +13,9 @@ class CoreNotifier(Plugin):
messages = [] messages = []
def __init__(self): def __init__(self):
addEvent('notify', self.notify) addEvent('notify', self.notify)
addEvent('notify.core_notifier', self.notify) addEvent('notify.core', self.frontend)
addEvent('core_notifier.frontend', self.frontend)
addApiView('core_notifier.listener', self.listener) addApiView('core_notifier.listener', self.listener)
@@ -29,7 +29,6 @@ class CoreNotifier(Plugin):
}) })
def frontend(self, type = 'notification', data = {}): def frontend(self, type = 'notification', data = {}):
self.messages.append({ self.messages.append({
'time': time.time(), 'time': time.time(),
'type': type, 'type': type,
@@ -38,12 +37,15 @@ class CoreNotifier(Plugin):
def listener(self): def listener(self):
messages = []
for message in self.messages: for message in self.messages:
print message['time'], (time.time() - 5)
#delete message older then 15s #delete message older then 15s
if message['time'] < (time.time() - 15): if message['time'] > (time.time() - 15):
del message messages.append(message)
self.messages = []
return jsonified({ return jsonified({
'success': True, 'success': True,
'result': self.messages, 'result': messages,
}) })
@@ -7,7 +7,7 @@ var NotificationBase = new Class({
var self = this; var self = this;
self.setOptions(options); self.setOptions(options);
//App.addEvent('load', self.request.bind(self)); App.addEvent('load', self.request.bind(self));
self.addEvent('notification', self.notify.bind(self)) self.addEvent('notification', self.notify.bind(self))
@@ -33,7 +33,7 @@ var NotificationBase = new Class({
var self = this; var self = this;
Array.each(json.result, function(result){ Array.each(json.result, function(result){
self.fireEvent(result.type, result.data) App.fireEvent(result.type, result.data)
}) })
} }
+5 -4
View File
@@ -96,9 +96,10 @@ class LibraryPlugin(Plugin):
library.files.append(file) library.files.append(file)
db.commit() db.commit()
except: except:
pass log.debug('Failed to attach to library: %s' % traceback.format_exc())
#log.debug('Failed to attach to library: %s' % traceback.format_exc())
fireEvent('library.update.after') library_dict = library.to_dict({'titles': {}, 'files':{}})
return library.to_dict({'titles': {}, 'files':{}}) fireEvent('notify.core', type = 'library.update', data = library_dict)
return library_dict
+6
View File
@@ -0,0 +1,6 @@
from .main import Logging
def start():
return Logging()
config = []
+30
View File
@@ -0,0 +1,30 @@
from couchpotato.api import addApiView
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
class Logging(Plugin):
def __init__(self):
addApiView('logging.get', self.get)
def get(self):
nr = int(getParam('nr', 0))
path = '%s%s' % (Env.get('log_path'), '.%s' % nr if nr > 0 else '')
# Reverse
f = open(path, 'r')
lines = []
for line in f.readlines():
lines.insert(0, line)
log = ''
for line in lines:
log += line
return jsonified({
'success': True,
'log': log,
})
+3 -3
View File
@@ -38,7 +38,7 @@ class MoviePlugin(Plugin):
movies = [] movies = []
for movie in results: for movie in results:
temp = movie.to_dict(deep = { temp = movie.to_dict(deep = {
'releases': {'status': {}, 'quality': {}, 'files':{}}, 'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {} 'files': {}
}) })
@@ -68,7 +68,7 @@ class MoviePlugin(Plugin):
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(deep = { fireEventAsync('searcher.single', movie.to_dict(deep = {
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {} 'files': {}
})) }))
@@ -119,7 +119,7 @@ class MoviePlugin(Plugin):
db.commit() db.commit()
movie_dict = m.to_dict(deep = { movie_dict = m.to_dict(deep = {
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
'library': {'titles': {}} 'library': {'titles': {}}
}) })
+37 -23
View File
@@ -3,10 +3,11 @@ var MovieList = new Class({
Implements: [Options], Implements: [Options],
options: { options: {
navigation: true navigation: false
}, },
movies: [], movies: [],
letters: {},
initialize: function(options){ initialize: function(options){
var self = this; var self = this;
@@ -19,6 +20,8 @@ var MovieList = new Class({
create: function(){ create: function(){
var self = this; var self = this;
self.el.empty();
// Create the alphabet nav // Create the alphabet nav
if(self.options.navigation) if(self.options.navigation)
self.createNavigation(); self.createNavigation();
@@ -29,14 +32,19 @@ var MovieList = new Class({
}, info); }, info);
$(m).inject(self.el); $(m).inject(self.el);
m.fireEvent('injected'); m.fireEvent('injected');
if(self.options.navigation){
var first_char = m.getTitle().substr(0, 1);
self.activateLetter(first_char);
}
}); });
self.el.addEvents({ self.el.addEvents({
'mouseenter:relay(.movie)': function(e, el){ 'mouseenter:relay(.movie)': function(e, el){
el.addClass('hover') el.addClass('hover');
}, },
'mouseleave:relay(.movie)': function(e, el){ 'mouseleave:relay(.movie)': function(e, el){
el.removeClass('hover') el.removeClass('hover');
} }
}); });
}, },
@@ -44,7 +52,6 @@ var MovieList = new Class({
createNavigation: function(){ createNavigation: function(){
var self = this; var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
var selected = 'Z';
self.navigation = new Element('div.alph_nav').adopt( self.navigation = new Element('div.alph_nav').adopt(
self.alpha = new Element('ul.inlay'), self.alpha = new Element('ul.inlay'),
@@ -54,32 +61,39 @@ var MovieList = new Class({
new Element('li.thumbnails'), new Element('li.thumbnails'),
new Element('li.text') new Element('li.text')
) )
).inject(this.el, 'top') ).inject(this.el, 'top');
chars.split('').each(function(c){ chars.split('').each(function(c){
new Element('li', { self.letters[c] = new Element('li', {
'text': c, 'text': c,
'class': c == selected ? 'selected' : '' 'class': 'letter_'+c
}).inject(self.alpha) }).inject(self.alpha);
}) });
}, },
activateLetter: function(letter){
this.letters[letter].addClass('active');
},
getMovies: function(status, onComplete){ update: function(){
var self = this var self = this;
if(self.movies.length == 0) self.getMovies();
Api.request('movie.list', { },
'data': {
'status': self.options.status getMovies: function(){
}, var self = this;
'onComplete': function(json){
self.store(json.movies); Api.request('movie.list', {
self.create(); 'data': {
} 'status': self.options.status
}) },
else 'onComplete': function(json){
self.list() self.store(json.movies);
self.create();
}
});
}, },
store: function(movies){ store: function(movies){
@@ -159,7 +159,11 @@
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
margin: 0 -1px 0 0; margin: 0 -1px 0 0;
color: #666;
} }
.movies .alph_nav li.active {
color: #fff;
}
.movies .alph_nav li:hover, .movies .alph_nav li.onlay { .movies .alph_nav li:hover, .movies .alph_nav li.onlay {
font-weight: bold; font-weight: bold;
+46 -3
View File
@@ -127,7 +127,8 @@ var MovieAction = new Class({
self.movie = movie; self.movie = movie;
self.create(); self.create();
self.el.addClass(self.class_name) if(self.el)
self.el.addClass(self.class_name)
}, },
create: function(){}, create: function(){},
@@ -141,7 +142,7 @@ var MovieAction = new Class({
}, },
toElement: function(){ toElement: function(){
return this.el return this.el || null
} }
}); });
@@ -173,4 +174,46 @@ var IMDBAction = new Class({
window.open('http://www.imdb.com/title/'+self.id+'/'); window.open('http://www.imdb.com/title/'+self.id+'/');
} }
}) });
var ReleaseAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.releases', {
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
'events': {
'click': self.show.bind(self)
}
});
},
show: function(e){
var self = this;
(e).stop();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
self.release_container = new Element('div.releases')
).inject(self.movie, 'top');
Array.each(self.movie.data.releases, function(release){
p(release);
new Element('div', {
'text': release.title
}).inject(self.release_container)
});
}
self.movie.slide('in');
},
});
+1 -1
View File
@@ -13,7 +13,7 @@ log = CPLog(__name__)
class QualityPlugin(Plugin): class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['1080p', 'bd25'], 'allow': [], 'ext':[], 'tags': ['x264', 'h264', 'blu ray']}, {'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']}, {'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, {'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
@@ -0,0 +1,6 @@
from .main import Release
def start():
return Release()
config = []
+75
View File
@@ -0,0 +1,75 @@
from couchpotato import get_session
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie
from sqlalchemy.sql.expression import and_, or_
log = CPLog(__name__)
class Release(Plugin):
def __init__(self):
addEvent('release.add', self.add)
def add(self, group):
db = get_session()
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
# Add movie
done_status = fireEvent('status.get', 'done', single = True)
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not movie:
movie = Movie(
library_id = group['library'].get('id'),
profile_id = 0,
status_id = done_status.get('id')
)
db.add(movie)
db.commit()
# Add release
snatched_status = fireEvent('status.get', 'snatched', single = True)
release = db.query(Release).filter(
or_(
Release.identifier == identifier,
and_(Release.identifier.startswith(group['library']['identifier'], Release.status_id == snatched_status.get('id')))
)
).first()
if not release:
release = Release(
identifier = identifier,
movie = movie,
quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
db.add(release)
db.commit()
# Add each file type
for type in group['files']:
for file in group['files'][type]:
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
release.files.append(added_file)
db.commit()
except Exception, e:
log.debug('Failed to attach "%s" to release: %s' % (file, e))
db.remove()
def saveFile(self, file, type = 'unknown', include_media_info = False):
properties = {}
# Get media info for files
if include_media_info:
properties = {}
# Check database and update/insert if necessary
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
+31 -20
View File
@@ -18,13 +18,14 @@ class Renamer(Plugin):
def __init__(self): def __init__(self):
addEvent('renamer.scan', self.scan) addEvent('renamer.scan', self.scan)
#addEvent('app.load', self.scan) addEvent('app.load', self.scan)
#fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every')) fireEvent('schedule.interval', 'renamer.scan', self.scan, minutes = self.conf('run_every'))
def scan(self): def scan(self):
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True) groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
if groups is None: return
destination = self.conf('to') destination = self.conf('to')
folder_name = self.conf('folder_name') folder_name = self.conf('folder_name')
@@ -151,42 +152,52 @@ class Renamer(Plugin):
# Before renaming, remove the lower quality files # Before renaming, remove the lower quality files
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True)
for movie in library.movies: for movie in library.movies:
for release in movie.releases: for release in movie.releases:
if release.quality.order < group['meta_data']['quality']['order']: if release.quality.order < group['meta_data']['quality']['order']:
log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) log.info('Removing older release for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
elif release.quality.order is group['meta_data']['quality']['order']: elif release.status_id is done_status.get('id'):
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label)) if release.quality.order is group['meta_data']['quality']['order']:
else: log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label))
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) else:
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label))
# Add _EXISTS_ to the parent dir # Add _EXISTS_ to the parent dir
if group['dirname']: if group['dirname']:
for rename_me in rename_files: # Don't rename anything in this group for rename_me in rename_files: # Don't rename anything in this group
rename_files[rename_me] = None rename_files[rename_me] = None
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_EXISTS_%s' % group['dirname']) rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_EXISTS_%s' % group['dirname'])
else: # Add it to filename else: # Add it to filename
for rename_me in rename_files: for rename_me in rename_files:
filename = os.path.basename(rename_me) filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename) rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
break break
for file in release.files: for file in release.files:
log.info('Removing "%s"' % file.path) log.info('Removing "%s"' % file.path)
# Rename # Rename
for rename_me in rename_files: for src in rename_files:
if rename_files[rename_me]: if rename_files[src]:
log.info('Renaming "%s" to "%s"' % (rename_me, rename_files[rename_me]))
path = os.path.dirname(rename_files[rename_me]) dst = rename_files[src]
log.info('Renaming "%s" to "%s"' % (src, dst))
path = os.path.dirname(dst)
try: try:
if not os.path.isdir(path): os.makedirs(path) if not os.path.isdir(path): os.makedirs(path)
except: except:
log.error('Failed creating dir %s: %s' % (path, traceback.format_exc())) log.error('Failed creating dir %s: %s' % (path, traceback.format_exc()))
continue continue
try:
shutil.move(src, dst)
except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
#print rename_me, rename_files[rename_me] #print rename_me, rename_files[rename_me]
# Search for trailers # Search for trailers
+6 -61
View File
@@ -7,7 +7,7 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File, Release, Movie from couchpotato.core.settings.model import File, Release, Movie
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import json from flask.helpers import json
from themoviedb.tmdb import opensubtitleHashFile from sqlalchemy.sql.expression import and_, or_
import os import os
import re import re
import subprocess import subprocess
@@ -70,6 +70,7 @@ class Scanner(Plugin):
def __init__(self): def __init__(self):
#addEvent('app.load', self.scanLibrary) #addEvent('app.load', self.scanLibrary)
addEvent('scanner.create_file_identifier', self.createStringIdentifier)
addEvent('scanner.scan', self.scan) addEvent('scanner.scan', self.scan)
@@ -95,7 +96,7 @@ class Scanner(Plugin):
#library = db.query(Library).filter_by(id = library.get('id')).one() #library = db.query(Library).filter_by(id = library.get('id')).one()
# Add release # Add release
self.addRelease(group) fireEvent('release.add', group = group)
# Add identifier for library update # Add identifier for library update
update_after.append(group['library'].get('identifier')) update_after.append(group['library'].get('identifier'))
@@ -133,7 +134,7 @@ class Scanner(Plugin):
is_dvd_file = self.isDVDFile(file_path) is_dvd_file = self.isDVDFile(file_path)
if os.path.getsize(file_path) > self.minimal_filesize['media'] or is_dvd_file: # Minimal 300MB files or is DVD file if os.path.getsize(file_path) > self.minimal_filesize['media'] or is_dvd_file: # Minimal 300MB files or is DVD file
identifier = self.createFileIdentifier(file_path, folder, exclude_filename = is_dvd_file) identifier = self.createStringIdentifier(file_path, folder, exclude_filename = is_dvd_file)
if not movie_files.get(identifier): if not movie_files.get(identifier):
movie_files[identifier] = { movie_files[identifier] = {
@@ -221,51 +222,6 @@ class Scanner(Plugin):
return movie_files return movie_files
def addRelease(self, group):
db = get_session()
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
# Add movie
done_status = fireEvent('status.get', 'done', single = True)
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not movie:
movie = Movie(
library_id = group['library'].get('id'),
profile_id = 0,
status_id = done_status.get('id')
)
db.add(movie)
db.commit()
# Add release
release = db.query(Release).filter_by(identifier = identifier).first()
if not release:
release = Release(
identifier = identifier,
movie = movie,
quality_id = group['meta_data']['quality'].get('id'),
status_id = done_status.get('id')
)
db.add(release)
db.commit()
# Add each file type
for type in group['files']:
for file in group['files'][type]:
added_file = self.saveFile(file, type = type, include_media_info = type is 'movie')
try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one()
release.files.append(added_file)
db.commit()
except Exception, e:
log.debug('Failed to attach "%s" to release: %s' % (file, e))
db.remove()
def getMetaData(self, group): def getMetaData(self, group):
data = {} data = {}
@@ -374,17 +330,6 @@ class Scanner(Plugin):
log.error('No imdb_id found for %s.' % group['identifiers']) log.error('No imdb_id found for %s.' % group['identifiers'])
return {} return {}
def saveFile(self, file, type = 'unknown', include_media_info = False):
properties = {}
# Get media info for files
if include_media_info:
properties = {}
# Check database and update/insert if necessary
return fireEvent('file.add', path = file, part = self.getPartNumber(file), type = self.file_types[type], properties = properties, single = True)
def getCPImdb(self, string): def getCPImdb(self, string):
try: try:
@@ -501,9 +446,9 @@ class Scanner(Plugin):
return False return False
def getGroupFiles(self, identifier, folder, file_pile): def getGroupFiles(self, identifier, folder, file_pile):
return set(filter(lambda s:identifier in self.createFileIdentifier(s, folder), file_pile)) return set(filter(lambda s:identifier in self.createStringIdentifier(s, folder), file_pile))
def createFileIdentifier(self, file_path, folder, exclude_filename = False): def createStringIdentifier(self, file_path, folder = '', exclude_filename = False):
identifier = file_path.replace(folder, '') # root folder identifier = file_path.replace(folder, '') # root folder
identifier = os.path.splitext(identifier)[0] # ext identifier = os.path.splitext(identifier)[0] # ext
+9 -11
View File
@@ -1,14 +1,16 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
name_scores = [ name_scores = [
'proper:2', 'repack:2', 'proper:5', 'repack:5',
'unrated:1', 'unrated:1',
'x264:1', 'x264:1', 'h264:1',
'DTS:4', 'AC3:2', 'DTS:4', 'AC3:2',
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', '720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
'metis:1', 'diamond:1', 'wiki:1', 'CBGB:1', 'imbt:1', 'cocain:1', 'vomit:1', 'fico:1', 'arrow:1', 'pukka:1', 'prism:1', 'devise:1', 'esir:1',
'metis:1', 'diamond:1', 'wiki:1', 'cbgb:1', 'crossbow:1', 'sinners:1', 'amiable:1', 'refined:1', 'twizted:1', 'felony:1', 'hubris:1', 'machd:1',
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10' 'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10'
] ]
@@ -40,12 +42,8 @@ def nameScore(name, year):
def nameRatioScore(nzb_name, movie_name): def nameRatioScore(nzb_name, movie_name):
nzb_words = re.split('\W+', simplifyString(nzb_name)) nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
movie_words = re.split('\W+', simplifyString(movie_name)) movie_words = re.split('\W+', simplifyString(movie_name))
# Replace .,-_ with space left_over = set(nzb_words) - set(movie_words)
left_over = len(nzb_words) - len(movie_words) return 10 - len(left_over)
if 2 <= left_over <= 6:
return 4
else:
return 0
+58 -17
View File
@@ -1,9 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
@@ -19,7 +20,7 @@ class Searcher(Plugin):
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.all, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
#addEvent('app.load', self.all) addEvent('app.load', self.all)
def all(self): def all(self):
@@ -29,48 +30,88 @@ class Searcher(Plugin):
Movie.status.has(identifier = 'active') Movie.status.has(identifier = 'active')
).all() ).all()
snatched_status = fireEvent('status.get', 'snatched', single = True)
for movie in movies: for movie in movies:
success = self.single(movie.to_dict(deep = { self.single(movie.to_dict(deep = {
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'files': {} 'files': {}
})) }))
# Mark as snatched on success
if success:
movie.status_id = snatched_status.get('id')
db.commit()
def single(self, movie): def single(self, movie):
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
available_status = fireEvent('status.get', 'available', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = False successful = False
for type in movie['profile']['types']: for type in movie['profile']['types']:
print type
has_better_quality = False has_better_quality = 0
default_title = movie['library']['titles'][0]['title']
# See if beter quality is available # See if beter quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] <= type['quality']['order']: if release['quality']['order'] <= type['quality']['order'] and release['status_id'] is not available_status.get('id'):
has_better_quality = True has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
if not has_better_quality: if has_better_quality is 0:
log.info('Search for %s in %s' % (movie['library']['titles'][0]['title'], type['quality']['label'])) log.info('Search for %s in %s' % (default_title, type['quality']['label']))
results = fireEvent('provider.yarr.search', movie, type['quality'], merge = True) results = fireEvent('provider.yarr.search', movie, type['quality'], merge = True)
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
# Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
successful = fireEvent('download', data = nzb, single = True) db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
if not rls:
rls = Release(
identifier = md5(nzb['url']),
movie_id = movie.get('id'),
quality_id = type.get('quality_id'),
status_id = available_status.get('id')
)
db.add(rls)
db.commit()
for info in nzb:
rls_info = ReleaseInfo(
identifier = info,
value = nzb[info]
)
rls.info.append(rls_info)
db.commit()
for nzb in sorted_results:
successful = fireEvent('download', data = nzb, movie = movie, single = True)
if successful: if successful:
log.info('Downloading of %s successful.' % nzb.get('name')) log.info('Downloading of %s successful.' % nzb.get('name'))
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
# Mark movie snatched if quality is finish-checked
if type['finish']:
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = snatched_status.get('id')
db.commit()
return True return True
return False
else:
log.info('Better quality (%s) already available or snatched for %s' % (type['quality']['label'], default_title))
break
return False return False
+1 -1
View File
@@ -118,7 +118,7 @@ class YarrProvider(Provider):
if identifier in qualities: if identifier in qualities:
return ids return ids
return False return [self.cat_backup_id]
def found(self, new): def found(self, new):
log.info('Found: score(%(score)s): %(name)s' % new) log.info('Found: score(%(score)s): %(name)s' % new)
@@ -54,7 +54,7 @@ class Newznab(NZBProvider, RSS):
}) })
url = "%s&%s" % (self.getUrl(self.urls['search']), arguments) url = "%s&%s" % (self.getUrl(self.urls['search']), arguments)
cache_key = '%s-%s' % (movie['library']['identifier'], cat_id[0]) cache_key = 'newznab.%s.%s' % (movie['library']['identifier'], cat_id[0])
single_cat = (len(cat_id) == 1 and cat_id[0] != self.cat_backup_id) single_cat = (len(cat_id) == 1 and cat_id[0] != self.cat_backup_id)
try: try:
@@ -0,0 +1,21 @@
from .main import NzbIndex
def start():
return NzbIndex()
config = [{
'name': 'nzbindex',
'groups': [
{
'tab': 'providers',
'name': 'nzbindex',
'description': 'Free provider, but less accurate.',
'options': [
{
'name': 'enabled',
'type': 'enabler',
},
],
},
],
}]
@@ -0,0 +1,97 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import NZBProvider
from dateutil.parser import parse
from urllib import urlencode
from urllib2 import URLError
import time
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS):
urls = {
'download': 'http://www.nzbindex.nl/download/%s/%s',
'api': 'http://www.nzbindex.nl/rss/', #http://www.nzbindex.nl/rss/?q=due+date+720p&age=1000&sort=agedesc&minsize=3500&maxsize=10000
}
time_between_searches = 1 # Seconds
def __init__(self):
addEvent('provider.nzb.search', self.search)
addEvent('provider.yarr.search', self.search)
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['api']):
return results
arguments = urlencode({
'q': '%s %s' % (simplifyString(movie['library']['titles'][0]['title']), quality.get('identifier')),
'sort': 'agedesc',
'minsize': quality.get('size_min'),
'maxsize': quality.get('size_max'),
'rating': '1',
})
url = "%s?%s" % (self.urls['api'], arguments)
cache_key = 'nzbindex.%s.%s' % (movie['library'].get('identifier'), quality.get('identifier'))
try:
data = self.getCache(cache_key)
if not data:
data = self.urlopen(url)
self.setCache(cache_key, data)
except (IOError, URLError):
log.error('Failed to open %s.' % url)
return results
if data:
try:
try:
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
return results
for nzb in nzbs:
enclosure = self.getElements(nzb, 'enclosure')[0].attrib
id = int(self.getTextElement(nzb, "link").split('/')[4])
new = {
'id': id,
'type': 'nzb',
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': enclosure['length'],
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from NZBMatrix.com')
return results
def isEnabled(self):
return NZBProvider.isEnabled(self) and self.conf('enabled')
@@ -53,7 +53,7 @@ class NZBMatrix(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments) url = "%s?%s" % (self.urls['search'], arguments)
log.info('Searching: %s' % url) log.info('Searching: %s' % url)
cache_key = '%s-%s' % (movie['library'].get('identifier'), cat_ids) cache_key = 'nzbmatrix.%s.%s' % (movie['library'].get('identifier'), cat_ids)
single_cat = True single_cat = True
try: try:
+3 -2
View File
@@ -50,7 +50,7 @@ class Nzbs(NZBProvider, RSS):
}) })
url = "%s?%s" % (self.urls['api'], arguments) url = "%s?%s" % (self.urls['api'], arguments)
cache_key = '%s-%s' % (movie['library'].get('identifier'), str(cat_id)) cache_key = 'nzbs.%s.%s' % (movie['library'].get('identifier'), str(cat_id))
try: try:
data = self.getCache(cache_key) data = self.getCache(cache_key)
@@ -72,8 +72,9 @@ class Nzbs(NZBProvider, RSS):
for nzb in nzbs: for nzb in nzbs:
id = int(self.getTextElement(nzb, "link").partition('nzbid=')[2])
new = { new = {
'id': int(self.getTextElement(nzb, "link").partition('nzbid=')[2]), 'id': id,
'type': 'nzb', 'type': 'nzb',
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
+1
View File
@@ -19,6 +19,7 @@ class Env:
_data_dir = "" _data_dir = ""
_cache_dir = "" _cache_dir = ""
_db_path = "" _db_path = ""
_log_path = ""
@staticmethod @staticmethod
def doDebug(): def doDebug():
+20 -1
View File
@@ -3,6 +3,25 @@ Page.Log = new Class({
Extends: PageBase, Extends: PageBase,
name: 'log', name: 'log',
title: 'Show recent logs.' title: 'Show recent logs.',
indexAction: function(){
var self = this;
if(self.log) self.log.destroy();
self.log = new Element('div.log', {
'text': 'loading...'
}).inject(self.el)
Api.request('logging.get', {
'data': {
'nr': 0
},
'onComplete': function(json){
self.log.set('html', '<pre>'+json.log+'</pre>')
}
})
}
}) })
+214 -196
View File
@@ -8,222 +8,240 @@ Page.Wanted = new Class({
indexAction: function(param){ indexAction: function(param){
var self = this; var self = this;
self.list = new MovieList({ if(!self.list){
'status': 'active',
'actions': Wanted.Action // Wanted movies
}); self.wanted = new MovieList({
$(self.list).inject(self.el); 'status': 'active',
'actions': WantedActions
});
$(self.wanted).inject(self.el);
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
// Snatched movies
self.snatched = new MovieList({
'status': 'snatched',
'actions': SnatchedActions
});
$(self.snatched).inject(self.el);
App.addEvent('library.update', self.snatched.update.bind(self.snatched));
}
} }
}); });
var Wanted = { var WantedActions = {
'Action': { 'IMBD': IMDBAction
'IMBD': IMDBAction //,'releases': ReleaseAction
}
}
Wanted.Action.Edit = new Class({ ,'Edit': new Class({
Extends: MovieAction, Extends: MovieAction,
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('a.edit', { self.el = new Element('a.edit', {
'title': 'Refresh the movie info and do a forced search', 'title': 'Refresh the movie info and do a forced search',
'events': { 'events': {
'click': self.editMovie.bind(self) 'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).stop();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
new Element('div.form', {
'styles': {
'line-height': self.movie.getHeight()
}
}).adopt(
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save',
'events': {
'click': self.save.bind(self)
}
})
)
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select);
});
Object.each(Quality.profiles, function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
self.profile_select.set('value', self.movie.profile.get('id'));
});
} }
}); self.movie.slide('in');
},
save: function(e){
(e).stop();
var self = this;
Api.request('movie.edit', {
'data': {
'id': self.movie.get('id'),
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': $(self.movie),
'onComplete': function(){
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'));
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
}
});
self.movie.slide('out');
}
})
}, ,'Refresh': new Class({
editMovie: function(e){ Extends: MovieAction,
var self = this;
(e).stop(); create: function(){
var self = this;
self.el = new Element('a.refresh', {
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doSearch.bind(self)
}
});
},
doSearch: function(e){
var self = this;
(e).stop();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
});
}
})
if(!self.options_container){ ,'Delete': new Class({
self.options_container = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(), Extends: MovieAction,
new Element('div.form', {
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).stop();
if(!self.delete_container){
self.delete_container = new Element('div.delete_container', {
'styles': { 'styles': {
'line-height': self.movie.getHeight() 'line-height': self.movie.getHeight()
} }
}).adopt( }).adopt(
self.title_select = new Element('select', { new Element('a.cancel', {
'name': 'title' 'text': 'Cancel',
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save',
'events': { 'events': {
'click': self.save.bind(self) 'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
} }
}) })
) ).inject(self.movie, 'top');
).inject(self.movie, 'top');
Array.each(self.movie.data.library.titles, function(alt){
new Element('option', {
'text': alt.title
}).inject(self.title_select)
});
Object.each(Quality.profiles, function(profile){
new Element('option', {
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select);
self.profile_select.set('value', self.movie.profile.get('id'));
});
}
self.movie.slide('in');
},
save: function(e){
(e).stop();
var self = this;
Api.request('movie.edit', {
'data': {
'id': self.movie.get('id'),
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': $(self.movie),
'onComplete': function(){
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'))
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'))
} }
});
self.movie.slide('in');
self.movie.slide('out');
} },
}) hideConfirm: function(e){
var self = this;
Wanted.Action.Refresh = new Class({ (e).stop();
Extends: MovieAction, self.movie.slide('out');
},
create: function(){
var self = this; del: function(e){
(e).stop();
self.el = new Element('a.refresh', { var self = this;
'title': 'Refresh the movie info and do a forced search',
'events': { var movie = $(self.movie);
'click': self.doSearch.bind(self)
} self.chain(
}); function(){
$(movie).mask().addClass('loading');
}, self.callChain();
},
doSearch: function(e){ function(){
var self = this; Api.request('movie.delete', {
(e).stop(); 'data': {
'id': self.movie.get('id')
Api.request('movie.refresh', { },
'data': { 'onComplete': function(){
'id': self.movie.get('id') movie.set('tween', {
} 'onComplete': function(){
}) movie.destroy();
} }
});
}) movie.tween('height', 0);
}
Wanted.Action.Delete = new Class({ });
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'title': 'Remove the movie from your wanted list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).stop();
if(!self.delete_container){
self.delete_container = new Element('div.delete_container', {
'styles': {
'line-height': self.movie.getHeight()
} }
}).adopt( );
new Element('a.cancel', {
'text': 'Cancel', self.callChain();
'events': {
'click': self.hideConfirm.bind(self)
}
}),
new Element('span.or', {
'text': 'or'
}),
new Element('a.button.delete', {
'text': 'Delete ' + self.movie.title.get('text'),
'events': {
'click': self.del.bind(self)
}
})
).inject(self.movie, 'top')
} }
self.movie.slide('in'); })
};
}, var SnatchedActions = {
'IMBD': IMDBAction
hideConfirm: function(e){ ,'Releases': ReleaseAction
var self = this; ,'Delete': WantedActions.Delete
(e).stop(); };
self.movie.slide('out');
},
del: function(e){
(e).stop()
var self = this;
var movie = $(self.movie);
self.chain(
function(){
$(movie).mask().addClass('loading')
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id')
},
'onComplete': function(){
movie.set('tween', {
'onComplete': function(){
movie.destroy();
}
})
movie.tween('height', 0)
}
})
}
);
self.callChain();
}
})
+3 -3
View File
@@ -31,10 +31,10 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/page.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/wanted.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/wanted.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/settings.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/log.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/log.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/soon.js') }}"></script> <script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/settings.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/manage.js') }}"></script> <!--<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/soon.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/manage.js') }}"></script>-->
<link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" /> <link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('.static', filename='images/homescreen.png') }}" /> <link rel="apple-touch-icon" href="{{ url_for('.static', filename='images/homescreen.png') }}" />