Movie search
Profile Settings
This commit is contained in:
+3
-1
@@ -3,6 +3,7 @@ from couchpotato import web
|
||||
from couchpotato.api import api
|
||||
from libs.daemon import createDaemon
|
||||
from logging import handlers
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
@@ -48,6 +49,7 @@ def cmd_couchpotato(base_path, args):
|
||||
Env.set('data_dir', options.data_dir)
|
||||
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', FileSystemCache(Env.get('cache_dir')))
|
||||
Env.set('quiet', options.quiet)
|
||||
Env.set('daemonize', options.daemonize)
|
||||
Env.set('args', args)
|
||||
@@ -133,7 +135,7 @@ def cmd_couchpotato(base_path, args):
|
||||
|
||||
# Register modules
|
||||
app.register_module(web, url_prefix = '%s/' % url_base)
|
||||
app.register_module(api, url_prefix = '%s/%s/%s/' % (url_base, 'api', api_key if not debug else 'apikey'))
|
||||
app.register_module(api, url_prefix = '%s/%s/' % (url_base, api_key if not debug else 'api'))
|
||||
|
||||
# Go go go!
|
||||
app.run(use_reloader = reloader)
|
||||
|
||||
@@ -24,3 +24,11 @@ def toUnicode(original, *args):
|
||||
except UnicodeDecodeError:
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
return unicode(ascii_text)
|
||||
|
||||
|
||||
def is_int(value):
|
||||
try:
|
||||
int(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -1,17 +1,67 @@
|
||||
from flask.globals import current_app
|
||||
from flask.helpers import json, jsonify
|
||||
from flask.helpers import json
|
||||
from libs.werkzeug.urls import url_decode
|
||||
import flask
|
||||
|
||||
import re
|
||||
|
||||
def getParams():
|
||||
return getattr(flask.request, 'args')
|
||||
|
||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
current = temp = {}
|
||||
for param, value in sorted(params.iteritems()):
|
||||
|
||||
nest = re.split("([\[\]]+)", param)
|
||||
if len(nest) > 1:
|
||||
nested = []
|
||||
for key in nest:
|
||||
if reg.match(key):
|
||||
nested.append(key)
|
||||
|
||||
current = temp
|
||||
|
||||
for item in nested:
|
||||
if item is nested[-1]:
|
||||
current[item] = value
|
||||
else:
|
||||
try:
|
||||
current[item]
|
||||
except:
|
||||
current[item] = {}
|
||||
|
||||
current = current[item]
|
||||
else:
|
||||
temp[param] = value
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
def dictToList(params):
|
||||
|
||||
if type(params) is dict:
|
||||
new = {}
|
||||
for x, value in params.iteritems():
|
||||
try:
|
||||
new_value = [dictToList(value2) for value2 in value.itervalues()]
|
||||
except:
|
||||
new_value = value
|
||||
|
||||
new[x] = new_value
|
||||
else:
|
||||
new = params
|
||||
|
||||
return new
|
||||
|
||||
def getParam(attr, default = None):
|
||||
return getattr(flask.request, 'args').get(attr, default)
|
||||
|
||||
def padded_jsonify(callback, *args, **kwargs):
|
||||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
||||
return current_app.response_class(content, mimetype = 'text/javascript')
|
||||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
||||
|
||||
def jsonify(mimetype, *args, **kwargs):
|
||||
content = json.dumps(dict(*args, **kwargs))
|
||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
||||
|
||||
def jsonified(*args, **kwargs):
|
||||
from couchpotato.environment import Env
|
||||
@@ -19,4 +69,5 @@ def jsonified(*args, **kwargs):
|
||||
if callback:
|
||||
return padded_jsonify(callback, *args, **kwargs)
|
||||
else:
|
||||
return jsonify(*args, **kwargs)
|
||||
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.plugins.library.main import LibraryPlugin
|
||||
|
||||
def start():
|
||||
return LibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,32 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library
|
||||
|
||||
|
||||
class LibraryPlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.add', self.add)
|
||||
|
||||
def add(self, attrs = {}):
|
||||
|
||||
db = get_session();
|
||||
|
||||
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||
|
||||
if not l:
|
||||
l = Library(
|
||||
name = attrs.get('name'),
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
description = attrs.get('description')
|
||||
)
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
return l
|
||||
|
||||
def update(self, item):
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.plugins.movie.main import MoviePlugin
|
||||
|
||||
def start():
|
||||
return MoviePlugin()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,94 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, Profile
|
||||
from couchpotato.environment import Env
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
class MoviePlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('movie.search', self.search)
|
||||
addApiView('movie.list', self.list)
|
||||
addApiView('movie.add', self.add)
|
||||
|
||||
def list(self):
|
||||
|
||||
a = getParams()
|
||||
|
||||
results = get_session().query(Movie).filter(
|
||||
Movie.releases.any(
|
||||
Release.status.has(identifier = 'wanted')
|
||||
)
|
||||
).all()
|
||||
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = {
|
||||
'id': movie.id,
|
||||
'name': movie.id,
|
||||
'releases': [],
|
||||
}
|
||||
for release in movie.releases:
|
||||
temp['releases'].append({
|
||||
'status': release.status.label,
|
||||
'quality': release.quality.label
|
||||
})
|
||||
|
||||
movies.append(temp)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
def search(self):
|
||||
|
||||
a = getParams()
|
||||
cache_key = '%s/%s' % (__name__, urlencode(a))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
if not movies:
|
||||
results = fireEvent('provider.movie.search', q = a.get('q'))
|
||||
|
||||
# Combine movie results
|
||||
movies = []
|
||||
for r in results:
|
||||
movies += r
|
||||
|
||||
Env.get('cache').set(cache_key, movies, timeout = 10)
|
||||
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
def add(self):
|
||||
|
||||
a = getParams()
|
||||
db = get_session();
|
||||
|
||||
library = fireEvent('library.add', attrs = a)
|
||||
profile = db.query(Profile).filter_by(identifier = a.get('profile_identifier'))
|
||||
|
||||
m = db.query(Movie).filter_by(library = library).first()
|
||||
|
||||
if not m:
|
||||
m = Movie(
|
||||
library = library,
|
||||
profile = profile,
|
||||
)
|
||||
db.add(m)
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'added': True,
|
||||
'params': a,
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
from couchpotato.core.plugins.movie_add.main import MovieAdd
|
||||
|
||||
def start():
|
||||
return MovieAdd()
|
||||
|
||||
config = []
|
||||
@@ -1,36 +0,0 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
class MovieAdd(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('movie.add.search', self.search)
|
||||
addApiView('movie.add.select', self.select)
|
||||
|
||||
def search(self):
|
||||
|
||||
a = getParams()
|
||||
|
||||
results = fireEvent('provider.movie.search', q = a.get('q'))
|
||||
|
||||
# Combine movie results
|
||||
movies = []
|
||||
for r in results:
|
||||
movies += r
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
def select(self):
|
||||
|
||||
a = getParams()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'added': True,
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
from couchpotato.core.plugins.movie_list.main import MovieList
|
||||
|
||||
def start():
|
||||
return MovieList()
|
||||
|
||||
config = []
|
||||
@@ -1,41 +0,0 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release
|
||||
|
||||
class MovieList(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('movie.list', self.list)
|
||||
|
||||
def list(self):
|
||||
|
||||
a = getParams()
|
||||
|
||||
results = get_session().query(Movie).filter(
|
||||
Movie.releases.any(
|
||||
Release.status.has(identifier = 'wanted')
|
||||
)
|
||||
).all()
|
||||
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = {
|
||||
'id': movie.id,
|
||||
'name': movie.id,
|
||||
'releases': [],
|
||||
}
|
||||
for release in movie.releases:
|
||||
temp['releases'].append({
|
||||
'status': release.status.label,
|
||||
'quality': release.quality.label
|
||||
})
|
||||
|
||||
movies.append(temp)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.plugins.profile.main import ProfilePlugin
|
||||
|
||||
def start():
|
||||
return ProfilePlugin()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,28 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
class ProfilePlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('profile.get', self.get)
|
||||
|
||||
addApiView('profile.save', self.save)
|
||||
addApiView('profile.delete', self.delete)
|
||||
|
||||
def get(self, key = ''):
|
||||
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
|
||||
a = getParams()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'a': a
|
||||
})
|
||||
|
||||
def delete(self):
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato.core.providers.tmdb.main import TMDB
|
||||
from couchpotato.core.providers.tmdb.main import TMDBWrapper
|
||||
|
||||
def start():
|
||||
return TMDB()
|
||||
return TMDBWrapper()
|
||||
|
||||
config = [{
|
||||
'name': 'themoviedb',
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
from libs.themoviedb import tmdb
|
||||
from urllib import quote_plus
|
||||
import copy
|
||||
import simplejson as json
|
||||
@@ -11,7 +12,7 @@ import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class TMDB(Provider):
|
||||
class TMDBWrapper(Provider):
|
||||
"""Api for theMovieDb"""
|
||||
|
||||
type = 'movie'
|
||||
@@ -20,6 +21,10 @@ class TMDB(Provider):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.movie.search', self.search)
|
||||
addEvent('provider.movie.info', self.getInfo)
|
||||
|
||||
# Use base wrapper
|
||||
tmdb.Config.api_key = self.conf('api_key')
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'themoviedb')
|
||||
@@ -32,31 +37,32 @@ class TMDB(Provider):
|
||||
|
||||
log.debug('TheMovieDB - Searching for movie: %s' % q)
|
||||
|
||||
url = "%s/%s/%s/json/%s/%s" % (self.apiUrl, 'Movie.search', 'en', self.conf('api_key'), quote_plus(simplifyString(q)))
|
||||
raw = tmdb.search(simplifyString(q))
|
||||
|
||||
log.info('Searching: %s' % url)
|
||||
#url = "%s/%s/%s/json/%s/%s" % (self.apiUrl, 'Movie.search', 'en', self.conf('api_key'), quote_plus(simplifyString(q)))
|
||||
|
||||
data = urllib2.urlopen(url)
|
||||
jsn = json.load(data)
|
||||
|
||||
return self.parse(jsn, limit, alternative = alternative)
|
||||
# data = urllib2.urlopen(url)
|
||||
# jsn = json.load(data)
|
||||
|
||||
def parse(self, data, limit, alternative = True):
|
||||
if data:
|
||||
if raw:
|
||||
log.debug('TheMovieDB - Parsing RSS')
|
||||
try:
|
||||
results = []
|
||||
nr = 0
|
||||
for movie in data:
|
||||
for movie in raw:
|
||||
|
||||
year = str(movie['released'])[:4]
|
||||
for k, x in movie.iteritems():
|
||||
print k
|
||||
print x
|
||||
|
||||
year = str(movie.get('released', 'none'))[:4]
|
||||
|
||||
# Poster url
|
||||
poster = ''
|
||||
for p in movie['posters']:
|
||||
p = p['image']
|
||||
if(p['size'] == 'thumb'):
|
||||
poster = p['url']
|
||||
for p in movie.get('images'):
|
||||
if(p.get('type') == 'poster'):
|
||||
poster = p.get('thumb')
|
||||
break
|
||||
|
||||
# 1900 is the same as None
|
||||
@@ -64,16 +70,16 @@ class TMDB(Provider):
|
||||
year = None
|
||||
|
||||
movie_data = {
|
||||
'id': int(movie['id']),
|
||||
'name': toUnicode(movie['name']),
|
||||
'id': int(movie.get('id', 0)),
|
||||
'name': toUnicode(movie.get('name')),
|
||||
'poster': poster,
|
||||
'imdb': movie['imdb_id'],
|
||||
'imdb': movie.get('imdb_id'),
|
||||
'year': year,
|
||||
'tagline': 'This is the tagline of the movie',
|
||||
}
|
||||
results.append(copy.deepcopy(movie_data))
|
||||
|
||||
alternativeName = movie['alternative_name']
|
||||
alternativeName = movie.get('alternative_name')
|
||||
if alternativeName and alternative:
|
||||
if alternativeName.lower() != movie['name'].lower() and alternativeName.lower() != 'none' and alternativeName != None:
|
||||
movie_data['name'] = toUnicode(alternativeName)
|
||||
@@ -88,6 +94,10 @@ class TMDB(Provider):
|
||||
log.error('TheMovieDB - Failed to parse XML response from TheMovieDb: %s' % e)
|
||||
return False
|
||||
|
||||
return results
|
||||
|
||||
def getInfo(self):
|
||||
pass
|
||||
|
||||
def isDisabled(self):
|
||||
if self.conf('api_key') == '':
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import is_int
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
import ConfigParser
|
||||
import os.path
|
||||
@@ -60,7 +61,7 @@ class Settings():
|
||||
return default
|
||||
|
||||
def cleanValue(self, value):
|
||||
if(self.is_int(value)):
|
||||
if(is_int(value)):
|
||||
return int(value)
|
||||
|
||||
if str(value).lower() in self.bool:
|
||||
@@ -91,13 +92,6 @@ class Settings():
|
||||
if not self.p.has_option(section, option):
|
||||
self.p.set(section, option, value)
|
||||
|
||||
def is_int(self, value):
|
||||
try:
|
||||
int(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def addOptions(self, section_name, options):
|
||||
self.options[section_name] = options
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ from elixir.entity import Entity
|
||||
from elixir.fields import Field
|
||||
from elixir.options import options_defaults
|
||||
from elixir.relationships import OneToMany, ManyToOne
|
||||
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean
|
||||
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean, \
|
||||
Float
|
||||
|
||||
options_defaults["shortnames"] = True
|
||||
|
||||
@@ -19,13 +20,26 @@ class Movie(Entity):
|
||||
The files belonging to the movie object are global for the whole movie
|
||||
such as trailers, nfo, thumbnails"""
|
||||
|
||||
mooli_id = Field(Integer)
|
||||
library = ManyToOne('Library')
|
||||
|
||||
profile = ManyToOne('Profile')
|
||||
releases = OneToMany('Release')
|
||||
files = OneToMany('File')
|
||||
|
||||
|
||||
class Library(Entity):
|
||||
|
||||
title = Field(Unicode)
|
||||
year = Field(Integer)
|
||||
identifier = Field(Unicode)
|
||||
rating = Field(Float)
|
||||
|
||||
plot = Field(UnicodeText)
|
||||
tagline = Field(UnicodeText(255))
|
||||
|
||||
movie = OneToMany('Movie')
|
||||
|
||||
|
||||
class Release(Entity):
|
||||
"""Logically groups all files that belong to a certain release, such as
|
||||
parts of a movie, subtitles."""
|
||||
@@ -55,6 +69,7 @@ class Quality(Entity):
|
||||
releases = OneToMany('Release')
|
||||
profile_types = ManyToOne('ProfileType')
|
||||
|
||||
|
||||
class Profile(Entity):
|
||||
""""""
|
||||
|
||||
@@ -66,6 +81,7 @@ class Profile(Entity):
|
||||
movie = OneToMany('Movie')
|
||||
profile_type = OneToMany('ProfileType')
|
||||
|
||||
|
||||
class ProfileType(Entity):
|
||||
""""""
|
||||
|
||||
@@ -76,6 +92,7 @@ class ProfileType(Entity):
|
||||
type = OneToMany('Quality')
|
||||
profile = ManyToOne('Profile')
|
||||
|
||||
|
||||
class File(Entity):
|
||||
"""File that belongs to a release."""
|
||||
|
||||
@@ -114,6 +131,7 @@ class History(Entity):
|
||||
message = Field(UnicodeText())
|
||||
release = ManyToOne('Release')
|
||||
|
||||
|
||||
class RenameHistory(Entity):
|
||||
"""Remembers from where to where files have been moved."""
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ class Env:
|
||||
_debug = False
|
||||
_settings = Settings()
|
||||
_loader = Loader()
|
||||
_cache = None
|
||||
_options = None
|
||||
_args = None
|
||||
_quiet = False
|
||||
|
||||
@@ -35,6 +35,9 @@ Block.Search = new Class({
|
||||
|
||||
self.OuterClickStack = new EventStack.OuterClick();
|
||||
|
||||
//debug
|
||||
//self.input.set('value', 'kick ass')
|
||||
//self.autocomplete()
|
||||
},
|
||||
|
||||
clear: function(e){
|
||||
@@ -98,7 +101,7 @@ Block.Search = new Class({
|
||||
self.hideResults(false)
|
||||
|
||||
if(!cache){
|
||||
self.api_request = Api.request('movie.add.search', {
|
||||
self.api_request = Api.request('movie.search', {
|
||||
'data': {
|
||||
'q': q
|
||||
},
|
||||
@@ -118,7 +121,7 @@ Block.Search = new Class({
|
||||
self.spinner.hide();
|
||||
self.cache[q] = json
|
||||
|
||||
self.movies = []
|
||||
self.movies = {}
|
||||
self.results.empty()
|
||||
|
||||
Object.each(json.movies, function(movie){
|
||||
@@ -126,9 +129,14 @@ Block.Search = new Class({
|
||||
if(!movie.imdb || (movie.imdb && !self.results.getElement('#'+movie.imdb))){
|
||||
var m = new Block.Search.Item(movie);
|
||||
$(m).inject(self.results)
|
||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||
}
|
||||
else {
|
||||
self.movies[movie.imdb].alternativeName({
|
||||
'name': movie.name
|
||||
})
|
||||
}
|
||||
|
||||
self.movies.include(m)
|
||||
});
|
||||
|
||||
},
|
||||
@@ -149,6 +157,7 @@ Block.Search.Item = new Class({
|
||||
var self = this;
|
||||
|
||||
self.info = info;
|
||||
self.alternative_names = [];
|
||||
|
||||
self.create();
|
||||
|
||||
@@ -163,11 +172,7 @@ Block.Search.Item = new Class({
|
||||
self.el = new Element('div.movie', {
|
||||
'id': info.imdb
|
||||
}).adopt(
|
||||
new Element('div.add').adopt(
|
||||
new Element('span', {
|
||||
'text': 'test'
|
||||
})
|
||||
),
|
||||
self.options = new Element('div.options'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
@@ -208,11 +213,23 @@ Block.Search.Item = new Class({
|
||||
}).inject(self.starring)
|
||||
})
|
||||
}
|
||||
|
||||
self.alternativeName({
|
||||
'name': info.name
|
||||
});
|
||||
},
|
||||
|
||||
alternativeName: function(alternative){
|
||||
var self = this;
|
||||
|
||||
self.alternative_names.include(alternative);
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.createOptions();
|
||||
|
||||
if(!self.width)
|
||||
self.width = self.data_container.getCoordinates().width
|
||||
|
||||
@@ -222,6 +239,80 @@ Block.Search.Item = new Class({
|
||||
|
||||
},
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
|
||||
Api.request('movie.add', {
|
||||
'data': {
|
||||
'identifier': self.info.imdb,
|
||||
'name': self.name_select.get('value'),
|
||||
'quality': self.quality_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': self.options,
|
||||
'onComplete': function(){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Movie succesfully added.'
|
||||
})
|
||||
);
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createOptions: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.options.hasClass('set')){
|
||||
|
||||
self.options.adopt(
|
||||
new Element('div').adopt(
|
||||
self.info.poster ? new Element('img.thumbnail', {
|
||||
'src': self.info.poster
|
||||
}) : null,
|
||||
self.name_select = new Element('select', {
|
||||
'name': 'name'
|
||||
}),
|
||||
self.quality_select = new Element('select', {
|
||||
'name': 'profile_identifier'
|
||||
}),
|
||||
new Element('a.button', {
|
||||
'text': 'Add',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
Array.each(self.alternative_names, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.name
|
||||
}).inject(self.name_select)
|
||||
})
|
||||
|
||||
Array.each(Quality.profiles, function(q){
|
||||
new Element('option', {
|
||||
'value': q.indentifier,
|
||||
'text': q.label
|
||||
}).inject(self.quality_select)
|
||||
});
|
||||
|
||||
self.options.addClass('set');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -232,4 +323,4 @@ Block.Search.Item = new Class({
|
||||
return this.el
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var CouchPotato = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
Implements: [Events, Options],
|
||||
|
||||
defaults: {
|
||||
page: 'wanted',
|
||||
@@ -11,7 +11,7 @@ var CouchPotato = new Class({
|
||||
pages: [],
|
||||
block: [],
|
||||
|
||||
initialize: function(options) {
|
||||
setup: function(options) {
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
@@ -62,6 +62,8 @@ var CouchPotato = new Class({
|
||||
$(pg).inject(self.content);
|
||||
});
|
||||
|
||||
self.fireEvent('load')
|
||||
|
||||
},
|
||||
|
||||
openPage: function(url) {
|
||||
@@ -85,9 +87,13 @@ var CouchPotato = new Class({
|
||||
|
||||
getBlock: function(block_name){
|
||||
return this.block[block_name]
|
||||
},
|
||||
|
||||
getPage: function(name){
|
||||
return this.pages[name]
|
||||
}
|
||||
});
|
||||
|
||||
window.App = new CouchPotato();
|
||||
|
||||
var ApiClass = new Class({
|
||||
|
||||
|
||||
@@ -105,20 +105,7 @@ Page.Settings = new Class({
|
||||
|
||||
// Create tabs
|
||||
Object.each(self.tabs, function(tab, tab_name){
|
||||
if(!self.tabs[tab_name].tab){
|
||||
var tab_el = new Element('li').adopt(
|
||||
new Element('a', {
|
||||
'href': '/'+self.name+'/'+tab_name+'/',
|
||||
'text': tab.label.capitalize()
|
||||
})
|
||||
).inject(self.tabs_container);
|
||||
|
||||
self.tabs[tab_name] = Object.merge(self.tabs[tab_name], {
|
||||
'tab': tab_el,
|
||||
'content': new Element('div.tab_content').inject(self.containers),
|
||||
'groups': {}
|
||||
})
|
||||
}
|
||||
self.createTab(tab_name, tab)
|
||||
});
|
||||
|
||||
// Add content to tabs
|
||||
@@ -128,17 +115,7 @@ Page.Settings = new Class({
|
||||
section.groups.sortBy('order').each(function(group){
|
||||
|
||||
// Create the group
|
||||
var group_el = new Element('fieldset', {
|
||||
'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels'
|
||||
}).adopt(
|
||||
new Element('h2', {
|
||||
'text': group.label
|
||||
}).adopt(
|
||||
new Element('span.hint', {
|
||||
'text': group.description
|
||||
})
|
||||
)
|
||||
).inject(self.tabs[group.tab].content);
|
||||
var group_el = self.createGroup(group).inject(self.tabs[group.tab].content);
|
||||
|
||||
self.tabs[group.tab].groups[group.name] = group_el
|
||||
|
||||
@@ -152,8 +129,57 @@ Page.Settings = new Class({
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
self.fireEvent('create');
|
||||
self.openTab();
|
||||
|
||||
},
|
||||
|
||||
createTab: function(tab_name, tab){
|
||||
var self = this;
|
||||
|
||||
if(self.tabs[tab_name] && self.tabs[tab_name].tab)
|
||||
return self.tabs[tab_name].tab
|
||||
|
||||
var tab_el = new Element('li').adopt(
|
||||
new Element('a', {
|
||||
'href': '/'+self.name+'/'+tab_name+'/',
|
||||
'text': tab.label.capitalize()
|
||||
})
|
||||
).inject(self.tabs_container);
|
||||
|
||||
if(!self.tabs[tab_name])
|
||||
self.tabs[tab_name] = {
|
||||
'label': tab.label
|
||||
}
|
||||
|
||||
self.tabs[tab_name] = Object.merge(self.tabs[tab_name], {
|
||||
'tab': tab_el,
|
||||
'content': new Element('div.tab_content').inject(self.containers),
|
||||
'groups': {}
|
||||
})
|
||||
|
||||
return self.tabs[tab_name]
|
||||
|
||||
},
|
||||
|
||||
createGroup: function(group){
|
||||
var self = this;
|
||||
|
||||
var group_el = new Element('fieldset', {
|
||||
'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels'
|
||||
}).adopt(
|
||||
new Element('h2', {
|
||||
'text': group.label
|
||||
}).adopt(
|
||||
new Element('span.hint', {
|
||||
'text': group.description
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
return group_el
|
||||
}
|
||||
|
||||
});
|
||||
@@ -226,7 +252,7 @@ var OptionBase = new Class({
|
||||
save: function(){
|
||||
var self = this;
|
||||
|
||||
self.api().request('setting.save', {
|
||||
Api.request('setting.save', {
|
||||
'data': {
|
||||
'section': self.section,
|
||||
'name': self.name,
|
||||
@@ -273,10 +299,6 @@ var OptionBase = new Class({
|
||||
return self.page.getValue(self.section, self.name);
|
||||
},
|
||||
|
||||
api: function(){
|
||||
return this.page.api();
|
||||
},
|
||||
|
||||
inject: function(el, position){
|
||||
this.el.inject(el, position);
|
||||
return this.el;
|
||||
@@ -448,7 +470,7 @@ Option.Directory = new Class({
|
||||
self.fillBrowser()
|
||||
}
|
||||
else {
|
||||
self.api().request('directory.list', {
|
||||
Api.request('directory.list', {
|
||||
'data': {
|
||||
'path': c
|
||||
},
|
||||
@@ -461,7 +483,7 @@ Option.Directory = new Class({
|
||||
var self = this;
|
||||
|
||||
var v = self.input.get('value');
|
||||
var sep = self.api().getOption('path_sep');
|
||||
var sep = Api.getOption('path_sep');
|
||||
var dirs = v.split(sep);
|
||||
dirs.pop();
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
var QualityBase = new Class({
|
||||
|
||||
tab: '',
|
||||
content: '',
|
||||
|
||||
setup: function(data){
|
||||
var self = this;
|
||||
|
||||
self.profiles = data.profiles;
|
||||
self.qualities = data.qualities;
|
||||
|
||||
App.addEvent('load', self.addSettings.bind(self))
|
||||
|
||||
},
|
||||
|
||||
addSettings: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createTab('profile', {
|
||||
'label': 'Profile',
|
||||
'name': 'profile'
|
||||
});
|
||||
|
||||
self.tab = tab.tab;
|
||||
self.content = tab.content;
|
||||
|
||||
self.createProfiles();
|
||||
self.createOrdering();
|
||||
self.createSizes();
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Profiles
|
||||
*/
|
||||
createProfiles: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Custom',
|
||||
'description': 'Discriptions'
|
||||
}).inject(self.content).adopt(
|
||||
new Element('a.add_new', {
|
||||
'text': 'Create a new quality profile',
|
||||
'events': {
|
||||
'click': self.createNewProfile.bind(self)
|
||||
}
|
||||
}),
|
||||
self.profile_container = new Element('div.container')
|
||||
)
|
||||
|
||||
Object.each(self.profiles, self.createNewProfile.bind(self))
|
||||
|
||||
},
|
||||
|
||||
createNewProfile: function(data, nr){
|
||||
var self = this;
|
||||
|
||||
self.profiles[nr] = new Profile(data);
|
||||
$(self.profiles[nr]).inject(self.profile_container)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Ordering
|
||||
*/
|
||||
createOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Order',
|
||||
'description': 'Discriptions'
|
||||
}).inject(self.content)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Sizes
|
||||
*/
|
||||
createSizes: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Sizes',
|
||||
'description': 'Discriptions',
|
||||
'advanced': true
|
||||
}).inject(self.content)
|
||||
}
|
||||
|
||||
});
|
||||
window.Quality = new QualityBase();
|
||||
|
||||
var Profile = new Class({
|
||||
|
||||
data: {},
|
||||
types: [],
|
||||
|
||||
initialize: function(data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.types = [];
|
||||
|
||||
self.create();
|
||||
|
||||
self.el.addEvents({
|
||||
'change:relay(select, input[type=checkbox])': self.save.bind(self, 0),
|
||||
'keyup:relay(input[type=text])': self.save.bind(self, [300])
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'profile'
|
||||
}).adopt(
|
||||
new Element('h4', {'text': data.label}),
|
||||
new Element('span.delete', {
|
||||
'html': 'del',
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('div', {
|
||||
'class': 'ctrlHolder'
|
||||
}).adopt(
|
||||
new Element('label', {'text':'Name'}),
|
||||
new Element('input.label.textInput.large', {
|
||||
'type':'text',
|
||||
'value': data.label
|
||||
})
|
||||
),
|
||||
new Element('div.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Wait'}),
|
||||
new Element('input.wait_for.textInput.xsmall', {
|
||||
'type':'text',
|
||||
'value': data.wait_for
|
||||
}),
|
||||
new Element('span', {'text':' day(s) for better quality.'})
|
||||
),
|
||||
new Element('div.ctrlHolder').adopt(
|
||||
new Element('label', {'text': 'Qualities'}),
|
||||
self.type_container = new Element('div.types').adopt(
|
||||
new Element('div.head').adopt(
|
||||
new Element('span.quality_type', {'text': 'Search for'}),
|
||||
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
|
||||
)
|
||||
),
|
||||
new Element('a.addType', {
|
||||
'text': 'Add another quality to search for.',
|
||||
'href': '#',
|
||||
'events': {
|
||||
'click': self.addType.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if(data.types)
|
||||
Object.each(data.types, self.addType.bind(self))
|
||||
},
|
||||
|
||||
save: function(delay){
|
||||
var self = this;
|
||||
|
||||
if(self.save_timer) clearTimeout(self.save_timer);
|
||||
self.save_timer = (function(){
|
||||
|
||||
Api.request('profile.save', {
|
||||
'data': self.getData(),
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
}
|
||||
});
|
||||
}).delay(delay, self)
|
||||
|
||||
},
|
||||
|
||||
getData: function(){
|
||||
var self = this;
|
||||
|
||||
var data = {
|
||||
'id' : self.data.id,
|
||||
'label' : self.el.getElement('.label').get('value'),
|
||||
'wait_for' : self.el.getElement('.wait_for').get('value'),
|
||||
'types': []
|
||||
}
|
||||
|
||||
Object.each(self.types, function(type){
|
||||
if(!type.deleted)
|
||||
data.types.include(type.getData());
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
addType: function(data){
|
||||
var self = this;
|
||||
|
||||
var t = new Profile.Type(data);
|
||||
$(t).inject(self.type_container);
|
||||
|
||||
self.types.include(t);
|
||||
|
||||
},
|
||||
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('profile.delete', {
|
||||
'data': {
|
||||
'id': self.data.id
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(){
|
||||
self.el.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Profile.Type = Class({
|
||||
|
||||
deleted: false,
|
||||
|
||||
initialize: function(data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.create();
|
||||
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.type').adopt(
|
||||
new Element('span.quality_type').adopt(
|
||||
self.fillQualities()
|
||||
),
|
||||
new Element('span.finish').adopt(
|
||||
self.finish = new Element('input', {
|
||||
'type':'checkbox',
|
||||
'class':'finish',
|
||||
'checked': data.finish
|
||||
})
|
||||
),
|
||||
new Element('span.delete', {
|
||||
'html': 'del',
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span', {
|
||||
'class':'handle'
|
||||
})
|
||||
)
|
||||
|
||||
},
|
||||
|
||||
fillQualities: function(){
|
||||
var self = this;
|
||||
|
||||
self.qualities = new Element('select');
|
||||
|
||||
Object.each(Quality.qualities, function(q){
|
||||
new Element('option', {
|
||||
'text': q.label,
|
||||
'value': q.identifier
|
||||
}).inject(self.qualities)
|
||||
});
|
||||
|
||||
self.qualities.set('value', self.data.quality);
|
||||
|
||||
return self.qualities;
|
||||
|
||||
},
|
||||
|
||||
getData: function(){
|
||||
var self = this;
|
||||
|
||||
return {
|
||||
'quality': self.qualities.get('value'),
|
||||
'finish': +self.finish.checked
|
||||
}
|
||||
},
|
||||
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.hide();
|
||||
self.deleted = true;
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
})
|
||||
@@ -101,6 +101,26 @@ form {
|
||||
background: #f7f7f7 url('../images/spinner.gif') no-repeat center;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x;
|
||||
padding: 5px 10px 6px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
|
||||
-moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.25);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*** Navigation ***/
|
||||
.header {
|
||||
background: #f7f7f7;
|
||||
|
||||
@@ -66,9 +66,30 @@
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.search_form .results .movie .add {
|
||||
min-height: 140px;
|
||||
.search_form .results .movie .options {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.search_form .results .movie .options > div {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.search_form .results .movie .options .thumbnail {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.search_form .results .movie .options select {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.search_form .results .movie .options select[name=name] { width: 180px; }
|
||||
.search_form .results .movie .options select[name=quality] { width: 90px; }
|
||||
|
||||
.search_form .results .movie .options .button {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.search_form .results .movie .data {
|
||||
padding: 0 15px;
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/page/settings.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/movie_add.css') }}">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/mootools.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/mootools_more.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools_more.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/uniform.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/uniform.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/couchpotato.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/history.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/eventstack.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/eventstack_outerclick.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/history.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/eventstack.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/eventstack_outerclick.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/block.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/block/navigation.js') }}"></script>
|
||||
@@ -29,6 +29,8 @@
|
||||
<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>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/quality.js') }}"></script>
|
||||
|
||||
<link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -39,9 +41,39 @@
|
||||
'url': '{{ url_for('api.index') }}',
|
||||
'path_sep': '{{ sep }}',
|
||||
'is_remote': false
|
||||
});
|
||||
|
||||
Quality.setup({
|
||||
'profiles': [
|
||||
{
|
||||
'identifier': 'hd',
|
||||
'label': 'HD',
|
||||
'wait_for': 7,
|
||||
'types': [
|
||||
{'quality': '720p', 'finish': true},
|
||||
{'quality': '1080p', 'finish': true}
|
||||
]
|
||||
},{
|
||||
'identifier': 'best',
|
||||
'label': 'Best',
|
||||
'wait_for': 7,
|
||||
'types': [
|
||||
{'quality': '720p', 'finish': true},
|
||||
{'quality': '1080p', 'finish': true},
|
||||
{'quality': 'brrip'},
|
||||
{'quality': 'dvdrip'}
|
||||
]
|
||||
}
|
||||
],
|
||||
'qualities': [
|
||||
{'identifier': '1080p', 'label': '1080P'},
|
||||
{'identifier': '720p', 'label': '720P'},
|
||||
{'identifier': 'brrip', 'label': 'BR-Rip'},
|
||||
{'identifier': 'dvdrip', 'label': 'DVD-Rip'}
|
||||
]
|
||||
})
|
||||
|
||||
var cp = new CouchPotato({
|
||||
App.setup({
|
||||
'base_url': '{{ request.path }}'
|
||||
});
|
||||
})
|
||||
|
||||
Executable
Executable
+554
@@ -0,0 +1,554 @@
|
||||
#!/usr/bin/env python
|
||||
#-*- coding:utf-8 -*-
|
||||
#author:doganaydin /// forked from dbr/Ben
|
||||
#project:themoviedb
|
||||
#repository:http://github.com/doganaydin/themoviedb
|
||||
#license: LGPLv2 http://www.gnu.org/licenses/lgpl.html
|
||||
|
||||
"""An interface to the themoviedb.org API"""
|
||||
|
||||
__author__ = "doganaydin"
|
||||
__version__ = "0.2"
|
||||
|
||||
class Config:
|
||||
|
||||
lang = 'en'
|
||||
api_key = 'API_KEY'
|
||||
urls = {
|
||||
'movie.search': "http://api.themoviedb.org/2.1/Movie.search/%(lang)s/xml/%(apikey)s/%%s",
|
||||
'movie.getInfo': "http://api.themoviedb.org/2.1/Movie.getInfo/%(lang)s/xml/%(apikey)s/%%s",
|
||||
'media.getInfo': "http://api.themoviedb.org/2.1/Media.getInfo/%(lang)s/xml/%(apikey)s/%%s/%%s",
|
||||
'imdb.lookUp': "http://api.themoviedb.org/2.1/Movie.imdbLookup/%(lang)s/xml/%(apikey)s/%%s",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def getUrl(type):
|
||||
return Config.urls[type] % {'lang': Config.lang, 'apikey': Config.api_key}
|
||||
|
||||
|
||||
import os, struct, urllib, urllib2, xml.etree.cElementTree as ElementTree
|
||||
|
||||
class TmdBaseError(Exception):
|
||||
pass
|
||||
class TmdNoResults(TmdBaseError):
|
||||
pass
|
||||
class TmdHttpError(TmdBaseError):
|
||||
pass
|
||||
class TmdXmlError(TmdBaseError):
|
||||
pass
|
||||
def opensubtitleHashFile(name):
|
||||
"""Hashes a file using OpenSubtitle's method.
|
||||
> In natural language it calculates: size + 64bit chksum of the first and
|
||||
> last 64k (even if they overlap because the file is smaller than 128k).
|
||||
A slightly more Pythonic version of the Python solution on..
|
||||
http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes
|
||||
"""
|
||||
longlongformat = 'q'
|
||||
bytesize = struct.calcsize(longlongformat)
|
||||
|
||||
f = open(name, "rb")
|
||||
|
||||
filesize = os.path.getsize(name)
|
||||
fhash = filesize
|
||||
|
||||
if filesize < 65536 * 2:
|
||||
raise ValueError("File size must be larger than %s bytes (is %s)" % (65536 * 2, filesize))
|
||||
|
||||
for x in range(65536 / bytesize):
|
||||
buf = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(longlongformat, buf)
|
||||
fhash += l_value
|
||||
fhash = fhash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number
|
||||
|
||||
f.seek(max(0, filesize - 65536), 0)
|
||||
for x in range(65536 / bytesize):
|
||||
buf = f.read(bytesize)
|
||||
(l_value,) = struct.unpack(longlongformat, buf)
|
||||
fhash += l_value
|
||||
fhash = fhash & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
f.close()
|
||||
return "%016x" % fhash
|
||||
|
||||
class XmlHandler:
|
||||
"""Deals with retrieval of XML files from API"""
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
def _grabUrl(self, url):
|
||||
try:
|
||||
urlhandle = urllib2.urlopen(url)
|
||||
except IOError, errormsg:
|
||||
raise TmdHttpError(errormsg)
|
||||
if urlhandle.code >= 400:
|
||||
raise TmdHttpError("HTTP status code was %d" % urlhandle.code)
|
||||
return urlhandle.read()
|
||||
|
||||
def getEt(self):
|
||||
xml = self._grabUrl(self.url)
|
||||
try:
|
||||
et = ElementTree.fromstring(xml)
|
||||
except SyntaxError, errormsg:
|
||||
raise TmdXmlError(errormsg)
|
||||
return et
|
||||
|
||||
class SearchResults(list):
|
||||
"""Stores a list of Movie's that matched the search"""
|
||||
def __repr__(self):
|
||||
return "<Search results: %s>" % (list.__repr__(self))
|
||||
|
||||
class MovieResult(dict):
|
||||
"""A dict containing the information about a specific search result"""
|
||||
def __repr__(self):
|
||||
return "<MovieResult: %s (%s)>" % (self.get("name"), self.get("released"))
|
||||
|
||||
def info(self):
|
||||
"""Performs a MovieDb.getMovieInfo search on the current id, returns
|
||||
a Movie object
|
||||
"""
|
||||
cur_id = self['id']
|
||||
info = MovieDb().getMovieInfo(cur_id)
|
||||
return info
|
||||
|
||||
class Movie(dict):
|
||||
"""A dict containing the information about the film"""
|
||||
def __repr__(self):
|
||||
return "<MovieResult: %s (%s)>" % (self.get("name"), self.get("released"))
|
||||
|
||||
class Categories(dict):
|
||||
"""Stores category information"""
|
||||
def set(self, category_et):
|
||||
"""Takes an elementtree Element ('category') and stores the url,
|
||||
using the type and name as the dict key.
|
||||
For example:
|
||||
<category type="genre" url="http://themoviedb.org/encyclopedia/category/80" name="Crime"/>
|
||||
..becomes:
|
||||
categories['genre']['Crime'] = 'http://themoviedb.org/encyclopedia/category/80'
|
||||
"""
|
||||
_type = category_et.get("type")
|
||||
name = category_et.get("name")
|
||||
url = category_et.get("url")
|
||||
self.setdefault(_type, {})[name] = url
|
||||
self[_type][name] = url
|
||||
|
||||
class Studios(dict):
|
||||
"""Stores category information"""
|
||||
def set(self, studio_et):
|
||||
"""Takes an elementtree Element ('studio') and stores the url,
|
||||
using the name as the dict key.
|
||||
For example:
|
||||
<studio url="http://www.themoviedb.org/encyclopedia/company/20" name="Miramax Films"/>
|
||||
..becomes:
|
||||
studios['name'] = 'http://www.themoviedb.org/encyclopedia/company/20'
|
||||
"""
|
||||
name = studio_et.get("name")
|
||||
url = studio_et.get("url")
|
||||
self[name] = url
|
||||
|
||||
class Countries(dict):
|
||||
"""Stores country information"""
|
||||
def set(self, country_et):
|
||||
"""Takes an elementtree Element ('country') and stores the url,
|
||||
using the name and code as the dict key.
|
||||
For example:
|
||||
<country url="http://www.themoviedb.org/encyclopedia/country/223" name="United States of America" code="US"/>
|
||||
..becomes:
|
||||
countries['code']['name'] = 'http://www.themoviedb.org/encyclopedia/country/223'
|
||||
"""
|
||||
code = country_et.get("code")
|
||||
name = country_et.get("name")
|
||||
url = country_et.get("url")
|
||||
self.setdefault(code, {})[name] = url
|
||||
|
||||
class Image(dict):
|
||||
"""Stores image information for a single poster/backdrop (includes
|
||||
multiple sizes)
|
||||
"""
|
||||
def __init__(self, _id, _type, size, url):
|
||||
self['id'] = _id
|
||||
self['type'] = _type
|
||||
|
||||
def largest(self):
|
||||
for csize in ["original", "mid", "cover", "thumb"]:
|
||||
if csize in self:
|
||||
return csize
|
||||
|
||||
def __repr__(self):
|
||||
return "<Image (%s for ID %s)>" % (self['type'], self['id'])
|
||||
|
||||
class ImagesList(list):
|
||||
"""Stores a list of Images, and functions to filter "only posters" etc"""
|
||||
def set(self, image_et):
|
||||
"""Takes an elementtree Element ('image') and stores the url,
|
||||
along with the type, id and size.
|
||||
Is a list containing each image as a dictionary (which includes the
|
||||
various sizes)
|
||||
For example:
|
||||
<image type="poster" size="original" url="http://images.themoviedb.org/posters/4181/67926_sin-city-02-color_122_207lo.jpg" id="4181"/>
|
||||
..becomes:
|
||||
images[0] = {'id':4181', 'type': 'poster', 'original': 'http://images.themov...'}
|
||||
"""
|
||||
_type = image_et.get("type")
|
||||
_id = image_et.get("id")
|
||||
size = image_et.get("size")
|
||||
url = image_et.get("url")
|
||||
cur = self.find_by('id', _id)
|
||||
if len(cur) == 0:
|
||||
nimg = Image(_id = _id, _type = _type, size = size, url = url)
|
||||
self.append(nimg)
|
||||
elif len(cur) == 1:
|
||||
cur[0][size] = url
|
||||
else:
|
||||
raise ValueError("Found more than one poster with id %s, this should never happen" % (_id))
|
||||
|
||||
def find_by(self, key, value):
|
||||
ret = []
|
||||
for cur in self:
|
||||
if cur[key] == value:
|
||||
ret.append(cur)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def posters(self):
|
||||
return self.find_by('type', 'poster')
|
||||
|
||||
@property
|
||||
def backdrops(self):
|
||||
return self.find_by('type', 'backdrop')
|
||||
|
||||
class CrewRoleList(dict):
|
||||
"""Stores a list of roles, such as director, actor etc
|
||||
>>> import tmdb
|
||||
>>> tmdb.getMovieInfo(550)['cast'].keys()[:5]
|
||||
['casting', 'producer', 'author', 'sound editor', 'actor']
|
||||
"""
|
||||
pass
|
||||
|
||||
class CrewList(list):
|
||||
"""Stores list of crew in specific role
|
||||
>>> import tmdb
|
||||
>>> tmdb.getMovieInfo(550)['cast']['author']
|
||||
[<author (id 7468): Chuck Palahniuk>, <author (id 7469): Jim Uhls>]
|
||||
"""
|
||||
pass
|
||||
|
||||
class Person(dict):
|
||||
"""Stores information about a specific member of cast"""
|
||||
def __init__(self, job, _id, name, character, url):
|
||||
self['job'] = job
|
||||
self['id'] = _id
|
||||
self['name'] = name
|
||||
self['character'] = character
|
||||
self['url'] = url
|
||||
|
||||
def __repr__(self):
|
||||
if self['character'] is None or self['character'] == "":
|
||||
return "<%(job)s (id %(id)s): %(name)s>" % self
|
||||
else:
|
||||
return "<%(job)s (id %(id)s): %(name)s (as %(character)s)>" % self
|
||||
|
||||
class MovieDb:
|
||||
"""Main interface to www.themoviedb.com
|
||||
The search() method searches for the film by title.
|
||||
The getMovieInfo() method retrieves information about a specific movie using themoviedb id.
|
||||
"""
|
||||
def _parseSearchResults(self, movie_element):
|
||||
cur_movie = MovieResult()
|
||||
cur_images = ImagesList()
|
||||
for item in movie_element.getchildren():
|
||||
if item.tag.lower() == "images":
|
||||
for subitem in item.getchildren():
|
||||
cur_images.set(subitem)
|
||||
else:
|
||||
cur_movie[item.tag] = item.text
|
||||
cur_movie['images'] = cur_images
|
||||
return cur_movie
|
||||
|
||||
def _parseMovie(self, movie_element):
|
||||
cur_movie = Movie()
|
||||
cur_categories = Categories()
|
||||
cur_studios = Studios()
|
||||
cur_countries = Countries()
|
||||
cur_images = ImagesList()
|
||||
cur_cast = CrewRoleList()
|
||||
for item in movie_element.getchildren():
|
||||
if item.tag.lower() == "categories":
|
||||
for subitem in item.getchildren():
|
||||
cur_categories.set(subitem)
|
||||
elif item.tag.lower() == "studios":
|
||||
for subitem in item.getchildren():
|
||||
cur_studios.set(subitem)
|
||||
elif item.tag.lower() == "countries":
|
||||
for subitem in item.getchildren():
|
||||
cur_countries.set(subitem)
|
||||
elif item.tag.lower() == "images":
|
||||
for subitem in item.getchildren():
|
||||
cur_images.set(subitem)
|
||||
elif item.tag.lower() == "cast":
|
||||
for subitem in item.getchildren():
|
||||
job = subitem.get("job").lower()
|
||||
p = Person(
|
||||
job = job,
|
||||
_id = subitem.get("id"),
|
||||
name = subitem.get("name"),
|
||||
character = subitem.get("character"),
|
||||
url = subitem.get("url"),
|
||||
)
|
||||
cur_cast.setdefault(job, CrewList()).append(p)
|
||||
else:
|
||||
cur_movie[item.tag] = item.text
|
||||
|
||||
cur_movie['categories'] = cur_categories
|
||||
cur_movie['studios'] = cur_studios
|
||||
cur_movie['countries'] = cur_countries
|
||||
cur_movie['images'] = cur_images
|
||||
cur_movie['cast'] = cur_cast
|
||||
return cur_movie
|
||||
|
||||
def search(self, title):
|
||||
"""Searches for a film by its title.
|
||||
Returns SearchResults (a list) containing all matches (Movie instances)
|
||||
"""
|
||||
title = urllib.quote(title.encode("utf-8"))
|
||||
url = Config.getUrl('movie.search') % (title)
|
||||
etree = XmlHandler(url).getEt()
|
||||
search_results = SearchResults()
|
||||
for cur_result in etree.find("movies").findall("movie"):
|
||||
cur_movie = self._parseSearchResults(cur_result)
|
||||
search_results.append(cur_movie)
|
||||
return search_results
|
||||
|
||||
def getMovieInfo(self, id):
|
||||
"""Returns movie info by it's TheMovieDb ID.
|
||||
Returns a Movie instance
|
||||
"""
|
||||
url = Config.getUrl('movie.getInfo') % (id)
|
||||
etree = XmlHandler(url).getEt()
|
||||
moviesTree = etree.find("movies").findall("movie")
|
||||
|
||||
if len(moviesTree) == 0:
|
||||
raise TmdNoResults("No results for id %s" % id)
|
||||
return self._parseMovie(moviesTree[0])
|
||||
|
||||
def mediaGetInfo(self, hash, size):
|
||||
"""Used to retrieve specific information about a movie but instead of
|
||||
passing a TMDb ID, you pass a file hash and filesize in bytes
|
||||
"""
|
||||
url = Config.getUrl('media.getInfo') % (hash, size)
|
||||
etree = XmlHandler(url).getEt()
|
||||
moviesTree = etree.find("movies").findall("movie")
|
||||
if len(moviesTree) == 0:
|
||||
raise TmdNoResults("No results for hash %s" % hash)
|
||||
return [self._parseMovie(x) for x in moviesTree]
|
||||
|
||||
def imdbLookup(self, id = 0, title = False):
|
||||
if id > 0:
|
||||
url = Config.getUrl('imdb.lookUp') % (id)
|
||||
else:
|
||||
_imdb_id = self.search(title)[0]["imdb_id"]
|
||||
url = Config.getUrl('imdb.lookUp') % (_imdb_id)
|
||||
etree = XmlHandler(url).getEt()
|
||||
lookup_results = SearchResults()
|
||||
for cur_lookup in etree.find("movies").findall("movie"):
|
||||
cur_movie = self._parseSearchResults(cur_lookup)
|
||||
lookup_results.append(cur_movie)
|
||||
return lookup_results
|
||||
|
||||
# Shortcuts for tmdb search method
|
||||
# using:
|
||||
# movie = tmdb.tmdb("Sin City")
|
||||
# print movie.getRating -> 7.0
|
||||
class tmdb:
|
||||
def __init__(self, name):
|
||||
"""Convenience wrapper for MovieDb.search - so you can do..
|
||||
>>> import tmdb
|
||||
>>> movie = tmdb.tmdb("Fight Club")
|
||||
>>> ranking = movie.getRanking() or votes = movie.getVotes()
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]>
|
||||
"""
|
||||
mdb = MovieDb()
|
||||
self.movie = mdb.search(name)[int(result)]
|
||||
def getRating(self, i):
|
||||
return self.movie[i]["rating"]
|
||||
def getVotes(self, i):
|
||||
return self.movie[i]["votes"]
|
||||
def getName(self, i):
|
||||
return self.movie[i]["name"]
|
||||
def getLanguage(self, i):
|
||||
return self.movie[i]["language"]
|
||||
def getCertification(self, i):
|
||||
return self.movie[i]["certification"]
|
||||
def getUrl(self, i):
|
||||
return self.movie[i]["url"]
|
||||
def getOverview(self, i):
|
||||
return self.movie[i]["overview"]
|
||||
def getPopularity(self, i):
|
||||
return self.movie[i]["popularity"]
|
||||
def getOriginalName(self, i):
|
||||
return self.movie[i]["original_name"]
|
||||
def getLastModified(self, i):
|
||||
return self.movie[i]["last_modified_at"]
|
||||
def getImdbId(self, i):
|
||||
return self.movie[i]["imdb_id"]
|
||||
def getReleased(self, i):
|
||||
return self.movie[i]["released"]
|
||||
def getScore(self, i):
|
||||
return self.movie[i]["score"]
|
||||
def getAdult(self, i):
|
||||
return self.movie[i]["adult"]
|
||||
def getVersion(self, i):
|
||||
return self.movie[i]["version"]
|
||||
def getTranslated(self, i):
|
||||
return self.movie[i]["translated"]
|
||||
def getType(self, i):
|
||||
return self.movie[i]["type"]
|
||||
def getId(self, i):
|
||||
return self.movie[i]["id"]
|
||||
def getAlternativeName(self, i):
|
||||
return self.movie[i]["alternative_name"]
|
||||
def getPoster(self, i, size):
|
||||
if size == "thumb" or size == "t":
|
||||
return self.movie[i]["images"][0]["thumb"]
|
||||
elif size == "cover" or size == "c":
|
||||
return self.movie[i]["images"][0]["cover"]
|
||||
else:
|
||||
return self.movie[i]["images"][0]["mid"]
|
||||
|
||||
def getBackdrop(self, i, size):
|
||||
if size == "poster" or size == "p":
|
||||
return self.movie[i]["images"][1]["poster"]
|
||||
else:
|
||||
return self.movie[i]["images"][1]["thumb"]
|
||||
|
||||
# Shortcuts for imdb lookup method
|
||||
# using:
|
||||
# movie = tmdb.imdb("Sin City")
|
||||
# print movie.getRating -> 7.0
|
||||
class imdb:
|
||||
def __init__(self, id = 0, title = False):
|
||||
# get first movie if result=0
|
||||
"""Convenience wrapper for MovieDb.search - so you can do..
|
||||
>>> import tmdb
|
||||
>>> movie = tmdb.imdb(title="Fight Club") # or movie = tmdb.imdb(id=imdb_id)
|
||||
>>> ranking = movie.getRanking() or votes = movie.getVotes()
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]>
|
||||
"""
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.mdb = MovieDb()
|
||||
self.movie = self.mdb.imdbLookup(self.id, self.title)
|
||||
def getRuntime(self, i):
|
||||
return self.movie[i]["runtime"]
|
||||
def getCategories(self):
|
||||
from xml.dom.minidom import parse
|
||||
adres = Config.getUrl('imdb.lookUp') % self.getImdbId()
|
||||
d = parse(urllib2.urlopen(adres))
|
||||
s = d.getElementsByTagName("categories")
|
||||
ds = []
|
||||
for i in range(len(s[0].childNodes)):
|
||||
if i % 2 > 0:
|
||||
ds.append(s[0].childNodes[i].getAttribute("name"))
|
||||
return ds
|
||||
def getRating(self, i):
|
||||
return self.movie[i]["rating"]
|
||||
def getVotes(self, i):
|
||||
return self.movie[i]["votes"]
|
||||
def getName(self, i):
|
||||
return self.movie[i]["name"]
|
||||
def getLanguage(self, i):
|
||||
return self.movie[i]["language"]
|
||||
def getCertification(self, i):
|
||||
return self.movie[i]["certification"]
|
||||
def getUrl(self, i):
|
||||
return self.movie[i]["url"]
|
||||
def getOverview(self, i):
|
||||
return self.movie[i]["overview"]
|
||||
def getPopularity(self, i):
|
||||
return self.movie[i]["popularity"]
|
||||
def getOriginalName(self, i):
|
||||
return self.movie[i]["original_name"]
|
||||
def getLastModified(self, i):
|
||||
return self.movie[i]["last_modified_at"]
|
||||
def getImdbId(self, i):
|
||||
return self.movie[i]["imdb_id"]
|
||||
def getReleased(self, i):
|
||||
return self.movie[i]["released"]
|
||||
def getAdult(self, i):
|
||||
return self.movie[i]["adult"]
|
||||
def getVersion(self, i):
|
||||
return self.movie[i]["version"]
|
||||
def getTranslated(self, i):
|
||||
return self.movie[i]["translated"]
|
||||
def getType(self, i):
|
||||
return self.movie[i]["type"]
|
||||
def getId(self, i):
|
||||
return self.movie[i]["id"]
|
||||
def getAlternativeName(self, i):
|
||||
return self.movie[i]["alternative_name"]
|
||||
def getPoster(self, i, size):
|
||||
poster = []
|
||||
if size == "thumb" or size == "t":
|
||||
_size = "thumb"
|
||||
elif size == "cover" or size == "c":
|
||||
_size = "cover"
|
||||
else:
|
||||
_size = "mid"
|
||||
for a in self.movie[i]["images"]:
|
||||
if a["type"] == "poster":
|
||||
poster.append(a[_size])
|
||||
return poster
|
||||
del poster
|
||||
def getBackdrop(self, i, size):
|
||||
backdrop = []
|
||||
if size == "thumb" or size == "t":
|
||||
_size = "thumb"
|
||||
elif size == "cover" or size == "c":
|
||||
_size = "cover"
|
||||
else:
|
||||
_size = "mid"
|
||||
for a in self.movie[i]["images"]:
|
||||
if a["type"] == "backdrop":
|
||||
backdrop.append(a[_size])
|
||||
return backdrop
|
||||
del backdrop
|
||||
|
||||
def imdbLookup(id = 0, title = False):
|
||||
"""Convenience wrapper for Imdb.Lookup - so you can do..
|
||||
>>> import tmdb
|
||||
>>> tmdb.imdbLookup("Fight Club")
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]>
|
||||
"""
|
||||
mdb = MovieDb()
|
||||
return mdb.imdbLookup(id, title)
|
||||
def search(name):
|
||||
"""Convenience wrapper for MovieDb.search - so you can do..
|
||||
>>> import tmdb
|
||||
>>> tmdb.search("Fight Club")
|
||||
<Search results: [<MovieResult: Fight Club (1999-09-16)>]>
|
||||
"""
|
||||
mdb = MovieDb()
|
||||
return mdb.search(name)
|
||||
def getMovieInfo(id):
|
||||
"""Convenience wrapper for MovieDb.search - so you can do..
|
||||
>>> import tmdb
|
||||
>>> tmdb.getMovieInfo(187)
|
||||
<MovieResult: Sin City (2005-04-01)>
|
||||
"""
|
||||
mdb = MovieDb()
|
||||
return mdb.getMovieInfo(id)
|
||||
|
||||
def mediaGetInfo(hash, size):
|
||||
"""Convenience wrapper for MovieDb.mediaGetInfo - so you can do..
|
||||
|
||||
>>> import tmdb
|
||||
>>> tmdb.mediaGetInfo('907172e7fe51ba57', size = 742086656)[0]
|
||||
<MovieResult: Sin City (2005-04-01)>
|
||||
"""
|
||||
mdb = MovieDb()
|
||||
return mdb.mediaGetInfo(hash, size)
|
||||
|
||||
def searchByHashingFile(filename):
|
||||
"""Searches for the specified file using the OpenSubtitle hashing method
|
||||
"""
|
||||
return mediaGetInfo(opensubtitleHashFile(filename), os.path.size(filename))
|
||||
|
||||
Reference in New Issue
Block a user