")[0]
+ except:
+ try:
+ split = splitString(html, split_on = "
")
+
+ if len(split) < 2:
+ log.error('Failed parsing IMDB page "%s", unexpected html.', url)
+ return []
+
+ html = BeautifulSoup(split[1])
+ for x in ['list compact', 'lister', 'list detail sub-list']:
+ html2 = html.find('div', attrs = {
+ 'class': x
+ })
+
+ if html2:
+ html = html2.contents
+ html = ''.join([str(x) for x in html])
+ break
+ except:
+ log.error('Failed parsing IMDB page "%s": %s', (url, traceback.format_exc()))
+
+ html = ss(html)
+ imdbs = getImdb(html, multiple = True) if html else []
+
+ return imdbs
+
+
+class IMDBWatchlist(IMDBBase):
+
+ enabled_option = 'automation_enabled'
+
+ def getIMDBids(self):
+
+ movies = []
+
+ watchlist_enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
+ watchlist_urls = splitString(self.conf('automation_urls'))
+
+ index = -1
+ for watchlist_url in watchlist_urls:
+
+ try:
+ # Get list ID
+ ids = re.findall('(?:list/|list_id=)([a-zA-Z0-9\-_]{11})', watchlist_url)
+ if len(ids) == 1:
+ watchlist_url = 'http://www.imdb.com/list/%s/?view=compact&sort=created:asc' % ids[0]
+ # Try find user id with watchlist
+ else:
+ userids = re.findall('(ur\d{7,9})', watchlist_url)
+ if len(userids) == 1:
+ watchlist_url = 'http://www.imdb.com/user/%s/watchlist?view=compact&sort=created:asc' % userids[0]
+ except:
+ log.error('Failed getting id from watchlist: %s', traceback.format_exc())
+
+ index += 1
+ if not watchlist_enablers[index]:
+ continue
+
+ start = 0
+ while True:
+ try:
+
+ w_url = '%s&start=%s' % (watchlist_url, start)
+ imdbs = self.getFromURL(w_url)
+
+ for imdb in imdbs:
+ if imdb not in movies:
+ movies.append(imdb)
+
+ if self.shuttingDown():
+ break
+
+ log.debug('Found %s movies on %s', (len(imdbs), w_url))
+
+ if len(imdbs) < 225:
+ break
+
+ start = len(movies)
+
+ except:
+ log.error('Failed loading IMDB watchlist: %s %s', (watchlist_url, traceback.format_exc()))
+ break
+
+ return movies
+
+
+class IMDBAutomation(IMDBBase):
+
+ enabled_option = 'automation_providers_enabled'
+
+ charts = {
+ 'theater': {
+ 'order': 1,
+ 'name': 'IMDB - Movies in Theaters',
+ 'url': 'http://www.imdb.com/movies-in-theaters/',
+ },
+ 'boxoffice': {
+ 'order': 2,
+ 'name': 'IMDB - Box Office',
+ 'url': 'http://www.imdb.com/boxoffice/',
+ },
+ 'rentals': {
+ 'order': 3,
+ 'name': 'IMDB - Top DVD rentals',
+ 'url': 'http://www.imdb.com/boxoffice/rentals',
+ 'type': 'json',
+ },
+ 'top250': {
+ 'order': 4,
+ 'name': 'IMDB - Top 250 Movies',
+ 'url': 'http://www.imdb.com/chart/top',
+ },
+ }
+
+ def getIMDBids(self):
+
+ movies = []
+
+ for name in self.charts:
+ chart = self.charts[name]
+ url = chart.get('url')
+
+ if self.conf('automation_charts_%s' % name):
+ imdb_ids = self.getFromURL(url)
+
+ try:
+ for imdb_id in imdb_ids:
+ info = self.getInfo(imdb_id)
+ if info and self.isMinimalMovie(info):
+ movies.append(imdb_id)
+
+ if self.shuttingDown():
+ break
+
+ except:
+ log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc()))
+
+ return movies
+
+ def getChartList(self):
+
+ # Nearly identical to 'getIMDBids', but we don't care about minimalMovie and return all movie data (not just id)
+ movie_lists = []
+ max_items = int(self.conf('max_items', section = 'charts', default=5))
+
+ for name in self.charts:
+ chart = self.charts[name].copy()
+ url = chart.get('url')
+
+ if self.conf('chart_display_%s' % name):
+
+ chart['list'] = []
+
+ imdb_ids = self.getFromURL(url)
+
+ try:
+ for imdb_id in imdb_ids[0:max_items]:
+
+ is_movie = fireEvent('movie.is_movie', identifier = imdb_id, single = True)
+ if not is_movie:
+ continue
+
+ info = self.getInfo(imdb_id)
+ chart['list'].append(info)
+
+ if self.shuttingDown():
+ break
+ except:
+ log.error('Failed loading IMDB chart results from %s: %s', (url, traceback.format_exc()))
+
+ if chart['list']:
+ movie_lists.append(chart)
+
+
+ return movie_lists
+
+
+config = [{
+ 'name': 'imdb',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'watchlist_providers',
+ 'name': 'imdb_automation_watchlist',
+ 'label': 'IMDB',
+ 'description': 'From any
public IMDB watchlists.',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'url',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ },
+ ],
+ },
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'imdb_automation_charts',
+ 'label': 'IMDB',
+ 'description': 'Import movies from IMDB Charts',
+ 'options': [
+ {
+ 'name': 'automation_providers_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_charts_theater',
+ 'type': 'bool',
+ 'label': 'In Theaters',
+ 'description': 'New Movies
In-Theaters chart',
+ 'default': True,
+ },
+ {
+ 'name': 'automation_charts_rentals',
+ 'type': 'bool',
+ 'label': 'DVD Rentals',
+ 'description': 'Top DVD
rentals chart',
+ 'default': True,
+ },
+ {
+ 'name': 'automation_charts_top250',
+ 'type': 'bool',
+ 'label': 'TOP 250',
+ 'description': 'IMDB
TOP 250 chart',
+ 'default': False,
+ },
+ {
+ 'name': 'automation_charts_boxoffice',
+ 'type': 'bool',
+ 'label': 'Box office TOP 10',
+ 'description': 'IMDB Box office
TOP 10 chart',
+ 'default': True,
+ },
+ ],
+ },
+ {
+ 'tab': 'display',
+ 'list': 'charts_providers',
+ 'name': 'imdb_charts_display',
+ 'label': 'IMDB',
+ 'description': 'Display movies from IMDB Charts',
+ 'options': [
+ {
+ 'name': 'chart_display_enabled',
+ 'default': True,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'chart_display_theater',
+ 'type': 'bool',
+ 'label': 'In Theaters',
+ 'description': 'New Movies
In-Theaters chart',
+ 'default': False,
+ },
+ {
+ 'name': 'chart_display_top250',
+ 'type': 'bool',
+ 'label': 'TOP 250',
+ 'description': 'IMDB
TOP 250 chart',
+ 'default': False,
+ },
+ {
+ 'name': 'chart_display_rentals',
+ 'type': 'bool',
+ 'label': 'DVD Rentals',
+ 'description': 'Top DVD
rentals chart',
+ 'default': True,
+ },
+ {
+ 'name': 'chart_display_boxoffice',
+ 'type': 'bool',
+ 'label': 'Box office TOP 10',
+ 'description': 'IMDB Box office
TOP 10 chart',
+ 'default': True,
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/media/movie/providers/automation/itunes.py
similarity index 61%
rename from couchpotato/core/providers/automation/itunes/main.py
rename to couchpotato/core/media/movie/providers/automation/itunes.py
index 086c981d..fcb20b12 100644
--- a/couchpotato/core/providers/automation/itunes/main.py
+++ b/couchpotato/core/media/movie/providers/automation/itunes.py
@@ -1,14 +1,18 @@
-from couchpotato.core.helpers.rss import RSS
-from couchpotato.core.helpers.variable import md5, splitString, tryInt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
from xml.etree.ElementTree import QName
import datetime
import traceback
import xml.etree.ElementTree as XMLTree
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.helpers.variable import md5, splitString, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
+
log = CPLog(__name__)
+autoload = 'ITunes'
+
class ITunes(Automation, RSS):
@@ -22,7 +26,7 @@ class ITunes(Automation, RSS):
urls = splitString(self.conf('automation_urls'))
namespace = 'http://www.w3.org/2005/Atom'
- namespace_im = 'https://rss.itunes.apple.com'
+ namespace_im = 'http://itunes.apple.com/rss'
index = -1
for url in urls:
@@ -58,3 +62,36 @@ class ITunes(Automation, RSS):
log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc()))
return movies
+
+
+config = [{
+ 'name': 'itunes',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'itunes_automation',
+ 'label': 'iTunes',
+ 'description': 'From any
iTunes Store feed. Url should be the RSS link.',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ 'default': ',',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'url',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ 'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/kinepolis/main.py b/couchpotato/core/media/movie/providers/automation/kinepolis.py
similarity index 50%
rename from couchpotato/core/providers/automation/kinepolis/main.py
rename to couchpotato/core/media/movie/providers/automation/kinepolis.py
index 4158d488..a0f25965 100644
--- a/couchpotato/core/providers/automation/kinepolis/main.py
+++ b/couchpotato/core/media/movie/providers/automation/kinepolis.py
@@ -1,10 +1,14 @@
-from couchpotato.core.helpers.rss import RSS
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
import datetime
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
+
log = CPLog(__name__)
+autoload = 'Kinepolis'
+
class Kinepolis(Automation, RSS):
@@ -27,3 +31,24 @@ class Kinepolis(Automation, RSS):
movies.append(imdb['imdb'])
return movies
+
+
+config = [{
+ 'name': 'kinepolis',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'kinepolis_automation',
+ 'label': 'Kinepolis',
+ 'description': 'Imports movies from the current top 10 of kinepolis.',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/media/movie/providers/automation/letterboxd.py
similarity index 51%
rename from couchpotato/core/providers/automation/letterboxd/main.py
rename to couchpotato/core/media/movie/providers/automation/letterboxd.py
index dbbf53b1..e9fc8741 100644
--- a/couchpotato/core/providers/automation/letterboxd/main.py
+++ b/couchpotato/core/media/movie/providers/automation/letterboxd.py
@@ -1,11 +1,15 @@
+import re
+
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, splitString, removeEmpty
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
-import re
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
log = CPLog(__name__)
+autoload = 'Letterboxd'
+
class Letterboxd(Automation):
@@ -46,6 +50,40 @@ class Letterboxd(Automation):
for movie in soup.find_all('a', attrs = {'class': 'frame'}):
match = removeEmpty(self.pattern.split(movie['title']))
- movies.append({'title': match[0], 'year': match[1] })
+ movies.append({
+ 'title': match[0],
+ 'year': match[1]
+ })
return movies
+
+
+config = [{
+ 'name': 'letterboxd',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'watchlist_providers',
+ 'name': 'letterboxd_automation',
+ 'label': 'Letterboxd',
+ 'description': 'Import movies from any public
Letterboxd watchlist',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'Username',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/media/movie/providers/automation/moviemeter.py b/couchpotato/core/media/movie/providers/automation/moviemeter.py
new file mode 100644
index 00000000..b06046fb
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/automation/moviemeter.py
@@ -0,0 +1,55 @@
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
+log = CPLog(__name__)
+
+autoload = 'Moviemeter'
+
+
+class Moviemeter(Automation, RSS):
+
+ interval = 1800
+ rss_url = 'http://www.moviemeter.nl/rss/cinema'
+
+ def getIMDBids(self):
+
+ movies = []
+
+ rss_movies = self.getRSSData(self.rss_url)
+
+ for movie in rss_movies:
+
+ title = self.getTextElement(movie, 'title')
+ name_year = fireEvent('scanner.name_year', title, single = True)
+ if name_year.get('name') and name_year.get('year'):
+ imdb = self.search(name_year.get('name'), name_year.get('year'))
+
+ if imdb and self.isMinimalMovie(imdb):
+ movies.append(imdb['imdb'])
+ else:
+ log.error('Failed getting name and year from: %s', title)
+
+ return movies
+
+
+config = [{
+ 'name': 'moviemeter',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'moviemeter_automation',
+ 'label': 'Moviemeter',
+ 'description': 'Imports movies from the current top 10 of moviemeter.nl.',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/movies_io/main.py b/couchpotato/core/media/movie/providers/automation/movies_io.py
similarity index 50%
rename from couchpotato/core/providers/automation/movies_io/main.py
rename to couchpotato/core/media/movie/providers/automation/movies_io.py
index 0737e2e6..3b0b54f2 100644
--- a/couchpotato/core/providers/automation/movies_io/main.py
+++ b/couchpotato/core/media/movie/providers/automation/movies_io.py
@@ -2,10 +2,12 @@ from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
+from couchpotato.core.media.movie.providers.automation.base import Automation
log = CPLog(__name__)
+autoload = 'MoviesIO'
+
class MoviesIO(Automation, RSS):
@@ -37,3 +39,34 @@ class MoviesIO(Automation, RSS):
movies.append(imdb)
return movies
+
+
+config = [{
+ 'name': 'moviesio',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'watchlist_providers',
+ 'name': 'moviesio',
+ 'label': 'Movies.IO',
+ 'description': 'Imports movies from
Movies.io RSS watchlists',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'url',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/media/movie/providers/automation/popularmovies.py b/couchpotato/core/media/movie/providers/automation/popularmovies.py
new file mode 100644
index 00000000..eb46ecef
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/automation/popularmovies.py
@@ -0,0 +1,47 @@
+from couchpotato import fireEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
+log = CPLog(__name__)
+
+autoload = 'PopularMovies'
+
+
+class PopularMovies(Automation):
+
+ interval = 1800
+ url = 'https://s3.amazonaws.com/popular-movies/movies.json'
+
+ def getIMDBids(self):
+
+ movies = []
+ retrieved_movies = self.getJsonData(self.url)
+
+ for movie in retrieved_movies.get('movies'):
+ imdb_id = movie.get('imdb_id')
+ info = fireEvent('movie.info', identifier = imdb_id, extended = False, merge = True)
+ if self.isMinimalMovie(info):
+ movies.append(imdb_id)
+
+ return movies
+
+
+config = [{
+ 'name': 'popularmovies',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'popularmovies_automation',
+ 'label': 'Popular Movies',
+ 'description': 'Imports the
top titles of movies that have been in theaters. Script provided by
Steven Lu',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py
similarity index 54%
rename from couchpotato/core/providers/automation/rottentomatoes/main.py
rename to couchpotato/core/media/movie/providers/automation/rottentomatoes.py
index c873a8e1..a01f76d2 100644
--- a/couchpotato/core/providers/automation/rottentomatoes/main.py
+++ b/couchpotato/core/media/movie/providers/automation/rottentomatoes.py
@@ -1,13 +1,17 @@
-from couchpotato.core.helpers.rss import RSS
-from couchpotato.core.helpers.variable import tryInt, splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
from xml.etree.ElementTree import QName
import datetime
import re
+from couchpotato.core.helpers.rss import RSS
+from couchpotato.core.helpers.variable import tryInt, splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
+
log = CPLog(__name__)
+autoload = 'Rottentomatoes'
+
class Rottentomatoes(Automation, RSS):
@@ -51,3 +55,42 @@ class Rottentomatoes(Automation, RSS):
movies.append(imdb['imdb'])
return movies
+
+
+config = [{
+ 'name': 'rottentomatoes',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'automation_providers',
+ 'name': 'rottentomatoes_automation',
+ 'label': 'Rottentomatoes',
+ 'description': 'Imports movies from rottentomatoes rss feeds specified below.',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_urls_use',
+ 'label': 'Use',
+ 'default': '1',
+ },
+ {
+ 'name': 'automation_urls',
+ 'label': 'url',
+ 'type': 'combined',
+ 'combine': ['automation_urls_use', 'automation_urls'],
+ 'default': 'http://www.rottentomatoes.com/syndication/rss/in_theaters.xml',
+ },
+ {
+ 'name': 'tomatometer_percent',
+ 'default': '80',
+ 'label': 'Tomatometer',
+ 'description': 'Use as extra scoring requirement',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/automation/trakt/main.py b/couchpotato/core/media/movie/providers/automation/trakt.py
similarity index 51%
rename from couchpotato/core/providers/automation/trakt/main.py
rename to couchpotato/core/media/movie/providers/automation/trakt.py
index 0109daf3..440a59d8 100644
--- a/couchpotato/core/providers/automation/trakt/main.py
+++ b/couchpotato/core/media/movie/providers/automation/trakt.py
@@ -1,11 +1,15 @@
+import base64
+
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import sha1
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.automation.base import Automation
-import base64
+from couchpotato.core.media.movie.providers.automation.base import Automation
+
log = CPLog(__name__)
+autoload = 'Trakt'
+
class Trakt(Automation):
@@ -42,3 +46,38 @@ class Trakt(Automation):
data = self.getJsonData(self.urls['base'] + method_url, headers = headers)
return data if data else []
+
+
+config = [{
+ 'name': 'trakt',
+ 'groups': [
+ {
+ 'tab': 'automation',
+ 'list': 'watchlist_providers',
+ 'name': 'trakt_automation',
+ 'label': 'Trakt',
+ 'description': 'import movies from your own watchlist',
+ 'options': [
+ {
+ 'name': 'automation_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'automation_api_key',
+ 'label': 'Apikey',
+ },
+ {
+ 'name': 'automation_username',
+ 'label': 'Username',
+ },
+ {
+ 'name': 'automation_password',
+ 'label': 'Password',
+ 'type': 'password',
+ 'description': 'When you have "Protect my data" checked
on trakt.',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/media/movie/providers/base.py b/couchpotato/core/media/movie/providers/base.py
new file mode 100644
index 00000000..4e80d5d3
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/base.py
@@ -0,0 +1,5 @@
+from couchpotato.core.media._base.providers.info.base import BaseInfoProvider
+
+
+class MovieProvider(BaseInfoProvider):
+ type = 'movie'
diff --git a/couchpotato/core/providers/userscript/__init__.py b/couchpotato/core/media/movie/providers/info/__init__.py
similarity index 100%
rename from couchpotato/core/providers/userscript/__init__.py
rename to couchpotato/core/media/movie/providers/info/__init__.py
diff --git a/couchpotato/core/providers/info/_modifier/main.py b/couchpotato/core/media/movie/providers/info/_modifier.py
similarity index 68%
rename from couchpotato/core/providers/info/_modifier/main.py
rename to couchpotato/core/media/movie/providers/info/_modifier.py
index 88d4381c..f6a3089b 100644
--- a/couchpotato/core/providers/info/_modifier/main.py
+++ b/couchpotato/core/media/movie/providers/info/_modifier.py
@@ -1,14 +1,18 @@
-from couchpotato import get_session
+import copy
+import traceback
+
+from CodernityDB.database import RecordNotFound
+from couchpotato import get_db
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts, randomString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Library
-import copy
-import traceback
+
log = CPLog(__name__)
+autoload = 'MovieResultModifier'
+
class MovieResultModifier(Plugin):
@@ -22,7 +26,14 @@ class MovieResultModifier(Plugin):
'backdrop': [],
'poster_original': [],
'backdrop_original': [],
- 'actors': {}
+ 'actors': {},
+ 'landscape': [],
+ 'logo': [],
+ 'clear_art': [],
+ 'disc_art': [],
+ 'banner': [],
+ 'extra_thumbs': [],
+ 'extra_fanart': []
},
'runtime': 0,
'plot': '',
@@ -86,21 +97,30 @@ class MovieResultModifier(Plugin):
}
# Add release info from current library
- db = get_session()
+ db = get_db()
try:
- l = db.query(Library).filter_by(identifier = imdb).first()
- if l:
- # Statuses
- active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
+ media = None
+ try:
+ media = db.get('media', 'imdb-%s' % imdb, with_doc = True)['doc']
+ except RecordNotFound:
+ pass
- for movie in l.movies:
- if movie.status_id == active_status['id']:
- temp['in_wanted'] = fireEvent('media.get', movie.id, single = True)
+ if media:
- for release in movie.releases:
- if release.status_id == done_status['id']:
- temp['in_library'] = fireEvent('media.get', movie.id, single = True)
+ if media.get('status') == 'active':
+ temp['in_wanted'] = media
+
+ try: temp['in_wanted']['profile'] = db.get('id', media['profile_id'])
+ except: temp['in_wanted']['profile'] = {'label': ''}
+
+ for release in fireEvent('release.for_media', media['_id'], single = True):
+ if release.get('status') == 'done':
+ if not temp['in_library']:
+ temp['in_library'] = media
+ temp['in_library']['releases'] = []
+
+ temp['in_library']['releases'].append(release)
except:
log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
diff --git a/couchpotato/core/providers/info/couchpotatoapi/main.py b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py
similarity index 94%
rename from couchpotato/core/providers/info/couchpotatoapi/main.py
rename to couchpotato/core/media/movie/providers/info/couchpotatoapi.py
index 848cbf05..4c65bf8c 100644
--- a/couchpotato/core/providers/info/couchpotatoapi/main.py
+++ b/couchpotato/core/media/movie/providers/info/couchpotatoapi.py
@@ -1,13 +1,17 @@
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.encoding import tryUrlencode
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.info.base import MovieProvider
-from couchpotato.environment import Env
import base64
import time
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.base import MovieProvider
+from couchpotato.environment import Env
+
+
log = CPLog(__name__)
+autoload = 'CouchPotatoApi'
+
class CouchPotatoApi(MovieProvider):
@@ -25,10 +29,12 @@ class CouchPotatoApi(MovieProvider):
api_version = 1
def __init__(self):
- addEvent('movie.info', self.getInfo, priority = 1)
+ addEvent('movie.info', self.getInfo, priority = 2)
+ addEvent('movie.info.release_date', self.getReleaseDate)
+
addEvent('info.search', self.search, priority = 1)
addEvent('movie.search', self.search, priority = 1)
- addEvent('movie.release_date', self.getReleaseDate)
+
addEvent('movie.suggest', self.getSuggestions)
addEvent('movie.is_movie', self.isMovie)
diff --git a/couchpotato/core/media/movie/providers/info/fanarttv.py b/couchpotato/core/media/movie/providers/info/fanarttv.py
new file mode 100644
index 00000000..8bfa92c8
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/info/fanarttv.py
@@ -0,0 +1,130 @@
+import traceback
+
+from couchpotato import tryInt
+from couchpotato.core.event import addEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+
+log = CPLog(__name__)
+
+autoload = 'FanartTV'
+
+
+class FanartTV(MovieProvider):
+
+ urls = {
+ 'api': 'http://api.fanart.tv/webservice/movie/b28b14e9be662e027cfbc7c3dd600405/%s/JSON/all/1/2'
+ }
+
+ MAX_EXTRAFANART = 20
+ http_time_between_calls = 0
+
+ def __init__(self):
+ addEvent('movie.info', self.getArt, priority = 1)
+
+ def getArt(self, identifier = None, **kwargs):
+
+ log.debug("Getting Extra Artwork from Fanart.tv...")
+ if not identifier:
+ return {}
+
+ images = {}
+
+ try:
+ url = self.urls['api'] % identifier
+ fanart_data = self.getJsonData(url)
+
+ if fanart_data:
+ name, resource = fanart_data.items()[0]
+ log.debug('Found images for %s', name)
+ images = self._parseMovie(resource)
+
+ except:
+ log.error('Failed getting extra art for %s: %s',
+ (identifier, traceback.format_exc()))
+ return {}
+
+ return {
+ 'images': images
+ }
+
+ def _parseMovie(self, movie):
+ images = {
+ 'landscape': self._getMultImages(movie.get('moviethumb', []), 1),
+ 'logo': [],
+ 'disc_art': self._getMultImages(self._trimDiscs(movie.get('moviedisc', [])), 1),
+ 'clear_art': self._getMultImages(movie.get('hdmovieart', []), 1),
+ 'banner': self._getMultImages(movie.get('moviebanner', []), 1),
+ 'extra_fanart': [],
+ }
+
+ if len(images['clear_art']) == 0:
+ images['clear_art'] = self._getMultImages(movie.get('movieart', []), 1)
+
+ images['logo'] = self._getMultImages(movie.get('hdmovielogo', []), 1)
+ if len(images['logo']) == 0:
+ images['logo'] = self._getMultImages(movie.get('movielogo', []), 1)
+
+ fanarts = self._getMultImages(movie.get('moviebackground', []), self.MAX_EXTRAFANART + 1)
+
+ if fanarts:
+ images['backdrop_original'] = [fanarts[0]]
+ images['extra_fanart'] = fanarts[1:]
+
+ return images
+
+ def _trimDiscs(self, disc_images):
+ """
+ Return a subset of discImages. Only bluray disc images will be returned.
+ """
+
+ trimmed = []
+ for disc in disc_images:
+ if disc.get('disc_type') == 'bluray':
+ trimmed.append(disc)
+
+ if len(trimmed) == 0:
+ return disc_images
+
+ return trimmed
+
+ def _getImage(self, images):
+ image_url = None
+ highscore = -1
+ for image in images:
+ if tryInt(image.get('likes')) > highscore:
+ highscore = tryInt(image.get('likes'))
+ image_url = image.get('url')
+
+ return image_url
+
+ def _getMultImages(self, images, n):
+ """
+ Chooses the best n images and returns them as a list.
+ If n<0, all images will be returned.
+ """
+ image_urls = []
+ pool = []
+ for image in images:
+ if image.get('lang') == 'en':
+ pool.append(image)
+ orig_pool_size = len(pool)
+
+ while len(pool) > 0 and (n < 0 or orig_pool_size - len(pool) < n):
+ best = None
+ highscore = -1
+ for image in pool:
+ if tryInt(image.get('likes')) > highscore:
+ highscore = tryInt(image.get('likes'))
+ best = image
+ image_urls.append(best.get('url'))
+ pool.remove(best)
+
+ return image_urls
+
+ def isDisabled(self):
+ if self.conf('api_key') == '':
+ log.error('No API key provided.')
+ return True
+ return False
diff --git a/couchpotato/core/providers/info/omdbapi/main.py b/couchpotato/core/media/movie/providers/info/omdbapi.py
old mode 100755
new mode 100644
similarity index 96%
rename from couchpotato/core/providers/info/omdbapi/main.py
rename to couchpotato/core/media/movie/providers/info/omdbapi.py
index 8f04d3b6..d3a83b63
--- a/couchpotato/core/providers/info/omdbapi/main.py
+++ b/couchpotato/core/media/movie/providers/info/omdbapi.py
@@ -1,14 +1,18 @@
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.encoding import tryUrlencode
-from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.info.base import MovieProvider
import json
import re
import traceback
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+
log = CPLog(__name__)
+autoload = 'OMDBAPI'
+
class OMDBAPI(MovieProvider):
@@ -77,6 +81,9 @@ class OMDBAPI(MovieProvider):
if movie.get('Response') == 'Parse Error' or movie.get('Response') == 'False':
return movie_data
+ if movie.get('Type').lower() != 'movie':
+ return movie_data
+
tmp_movie = movie.copy()
for key in tmp_movie:
if tmp_movie.get(key).lower() == 'n/a':
diff --git a/couchpotato/core/providers/info/themoviedb/main.py b/couchpotato/core/media/movie/providers/info/themoviedb.py
similarity index 74%
rename from couchpotato/core/providers/info/themoviedb/main.py
rename to couchpotato/core/media/movie/providers/info/themoviedb.py
index d301db2b..4a397edd 100644
--- a/couchpotato/core/providers/info/themoviedb/main.py
+++ b/couchpotato/core/media/movie/providers/info/themoviedb.py
@@ -1,20 +1,22 @@
+import traceback
+
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.info.base import MovieProvider
+from couchpotato.core.media.movie.providers.base import MovieProvider
import tmdb3
-import traceback
log = CPLog(__name__)
+autoload = 'TheMovieDb'
+
class TheMovieDb(MovieProvider):
+ MAX_EXTRATHUMBS = 4
def __init__(self):
- #addEvent('info.search', self.search, priority = 2)
- #addEvent('movie.search', self.search, priority = 2)
- addEvent('movie.info', self.getInfo, priority = 2)
+ addEvent('movie.info', self.getInfo, priority = 3)
addEvent('movie.info_by_tmdb', self.getInfo)
# Configure TMDB settings
@@ -73,6 +75,7 @@ class TheMovieDb(MovieProvider):
if not result:
try:
log.debug('Getting info: %s', cache_key)
+ # noinspection PyArgumentList
movie = tmdb3.Movie(identifier)
try: exists = movie.title is not None
except: exists = False
@@ -95,16 +98,18 @@ class TheMovieDb(MovieProvider):
if not movie_data:
# Images
- poster = self.getImage(movie, type = 'poster', size = 'poster')
+ poster = self.getImage(movie, type = 'poster', size = 'w154')
poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
+ extra_thumbs = self.getMultImages(movie, type = 'backdrops', size = 'original', n = self.MAX_EXTRATHUMBS, skipfirst = True)
images = {
'poster': [poster] if poster else [],
#'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [],
- 'actors': {}
+ 'actors': {},
+ 'extra_thumbs': extra_thumbs
}
# Genres
@@ -148,8 +153,10 @@ class TheMovieDb(MovieProvider):
movie_data = dict((k, v) for k, v in movie_data.items() if v)
# Add alternative names
+ if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
+ movie_data['titles'].insert(0, movie_data['original_title'])
+
if extended:
- movie_data['titles'].append(movie.originaltitle)
for alt in movie.alternate_titles:
alt_name = alt.title
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:
@@ -170,8 +177,53 @@ class TheMovieDb(MovieProvider):
return image_url
+ def getMultImages(self, movie, type = 'backdrops', size = 'original', n = -1, skipfirst = False):
+ """
+ If n < 0, return all images. Otherwise return n images.
+ If n > len(getattr(movie, type)), then return all images.
+ If skipfirst is True, then it will skip getattr(movie, type)[0]. This
+ is because backdrops[0] is typically backdrop.
+ """
+
+ image_urls = []
+ try:
+ images = getattr(movie, type)
+ if n < 0 or n > len(images):
+ num_images = len(images)
+ else:
+ num_images = n
+
+ for i in range(int(skipfirst), num_images + int(skipfirst)):
+ image_urls.append(images[i].geturl(size = size))
+
+ except:
+ log.debug('Failed getting %i %s.%s for "%s"', (n, type, size, ss(str(movie))))
+
+ return image_urls
+
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
return True
return False
+
+
+config = [{
+ 'name': 'themoviedb',
+ 'groups': [
+ {
+ 'tab': 'providers',
+ 'name': 'tmdb',
+ 'label': 'TheMovieDB',
+ 'hidden': True,
+ 'description': 'Used for all calls to TheMovieDB.',
+ 'options': [
+ {
+ 'name': 'api_key',
+ 'default': '9b939aee0aaafc12a65bf448e4af9543',
+ 'label': 'Api Key',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/libs/migrate/versioning/templates/__init__.py b/couchpotato/core/media/movie/providers/metadata/__init__.py
similarity index 100%
rename from libs/migrate/versioning/templates/__init__.py
rename to couchpotato/core/media/movie/providers/metadata/__init__.py
diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py
new file mode 100644
index 00000000..7968000b
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/metadata/base.py
@@ -0,0 +1,187 @@
+import os
+import shutil
+import traceback
+
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import sp
+from couchpotato.core.helpers.variable import getIdentifier, underscoreToCamel
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.metadata.base import MetaDataBase
+from couchpotato.environment import Env
+
+
+log = CPLog(__name__)
+
+
+class MovieMetaData(MetaDataBase):
+
+ enabled_option = 'meta_enabled'
+
+ def __init__(self):
+ addEvent('renamer.after', self.create)
+
+ def create(self, message = None, group = None):
+ if self.isDisabled(): return
+ if not group: group = {}
+
+ log.info('Creating %s metadata.', self.getName())
+
+ # Update library to get latest info
+ try:
+ group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True)
+ except:
+ log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
+
+ root_name = self.getRootName(group)
+ meta_name = os.path.basename(root_name)
+ root = os.path.dirname(root_name)
+
+ movie_info = group['media'].get('info')
+
+ for file_type in ['nfo']:
+ try:
+ self._createType(meta_name, root, movie_info, group, file_type, 0)
+ except:
+ log.error('Unable to create %s file: %s', ('nfo', traceback.format_exc()))
+
+ for file_type in ['thumbnail', 'fanart', 'banner', 'disc_art', 'logo', 'clear_art', 'landscape', 'extra_thumbs', 'extra_fanart']:
+ try:
+ if file_type == 'thumbnail':
+ num_images = len(movie_info['images']['poster_original'])
+ elif file_type == 'fanart':
+ num_images = len(movie_info['images']['backdrop_original'])
+ else:
+ num_images = len(movie_info['images'][file_type])
+
+ for i in range(num_images):
+ self._createType(meta_name, root, movie_info, group, file_type, i)
+ except:
+ log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
+
+ def _createType(self, meta_name, root, movie_info, group, file_type, i): # Get file path
+ camelcase_method = underscoreToCamel(file_type.capitalize())
+ name = getattr(self, 'get' + camelcase_method + 'Name')(meta_name, root, i)
+
+ if name and (self.conf('meta_' + file_type) or self.conf('meta_' + file_type) is None):
+
+ # Get file content
+ content = getattr(self, 'get' + camelcase_method)(movie_info = movie_info, data = group, i = i)
+ if content:
+ log.debug('Creating %s file: %s', (file_type, name))
+ if os.path.isfile(content):
+ content = sp(content)
+ name = sp(name)
+
+ if not os.path.exists(os.path.dirname(name)):
+ os.makedirs(os.path.dirname(name))
+
+ shutil.copy2(content, name)
+ shutil.copyfile(content, name)
+
+ # Try and copy stats seperately
+ try: shutil.copystat(content, name)
+ except: pass
+ else:
+ self.createFile(name, content)
+ group['renamed_files'].append(name)
+
+ try:
+ os.chmod(sp(name), Env.getPermission('file'))
+ except:
+ log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc()))
+
+ def getRootName(self, data = None):
+ if not data: data = {}
+ return os.path.join(data['destination_dir'], data['filename'])
+
+ def getFanartName(self, name, root, i):
+ return
+
+ def getThumbnailName(self, name, root, i):
+ return
+
+ def getBannerName(self, name, root, i):
+ return
+
+ def getClearArtName(self, name, root, i):
+ return
+
+ def getLogoName(self, name, root, i):
+ return
+
+ def getDiscArtName(self, name, root, i):
+ return
+
+ def getLandscapeName(self, name, root, i):
+ return
+
+ def getExtraThumbsName(self, name, root, i):
+ return
+
+ def getExtraFanartName(self, name, root, i):
+ return
+
+ def getNfoName(self, name, root, i):
+ return
+
+ def getNfo(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+
+ def getThumbnail(self, movie_info = None, data = None, wanted_file_type = 'poster_original', i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+
+ # See if it is in current files
+ files = data['media'].get('files')
+ if files.get('image_' + wanted_file_type):
+ if os.path.isfile(files['image_' + wanted_file_type][i]):
+ return files['image_' + wanted_file_type][i]
+
+ # Download using existing info
+ try:
+ images = movie_info['images'][wanted_file_type]
+ file_path = fireEvent('file.download', url = images[i], single = True)
+ return file_path
+ except:
+ pass
+
+ def getFanart(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original', i = i)
+
+ def getBanner(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'banner', i = i)
+
+ def getClearArt(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'clear_art', i = i)
+
+ def getLogo(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'logo', i = i)
+
+ def getDiscArt(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'disc_art', i = i)
+
+ def getLandscape(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data= data, wanted_file_type = 'landscape', i = i)
+
+ def getExtraThumbs(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_thumbs', i = i)
+
+ def getExtraFanart(self, movie_info = None, data = None, i = 0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+ return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'extra_fanart', i = i)
diff --git a/couchpotato/core/media/movie/providers/metadata/mediabrowser.py b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py
new file mode 100644
index 00000000..6e40e4c1
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/metadata/mediabrowser.py
@@ -0,0 +1,36 @@
+import os
+
+from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData
+
+
+autoload = 'MediaBrowser'
+
+
+class MediaBrowser(MovieMetaData):
+
+ def getThumbnailName(self, name, root, i):
+ return os.path.join(root, 'folder.jpg')
+
+ def getFanartName(self, name, root, i):
+ return os.path.join(root, 'backdrop.jpg')
+
+
+config = [{
+ 'name': 'mediabrowser',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'subtab': 'metadata',
+ 'name': 'mediabrowser_metadata',
+ 'label': 'MediaBrowser',
+ 'description': 'Generate folder.jpg and backdrop.jpg',
+ 'options': [
+ {
+ 'name': 'meta_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/media/movie/providers/metadata/ps3.py b/couchpotato/core/media/movie/providers/metadata/ps3.py
new file mode 100644
index 00000000..05df0a53
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/metadata/ps3.py
@@ -0,0 +1,33 @@
+import os
+
+from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData
+
+
+autoload = 'SonyPS3'
+
+
+class SonyPS3(MovieMetaData):
+
+ def getThumbnailName(self, name, root, i):
+ return os.path.join(root, 'cover.jpg')
+
+
+config = [{
+ 'name': 'sonyps3',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'subtab': 'metadata',
+ 'name': 'sonyps3_metadata',
+ 'label': 'Sony PS3',
+ 'description': 'Generate cover.jpg',
+ 'options': [
+ {
+ 'name': 'meta_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/providers/metadata/wmc/__init__.py b/couchpotato/core/media/movie/providers/metadata/wmc.py
similarity index 66%
rename from couchpotato/core/providers/metadata/wmc/__init__.py
rename to couchpotato/core/media/movie/providers/metadata/wmc.py
index 167a24d7..3cb9e3c7 100644
--- a/couchpotato/core/providers/metadata/wmc/__init__.py
+++ b/couchpotato/core/media/movie/providers/metadata/wmc.py
@@ -1,8 +1,16 @@
-from .main import WindowsMediaCenter
+import os
+
+from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData
-def start():
- return WindowsMediaCenter()
+autoload = 'WindowsMediaCenter'
+
+
+class WindowsMediaCenter(MovieMetaData):
+
+ def getThumbnailName(self, name, root, i):
+ return os.path.join(root, 'folder.jpg')
+
config = [{
'name': 'windowsmediacenter',
diff --git a/couchpotato/core/media/movie/providers/metadata/xbmc.py b/couchpotato/core/media/movie/providers/metadata/xbmc.py
new file mode 100644
index 00000000..ff0c119f
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/metadata/xbmc.py
@@ -0,0 +1,370 @@
+from xml.etree.ElementTree import Element, SubElement, tostring
+import os
+import re
+import traceback
+import xml.dom.minidom
+
+from couchpotato.core.media.movie.providers.metadata.base import MovieMetaData
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import getTitle
+from couchpotato.core.logger import CPLog
+
+
+log = CPLog(__name__)
+
+autoload = 'XBMC'
+
+
+class XBMC(MovieMetaData):
+
+ def getFanartName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_fanart_name'), name, root)
+
+ def getThumbnailName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_thumbnail_name'), name, root)
+
+ def getNfoName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_nfo_name'), name, root)
+
+ def getBannerName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_banner_name'), name, root)
+
+ def getClearArtName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_clear_art_name'), name, root)
+
+ def getLogoName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_logo_name'), name, root)
+
+ def getDiscArtName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_disc_art_name'), name, root)
+
+ def getLandscapeName(self, name, root, i):
+ return self.createMetaName(self.conf('meta_landscape_name'), name, root)
+
+ def getExtraThumbsName(self, name, root, i):
+ return self.createMetaNameMult(self.conf('meta_extra_thumbs_name'), name, root, i)
+
+ def getExtraFanartName(self, name, root, i):
+ return self.createMetaNameMult(self.conf('meta_extra_fanart_name'), name, root, i)
+
+ def createMetaName(self, basename, name, root):
+ return os.path.join(root, basename.replace('%s', name))
+
+ def createMetaNameMult(self, basename, name, root, i):
+ return os.path.join(root, basename.replace('%s', name).replace('
', str(i + 1)))
+
+ def getNfo(self, movie_info=None, data=None, i=0):
+ if not data: data = {}
+ if not movie_info: movie_info = {}
+
+ # return imdb url only
+ if self.conf('meta_url_only'):
+ return 'http://www.imdb.com/title/%s/' % toUnicode(data['identifier'])
+
+ nfoxml = Element('movie')
+
+ # Title
+ try:
+ el = SubElement(nfoxml, 'title')
+ el.text = toUnicode(getTitle(data))
+ except:
+ pass
+
+ # IMDB id
+ try:
+ el = SubElement(nfoxml, 'id')
+ el.text = toUnicode(data['identifier'])
+ except:
+ pass
+
+ # Runtime
+ try:
+ runtime = SubElement(nfoxml, 'runtime')
+ runtime.text = '%s min' % movie_info.get('runtime')
+ except:
+ pass
+
+ # Other values
+ types = ['year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released']
+ for type in types:
+
+ if ':' in type:
+ name, type = type.split(':')
+ else:
+ name = type
+
+ try:
+ if movie_info.get(type):
+ el = SubElement(nfoxml, name)
+ el.text = toUnicode(movie_info.get(type, ''))
+ except:
+ pass
+
+ # Rating
+ for rating_type in ['imdb', 'rotten', 'tmdb']:
+ try:
+ r, v = movie_info['rating'][rating_type]
+ rating = SubElement(nfoxml, 'rating')
+ rating.text = str(r)
+ votes = SubElement(nfoxml, 'votes')
+ votes.text = str(v)
+ break
+ except:
+ log.debug('Failed adding rating info from %s: %s', (rating_type, traceback.format_exc()))
+
+ # Genre
+ for genre in movie_info.get('genres', []):
+ genres = SubElement(nfoxml, 'genre')
+ genres.text = toUnicode(genre)
+
+ # Actors
+ for actor_name in movie_info.get('actor_roles', {}):
+ role_name = movie_info['actor_roles'][actor_name]
+
+ actor = SubElement(nfoxml, 'actor')
+ name = SubElement(actor, 'name')
+ name.text = toUnicode(actor_name)
+ if role_name:
+ role = SubElement(actor, 'role')
+ role.text = toUnicode(role_name)
+ if movie_info['images']['actors'].get(actor_name):
+ thumb = SubElement(actor, 'thumb')
+ thumb.text = toUnicode(movie_info['images']['actors'].get(actor_name))
+
+ # Directors
+ for director_name in movie_info.get('directors', []):
+ director = SubElement(nfoxml, 'director')
+ director.text = toUnicode(director_name)
+
+ # Writers
+ for writer in movie_info.get('writers', []):
+ writers = SubElement(nfoxml, 'credits')
+ writers.text = toUnicode(writer)
+
+ # Sets or collections
+ collection_name = movie_info.get('collection')
+ if collection_name:
+ collection = SubElement(nfoxml, 'set')
+ collection.text = toUnicode(collection_name)
+ sorttitle = SubElement(nfoxml, 'sorttitle')
+ sorttitle.text = '%s %s' % (toUnicode(collection_name), movie_info.get('year'))
+
+ # Images
+ for image_url in movie_info['images']['poster_original']:
+ image = SubElement(nfoxml, 'thumb')
+ image.text = toUnicode(image_url)
+
+ image_types = [
+ ('fanart', 'backdrop_original'),
+ ('banner', 'banner'),
+ ('discart', 'disc_art'),
+ ('logo', 'logo'),
+ ('clearart', 'clear_art'),
+ ('landscape', 'landscape'),
+ ('extrathumb', 'extra_thumbs'),
+ ('extrafanart', 'extra_fanart'),
+ ]
+
+ for image_type in image_types:
+ sub, type = image_type
+
+ sub_element = SubElement(nfoxml, sub)
+ for image_url in movie_info['images'][type]:
+ image = SubElement(sub_element, 'thumb')
+ image.text = toUnicode(image_url)
+
+ # Add trailer if found
+ trailer_found = False
+ if data.get('renamed_files'):
+ for filename in data.get('renamed_files'):
+ if 'trailer' in filename:
+ trailer = SubElement(nfoxml, 'trailer')
+ trailer.text = toUnicode(filename)
+ trailer_found = True
+ if not trailer_found and data['files'].get('trailer'):
+ trailer = SubElement(nfoxml, 'trailer')
+ trailer.text = toUnicode(data['files']['trailer'][0])
+
+ # Add file metadata
+ fileinfo = SubElement(nfoxml, 'fileinfo')
+ streamdetails = SubElement(fileinfo, 'streamdetails')
+
+ # Video data
+ if data['meta_data'].get('video'):
+ video = SubElement(streamdetails, 'video')
+ codec = SubElement(video, 'codec')
+ codec.text = toUnicode(data['meta_data']['video'])
+ aspect = SubElement(video, 'aspect')
+ aspect.text = str(data['meta_data']['aspect'])
+ width = SubElement(video, 'width')
+ width.text = str(data['meta_data']['resolution_width'])
+ height = SubElement(video, 'height')
+ height.text = str(data['meta_data']['resolution_height'])
+
+ # Audio data
+ if data['meta_data'].get('audio'):
+ audio = SubElement(streamdetails, 'audio')
+ codec = SubElement(audio, 'codec')
+ codec.text = toUnicode(data['meta_data'].get('audio'))
+ channels = SubElement(audio, 'channels')
+ channels.text = toUnicode(data['meta_data'].get('audio_channels'))
+
+ # Clean up the xml and return it
+ nfoxml = xml.dom.minidom.parseString(tostring(nfoxml))
+ xml_string = nfoxml.toprettyxml(indent = ' ')
+ text_re = re.compile('>\n\s+([^<>\s].*?)\n\s+', re.DOTALL)
+ xml_string = text_re.sub('>\g<1>', xml_string)
+
+ return xml_string.encode('utf-8')
+
+
+config = [{
+ 'name': 'xbmc',
+ 'groups': [
+ {
+ 'tab': 'renamer',
+ 'subtab': 'metadata',
+ 'name': 'xbmc_metadata',
+ 'label': 'XBMC',
+ 'description': 'Enable metadata XBMC can understand',
+ 'options': [
+ {
+ 'name': 'meta_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'meta_nfo',
+ 'label': 'NFO',
+ 'default': True,
+ 'type': 'bool',
+ },
+ {
+ 'name': 'meta_nfo_name',
+ 'label': 'NFO filename',
+ 'default': '%s.nfo',
+ 'advanced': True,
+ 'description': '%s is the rootname of the movie. For example "/path/to/movie cd1.mkv" will be "/path/to/movie"'
+ },
+ {
+ 'name': 'meta_url_only',
+ 'label': 'Only IMDB URL',
+ 'default': False,
+ 'advanced': True,
+ 'description': 'Create a nfo with only the IMDB url inside',
+ 'type': 'bool',
+ },
+ {
+ 'name': 'meta_fanart',
+ 'label': 'Fanart',
+ 'default': True,
+ 'type': 'bool',
+ },
+ {
+ 'name': 'meta_fanart_name',
+ 'label': 'Fanart filename',
+ 'default': '%s-fanart.jpg',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_thumbnail',
+ 'label': 'Thumbnail',
+ 'default': True,
+ 'type': 'bool',
+ },
+ {
+ 'name': 'meta_thumbnail_name',
+ 'label': 'Thumbnail filename',
+ 'default': '%s.tbn',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_banner',
+ 'label': 'Banner',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_banner_name',
+ 'label': 'Banner filename',
+ 'default': 'banner.jpg',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_clear_art',
+ 'label': 'ClearArt',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_clear_art_name',
+ 'label': 'ClearArt filename',
+ 'default': 'clearart.png',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_disc_art',
+ 'label': 'DiscArt',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_disc_art_name',
+ 'label': 'DiscArt filename',
+ 'default': 'disc.png',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_landscape',
+ 'label': 'Landscape',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_landscape_name',
+ 'label': 'Landscape filename',
+ 'default': 'landscape.jpg',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_logo',
+ 'label': 'ClearLogo',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_logo_name',
+ 'label': 'ClearLogo filename',
+ 'default': 'logo.png',
+ 'advanced': True,
+ },
+ {
+ 'name': 'meta_extra_thumbs',
+ 'label': 'Extrathumbs',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_extra_thumbs_name',
+ 'label': 'Extrathumbs filename',
+ 'description': '<i> is the image number, and must be included to have multiple images',
+ 'default': 'extrathumbs/thumb.jpg',
+ 'advanced': True
+ },
+ {
+ 'name': 'meta_extra_fanart',
+ 'label': 'Extrafanart',
+ 'default': False,
+ 'type': 'bool'
+ },
+ {
+ 'name': 'meta_extra_fanart_name',
+ 'label': 'Extrafanart filename',
+ 'default': 'extrafanart/extrafanart.jpg',
+ 'description': '<i> is the image number, and must be included to have multiple images',
+ 'advanced': True
+ }
+ ],
+ },
+ ],
+}]
diff --git a/libs/migrate/versioning/templates/repository/__init__.py b/couchpotato/core/media/movie/providers/nzb/__init__.py
similarity index 100%
rename from libs/migrate/versioning/templates/repository/__init__.py
rename to couchpotato/core/media/movie/providers/nzb/__init__.py
diff --git a/libs/migrate/versioning/templates/repository/default/__init__.py b/couchpotato/core/media/movie/providers/nzb/base.py
similarity index 100%
rename from libs/migrate/versioning/templates/repository/default/__init__.py
rename to couchpotato/core/media/movie/providers/nzb/base.py
diff --git a/couchpotato/core/media/movie/providers/nzb/binsearch.py b/couchpotato/core/media/movie/providers/nzb/binsearch.py
new file mode 100644
index 00000000..d6f48522
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/nzb/binsearch.py
@@ -0,0 +1,27 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.variable import getIdentifier
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.nzb.binsearch import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+from couchpotato.environment import Env
+
+log = CPLog(__name__)
+
+autoload = 'BinSearch'
+
+
+class BinSearch(MovieProvider, Base):
+
+ def buildUrl(self, media, quality):
+ query = tryUrlencode({
+ 'q': getIdentifier(media),
+ 'm': 'n',
+ 'max': 400,
+ 'adv_age': Env.setting('retention', 'nzb'),
+ 'adv_sort': 'date',
+ 'adv_col': 'on',
+ 'adv_nfo': 'on',
+ 'minsize': quality.get('size_min'),
+ 'maxsize': quality.get('size_max'),
+ })
+ return query
diff --git a/couchpotato/core/media/movie/providers/nzb/newznab.py b/couchpotato/core/media/movie/providers/nzb/newznab.py
new file mode 100644
index 00000000..fc94acbf
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/nzb/newznab.py
@@ -0,0 +1,26 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.variable import getIdentifier
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.nzb.newznab import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'Newznab'
+
+
+class Newznab(MovieProvider, Base):
+
+ def buildUrl(self, media, host):
+
+ query = tryUrlencode({
+ 't': 'movie',
+ 'imdbid': getIdentifier(media).replace('tt', ''),
+ 'apikey': host['api_key'],
+ 'extended': 1
+ })
+
+ if len(host.get('custom_tag', '')) > 0:
+ query = '%s&%s' % (query, host.get('custom_tag'))
+
+ return query
diff --git a/couchpotato/core/media/movie/providers/nzb/nzbclub.py b/couchpotato/core/media/movie/providers/nzb/nzbclub.py
new file mode 100644
index 00000000..2a43ba2f
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/nzb/nzbclub.py
@@ -0,0 +1,27 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.nzb.nzbclub import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'NZBClub'
+
+
+class NZBClub(MovieProvider, Base):
+
+ def buildUrl(self, media):
+
+ q = tryUrlencode({
+ 'q': '"%s"' % fireEvent('library.query', media, single = True),
+ })
+
+ query = tryUrlencode({
+ 'ig': 1,
+ 'rpp': 200,
+ 'st': 5,
+ 'sp': 1,
+ 'ns': 1,
+ })
+ return '%s&%s' % (q, query)
diff --git a/couchpotato/core/media/movie/providers/nzb/nzbindex.py b/couchpotato/core/media/movie/providers/nzb/nzbindex.py
new file mode 100644
index 00000000..70e939dc
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/nzb/nzbindex.py
@@ -0,0 +1,30 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.nzb.nzbindex import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+from couchpotato.environment import Env
+
+log = CPLog(__name__)
+
+autoload = 'NzbIndex'
+
+
+class NzbIndex(MovieProvider, Base):
+
+ def buildUrl(self, media, quality):
+ title = fireEvent('library.query', media, include_year = False, single = True)
+ year = media['info']['year']
+
+ query = tryUrlencode({
+ 'q': '"%s %s" | "%s (%s)"' % (title, year, title, year),
+ 'age': Env.setting('retention', 'nzb'),
+ 'sort': 'agedesc',
+ 'minsize': quality.get('size_min'),
+ 'maxsize': quality.get('size_max'),
+ 'rating': 1,
+ 'max': 250,
+ 'more': 1,
+ 'complete': 1,
+ })
+ return query
diff --git a/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py
new file mode 100644
index 00000000..f4527f6d
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/nzb/omgwtfnzbs.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.nzb.omgwtfnzbs import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'OMGWTFNZBs'
+
+
+class OMGWTFNZBs(MovieProvider, Base):
+ pass
diff --git a/libs/migrate/versioning/templates/repository/default/versions/__init__.py b/couchpotato/core/media/movie/providers/torrent/__init__.py
similarity index 100%
rename from libs/migrate/versioning/templates/repository/default/versions/__init__.py
rename to couchpotato/core/media/movie/providers/torrent/__init__.py
diff --git a/couchpotato/core/media/movie/providers/torrent/awesomehd.py b/couchpotato/core/media/movie/providers/torrent/awesomehd.py
new file mode 100644
index 00000000..b1c81f18
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/awesomehd.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.awesomehd import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'AwesomeHD'
+
+
+class AwesomeHD(MovieProvider, Base):
+ pass
diff --git a/couchpotato/core/media/movie/providers/torrent/bithdtv.py b/couchpotato/core/media/movie/providers/torrent/bithdtv.py
new file mode 100644
index 00000000..da6954c8
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/bithdtv.py
@@ -0,0 +1,23 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.torrent.bithdtv import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'BiTHDTV'
+
+
+class BiTHDTV(MovieProvider, Base):
+ cat_ids = [
+ ([2], ['bd50']),
+ ]
+ cat_backup_id = 7 # Movies
+
+ def buildUrl(self, media, quality):
+ query = tryUrlencode({
+ 'search': fireEvent('library.query', media, single = True),
+ 'cat': self.getCatId(quality)[0]
+ })
+ return query
diff --git a/couchpotato/core/media/movie/providers/torrent/bitsoup.py b/couchpotato/core/media/movie/providers/torrent/bitsoup.py
new file mode 100644
index 00000000..e9d69fe5
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/bitsoup.py
@@ -0,0 +1,25 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.bitsoup import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'Bitsoup'
+
+
+class Bitsoup(MovieProvider, Base):
+ cat_ids = [
+ ([17], ['3d']),
+ ([41], ['720p', '1080p']),
+ ([20], ['dvdr']),
+ ([19], ['brrip', 'dvdrip']),
+ ]
+ cat_backup_id = 0
+
+ def buildUrl(self, title, media, quality):
+ query = tryUrlencode({
+ 'search': '"%s" %s' % (title, media['info']['year']),
+ 'cat': self.getCatId(quality)[0],
+ })
+ return query
diff --git a/couchpotato/core/media/movie/providers/torrent/hdbits.py b/couchpotato/core/media/movie/providers/torrent/hdbits.py
new file mode 100644
index 00000000..016f1a12
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/hdbits.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.hdbits import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'HDBits'
+
+
+class HDBits(MovieProvider, Base):
+ pass
diff --git a/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py
new file mode 100644
index 00000000..cfd773ad
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/ilovetorrents.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.ilovetorrents import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'ILoveTorrents'
+
+
+class ILoveTorrents(MovieProvider, Base):
+ pass
diff --git a/couchpotato/core/media/movie/providers/torrent/iptorrents.py b/couchpotato/core/media/movie/providers/torrent/iptorrents.py
new file mode 100644
index 00000000..89aeee80
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/iptorrents.py
@@ -0,0 +1,23 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.iptorrents import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'IPTorrents'
+
+
+class IPTorrents(MovieProvider, Base):
+
+ cat_ids = [
+ ([87], ['3d']),
+ ([48], ['720p', '1080p', 'bd50']),
+ ([72], ['cam', 'ts', 'tc', 'r5', 'scr']),
+ ([7], ['dvdrip', 'brrip']),
+ ([6], ['dvdr']),
+ ]
+
+ def buildUrl(self, title, media, quality):
+ query = '"%s" %s' % (title.replace(':', ''), media['info']['year'])
+
+ return self._buildUrl(query, quality)
diff --git a/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py
new file mode 100644
index 00000000..2b9b1969
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/kickasstorrents.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.kickasstorrents import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'KickAssTorrents'
+
+
+class KickAssTorrents(MovieProvider, Base):
+ pass
diff --git a/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py
new file mode 100644
index 00000000..bbaea265
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/passthepopcorn.py
@@ -0,0 +1,38 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.passthepopcorn import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'PassThePopcorn'
+
+
+class PassThePopcorn(MovieProvider, Base):
+
+ quality_search_params = {
+ 'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
+ '1080p': {'resolution': '1080p'},
+ '720p': {'resolution': '720p'},
+ 'brrip': {'media': 'Blu-ray'},
+ 'dvdr': {'resolution': 'anysd'},
+ 'dvdrip': {'media': 'DVD'},
+ 'scr': {'media': 'DVD-Screener'},
+ 'r5': {'media': 'R5'},
+ 'tc': {'media': 'TC'},
+ 'ts': {'media': 'TS'},
+ 'cam': {'media': 'CAM'}
+ }
+
+ post_search_filters = {
+ 'bd50': {'Codec': ['BD50']},
+ '1080p': {'Resolution': ['1080p']},
+ '720p': {'Resolution': ['720p']},
+ 'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
+ 'dvdr': {'Codec': ['DVD5', 'DVD9']},
+ 'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
+ 'scr': {'Source': ['DVD-Screener']},
+ 'r5': {'Source': ['R5']},
+ 'tc': {'Source': ['TC']},
+ 'ts': {'Source': ['TS']},
+ 'cam': {'Source': ['CAM']}
+ }
diff --git a/couchpotato/core/media/movie/providers/torrent/sceneaccess.py b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py
new file mode 100644
index 00000000..579103af
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/sceneaccess.py
@@ -0,0 +1,29 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.sceneaccess import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'SceneAccess'
+
+
+class SceneAccess(MovieProvider, Base):
+
+ cat_ids = [
+ ([22], ['720p', '1080p']),
+ ([7], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
+ ([8], ['dvdr']),
+ ]
+
+ def buildUrl(self, title, media, quality):
+ cat_id = self.getCatId(quality)[0]
+ url = self.urls['search'] % (cat_id, cat_id)
+
+ arguments = tryUrlencode({
+ 'search': '%s %s' % (title, media['info']['year']),
+ 'method': 2,
+ })
+ query = "%s&%s" % (url, arguments)
+
+ return query
diff --git a/couchpotato/core/media/movie/providers/torrent/thepiratebay.py b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py
new file mode 100644
index 00000000..0dc8313d
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/thepiratebay.py
@@ -0,0 +1,27 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.torrent.thepiratebay import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'ThePirateBay'
+
+
+class ThePirateBay(MovieProvider, Base):
+
+ cat_ids = [
+ ([209], ['3d']),
+ ([207], ['720p', '1080p', 'bd50']),
+ ([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
+ ([201, 207], ['brrip']),
+ ([202], ['dvdr'])
+ ]
+
+ def buildUrl(self, media, page, cats):
+ return (
+ tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)),
+ page,
+ ','.join(str(x) for x in cats)
+ )
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentbytes.py b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py
new file mode 100644
index 00000000..48fc68a4
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentbytes.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.torrentbytes import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'TorrentBytes'
+
+
+class TorrentBytes(MovieProvider, Base):
+ pass
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentday.py b/couchpotato/core/media/movie/providers/torrent/torrentday.py
new file mode 100644
index 00000000..768d3043
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentday.py
@@ -0,0 +1,17 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.torrentday import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'TorrentDay'
+
+
+class TorrentDay(MovieProvider, Base):
+
+ cat_ids = [
+ ([11], ['720p', '1080p']),
+ ([1, 21, 25], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
+ ([3], ['dvdr']),
+ ([5], ['bd50']),
+ ]
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentleech.py b/couchpotato/core/media/movie/providers/torrent/torrentleech.py
new file mode 100644
index 00000000..191ceba8
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentleech.py
@@ -0,0 +1,27 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.torrentleech import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'TorrentLeech'
+
+
+class TorrentLeech(MovieProvider, Base):
+
+ cat_ids = [
+ ([13], ['720p', '1080p']),
+ ([8], ['cam']),
+ ([9], ['ts', 'tc']),
+ ([10], ['r5', 'scr']),
+ ([11], ['dvdrip']),
+ ([14], ['brrip']),
+ ([12], ['dvdr']),
+ ]
+
+ def buildUrl(self, title, media, quality):
+ return (
+ tryUrlencode(title.replace(':', '')),
+ self.getCatId(quality)[0]
+ )
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentpotato.py b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py
new file mode 100644
index 00000000..67573537
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentpotato.py
@@ -0,0 +1,20 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.helpers.variable import getIdentifier
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.torrentpotato import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'TorrentPotato'
+
+
+class TorrentPotato(MovieProvider, Base):
+
+ def buildUrl(self, media, host):
+ arguments = tryUrlencode({
+ 'user': host['name'],
+ 'passkey': host['pass_key'],
+ 'imdbid': getIdentifier(media),
+ })
+ return '%s?%s' % (host['host'], arguments)
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentshack.py b/couchpotato/core/media/movie/providers/torrent/torrentshack.py
new file mode 100644
index 00000000..01eb6d6a
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentshack.py
@@ -0,0 +1,36 @@
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.torrentshack import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'TorrentShack'
+
+
+class TorrentShack(MovieProvider, Base):
+
+ # TorrentShack movie search categories
+ # Movies/x264 - 300
+ # Movies/DVD-R - 350
+ # Movies/XviD - 400
+ # Full Blu-ray - 970
+ #
+ # REMUX - 320 (not included)
+ # Movies-HD Pack - 982 (not included)
+ # Movies-SD Pack - 983 (not included)
+
+ cat_ids = [
+ ([970], ['bd50']),
+ ([300], ['720p', '1080p']),
+ ([350], ['dvdr']),
+ ([400], ['brrip', 'dvdrip']),
+ ]
+ cat_backup_id = 400
+
+ def buildUrl(self, media, quality):
+ query = (tryUrlencode(fireEvent('library.query', media, single = True)),
+ self.getSceneOnly(),
+ self.getCatId(quality)[0])
+ return query
diff --git a/couchpotato/core/media/movie/providers/torrent/torrentz.py b/couchpotato/core/media/movie/providers/torrent/torrentz.py
new file mode 100644
index 00000000..742554c4
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/torrentz.py
@@ -0,0 +1,15 @@
+from couchpotato.core.helpers.encoding import tryUrlencode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.torrent.torrentz import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'Torrentz'
+
+
+class Torrentz(MovieProvider, Base):
+
+ def buildUrl(self, media):
+ return tryUrlencode('"%s"' % fireEvent('library.query', media, single = True))
diff --git a/couchpotato/core/media/movie/providers/torrent/yify.py b/couchpotato/core/media/movie/providers/torrent/yify.py
new file mode 100644
index 00000000..d30132d6
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/torrent/yify.py
@@ -0,0 +1,11 @@
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.torrent.yify import Base
+from couchpotato.core.media.movie.providers.base import MovieProvider
+
+log = CPLog(__name__)
+
+autoload = 'Yify'
+
+
+class Yify(MovieProvider, Base):
+ pass
diff --git a/libs/migrate/versioning/templates/repository/pylons/__init__.py b/couchpotato/core/media/movie/providers/trailer/__init__.py
similarity index 100%
rename from libs/migrate/versioning/templates/repository/pylons/__init__.py
rename to couchpotato/core/media/movie/providers/trailer/__init__.py
diff --git a/couchpotato/core/providers/trailer/base.py b/couchpotato/core/media/movie/providers/trailer/base.py
similarity index 66%
rename from couchpotato/core/providers/trailer/base.py
rename to couchpotato/core/media/movie/providers/trailer/base.py
index 338ca9b3..9cd1dbe6 100644
--- a/couchpotato/core/providers/trailer/base.py
+++ b/couchpotato/core/media/movie/providers/trailer/base.py
@@ -1,6 +1,6 @@
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.base import Provider
+from couchpotato.core.media._base.providers.base import Provider
log = CPLog(__name__)
@@ -11,3 +11,6 @@ class TrailerProvider(Provider):
def __init__(self):
addEvent('trailer.search', self.search)
+
+ def search(self, *args, **kwargs):
+ pass
diff --git a/couchpotato/core/providers/trailer/hdtrailers/main.py b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py
similarity index 89%
rename from couchpotato/core/providers/trailer/hdtrailers/main.py
rename to couchpotato/core/media/movie/providers/trailer/hdtrailers.py
index cba7609f..79425332 100644
--- a/couchpotato/core/providers/trailer/hdtrailers/main.py
+++ b/couchpotato/core/media/movie/providers/trailer/hdtrailers.py
@@ -1,14 +1,18 @@
+from string import digits, ascii_letters
+import re
+
from bs4 import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts, getTitle
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.trailer.base import TrailerProvider
-from string import digits, ascii_letters
-from urllib2 import HTTPError
-import re
+from couchpotato.core.media.movie.providers.trailer.base import TrailerProvider
+from requests import HTTPError
+
log = CPLog(__name__)
+autoload = 'HDTrailers'
+
class HDTrailers(TrailerProvider):
@@ -20,11 +24,11 @@ class HDTrailers(TrailerProvider):
def search(self, group):
- movie_name = getTitle(group['library'])
+ movie_name = getTitle(group)
url = self.urls['api'] % self.movieUrlName(movie_name)
try:
- data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url, show_error = False)
+ data = self.getCache('hdtrailers.%s' % group['identifier'], url, show_error = False)
except HTTPError:
log.debug('No page found for: %s', movie_name)
data = None
@@ -48,13 +52,13 @@ class HDTrailers(TrailerProvider):
return result_data
def findViaAlternative(self, group):
- results = {'480p':[], '720p':[], '1080p':[]}
+ results = {'480p': [], '720p': [], '1080p': []}
- movie_name = getTitle(group['library'])
+ movie_name = getTitle(group)
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
try:
- data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url, show_error = False)
+ data = self.getCache('hdtrailers.alt.%s' % group['identifier'], url, show_error = False)
except HTTPError:
log.debug('No alternative page found for: %s', movie_name)
data = None
diff --git a/libs/migrate/versioning/templates/repository/pylons/versions/__init__.py b/couchpotato/core/media/movie/providers/userscript/__init__.py
similarity index 100%
rename from libs/migrate/versioning/templates/repository/pylons/versions/__init__.py
rename to couchpotato/core/media/movie/providers/userscript/__init__.py
diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/media/movie/providers/userscript/allocine.py
similarity index 88%
rename from couchpotato/core/providers/userscript/allocine/main.py
rename to couchpotato/core/media/movie/providers/userscript/allocine.py
index f8ca630d..238a6b54 100644
--- a/couchpotato/core/providers/userscript/allocine/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/allocine.py
@@ -1,9 +1,13 @@
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.userscript.base import UserscriptBase
import traceback
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
log = CPLog(__name__)
+autoload = 'AlloCine'
+
class AlloCine(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/appletrailers/main.py b/couchpotato/core/media/movie/providers/userscript/appletrailers.py
similarity index 81%
rename from couchpotato/core/providers/userscript/appletrailers/main.py
rename to couchpotato/core/media/movie/providers/userscript/appletrailers.py
index 693065d1..c59722c7 100644
--- a/couchpotato/core/providers/userscript/appletrailers/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/appletrailers.py
@@ -1,6 +1,10 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
import re
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
+autoload = 'AppleTrailers'
+
class AppleTrailers(UserscriptBase):
diff --git a/couchpotato/core/media/movie/providers/userscript/criticker.py b/couchpotato/core/media/movie/providers/userscript/criticker.py
new file mode 100644
index 00000000..cc0bee84
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/criticker.py
@@ -0,0 +1,8 @@
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'Criticker'
+
+
+class Criticker(UserscriptBase):
+
+ includes = ['http://www.criticker.com/film/*']
diff --git a/couchpotato/core/media/movie/providers/userscript/filmcentrum.py b/couchpotato/core/media/movie/providers/userscript/filmcentrum.py
new file mode 100644
index 00000000..b2b15a9e
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/filmcentrum.py
@@ -0,0 +1,8 @@
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'FilmCentrum'
+
+
+class FilmCentrum(UserscriptBase):
+
+ includes = ['*://filmcentrum.nl/films/*']
diff --git a/couchpotato/core/media/movie/providers/userscript/filmstarts.py b/couchpotato/core/media/movie/providers/userscript/filmstarts.py
new file mode 100644
index 00000000..59027e03
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/filmstarts.py
@@ -0,0 +1,30 @@
+from bs4 import BeautifulSoup
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'Filmstarts'
+
+
+class Filmstarts(UserscriptBase):
+
+ includes = ['*://www.filmstarts.de/kritiken/*']
+
+ def getMovie(self, url):
+ try:
+ data = self.getUrl(url)
+ except:
+ return
+
+ html = BeautifulSoup(data)
+ table = html.find("table", attrs={"class": "table table-standard thead-standard table-striped_2 fs11"})
+
+ if table.find(text='Originaltitel'):
+ # Get original film title from the table specified above
+ name = table.find("div", text="Originaltitel").parent.parent.parent.td.text
+ else:
+ # If none is available get the title from the meta data
+ name = html.find("meta", {"property":"og:title"})['content']
+
+ # Year of production is not available in the meta data, so get it from the table
+ year = table.find("tr", text="Produktionsjahr").parent.parent.parent.td.text
+
+ return self.search(name, year)
\ No newline at end of file
diff --git a/couchpotato/core/providers/userscript/filmweb/main.py b/couchpotato/core/media/movie/providers/userscript/filmweb.py
similarity index 87%
rename from couchpotato/core/providers/userscript/filmweb/main.py
rename to couchpotato/core/media/movie/providers/userscript/filmweb.py
index 66c5fcdb..a4a51a08 100644
--- a/couchpotato/core/providers/userscript/filmweb/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/filmweb.py
@@ -1,6 +1,10 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
import re
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
+autoload = 'Filmweb'
+
class Filmweb(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/flickchart/main.py b/couchpotato/core/media/movie/providers/userscript/flickchart.py
similarity index 87%
rename from couchpotato/core/providers/userscript/flickchart/main.py
rename to couchpotato/core/media/movie/providers/userscript/flickchart.py
index a66bd38f..c54a8cab 100644
--- a/couchpotato/core/providers/userscript/flickchart/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/flickchart.py
@@ -1,10 +1,14 @@
-from couchpotato.core.event import fireEvent
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.userscript.base import UserscriptBase
import traceback
+from couchpotato.core.event import fireEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
log = CPLog(__name__)
+autoload = 'Flickchart'
+
class Flickchart(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/imdb/main.py b/couchpotato/core/media/movie/providers/userscript/imdb.py
similarity index 66%
rename from couchpotato/core/providers/userscript/imdb/main.py
rename to couchpotato/core/media/movie/providers/userscript/imdb.py
index 2a6efd6b..dccc4832 100644
--- a/couchpotato/core/providers/userscript/imdb/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/imdb.py
@@ -1,6 +1,7 @@
-from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import getImdb
-from couchpotato.core.providers.userscript.base import UserscriptBase
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'IMDB'
class IMDB(UserscriptBase):
diff --git a/couchpotato/core/media/movie/providers/userscript/letterboxd.py b/couchpotato/core/media/movie/providers/userscript/letterboxd.py
new file mode 100644
index 00000000..43b5d309
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/letterboxd.py
@@ -0,0 +1,8 @@
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'Letterboxd'
+
+
+class Letterboxd(UserscriptBase):
+
+ includes = ['*://letterboxd.com/film/*']
diff --git a/couchpotato/core/providers/userscript/moviemeter/main.py b/couchpotato/core/media/movie/providers/userscript/moviemeter.py
similarity index 52%
rename from couchpotato/core/providers/userscript/moviemeter/main.py
rename to couchpotato/core/media/movie/providers/userscript/moviemeter.py
index 3593d432..4c9bb221 100644
--- a/couchpotato/core/providers/userscript/moviemeter/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/moviemeter.py
@@ -1,4 +1,6 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'MovieMeter'
class MovieMeter(UserscriptBase):
diff --git a/couchpotato/core/media/movie/providers/userscript/moviesio.py b/couchpotato/core/media/movie/providers/userscript/moviesio.py
new file mode 100644
index 00000000..0381d64a
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/moviesio.py
@@ -0,0 +1,8 @@
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'MoviesIO'
+
+
+class MoviesIO(UserscriptBase):
+
+ includes = ['*://movies.io/m/*']
diff --git a/couchpotato/core/providers/userscript/reddit/main.py b/couchpotato/core/media/movie/providers/userscript/reddit.py
similarity index 71%
rename from couchpotato/core/providers/userscript/reddit/main.py
rename to couchpotato/core/media/movie/providers/userscript/reddit.py
index 9790f6e2..8cb81079 100644
--- a/couchpotato/core/providers/userscript/reddit/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/reddit.py
@@ -1,6 +1,8 @@
from couchpotato import fireEvent
from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.providers.userscript.base import UserscriptBase
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'Reddit'
class Reddit(UserscriptBase):
@@ -8,7 +10,8 @@ class Reddit(UserscriptBase):
includes = ['*://www.reddit.com/r/Ijustwatched/comments/*']
def getMovie(self, url):
- name = splitString(url, '/')[-1]
+ name = splitString(splitString(url, '/ijw_')[-1], '/')[0]
+
if name.startswith('ijw_'):
name = name[4:]
diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py
similarity index 89%
rename from couchpotato/core/providers/userscript/rottentomatoes/main.py
rename to couchpotato/core/media/movie/providers/userscript/rottentomatoes.py
index 0b16a441..902192e2 100644
--- a/couchpotato/core/providers/userscript/rottentomatoes/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/rottentomatoes.py
@@ -1,10 +1,14 @@
-from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.userscript.base import UserscriptBase
import re
import traceback
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
log = CPLog(__name__)
+autoload = 'RottenTomatoes'
+
class RottenTomatoes(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/sharethe/main.py b/couchpotato/core/media/movie/providers/userscript/sharethe.py
similarity index 52%
rename from couchpotato/core/providers/userscript/sharethe/main.py
rename to couchpotato/core/media/movie/providers/userscript/sharethe.py
index d22b67eb..ef2f537e 100644
--- a/couchpotato/core/providers/userscript/sharethe/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/sharethe.py
@@ -1,4 +1,6 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'ShareThe'
class ShareThe(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/tmdb/main.py b/couchpotato/core/media/movie/providers/userscript/tmdb.py
similarity index 79%
rename from couchpotato/core/providers/userscript/tmdb/main.py
rename to couchpotato/core/media/movie/providers/userscript/tmdb.py
index b718fc3b..f11dba27 100644
--- a/couchpotato/core/providers/userscript/tmdb/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/tmdb.py
@@ -1,7 +1,11 @@
-from couchpotato.core.event import fireEvent
-from couchpotato.core.providers.userscript.base import UserscriptBase
import re
+from couchpotato.core.event import fireEvent
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
+autoload = 'TMDB'
+
class TMDB(UserscriptBase):
diff --git a/couchpotato/core/providers/userscript/trakt/main.py b/couchpotato/core/media/movie/providers/userscript/trakt.py
similarity index 63%
rename from couchpotato/core/providers/userscript/trakt/main.py
rename to couchpotato/core/media/movie/providers/userscript/trakt.py
index 43e06dee..cd40c69c 100644
--- a/couchpotato/core/providers/userscript/trakt/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/trakt.py
@@ -1,4 +1,6 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'Trakt'
class Trakt(UserscriptBase):
diff --git a/couchpotato/core/media/movie/providers/userscript/whiwa.py b/couchpotato/core/media/movie/providers/userscript/whiwa.py
new file mode 100644
index 00000000..bd602d20
--- /dev/null
+++ b/couchpotato/core/media/movie/providers/userscript/whiwa.py
@@ -0,0 +1,8 @@
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+autoload = 'WHiWA'
+
+
+class WHiWA(UserscriptBase):
+
+ includes = ['http://whiwa.net/stats/movie/*']
diff --git a/couchpotato/core/providers/userscript/youteather/main.py b/couchpotato/core/media/movie/providers/userscript/youteather.py
similarity index 80%
rename from couchpotato/core/providers/userscript/youteather/main.py
rename to couchpotato/core/media/movie/providers/userscript/youteather.py
index 3efd3686..e7e63b46 100644
--- a/couchpotato/core/providers/userscript/youteather/main.py
+++ b/couchpotato/core/media/movie/providers/userscript/youteather.py
@@ -1,6 +1,11 @@
-from couchpotato.core.providers.userscript.base import UserscriptBase
import re
+from couchpotato.core.media._base.providers.userscript.base import UserscriptBase
+
+
+autoload = 'YouTheater'
+
+
class YouTheater(UserscriptBase):
id_re = re.compile("view\.php\?id=(\d+)")
includes = ['http://www.youtheater.com/view.php?id=*', 'http://youtheater.com/view.php?id=*',
diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py
new file mode 100644
index 00000000..1053ec3d
--- /dev/null
+++ b/couchpotato/core/media/movie/searcher.py
@@ -0,0 +1,473 @@
+from datetime import date
+import random
+import re
+import time
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
+from couchpotato.core.helpers.encoding import simplifyString
+from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.media._base.searcher.base import SearcherBase
+from couchpotato.core.media.movie import MovieTypeBase
+from couchpotato.environment import Env
+
+
+log = CPLog(__name__)
+
+autoload = 'MovieSearcher'
+
+
+class MovieSearcher(SearcherBase, MovieTypeBase):
+
+ in_progress = False
+
+ def __init__(self):
+ super(MovieSearcher, self).__init__()
+
+ addEvent('movie.searcher.all', self.searchAll)
+ addEvent('movie.searcher.all_view', self.searchAllView)
+ addEvent('movie.searcher.single', self.single)
+ addEvent('movie.searcher.try_next_release', self.tryNextRelease)
+ addEvent('movie.searcher.could_be_released', self.couldBeReleased)
+ addEvent('searcher.correct_release', self.correctRelease)
+ addEvent('searcher.get_search_title', self.getSearchTitle)
+
+ addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
+ 'desc': 'Marks the snatched results as ignored and try the next best release',
+ 'params': {
+ 'media_id': {'desc': 'The id of the media'},
+ },
+ })
+
+ addApiView('movie.searcher.full_search', self.searchAllView, docs = {
+ 'desc': 'Starts a full search for all wanted movies',
+ })
+
+ addApiView('movie.searcher.progress', self.getProgress, docs = {
+ 'desc': 'Get the progress of current full search',
+ 'return': {'type': 'object', 'example': """{
+ 'progress': False || object, total & to_go,
+}"""},
+ })
+
+ if self.conf('run_on_launch'):
+ addEvent('app.load', self.searchAll)
+
+ def searchAllView(self, **kwargs):
+
+ fireEventAsync('movie.searcher.all', manual = True)
+
+ return {
+ 'success': not self.in_progress
+ }
+
+ def searchAll(self, manual = False):
+
+ if self.in_progress:
+ log.info('Search already in progress')
+ fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress')
+ return
+
+ self.in_progress = True
+ fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
+
+ medias = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)]
+ random.shuffle(medias)
+
+ total = len(medias)
+ self.in_progress = {
+ 'total': total,
+ 'to_go': total,
+ }
+
+ try:
+ search_protocols = fireEvent('searcher.protocols', single = True)
+
+ for media_id in medias:
+
+ media = fireEvent('media.get', media_id, single = True)
+
+ try:
+ self.single(media, search_protocols, manual = manual)
+ except IndexError:
+ log.error('Forcing library update for %s, if you see this often, please report: %s', (getIdentifier(media), traceback.format_exc()))
+ fireEvent('movie.update_info', media_id)
+ except:
+ log.error('Search failed for %s: %s', (getIdentifier(media), traceback.format_exc()))
+
+ self.in_progress['to_go'] -= 1
+
+ # Break if CP wants to shut down
+ if self.shuttingDown():
+ break
+
+ except SearchSetupError:
+ pass
+
+ self.in_progress = False
+
+ def single(self, movie, search_protocols = None, manual = False, force_download = False):
+
+ # Find out search type
+ try:
+ if not search_protocols:
+ search_protocols = fireEvent('searcher.protocols', single = True)
+ except SearchSetupError:
+ return
+
+ if not movie['profile_id'] or (movie['status'] == 'done' and not manual):
+ log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
+ return
+
+ pre_releases = fireEvent('quality.pre_releases', single = True)
+ release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True)
+
+ found_releases = []
+ previous_releases = movie.get('releases', [])
+ too_early_to_search = []
+ outside_eta_results = 0
+ alway_search = self.conf('always_search')
+ ignore_eta = manual
+ total_result_count = 0
+
+ default_title = getTitle(movie)
+ if not default_title:
+ log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
+ fireEvent('media.delete', movie['_id'], single = True)
+ return
+
+ fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title)
+
+ # Ignore eta once every 7 days
+ if not alway_search:
+ prop_name = 'last_ignored_eta.%s' % movie['_id']
+ last_ignored_eta = float(Env.prop(prop_name, default = 0))
+ if last_ignored_eta > time.time() - 604800:
+ ignore_eta = True
+ Env.prop(prop_name, value = time.time())
+
+ db = get_db()
+
+ profile = db.get('id', movie['profile_id'])
+ ret = False
+
+ index = 0
+ for q_identifier in profile.get('qualities'):
+ quality_custom = {
+ 'index': index,
+ 'quality': q_identifier,
+ 'finish': profile['finish'][index],
+ 'wait_for': tryInt(profile['wait_for'][index]),
+ '3d': profile['3d'][index] if profile.get('3d') else False
+ }
+
+ index += 1
+
+ could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year'])
+ if not alway_search and could_not_be_released:
+ too_early_to_search.append(q_identifier)
+
+ # Skip release, if ETA isn't ignored
+ if not ignore_eta:
+ continue
+
+ has_better_quality = 0
+
+ # See if better quality is available
+ for release in movie.get('releases', []):
+ if release['status'] not in ['available', 'ignored', 'failed']:
+ is_higher = fireEvent('quality.ishigher', \
+ {'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \
+ {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \
+ profile, single = True)
+ if is_higher != 'higher':
+ has_better_quality += 1
+
+ # Don't search for quality lower then already available.
+ if has_better_quality > 0:
+ log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title))
+ fireEvent('media.restatus', movie['_id'])
+ break
+
+ quality = fireEvent('quality.single', identifier = q_identifier, single = True)
+ log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if alway_search or ignore_eta else ''))
+
+ # Extend quality with profile customs
+ quality['custom'] = quality_custom
+
+ results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
+ results_count = len(results)
+ total_result_count += results_count
+ if results_count == 0:
+ log.debug('Nothing found for %s in %s', (default_title, quality['label']))
+
+ # Keep track of releases found outside ETA window
+ outside_eta_results += results_count if could_not_be_released else 0
+
+ # Check if movie isn't deleted while searching
+ if not fireEvent('media.get', movie.get('_id'), single = True):
+ break
+
+ # Add them to this movie releases list
+ found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True)
+
+ # Don't trigger download, but notify user of available releases
+ if could_not_be_released:
+ if results_count > 0:
+ log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
+
+ # Try find a valid result and download it
+ if (force_download or not could_not_be_released or alway_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
+ ret = True
+
+ # Remove releases that aren't found anymore
+ temp_previous_releases = []
+ for release in previous_releases:
+ if release.get('status') == 'available' and release.get('identifier') not in found_releases:
+ fireEvent('release.delete', release.get('_id'), single = True)
+ else:
+ temp_previous_releases.append(release)
+ previous_releases = temp_previous_releases
+ del temp_previous_releases
+
+ # Break if CP wants to shut down
+ if self.shuttingDown() or ret:
+ break
+
+ if total_result_count > 0:
+ fireEvent('media.tag', movie['_id'], 'recent', single = True)
+
+ if len(too_early_to_search) > 0:
+ log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
+
+ if outside_eta_results > 0:
+ message = 'Found %s releases for "%s" before ETA. Select and download via the dashboard.' % (outside_eta_results, default_title)
+ log.info(message)
+
+ if not manual:
+ fireEvent('media.available', message = message, data = {})
+
+ fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']})
+
+ return ret
+
+ def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
+
+ if media.get('type') != 'movie': return
+
+ media_title = fireEvent('searcher.get_search_title', media, single = True)
+
+ imdb_results = kwargs.get('imdb_results', False)
+ retention = Env.setting('retention', section = 'nzb')
+
+ if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
+ log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
+ return False
+
+ # Check for required and ignored words
+ if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
+ return False
+
+ preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
+
+ # Contains lower quality string
+ contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True)
+ if contains_other != False:
+ log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality'))
+ return False
+
+ # Contains lower quality string
+ if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, single = True):
+ log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label']))
+ return False
+
+ # File to small
+ if nzb['size'] and tryInt(preferred_quality['size_min']) > tryInt(nzb['size']):
+ log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
+ return False
+
+ # File to large
+ if nzb['size'] and tryInt(preferred_quality['size_max']) < tryInt(nzb['size']):
+ log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
+ return False
+
+ # Provider specific functions
+ get_more = nzb.get('get_more_info')
+ if get_more:
+ get_more(nzb)
+
+ extra_check = nzb.get('extra_check')
+ if extra_check and not extra_check(nzb):
+ return False
+
+
+ if imdb_results:
+ return True
+
+ # Check if nzb contains imdb link
+ if getImdb(nzb.get('description', '')) == getIdentifier(media):
+ return True
+
+ for raw_title in media['info']['titles']:
+ for movie_title in possibleTitles(raw_title):
+ movie_words = re.split('\W+', simplifyString(movie_title))
+
+ if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
+ # if no IMDB link, at least check year range 1
+ if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 1, single = True):
+ return True
+
+ # if no IMDB link, at least check year
+ if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 0, single = True):
+ return True
+
+ log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['info']['year']))
+ return False
+
+ def couldBeReleased(self, is_pre_release, dates, year = None):
+
+ now = int(time.time())
+ now_year = date.today().year
+ now_month = date.today().month
+
+ if (year is None or year < now_year - 1 or (year <= now_year - 1 and now_month > 4)) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
+ return True
+ else:
+
+ # Don't allow movies with years to far in the future
+ add_year = 1 if now_month > 10 else 0 # Only allow +1 year if end of the year
+ if year is not None and year > (now_year + add_year):
+ return False
+
+ # For movies before 1972
+ if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
+ return True
+
+ if is_pre_release:
+ # Prerelease 1 week before theaters
+ if dates.get('theater') - 604800 < now:
+ return True
+ else:
+ # 12 weeks after theater release
+ if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
+ return True
+
+ if dates.get('dvd') > 0:
+
+ # 4 weeks before dvd release
+ if dates.get('dvd') - 2419200 < now:
+ return True
+
+ # Dvd should be released
+ if dates.get('dvd') < now:
+ return True
+
+
+ return False
+
+ def tryNextReleaseView(self, media_id = None, **kwargs):
+
+ trynext = self.tryNextRelease(media_id, manual = True, force_download = True)
+
+ return {
+ 'success': trynext
+ }
+
+ def tryNextRelease(self, media_id, manual = False, force_download = False):
+
+ try:
+ db = get_db()
+ rels = fireEvent('media.with_status', ['snatched', 'done'], single = True)
+
+ for rel in rels:
+ rel['status'] = 'ignored'
+ db.update(rel)
+
+ movie_dict = fireEvent('media.get', media_id, single = True)
+ log.info('Trying next release for: %s', getTitle(movie_dict))
+ self.single(movie_dict, manual = manual, force_download = force_download)
+
+ return True
+
+ except:
+ log.error('Failed searching for next release: %s', traceback.format_exc())
+ return False
+
+ def getSearchTitle(self, media):
+ if media['type'] == 'movie':
+ return getTitle(media)
+
+class SearchSetupError(Exception):
+ pass
+
+
+config = [{
+ 'name': 'moviesearcher',
+ 'order': 20,
+ 'groups': [
+ {
+ 'tab': 'searcher',
+ 'name': 'movie_searcher',
+ 'label': 'Movie search',
+ 'description': 'Search options for movies',
+ 'advanced': True,
+ 'options': [
+ {
+ 'name': 'always_search',
+ 'default': False,
+ 'migrate_from': 'searcher',
+ 'type': 'bool',
+ 'label': 'Always search',
+ 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
+ },
+ {
+ 'name': 'run_on_launch',
+ 'migrate_from': 'searcher',
+ 'label': 'Run on launch',
+ 'advanced': True,
+ 'default': 0,
+ 'type': 'bool',
+ 'description': 'Force run the searcher after (re)start.',
+ },
+ {
+ 'name': 'search_on_add',
+ 'label': 'Search after add',
+ 'advanced': True,
+ 'default': 1,
+ 'type': 'bool',
+ 'description': 'Disable this to only search for movies on cron.',
+ },
+ {
+ 'name': 'cron_day',
+ 'migrate_from': 'searcher',
+ 'label': 'Day',
+ 'advanced': True,
+ 'default': '*',
+ 'type': 'string',
+ 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.',
+ },
+ {
+ 'name': 'cron_hour',
+ 'migrate_from': 'searcher',
+ 'label': 'Hour',
+ 'advanced': True,
+ 'default': random.randint(0, 23),
+ 'type': 'string',
+ 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.',
+ },
+ {
+ 'name': 'cron_minute',
+ 'migrate_from': 'searcher',
+ 'label': 'Minute',
+ 'advanced': True,
+ 'default': random.randint(0, 59),
+ 'type': 'string',
+ 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
+ },
+ ],
+ },
+ ],
+}]
diff --git a/couchpotato/core/media/movie/searcher/__init__.py b/couchpotato/core/media/movie/searcher/__init__.py
deleted file mode 100644
index 4ae1ed32..00000000
--- a/couchpotato/core/media/movie/searcher/__init__.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from .main import MovieSearcher
-import random
-
-
-def start():
- return MovieSearcher()
-
-config = [{
- 'name': 'moviesearcher',
- 'order': 20,
- 'groups': [
- {
- 'tab': 'searcher',
- 'name': 'movie_searcher',
- 'label': 'Movie search',
- 'description': 'Search options for movies',
- 'advanced': True,
- 'options': [
- {
- 'name': 'always_search',
- 'default': False,
- 'migrate_from': 'searcher',
- 'type': 'bool',
- 'label': 'Always search',
- 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
- },
- {
- 'name': 'run_on_launch',
- 'migrate_from': 'searcher',
- 'label': 'Run on launch',
- 'advanced': True,
- 'default': 0,
- 'type': 'bool',
- 'description': 'Force run the searcher after (re)start.',
- },
- {
- 'name': 'search_on_add',
- 'label': 'Search after add',
- 'advanced': True,
- 'default': 1,
- 'type': 'bool',
- 'description': 'Disable this to only search for movies on cron.',
- },
- {
- 'name': 'cron_day',
- 'migrate_from': 'searcher',
- 'label': 'Day',
- 'advanced': True,
- 'default': '*',
- 'type': 'string',
- 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.',
- },
- {
- 'name': 'cron_hour',
- 'migrate_from': 'searcher',
- 'label': 'Hour',
- 'advanced': True,
- 'default': random.randint(0, 23),
- 'type': 'string',
- 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.',
- },
- {
- 'name': 'cron_minute',
- 'migrate_from': 'searcher',
- 'label': 'Minute',
- 'advanced': True,
- 'default': random.randint(0, 59),
- 'type': 'string',
- 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
- },
- ],
- },
- ],
-}]
diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py
deleted file mode 100644
index 1c22e18c..00000000
--- a/couchpotato/core/media/movie/searcher/main.py
+++ /dev/null
@@ -1,363 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
-from couchpotato.core.helpers.encoding import simplifyString
-from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb
-from couchpotato.core.logger import CPLog
-from couchpotato.core.media._base.searcher.base import SearcherBase
-from couchpotato.core.media.movie import MovieTypeBase
-from couchpotato.core.settings.model import Media, Release
-from couchpotato.environment import Env
-from datetime import date
-import random
-import re
-import time
-import traceback
-
-log = CPLog(__name__)
-
-
-class MovieSearcher(SearcherBase, MovieTypeBase):
-
- in_progress = False
-
- def __init__(self):
- super(MovieSearcher, self).__init__()
-
- addEvent('movie.searcher.all', self.searchAll)
- addEvent('movie.searcher.all_view', self.searchAllView)
- addEvent('movie.searcher.single', self.single)
- addEvent('movie.searcher.try_next_release', self.tryNextRelease)
- addEvent('movie.searcher.could_be_released', self.couldBeReleased)
- addEvent('searcher.correct_release', self.correctRelease)
- addEvent('searcher.get_search_title', self.getSearchTitle)
-
- addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
- 'desc': 'Marks the snatched results as ignored and try the next best release',
- 'params': {
- 'id': {'desc': 'The id of the movie'},
- },
- })
-
- addApiView('movie.searcher.full_search', self.searchAllView, docs = {
- 'desc': 'Starts a full search for all wanted movies',
- })
-
- addApiView('movie.searcher.progress', self.getProgress, docs = {
- 'desc': 'Get the progress of current full search',
- 'return': {'type': 'object', 'example': """{
- 'progress': False || object, total & to_go,
-}"""},
- })
-
- if self.conf('run_on_launch'):
- addEvent('app.load', self.searchAll)
-
- def searchAllView(self, **kwargs):
-
- fireEventAsync('movie.searcher.all')
-
- return {
- 'success': not self.in_progress
- }
-
- def searchAll(self):
-
- if self.in_progress:
- log.info('Search already in progress')
- fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress')
- return
-
- self.in_progress = True
- fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
-
- db = get_session()
-
- movies_raw = db.query(Media).filter(
- Media.status.has(identifier = 'active')
- ).all()
-
- random.shuffle(movies_raw)
-
- movies = []
- for m in movies_raw:
- movies.append(m.to_dict({
- 'category': {},
- 'profile': {'types': {'quality': {}}},
- 'releases': {'status': {}, 'quality': {}},
- 'library': {'titles': {}, 'files': {}},
- 'files': {},
- }))
-
- self.in_progress = {
- 'total': len(movies),
- 'to_go': len(movies),
- }
-
- try:
- search_protocols = fireEvent('searcher.protocols', single = True)
-
- for movie in movies:
-
- try:
- self.single(movie, search_protocols)
- except IndexError:
- log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['library']['identifier'], traceback.format_exc()))
- fireEvent('library.update.movie', movie['library']['identifier'])
- except:
- log.error('Search failed for %s: %s', (movie['library']['identifier'], traceback.format_exc()))
-
- self.in_progress['to_go'] -= 1
-
- # Break if CP wants to shut down
- if self.shuttingDown():
- break
-
- except SearchSetupError:
- pass
-
- self.in_progress = False
-
- def single(self, movie, search_protocols = None, manual = False):
-
- # movies don't contain 'type' yet, so just set to default here
- if 'type' not in movie:
- movie['type'] = 'movie'
-
- # Find out search type
- try:
- if not search_protocols:
- search_protocols = fireEvent('searcher.protocols', single = True)
- except SearchSetupError:
- return
-
- done_status = fireEvent('status.get', 'done', single = True)
-
- if not movie['profile'] or (movie['status_id'] == done_status.get('id') and not manual):
- log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
- return
-
- pre_releases = fireEvent('quality.pre_releases', single = True)
- release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
- available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
-
- found_releases = []
- too_early_to_search = []
-
- default_title = getTitle(movie['library'])
- if not default_title:
- log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
- fireEvent('media.delete', movie['id'], single = True)
- return
-
- fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
-
- db = get_session()
-
- ret = False
- for quality_type in movie['profile']['types']:
- if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
- too_early_to_search.append(quality_type['quality']['identifier'])
- continue
-
- has_better_quality = 0
-
- # See if better quality is available
- for release in movie['releases']:
- if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
- has_better_quality += 1
-
- # Don't search for quality lower then already available.
- if has_better_quality is 0:
-
- log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
- quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
-
- results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
- if len(results) == 0:
- log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
-
- # Check if movie isn't deleted while searching
- if not db.query(Media).filter_by(id = movie.get('id')).first():
- break
-
- # Add them to this movie releases list
- found_releases += fireEvent('release.create_from_search', results, movie, quality_type, single = True)
-
- # Try find a valid result and download it
- if fireEvent('release.try_download_result', results, movie, quality_type, manual, single = True):
- ret = True
-
- # Remove releases that aren't found anymore
- for release in movie.get('releases', []):
- if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
- fireEvent('release.delete', release.get('id'), single = True)
-
- else:
- log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
- fireEvent('media.restatus', movie['id'])
- break
-
- # Break if CP wants to shut down
- if self.shuttingDown() or ret:
- break
-
- if len(too_early_to_search) > 0:
- log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
-
- fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
-
- return ret
-
- def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
-
- if media.get('type') != 'movie': return
-
- media_title = fireEvent('searcher.get_search_title', media, single = True)
-
- imdb_results = kwargs.get('imdb_results', False)
- retention = Env.setting('retention', section = 'nzb')
-
- if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
- log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
- return False
-
- # Check for required and ignored words
- if not fireEvent('searcher.correct_words', nzb['name'], media, single = True):
- return False
-
- preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
-
- # Contains lower quality string
- if fireEvent('searcher.contains_other_quality', nzb, movie_year = media['library']['year'], preferred_quality = preferred_quality, single = True):
- log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
- return False
-
-
- # File to small
- if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
- log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
- return False
-
- # File to large
- if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
- log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
- return False
-
-
- # Provider specific functions
- get_more = nzb.get('get_more_info')
- if get_more:
- get_more(nzb)
-
- extra_check = nzb.get('extra_check')
- if extra_check and not extra_check(nzb):
- return False
-
-
- if imdb_results:
- return True
-
- # Check if nzb contains imdb link
- if getImdb(nzb.get('description', '')) == media['library']['identifier']:
- return True
-
- for raw_title in media['library']['titles']:
- for movie_title in possibleTitles(raw_title['title']):
- movie_words = re.split('\W+', simplifyString(movie_title))
-
- if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
- # if no IMDB link, at least check year range 1
- if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 1, single = True):
- return True
-
- # if no IMDB link, at least check year
- if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['library']['year'], 0, single = True):
- return True
-
- log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['library']['year']))
- return False
-
- def couldBeReleased(self, is_pre_release, dates, year = None):
-
- now = int(time.time())
- now_year = date.today().year
- now_month = date.today().month
-
- if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
- return True
- else:
-
- # Don't allow movies with years to far in the future
- add_year = 1 if now_month > 10 else 0 # Only allow +1 year if end of the year
- if year is not None and year > (now_year + add_year):
- return False
-
- # For movies before 1972
- if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
- return True
-
- if is_pre_release:
- # Prerelease 1 week before theaters
- if dates.get('theater') - 604800 < now:
- return True
- else:
- # 12 weeks after theater release
- if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
- return True
-
- if dates.get('dvd') > 0:
-
- # 4 weeks before dvd release
- if dates.get('dvd') - 2419200 < now:
- return True
-
- # Dvd should be released
- if dates.get('dvd') < now:
- return True
-
-
- return False
-
- def tryNextReleaseView(self, id = None, **kwargs):
-
- trynext = self.tryNextRelease(id, manual = True)
-
- return {
- 'success': trynext
- }
-
- def tryNextRelease(self, media_id, manual = False):
-
- snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
-
- try:
- db = get_session()
- rels = db.query(Release) \
- .filter_by(movie_id = media_id) \
- .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
- .all()
-
- for rel in rels:
- rel.status_id = ignored_status.get('id')
- db.commit()
-
- movie_dict = fireEvent('media.get', media_id = media_id, single = True)
- log.info('Trying next release for: %s', getTitle(movie_dict['library']))
- fireEvent('movie.searcher.single', movie_dict, manual = manual)
-
- return True
-
- except:
- log.error('Failed searching for next release: %s', traceback.format_exc())
- db.rollback()
- return False
- finally:
- db.close()
-
- def getSearchTitle(self, media):
- if media['type'] == 'movie':
- return getTitle(media['library'])
-
-class SearchSetupError(Exception):
- pass
diff --git a/couchpotato/core/media/movie/suggestion/__init__.py b/couchpotato/core/media/movie/suggestion/__init__.py
index 50083fe7..e69de29b 100644
--- a/couchpotato/core/media/movie/suggestion/__init__.py
+++ b/couchpotato/core/media/movie/suggestion/__init__.py
@@ -1,7 +0,0 @@
-from .main import Suggestion
-
-
-def start():
- return Suggestion()
-
-config = []
diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py
index 22e23fe2..c2cc9071 100644
--- a/couchpotato/core/media/movie/suggestion/main.py
+++ b/couchpotato/core/media/movie/suggestion/main.py
@@ -1,12 +1,12 @@
-from couchpotato import get_session
+from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
-from couchpotato.core.helpers.variable import splitString, removeDuplicate
+from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Media, Library
from couchpotato.environment import Env
-from sqlalchemy.orm import joinedload_all
-from sqlalchemy.sql.expression import or_
+
+
+autoload = 'Suggestion'
class Suggestion(Plugin):
@@ -28,11 +28,8 @@ class Suggestion(Plugin):
else:
if not movies or len(movies) == 0:
- db = get_session()
- active_movies = db.query(Media) \
- .options(joinedload_all('library')) \
- .filter(or_(*[Media.status.has(identifier = s) for s in ['active', 'done']])).all()
- movies = [x.library.identifier for x in active_movies]
+ active_movies = fireEvent('media.with_status', ['active', 'done'], single = True)
+ movies = [getIdentifier(x) for x in active_movies]
if not ignored or len(ignored) == 0:
ignored = splitString(Env.prop('suggest_ignore', default = ''))
@@ -87,15 +84,8 @@ class Suggestion(Plugin):
# Get new results and add them
if len(new_suggestions) - 1 < limit:
-
- active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
-
- db = get_session()
- active_movies = db.query(Media) \
- .join(Library) \
- .with_entities(Library.identifier) \
- .filter(Media.status_id.in_([active_status.get('id'), done_status.get('id')])).all()
- movies = [x[0] for x in active_movies]
+ active_movies = fireEvent('media.with_status', ['active', 'done'], single = True)
+ movies = [getIdentifier(x) for x in active_movies]
movies.extend(seen)
ignored.extend([x.get('imdb') for x in cached_suggestion])
diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.css b/couchpotato/core/media/movie/suggestion/static/suggest.css
index d4ba734b..8e747849 100644
--- a/couchpotato/core/media/movie/suggestion/static/suggest.css
+++ b/couchpotato/core/media/movie/suggestion/static/suggest.css
@@ -1,4 +1,7 @@
.suggestions {
+ clear: both;
+ padding-top: 10px;
+ margin-bottom: 30px;
}
.suggestions > h2 {
@@ -91,7 +94,6 @@
padding: 0 3px 10px 0;
}
.suggestions .media_result .data:before {
- bottom: 0;
content: '';
display: block;
height: 10px;
@@ -107,7 +109,7 @@
z-index: 3;
pointer-events: none;
}
-
+
.suggestions .media_result .data .info .plot.full {
top: 0;
overflow: auto;
@@ -123,14 +125,14 @@
.suggestions .media_result .options select[name=title] { width: 100%; }
.suggestions .media_result .options select[name=profile] { width: 100%; }
.suggestions .media_result .options select[name=category] { width: 100%; }
-
- .suggestions .media_result .button {
+
+ .suggestions .media_result .button {
position: absolute;
margin: 2px 0 0 0;
right: 15px;
bottom: 15px;
}
-
+
.suggestions .media_result .thumbnail {
width: 100px;
diff --git a/couchpotato/core/media/movie/suggestion/static/suggest.js b/couchpotato/core/media/movie/suggestion/static/suggest.js
index c4e5630a..494f0459 100644
--- a/couchpotato/core/media/movie/suggestion/static/suggest.js
+++ b/couchpotato/core/media/movie/suggestion/static/suggest.js
@@ -42,11 +42,10 @@ var SuggestList = new Class({
}
}
- }).grab(
- new Element('h2', {
- 'text': 'You might like these'
- })
- );
+ });
+
+ var cookie_menu_select = Cookie.read('suggestions_charts_menu_selected');
+ if( cookie_menu_select === 'suggestions' || cookie_menu_select === null ) self.el.show(); else self.el.hide();
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
@@ -116,7 +115,7 @@ var SuggestList = new Class({
}
}
}) : null
- )
+ );
$(m).inject(self.el);
@@ -150,4 +149,4 @@ var SuggestList = new Class({
return this.el;
}
-})
+});
diff --git a/couchpotato/core/migration/migrate.cfg b/couchpotato/core/migration/migrate.cfg
deleted file mode 100644
index f17e967a..00000000
--- a/couchpotato/core/migration/migrate.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-[db_settings]
-repository_id = CouchPotato
-version_table = migrate_version
-required_dbs = ['sqlite']
diff --git a/couchpotato/core/migration/versions/001_Releases_last_edit.py b/couchpotato/core/migration/versions/001_Releases_last_edit.py
deleted file mode 100644
index d4b12080..00000000
--- a/couchpotato/core/migration/versions/001_Releases_last_edit.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from migrate.changeset.schema import create_column
-from sqlalchemy.schema import MetaData, Column, Table, Index
-from sqlalchemy.types import Integer
-
-meta = MetaData()
-
-
-def upgrade(migrate_engine):
- meta.bind = migrate_engine
-
- # Change release, add last_edit and index
- last_edit_column = Column('last_edit', Integer)
- release = Table('release', meta, last_edit_column)
-
- create_column(last_edit_column, release)
- Index('ix_release_last_edit', release.c.last_edit).create()
-
- # Change movie last_edit
- last_edit_column = Column('last_edit', Integer)
- movie = Table('movie', meta, last_edit_column)
- Index('ix_movie_last_edit', movie.c.last_edit).create()
-
-
-def downgrade(migrate_engine):
- pass
diff --git a/couchpotato/core/migration/versions/002_Movie_category.py b/couchpotato/core/migration/versions/002_Movie_category.py
deleted file mode 100644
index 023e47c6..00000000
--- a/couchpotato/core/migration/versions/002_Movie_category.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from migrate.changeset.schema import create_column
-from sqlalchemy.schema import MetaData, Column, Table, Index
-from sqlalchemy.types import Integer
-
-meta = MetaData()
-
-
-def upgrade(migrate_engine):
- meta.bind = migrate_engine
-
- category_column = Column('category_id', Integer)
- movie = Table('movie', meta, category_column)
- create_column(category_column, movie)
- Index('ix_movie_category_id', movie.c.category_id).create()
-
-
-def downgrade(migrate_engine):
- pass
diff --git a/couchpotato/core/migration/versions/__init__.py b/couchpotato/core/migration/versions/__init__.py
deleted file mode 100755
index 7e6e44bf..00000000
--- a/couchpotato/core/migration/versions/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
- Examples
-
- Adding a column:
-
- from migrate import *
- from migrate.changeset.schema import create_column
- from sqlalchemy import *
-
- meta = MetaData()
-
- def upgrade(migrate_engine):
- meta.bind = migrate_engine
-
- #print changeset.schema
- path_column = Column('path', String)
- resource = Table('resource', meta, path_column)
-
- create_column(path_column, resource)
-
-
-
- Adding Relation table: http://www.mail-archive.com/sqlelixir@googlegroups.com/msg02061.html
-
- person = Table('person', metadata, Column('id', Integer))
- person_column = Column('person_id', Integer, ForeignKey('person.id'), nullable=False)
- movie = Table('movie', metadata, person_column)
- person_constraint = ForeignKeyConstraint(['person_id'], ['person.id'], ondelete="restrict", table=movie)
-
-"""
diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py
index 63d2075e..725704e0 100644
--- a/couchpotato/core/notifications/base.py
+++ b/couchpotato/core/notifications/base.py
@@ -1,7 +1,7 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
-from couchpotato.core.providers.base import Provider
+from couchpotato.core.media._base.providers.base import Provider
from couchpotato.environment import Env
log = CPLog(__name__)
@@ -15,6 +15,7 @@ class Notification(Provider):
test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = [
+ 'media.available',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message.important',
diff --git a/couchpotato/core/notifications/boxcar/__init__.py b/couchpotato/core/notifications/boxcar/__init__.py
deleted file mode 100644
index faab7a5c..00000000
--- a/couchpotato/core/notifications/boxcar/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import Boxcar
-
-
-def start():
- return Boxcar()
-
-config = [{
- 'name': 'boxcar',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'boxcar',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'email',
- 'description': 'Your Boxcar registration emailaddress.'
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/boxcar/main.py b/couchpotato/core/notifications/boxcar/main.py
deleted file mode 100644
index 49aab316..00000000
--- a/couchpotato/core/notifications/boxcar/main.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-import time
-
-log = CPLog(__name__)
-
-
-class Boxcar(Notification):
-
- url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- try:
- message = message.strip()
-
- data = {
- 'email': self.conf('email'),
- 'notification[from_screen_name]': self.default_title,
- 'notification[message]': toUnicode(message),
- 'notification[from_remote_service_id]': int(time.time()),
- }
-
- self.urlopen(self.url, data = data)
- except:
- log.error('Check your email and added services on boxcar.io')
- return False
-
- log.info('Boxcar notification successful.')
- return True
-
- def isEnabled(self):
- return super(Boxcar, self).isEnabled() and self.conf('email')
diff --git a/couchpotato/core/notifications/boxcar2/main.py b/couchpotato/core/notifications/boxcar2.py
similarity index 55%
rename from couchpotato/core/notifications/boxcar2/main.py
rename to couchpotato/core/notifications/boxcar2.py
index 6633ca70..04ce4f38 100644
--- a/couchpotato/core/notifications/boxcar2/main.py
+++ b/couchpotato/core/notifications/boxcar2.py
@@ -4,6 +4,8 @@ from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
+autoload = 'Boxcar2'
+
class Boxcar2(Notification):
@@ -23,7 +25,7 @@ class Boxcar2(Notification):
data = {
'user_credentials': self.conf('token'),
- 'notification[title]': toUnicode(message),
+ 'notification[title]': toUnicode('%s - %s' % (self.default_title, message)),
'notification[long_message]': toUnicode(long_message),
}
@@ -37,3 +39,33 @@ class Boxcar2(Notification):
def isEnabled(self):
return super(Boxcar2, self).isEnabled() and self.conf('token')
+
+
+config = [{
+ 'name': 'boxcar2',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'boxcar2',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'token',
+ 'description': ('Your Boxcar access token.', 'Can be found in the app under settings')
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/boxcar2/__init__.py b/couchpotato/core/notifications/boxcar2/__init__.py
deleted file mode 100644
index da7f99c0..00000000
--- a/couchpotato/core/notifications/boxcar2/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import Boxcar2
-
-
-def start():
- return Boxcar2()
-
-config = [{
- 'name': 'boxcar2',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'boxcar2',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'token',
- 'description': ('Your Boxcar access token.', 'Can be found in the app under settings')
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/core/__init__.py b/couchpotato/core/notifications/core/__init__.py
index b68a915a..9c4fb373 100644
--- a/couchpotato/core/notifications/core/__init__.py
+++ b/couchpotato/core/notifications/core/__init__.py
@@ -1,7 +1,5 @@
from .main import CoreNotifier
-def start():
+def autoload():
return CoreNotifier()
-
-config = []
diff --git a/couchpotato/core/notifications/core/index.py b/couchpotato/core/notifications/core/index.py
new file mode 100644
index 00000000..c7985b5b
--- /dev/null
+++ b/couchpotato/core/notifications/core/index.py
@@ -0,0 +1,37 @@
+from CodernityDB.tree_index import TreeBasedIndex
+
+
+class NotificationIndex(TreeBasedIndex):
+ _version = 1
+
+ custom_header = """from CodernityDB.tree_index import TreeBasedIndex
+import time"""
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = 'I'
+ super(NotificationIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'notification':
+ return data.get('time'), None
+
+
+class NotificationUnreadIndex(TreeBasedIndex):
+ _version = 1
+
+ custom_header = """from CodernityDB.tree_index import TreeBasedIndex
+import time"""
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = 'I'
+ super(NotificationUnreadIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'notification' and not data.get('read'):
+ return data.get('time'), None
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index 93f94d6a..5190218e 100644
--- a/couchpotato/core/notifications/core/main.py
+++ b/couchpotato/core/notifications/core/main.py
@@ -1,27 +1,34 @@
-from couchpotato import get_session
+from operator import itemgetter
+import threading
+import time
+import traceback
+import uuid
+
+from couchpotato import get_db
from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
-from couchpotato.core.settings.model import Notification as Notif
+from .index import NotificationIndex, NotificationUnreadIndex
from couchpotato.environment import Env
-from operator import itemgetter
-from sqlalchemy.sql.expression import or_
-import threading
-import time
-import traceback
-import uuid
+
log = CPLog(__name__)
class CoreNotifier(Notification):
+ _database = {
+ 'notification': NotificationIndex,
+ 'notification_unread': NotificationUnreadIndex
+ }
+
m_lock = None
listen_to = [
+ 'media.available',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message', 'core.message.important',
@@ -66,40 +73,29 @@ class CoreNotifier(Notification):
self.m_lock = threading.Lock()
def clean(self):
-
try:
- db = get_session()
- db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
- db.commit()
+ db = get_db()
+ for n in db.all('notification', with_doc = True):
+ if n['doc'].get('time', 0) <= (int(time.time()) - 2419200):
+ db.delete(n['doc'])
except:
log.error('Failed cleaning notification: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
def markAsRead(self, ids = None, **kwargs):
ids = splitString(ids) if ids else None
try:
- db = get_session()
-
- if ids:
- q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids]))
- else:
- q = db.query(Notif).filter_by(read = False)
-
- q.update({Notif.read: True})
- db.commit()
-
+ db = get_db()
+ for x in db.all('notification_unread', with_doc = True):
+ if not ids or x['_id'] in ids:
+ x['doc']['read'] = True
+ db.update(x['doc'])
return {
'success': True
}
except:
log.error('Failed mark as read: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
return {
'success': False
@@ -107,24 +103,19 @@ class CoreNotifier(Notification):
def listView(self, limit_offset = None, **kwargs):
- db = get_session()
-
- q = db.query(Notif)
+ db = get_db()
if limit_offset:
splt = splitString(limit_offset)
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
- q = q.limit(limit).offset(offset)
+ results = db.get_many('notification', limit = limit, offset = offset, with_doc = True)
else:
- q = q.limit(200)
+ results = db.get_many('notification', limit = 200, with_doc = True)
- results = q.all()
notifications = []
for n in results:
- ndict = n.to_dict()
- ndict['type'] = 'notification'
- notifications.append(ndict)
+ notifications.append(n['doc'])
return {
'success': True,
@@ -141,7 +132,7 @@ class CoreNotifier(Notification):
for message in messages:
if message.get('time') > last_check:
- message['sticky'] = True # Always sticky core messages
+ message['sticky'] = True # Always sticky core messages
message_type = 'core.message.important' if message.get('important') else 'core.message'
fireEvent(message_type, message = message.get('message'), data = message)
@@ -155,29 +146,23 @@ class CoreNotifier(Notification):
if not data: data = {}
try:
- db = get_session()
+ db = get_db()
data['notification_type'] = listener if listener else 'unknown'
- n = Notif(
- message = toUnicode(message),
- data = data
- )
- db.add(n)
- db.commit()
+ n = {
+ '_t': 'notification',
+ 'time': int(time.time()),
+ 'message': toUnicode(message),
+ 'data': data
+ }
+ db.insert(n)
- ndict = n.to_dict()
- ndict['type'] = 'notification'
- ndict['time'] = time.time()
-
- self.frontend(type = listener, data = data)
+ self.frontend(type = listener, data = n)
return True
except:
log.error('Failed notify: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {}
@@ -274,18 +259,15 @@ class CoreNotifier(Notification):
messages = []
- # Get unread
+ # Get last message
if init:
- db = get_session()
+ db = get_db()
- notifications = db.query(Notif) \
- .filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
- .all()
+ notifications = db.all('notification', with_doc = True)
for n in notifications:
- ndict = n.to_dict()
- ndict['type'] = 'notification'
- messages.append(ndict)
+ if n['doc'].get('time') > (time.time() - 604800):
+ messages.append(n['doc'])
return {
'success': True,
diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
index 18d09e76..93bfa15d 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -14,17 +14,17 @@ var NotificationBase = new Class({
App.on('message', self.showMessage.bind(self));
// Add test buttons to settings page
- App.addEvent('load', self.addTestButtons.bind(self));
+ App.addEvent('loadSettings', self.addTestButtons.bind(self));
// Notification bar
- self.notifications = []
+ self.notifications = [];
App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, {
'button_class': 'icon2.eye-open',
'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self)
- })
+ });
$(App.block.notification).inject(App.getBlock('search'), 'after');
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
@@ -40,7 +40,7 @@ var NotificationBase = new Class({
var self = this;
var added = new Date();
- added.setTime(result.added*1000)
+ added.setTime(result.added*1000);
result.el = App.getBlock('notification').addLink(
new Element('span.'+(result.read ? 'read' : '' )).adopt(
@@ -51,7 +51,7 @@ var NotificationBase = new Class({
self.notifications.include(result);
if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
- var sticky = true
+ var sticky = true;
App.trigger('message', [result.message, sticky, result])
}
else if(!result.read){
@@ -62,7 +62,7 @@ var NotificationBase = new Class({
setBadge: function(value){
var self = this;
- self.badge.set('text', value)
+ self.badge.set('text', value);
self.badge[value ? 'show' : 'hide']()
},
@@ -73,11 +73,11 @@ var NotificationBase = new Class({
if(!force_ids) {
var rn = self.notifications.filter(function(n){
return !n.read && n.data.important === undefined
- })
+ });
- var ids = []
+ var ids = [];
rn.each(function(n){
- ids.include(n.id)
+ ids.include(n._id)
})
}
@@ -103,8 +103,10 @@ var NotificationBase = new Class({
self.request = Api.request('notification.listener', {
'data': {'init':true},
- 'onSuccess': self.processData.bind(self)
- }).send()
+ 'onSuccess': function(json){
+ self.processData(json, true)
+ }
+ }).send();
setInterval(function(){
@@ -120,11 +122,16 @@ var NotificationBase = new Class({
startPoll: function(){
var self = this;
- if(self.stopped || (self.request && self.request.isRunning()))
+ if(self.stopped)
return;
+ if(self.request && self.request.isRunning())
+ self.request.cancel();
+
self.request = Api.request('nonblock/notification.listener', {
- 'onSuccess': self.processData.bind(self),
+ 'onSuccess': function(json){
+ self.processData(json, false)
+ },
'data': {
'last_id': self.last_id
},
@@ -137,20 +144,20 @@ var NotificationBase = new Class({
stopPoll: function(){
if(this.request)
- this.request.cancel()
+ this.request.cancel();
this.stopped = true;
},
- processData: function(json){
+ processData: function(json, init){
var self = this;
// Process data
- if(json){
+ if(json && json.result){
Array.each(json.result, function(result){
- App.trigger(result.type, [result]);
- if(result.message && result.read === undefined)
+ App.trigger(result._t || result.type, [result]);
+ if(result.message && result.read === undefined && !init)
self.showMessage(result.message);
- })
+ });
if(json.result.length > 0)
self.last_id = json.result.getLast().message_id
@@ -176,18 +183,18 @@ var NotificationBase = new Class({
}, 10);
var hide_message = function(){
- new_message.addClass('hide')
+ new_message.addClass('hide');
setTimeout(function(){
new_message.destroy();
}, 1000);
- }
+ };
if(sticky)
new_message.grab(
new Element('a.close.icon2', {
'events': {
'click': function(){
- self.markAsRead([data.id]);
+ self.markAsRead([data._id]);
hide_message();
}
}
@@ -202,7 +209,7 @@ var NotificationBase = new Class({
addTestButtons: function(){
var self = this;
- var setting_page = App.getPage('Settings')
+ var setting_page = App.getPage('Settings');
setting_page.addEvent('create', function(){
Object.each(setting_page.tabs.notifications.groups, self.addTestButton.bind(self))
})
diff --git a/couchpotato/core/notifications/email/__init__.py b/couchpotato/core/notifications/email/__init__.py
deleted file mode 100644
index aaf087b9..00000000
--- a/couchpotato/core/notifications/email/__init__.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from .main import Email
-
-
-def start():
- return Email()
-
-config = [{
- 'name': 'email',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'email',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'from',
- 'label': 'Send e-mail from',
- },
- {
- 'name': 'to',
- 'label': 'Send e-mail to',
- },
- {
- 'name': 'smtp_server',
- 'label': 'SMTP server',
- },
- { 'name': 'smtp_port',
- 'label': 'SMTP server port',
- 'default': '25',
- 'type': 'int',
- },
- {
- 'name': 'ssl',
- 'label': 'Enable SSL',
- 'default': 0,
- 'type': 'bool',
- },
- {
- 'name': 'starttls',
- 'label': 'Enable StartTLS',
- 'default': 0,
- 'type': 'bool',
- },
- {
- 'name': 'smtp_user',
- 'label': 'SMTP user',
- },
- {
- 'name': 'smtp_pass',
- 'label': 'SMTP password',
- 'type': 'password',
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email_.py
similarity index 56%
rename from couchpotato/core/notifications/email/main.py
rename to couchpotato/core/notifications/email_.py
index b8544016..a63eb3de 100644
--- a/couchpotato/core/notifications/email/main.py
+++ b/couchpotato/core/notifications/email_.py
@@ -1,15 +1,19 @@
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from couchpotato.environment import Env
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
import smtplib
import traceback
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+from couchpotato.environment import Env
+
+
log = CPLog(__name__)
+autoload = 'Email'
+
class Email(Notification):
@@ -66,3 +70,68 @@ class Email(Notification):
log.error('E-mail failed: %s', traceback.format_exc())
return False
+
+
+config = [{
+ 'name': 'email',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'email',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'from',
+ 'label': 'Send e-mail from',
+ },
+ {
+ 'name': 'to',
+ 'label': 'Send e-mail to',
+ },
+ {
+ 'name': 'smtp_server',
+ 'label': 'SMTP server',
+ },
+ {
+ 'name': 'smtp_port',
+ 'label': 'SMTP server port',
+ 'default': '25',
+ 'type': 'int',
+ },
+ {
+ 'name': 'ssl',
+ 'label': 'Enable SSL',
+ 'default': 0,
+ 'type': 'bool',
+ },
+ {
+ 'name': 'starttls',
+ 'label': 'Enable StartTLS',
+ 'default': 0,
+ 'type': 'bool',
+ },
+ {
+ 'name': 'smtp_user',
+ 'label': 'SMTP user',
+ },
+ {
+ 'name': 'smtp_pass',
+ 'label': 'SMTP password',
+ 'type': 'password',
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl.py
similarity index 61%
rename from couchpotato/core/notifications/growl/main.py
rename to couchpotato/core/notifications/growl.py
index a3927ed2..e60e7ef7 100644
--- a/couchpotato/core/notifications/growl/main.py
+++ b/couchpotato/core/notifications/growl.py
@@ -1,12 +1,16 @@
+import traceback
+
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from gntp import notifier
-import traceback
+
log = CPLog(__name__)
+autoload = 'Growl'
+
class Growl(Notification):
@@ -15,6 +19,8 @@ class Growl(Notification):
def __init__(self):
super(Growl, self).__init__()
+ self.growl = None
+
if self.isEnabled():
addEvent('app.load', self.register)
@@ -64,3 +70,44 @@ class Growl(Notification):
return False
+
+config = [{
+ 'name': 'growl',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'growl',
+ 'description': 'Version 1.4+',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': False,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ {
+ 'name': 'hostname',
+ 'description': 'Notify growl over network. Needs restart.',
+ 'advanced': True,
+ },
+ {
+ 'name': 'port',
+ 'type': 'int',
+ 'advanced': True,
+ },
+ {
+ 'name': 'password',
+ 'type': 'password',
+ 'advanced': True,
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/growl/__init__.py b/couchpotato/core/notifications/growl/__init__.py
deleted file mode 100644
index dd01cb91..00000000
--- a/couchpotato/core/notifications/growl/__init__.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from .main import Growl
-
-
-def start():
- return Growl()
-
-config = [{
- 'name': 'growl',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'growl',
- 'description': 'Version 1.4+',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'on_snatch',
- 'default': False,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- {
- 'name': 'hostname',
- 'description': 'Notify growl over network. Needs restart.',
- 'advanced': True,
- },
- {
- 'name': 'port',
- 'type': 'int',
- 'advanced': True,
- },
- {
- 'name': 'password',
- 'type': 'password',
- 'advanced': True,
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/nmj/main.py b/couchpotato/core/notifications/nmj.py
similarity index 83%
rename from couchpotato/core/notifications/nmj/main.py
rename to couchpotato/core/notifications/nmj.py
index 967b70e7..665837f1 100644
--- a/couchpotato/core/notifications/nmj/main.py
+++ b/couchpotato/core/notifications/nmj.py
@@ -1,10 +1,12 @@
+import re
+import telnetlib
+
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
-import re
-import telnetlib
+
try:
import xml.etree.cElementTree as etree
@@ -13,14 +15,18 @@ except ImportError:
log = CPLog(__name__)
+autoload = 'NMJ'
+
class NMJ(Notification):
+ # noinspection PyMissingConstructor
def __init__(self):
- addEvent('renamer.after', self.addToLibrary)
addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig)
+ addEvent('renamer.after', self.addToLibrary)
+
def autoConfig(self, host = 'localhost', **kwargs):
mount = ''
@@ -118,3 +124,31 @@ class NMJ(Notification):
}
+config = [{
+ 'name': 'nmj',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'nmj',
+ 'label': 'NMJ',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'host',
+ 'default': 'localhost',
+ },
+ {
+ 'name': 'database',
+ },
+ {
+ 'name': 'mount',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/nmj/__init__.py b/couchpotato/core/notifications/nmj/__init__.py
deleted file mode 100644
index 461a450e..00000000
--- a/couchpotato/core/notifications/nmj/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import NMJ
-
-
-def start():
- return NMJ()
-
-config = [{
- 'name': 'nmj',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'nmj',
- 'label': 'NMJ',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'host',
- 'default': 'localhost',
- },
- {
- 'name': 'database',
- },
- {
- 'name': 'mount',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/notifymyandroid/__init__.py b/couchpotato/core/notifications/notifymyandroid.py
similarity index 53%
rename from couchpotato/core/notifications/notifymyandroid/__init__.py
rename to couchpotato/core/notifications/notifymyandroid.py
index 7d4f4aeb..ed7a24c8 100644
--- a/couchpotato/core/notifications/notifymyandroid/__init__.py
+++ b/couchpotato/core/notifications/notifymyandroid.py
@@ -1,8 +1,41 @@
-from .main import NotifyMyAndroid
+from couchpotato.core.helpers.variable import splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+import pynma
+import six
+
+log = CPLog(__name__)
+
+autoload = 'NotifyMyAndroid'
-def start():
- return NotifyMyAndroid()
+class NotifyMyAndroid(Notification):
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ nma = pynma.PyNMA()
+ keys = splitString(self.conf('api_key'))
+ nma.addkey(keys)
+ nma.developerkey(self.conf('dev_key'))
+
+ response = nma.push(
+ application = self.default_title,
+ event = message.split(' ')[0],
+ description = message,
+ priority = self.conf('priority'),
+ batch_mode = len(keys) > 1
+ )
+
+ successful = 0
+ for key in keys:
+ if not response[str(key)]['code'] == six.u('200'):
+ log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
+ else:
+ successful += 1
+
+ return successful == len(keys)
+
config = [{
'name': 'notifymyandroid',
diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py
deleted file mode 100644
index 16465101..00000000
--- a/couchpotato/core/notifications/notifymyandroid/main.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-import pynma
-import six
-
-log = CPLog(__name__)
-
-
-class NotifyMyAndroid(Notification):
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- nma = pynma.PyNMA()
- keys = splitString(self.conf('api_key'))
- nma.addkey(keys)
- nma.developerkey(self.conf('dev_key'))
-
- response = nma.push(
- application = self.default_title,
- event = message.split(' ')[0],
- description = message,
- priority = self.conf('priority'),
- batch_mode = len(keys) > 1
- )
-
- successful = 0
- for key in keys:
- if not response[str(key)]['code'] == six.u('200'):
- log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
- else:
- successful += 1
-
- return successful == len(keys)
diff --git a/couchpotato/core/notifications/notifymywp/__init__.py b/couchpotato/core/notifications/notifymywp.py
similarity index 57%
rename from couchpotato/core/notifications/notifymywp/__init__.py
rename to couchpotato/core/notifications/notifymywp.py
index 4fcf1a9a..262fd8d1 100644
--- a/couchpotato/core/notifications/notifymywp/__init__.py
+++ b/couchpotato/core/notifications/notifymywp.py
@@ -1,8 +1,31 @@
-from .main import NotifyMyWP
+from couchpotato.core.helpers.variable import splitString
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+from pynmwp import PyNMWP
+import six
+
+log = CPLog(__name__)
+
+autoload = 'NotifyMyWP'
-def start():
- return NotifyMyWP()
+class NotifyMyWP(Notification):
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ keys = splitString(self.conf('api_key'))
+ p = PyNMWP(keys, self.conf('dev_key'))
+
+ response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
+
+ for key in keys:
+ if not response[key]['Code'] == six.u('200'):
+ log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
+ return False
+
+ return response
+
config = [{
'name': 'notifymywp',
diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py
deleted file mode 100644
index 74010441..00000000
--- a/couchpotato/core/notifications/notifymywp/main.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from pynmwp import PyNMWP
-import six
-
-log = CPLog(__name__)
-
-
-class NotifyMyWP(Notification):
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- keys = splitString(self.conf('api_key'))
- p = PyNMWP(keys, self.conf('dev_key'))
-
- response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
-
- for key in keys:
- if not response[key]['Code'] == six.u('200'):
- log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
- return False
-
- return response
diff --git a/couchpotato/core/notifications/plex/__init__.py b/couchpotato/core/notifications/plex/__init__.py
index 0de92ca3..4a64ec5b 100755
--- a/couchpotato/core/notifications/plex/__init__.py
+++ b/couchpotato/core/notifications/plex/__init__.py
@@ -1,7 +1,7 @@
from .main import Plex
-def start():
+def autoload():
return Plex()
config = [{
diff --git a/couchpotato/core/notifications/plex/client.py b/couchpotato/core/notifications/plex/client.py
index 8864230d..84cf7af6 100644
--- a/couchpotato/core/notifications/plex/client.py
+++ b/couchpotato/core/notifications/plex/client.py
@@ -1,9 +1,11 @@
import json
+
from couchpotato import CPLog
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
import requests
+
log = CPLog(__name__)
diff --git a/couchpotato/core/notifications/plex/server.py b/couchpotato/core/notifications/plex/server.py
index b66db8fe..cd11f49b 100644
--- a/couchpotato/core/notifications/plex/server.py
+++ b/couchpotato/core/notifications/plex/server.py
@@ -1,9 +1,10 @@
from datetime import timedelta, datetime
-from couchpotato.core.helpers.variable import cleanHost
-from couchpotato import CPLog
from urlparse import urlparse
import traceback
+from couchpotato.core.helpers.variable import cleanHost
+from couchpotato import CPLog
+
try:
import xml.etree.cElementTree as etree
diff --git a/couchpotato/core/notifications/prowl/__init__.py b/couchpotato/core/notifications/prowl.py
similarity index 50%
rename from couchpotato/core/notifications/prowl/__init__.py
rename to couchpotato/core/notifications/prowl.py
index 3721a0ad..fdece326 100644
--- a/couchpotato/core/notifications/prowl/__init__.py
+++ b/couchpotato/core/notifications/prowl.py
@@ -1,8 +1,43 @@
-from .main import Prowl
+import traceback
+
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
-def start():
- return Prowl()
+log = CPLog(__name__)
+
+autoload = 'Prowl'
+
+
+class Prowl(Notification):
+
+ urls = {
+ 'api': 'https://api.prowlapp.com/publicapi/add'
+ }
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ data = {
+ 'apikey': self.conf('api_key'),
+ 'application': self.default_title,
+ 'description': toUnicode(message),
+ 'priority': self.conf('priority'),
+ }
+ headers = {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+
+ try:
+ self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
+ log.info('Prowl notifications sent.')
+ return True
+ except:
+ log.error('Prowl failed: %s', traceback.format_exc())
+
+ return False
+
config = [{
'name': 'prowl',
diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py
deleted file mode 100644
index b3385863..00000000
--- a/couchpotato/core/notifications/prowl/main.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-import traceback
-
-log = CPLog(__name__)
-
-
-class Prowl(Notification):
-
- urls = {
- 'api': 'https://api.prowlapp.com/publicapi/add'
- }
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- data = {
- 'apikey': self.conf('api_key'),
- 'application': self.default_title,
- 'description': toUnicode(message),
- 'priority': self.conf('priority'),
- }
- headers = {
- 'Content-type': 'application/x-www-form-urlencoded'
- }
-
- try:
- self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
- log.info('Prowl notifications sent.')
- return True
- except:
- log.error('Prowl failed: %s', traceback.format_exc())
-
- return False
diff --git a/couchpotato/core/notifications/pushalot/__init__.py b/couchpotato/core/notifications/pushalot.py
similarity index 54%
rename from couchpotato/core/notifications/pushalot/__init__.py
rename to couchpotato/core/notifications/pushalot.py
index ad0c853f..fa781bc5 100644
--- a/couchpotato/core/notifications/pushalot/__init__.py
+++ b/couchpotato/core/notifications/pushalot.py
@@ -1,8 +1,46 @@
-from .main import Pushalot
+import traceback
+
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
-def start():
- return Pushalot()
+log = CPLog(__name__)
+
+autoload = 'Pushalot'
+
+
+class Pushalot(Notification):
+
+ urls = {
+ 'api': 'https://pushalot.com/api/sendmessage'
+ }
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ data = {
+ 'AuthorizationToken': self.conf('auth_token'),
+ 'Title': self.default_title,
+ 'Body': toUnicode(message),
+ 'IsImportant': self.conf('important'),
+ 'IsSilent': self.conf('silent'),
+ 'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
+ 'Source': toUnicode(self.default_title)
+ }
+
+ headers = {
+ 'Content-type': 'application/x-www-form-urlencoded'
+ }
+
+ try:
+ self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
+ return True
+ except:
+ log.error('PushAlot failed: %s', traceback.format_exc())
+
+ return False
+
config = [{
'name': 'pushalot',
diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py
deleted file mode 100644
index 306ee1d1..00000000
--- a/couchpotato/core/notifications/pushalot/main.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-import traceback
-
-log = CPLog(__name__)
-
-
-class Pushalot(Notification):
-
- urls = {
- 'api': 'https://pushalot.com/api/sendmessage'
- }
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- data = {
- 'AuthorizationToken': self.conf('auth_token'),
- 'Title': self.default_title,
- 'Body': toUnicode(message),
- 'IsImportant': self.conf('important'),
- 'IsSilent': self.conf('silent'),
- 'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
- 'Source': toUnicode(self.default_title)
- }
-
- headers = {
- 'Content-type': 'application/x-www-form-urlencoded'
- }
-
- try:
- self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
- return True
- except:
- log.error('PushAlot failed: %s', traceback.format_exc())
-
- return False
diff --git a/couchpotato/core/notifications/pushbullet/main.py b/couchpotato/core/notifications/pushbullet.py
similarity index 61%
rename from couchpotato/core/notifications/pushbullet/main.py
rename to couchpotato/core/notifications/pushbullet.py
index 487fb3aa..361294e4 100644
--- a/couchpotato/core/notifications/pushbullet/main.py
+++ b/couchpotato/core/notifications/pushbullet.py
@@ -1,16 +1,20 @@
+import base64
+import json
+
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
-import base64
-import json
+
log = CPLog(__name__)
+autoload = 'Pushbullet'
+
class Pushbullet(Notification):
- url = 'https://api.pushbullet.com/api/%s'
+ url = 'https://api.pushbullet.com/v2/%s'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
@@ -21,11 +25,7 @@ class Pushbullet(Notification):
# Get all the device IDs linked to this user
if not len(devices):
- response = self.request('devices')
- if not response:
- return False
-
- devices += [device.get('id') for device in response['devices']]
+ devices = [None]
successful = 0
for device in devices:
@@ -67,3 +67,39 @@ class Pushbullet(Notification):
log.debug(ex)
return None
+
+
+config = [{
+ 'name': 'pushbullet',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'pushbullet',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'api_key',
+ 'label': 'User API Key'
+ },
+ {
+ 'name': 'devices',
+ 'default': '',
+ 'advanced': True,
+ 'description': 'IDs of devices to send notifications to, empty = all devices'
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/pushbullet/__init__.py b/couchpotato/core/notifications/pushbullet/__init__.py
deleted file mode 100644
index c52e7781..00000000
--- a/couchpotato/core/notifications/pushbullet/__init__.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from .main import Pushbullet
-
-
-def start():
- return Pushbullet()
-
-config = [{
- 'name': 'pushbullet',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'pushbullet',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'api_key',
- 'label': 'User API Key'
- },
- {
- 'name': 'devices',
- 'default': '',
- 'advanced': True,
- 'description': 'IDs of devices to send notifications to, empty = all devices'
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/pushover.py b/couchpotato/core/notifications/pushover.py
new file mode 100644
index 00000000..d9ef226c
--- /dev/null
+++ b/couchpotato/core/notifications/pushover.py
@@ -0,0 +1,99 @@
+from httplib import HTTPSConnection
+
+from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
+from couchpotato.core.helpers.variable import getTitle
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+
+
+log = CPLog(__name__)
+
+autoload = 'Pushover'
+
+
+class Pushover(Notification):
+
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ http_handler = HTTPSConnection("api.pushover.net:443")
+
+ api_data = {
+ 'user': self.conf('user_key'),
+ 'token': self.conf('api_token'),
+ 'message': toUnicode(message),
+ 'priority': self.conf('priority'),
+ 'sound': self.conf('sound'),
+ }
+
+ if data and data.get('identifier'):
+ api_data.update({
+ 'url': toUnicode('http://www.imdb.com/title/%s/' % data['identifier']),
+ 'url_title': toUnicode('%s on IMDb' % getTitle(data)),
+ })
+
+ http_handler.request('POST', '/1/messages.json',
+ headers = {'Content-type': 'application/x-www-form-urlencoded'},
+ body = tryUrlencode(api_data)
+ )
+
+ response = http_handler.getresponse()
+ request_status = response.status
+
+ if request_status == 200:
+ log.info('Pushover notifications sent.')
+ return True
+ elif request_status == 401:
+ log.error('Pushover auth failed: %s', response.reason)
+ return False
+ else:
+ log.error('Pushover notification failed: %s', request_status)
+ return False
+
+
+config = [{
+ 'name': 'pushover',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'pushover',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'user_key',
+ 'description': 'Register on pushover.net to get one.'
+ },
+ {
+ 'name': 'api_token',
+ 'description': 'Register on pushover.net to get one.',
+ 'advanced': True,
+ 'default': 'YkxHMYDZp285L265L3IwH3LmzkTaCy',
+ },
+ {
+ 'name': 'priority',
+ 'default': 0,
+ 'type': 'dropdown',
+ 'values': [('Normal', 0), ('High', 1)],
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ {
+ 'name': 'sound',
+ 'advanced': True,
+ 'description': 'Define custom sound for Pushover alert.'
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/pushover/__init__.py b/couchpotato/core/notifications/pushover/__init__.py
deleted file mode 100644
index da764860..00000000
--- a/couchpotato/core/notifications/pushover/__init__.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from .main import Pushover
-
-
-def start():
- return Pushover()
-
-config = [{
- 'name': 'pushover',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'pushover',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'user_key',
- 'description': 'Register on pushover.net to get one.'
- },
- {
- 'name': 'priority',
- 'default': 0,
- 'type': 'dropdown',
- 'values': [('Normal', 0), ('High', 1)],
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py
deleted file mode 100644
index ba954a54..00000000
--- a/couchpotato/core/notifications/pushover/main.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
-from couchpotato.core.helpers.variable import getTitle
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from httplib import HTTPSConnection
-
-log = CPLog(__name__)
-
-
-class Pushover(Notification):
-
- app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- http_handler = HTTPSConnection("api.pushover.net:443")
-
- api_data = {
- 'user': self.conf('user_key'),
- 'token': self.app_token,
- 'message': toUnicode(message),
- 'priority': self.conf('priority'),
- }
-
- if data and data.get('library'):
- api_data.update({
- 'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
- 'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
- })
-
- http_handler.request('POST',
- "/1/messages.json",
- headers = {'Content-type': 'application/x-www-form-urlencoded'},
- body = tryUrlencode(api_data)
- )
-
- response = http_handler.getresponse()
- request_status = response.status
-
- if request_status == 200:
- log.info('Pushover notifications sent.')
- return True
- elif request_status == 401:
- log.error('Pushover auth failed: %s', response.reason)
- return False
- else:
- log.error('Pushover notification failed.')
- return False
diff --git a/couchpotato/core/notifications/synoindex/main.py b/couchpotato/core/notifications/synoindex.py
similarity index 64%
rename from couchpotato/core/notifications/synoindex/main.py
rename to couchpotato/core/notifications/synoindex.py
index ec7a64ef..b14e1a03 100644
--- a/couchpotato/core/notifications/synoindex/main.py
+++ b/couchpotato/core/notifications/synoindex.py
@@ -1,18 +1,24 @@
-from couchpotato.core.event import addEvent
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
import os
import subprocess
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+
+
log = CPLog(__name__)
+autoload = 'Synoindex'
+
class Synoindex(Notification):
index_path = '/usr/syno/bin/synoindex'
def __init__(self):
- super(Synoindex, self).__init__()
+ addApiView(self.testNotifyName(), self.test)
+
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = None):
@@ -35,3 +41,23 @@ class Synoindex(Notification):
return {
'success': os.path.isfile(self.index_path)
}
+
+
+config = [{
+ 'name': 'synoindex',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'synoindex',
+ 'description': 'Automaticly adds index to Synology Media Server.',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ }
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/synoindex/__init__.py b/couchpotato/core/notifications/synoindex/__init__.py
deleted file mode 100644
index 89d07b06..00000000
--- a/couchpotato/core/notifications/synoindex/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from .main import Synoindex
-
-
-def start():
- return Synoindex()
-
-config = [{
- 'name': 'synoindex',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'synoindex',
- 'description': 'Automaticly adds index to Synology Media Server.',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- }
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty.py
similarity index 54%
rename from couchpotato/core/notifications/toasty/main.py
rename to couchpotato/core/notifications/toasty.py
index ea1f2192..919a9d1d 100644
--- a/couchpotato/core/notifications/toasty/main.py
+++ b/couchpotato/core/notifications/toasty.py
@@ -1,10 +1,14 @@
+import traceback
+
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
-import traceback
+
log = CPLog(__name__)
+autoload = 'Toasty'
+
class Toasty(Notification):
@@ -29,3 +33,33 @@ class Toasty(Notification):
log.error('Toasty failed: %s', traceback.format_exc())
return False
+
+
+config = [{
+ 'name': 'toasty',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'toasty',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'api_key',
+ 'label': 'Device ID',
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/toasty/__init__.py b/couchpotato/core/notifications/toasty/__init__.py
deleted file mode 100644
index 31e055a0..00000000
--- a/couchpotato/core/notifications/toasty/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from .main import Toasty
-
-
-def start():
- return Toasty()
-
-config = [{
- 'name': 'toasty',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'toasty',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'api_key',
- 'label': 'Device ID',
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt.py
similarity index 63%
rename from couchpotato/core/notifications/trakt/main.py
rename to couchpotato/core/notifications/trakt.py
index 399f76d8..8f35deab 100644
--- a/couchpotato/core/notifications/trakt/main.py
+++ b/couchpotato/core/notifications/trakt.py
@@ -1,8 +1,11 @@
+from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
+autoload = 'Trakt'
+
class Trakt(Notification):
@@ -35,9 +38,9 @@ class Trakt(Notification):
'username': self.conf('automation_username'),
'password': self.conf('automation_password'),
'movies': [{
- 'imdb_id': data['library']['identifier'],
- 'title': data['library']['titles'][0]['title'],
- 'year': data['library']['year']
+ 'imdb_id': data['identifier'],
+ 'title': getTitle(data),
+ 'year': data['info']['year']
}] if data else []
}
@@ -61,3 +64,30 @@ class Trakt(Notification):
log.error('Failed to call trakt, check your login.')
return False
+
+
+config = [{
+ 'name': 'trakt',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'trakt',
+ 'label': 'Trakt',
+ 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings',
+ 'options': [
+ {
+ 'name': 'notification_enabled',
+ 'default': False,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'remove_watchlist_enabled',
+ 'label': 'Remove from watchlist',
+ 'default': False,
+ 'type': 'bool',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py
deleted file mode 100644
index 20e2e3f9..00000000
--- a/couchpotato/core/notifications/trakt/__init__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from .main import Trakt
-
-
-def start():
- return Trakt()
-
-config = [{
- 'name': 'trakt',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'trakt',
- 'label': 'Trakt',
- 'description': 'add movies to your collection once downloaded. Fill in your username and password in the Automation Trakt settings',
- 'options': [
- {
- 'name': 'notification_enabled',
- 'default': False,
- 'type': 'enabler',
- },
- {
- 'name': 'remove_watchlist_enabled',
- 'label': 'Remove from watchlist',
- 'default': False,
- 'type': 'bool',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/twitter/__init__.py b/couchpotato/core/notifications/twitter/__init__.py
index 1b9c7699..b6b42bb5 100644
--- a/couchpotato/core/notifications/twitter/__init__.py
+++ b/couchpotato/core/notifications/twitter/__init__.py
@@ -1,7 +1,7 @@
from .main import Twitter
-def start():
+def autoload():
return Twitter()
config = [{
diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py
index 559c830f..0d02191e 100644
--- a/couchpotato/core/notifications/twitter/main.py
+++ b/couchpotato/core/notifications/twitter/main.py
@@ -1,3 +1,5 @@
+from urlparse import parse_qsl
+
from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
@@ -5,9 +7,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from pytwitter import Api
-from urlparse import parse_qsl
import oauth2
+
log = CPLog(__name__)
diff --git a/couchpotato/core/notifications/twitter/static/twitter.js b/couchpotato/core/notifications/twitter/static/twitter.js
index 2c4e6e31..75c96a8e 100644
--- a/couchpotato/core/notifications/twitter/static/twitter.js
+++ b/couchpotato/core/notifications/twitter/static/twitter.js
@@ -2,7 +2,7 @@ var TwitterNotification = new Class({
initialize: function(){
var self = this;
- App.addEvent('load', self.addRegisterButton.bind(self));
+ App.addEvent('loadSettings', self.addRegisterButton.bind(self));
},
addRegisterButton: function(){
@@ -59,7 +59,7 @@ var TwitterNotification = new Class({
).inject(fieldset.getElement('.test_button'), 'before');
})
- },
+ }
});
diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc.py
old mode 100755
new mode 100644
similarity index 68%
rename from couchpotato/core/notifications/xbmc/main.py
rename to couchpotato/core/notifications/xbmc.py
index bfda85e1..8dbf936b
--- a/couchpotato/core/notifications/xbmc/main.py
+++ b/couchpotato/core/notifications/xbmc.py
@@ -1,16 +1,20 @@
-from couchpotato.core.helpers.variable import splitString
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
import base64
import json
import socket
import traceback
import urllib
+
+from couchpotato.core.helpers.variable import splitString, getTitle
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
import requests
from requests.packages.urllib3.exceptions import MaxRetryError
+
log = CPLog(__name__)
+autoload = 'XBMC'
+
class XBMC(Notification):
@@ -32,7 +36,7 @@ class XBMC(Notification):
if self.use_json_notifications.get(host):
calls = [
- ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
+ ('GUI.ShowNotification', None, {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
]
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
@@ -40,7 +44,7 @@ class XBMC(Notification):
if not self.conf('force_full_scan') and (self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0])):
param = {'directory': data['destination_dir']}
- calls.append(('VideoLibrary.Scan', param))
+ calls.append(('VideoLibrary.Scan', None, param))
max_successful += len(calls)
response = self.request(host, calls)
@@ -48,7 +52,7 @@ class XBMC(Notification):
response = self.notifyXBMCnoJSON(host, {'title': self.default_title, 'message': message})
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
- response += self.request(host, [('VideoLibrary.Scan', {})])
+ response += self.request(host, [('VideoLibrary.Scan', None, {})])
max_successful += 1
max_successful += 1
@@ -71,7 +75,7 @@ class XBMC(Notification):
# XBMC JSON-RPC version request
response = self.request(host, [
- ('JSONRPC.Version', {})
+ ('JSONRPC.Version', None, {})
])
for result in response:
if result.get('result') and type(result['result']['version']).__name__ == 'int':
@@ -85,14 +89,14 @@ class XBMC(Notification):
self.use_json_notifications[host] = False
# send the text message
- resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
- for result in resp:
- if result.get('result') and result['result'] == 'OK':
+ resp = self.notifyXBMCnoJSON(host, {'title': self.default_title, 'message': message})
+ for r in resp:
+ if r.get('result') and r['result'] == 'OK':
log.debug('Message delivered successfully!')
success = True
break
- elif result.get('error'):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
+ elif r.get('error'):
+ log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code']))
break
elif result.get('result') and type(result['result']['version']).__name__ == 'dict':
@@ -108,14 +112,14 @@ class XBMC(Notification):
self.use_json_notifications[host] = True
# send the text message
- resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
- for result in resp:
- if result.get('result') and result['result'] == 'OK':
+ resp = self.request(host, [('GUI.ShowNotification', None, {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
+ for r in resp:
+ if r.get('result') and r['result'] == 'OK':
log.debug('Message delivered successfully!')
success = True
break
- elif result.get('error'):
- log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
+ elif r.get('error'):
+ log.error('XBMC error; %s: %s (%s)', (r['id'], r['error']['message'], r['error']['code']))
break
# error getting version info (we do have contact with XBMC though)
@@ -131,7 +135,7 @@ class XBMC(Notification):
server = 'http://%s/xbmcCmds/' % host
# Notification(title, message [, timeout , image])
- cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
+ cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(getTitle(data)), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :)
@@ -180,12 +184,13 @@ class XBMC(Notification):
data = []
for req in do_requests:
- method, kwargs = req
+ method, id, kwargs = req
+
data.append({
'method': method,
'params': kwargs,
'jsonrpc': '2.0',
- 'id': method,
+ 'id': id if id else method,
})
data = json.dumps(data)
@@ -210,3 +215,66 @@ class XBMC(Notification):
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
+
+config = [{
+ 'name': 'xbmc',
+ 'groups': [
+ {
+ 'tab': 'notifications',
+ 'list': 'notification_providers',
+ 'name': 'xbmc',
+ 'label': 'XBMC',
+ 'description': 'v11 (Eden), v12 (Frodo), v13 (Gotham)',
+ 'options': [
+ {
+ 'name': 'enabled',
+ 'default': 0,
+ 'type': 'enabler',
+ },
+ {
+ 'name': 'host',
+ 'default': 'localhost:8080',
+ },
+ {
+ 'name': 'username',
+ 'default': 'xbmc',
+ },
+ {
+ 'name': 'password',
+ 'default': '',
+ 'type': 'password',
+ },
+ {
+ 'name': 'only_first',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Only update the first host when movie snatched, useful for synced XBMC',
+ },
+ {
+ 'name': 'remote_dir_scan',
+ 'label': 'Remote Folder Scan',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': ('Only scan new movie folder at remote XBMC servers.', 'Useful if the XBMC path is different from the path CPS uses.'),
+ },
+ {
+ 'name': 'force_full_scan',
+ 'label': 'Always do a full scan',
+ 'default': 0,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': ('Do a full scan instead of only the new movie.', 'Useful if the XBMC path is different from the path CPS uses.'),
+ },
+ {
+ 'name': 'on_snatch',
+ 'default': False,
+ 'type': 'bool',
+ 'advanced': True,
+ 'description': 'Also send message when movie is snatched.',
+ },
+ ],
+ }
+ ],
+}]
diff --git a/couchpotato/core/notifications/xbmc/__init__.py b/couchpotato/core/notifications/xbmc/__init__.py
deleted file mode 100644
index 34fed632..00000000
--- a/couchpotato/core/notifications/xbmc/__init__.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from .main import XBMC
-
-
-def start():
- return XBMC()
-
-config = [{
- 'name': 'xbmc',
- 'groups': [
- {
- 'tab': 'notifications',
- 'list': 'notification_providers',
- 'name': 'xbmc',
- 'label': 'XBMC',
- 'description': 'v11 (Eden) and v12 (Frodo)',
- 'options': [
- {
- 'name': 'enabled',
- 'default': 0,
- 'type': 'enabler',
- },
- {
- 'name': 'host',
- 'default': 'localhost:8080',
- },
- {
- 'name': 'username',
- 'default': 'xbmc',
- },
- {
- 'name': 'password',
- 'default': '',
- 'type': 'password',
- },
- {
- 'name': 'only_first',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Only update the first host when movie snatched, useful for synced XBMC',
- },
- {
- 'name': 'remote_dir_scan',
- 'label': 'Remote Folder Scan',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
- },
- {
- 'name': 'force_full_scan',
- 'label': 'Always do a full scan',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Do a full scan instead of only the new movie. Useful if the XBMC path is different from the path CPS uses.',
- },
- {
- 'name': 'on_snatch',
- 'default': 0,
- 'type': 'bool',
- 'advanced': True,
- 'description': 'Also send message when movie is snatched.',
- },
- ],
- }
- ],
-}]
diff --git a/couchpotato/core/notifications/xmpp/main.py b/couchpotato/core/notifications/xmpp/main.py
deleted file mode 100644
index 0011e41c..00000000
--- a/couchpotato/core/notifications/xmpp/main.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from couchpotato.core.logger import CPLog
-from couchpotato.core.notifications.base import Notification
-from time import sleep
-import traceback
-import xmpp
-
-log = CPLog(__name__)
-
-
-class Xmpp(Notification):
-
- def notify(self, message = '', data = None, listener = None):
- if not data: data = {}
-
- try:
- jid = xmpp.protocol.JID(self.conf('username'))
- client = xmpp.Client(jid.getDomain(), debug = [])
-
- # Connect
- if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
- log.error('XMPP failed: Connection to server failed.')
- return False
-
- # Authenticate
- if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
- log.error('XMPP failed: Failed to authenticate.')
- return False
-
- # Send message
- client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
-
- # Disconnect
- # some older servers will not send the message if you disconnect immediately after sending
- sleep(1)
- client.disconnect()
-
- log.info('XMPP notifications sent.')
- return True
-
- except:
- log.error('XMPP failed: %s', traceback.format_exc())
-
- return False
diff --git a/couchpotato/core/notifications/xmpp/__init__.py b/couchpotato/core/notifications/xmpp_.py
similarity index 52%
rename from couchpotato/core/notifications/xmpp/__init__.py
rename to couchpotato/core/notifications/xmpp_.py
index 0e3e14d9..f9916cd0 100644
--- a/couchpotato/core/notifications/xmpp/__init__.py
+++ b/couchpotato/core/notifications/xmpp_.py
@@ -1,8 +1,51 @@
-from .main import Xmpp
+from time import sleep
+import traceback
+
+from couchpotato.core.logger import CPLog
+from couchpotato.core.notifications.base import Notification
+import xmpp
-def start():
- return Xmpp()
+log = CPLog(__name__)
+
+autoload = 'Xmpp'
+
+
+class Xmpp(Notification):
+
+ def notify(self, message = '', data = None, listener = None):
+ if not data: data = {}
+
+ try:
+ jid = xmpp.protocol.JID(self.conf('username'))
+ client = xmpp.Client(jid.getDomain(), debug = [])
+
+ # Connect
+ if not client.connect(server = (self.conf('hostname'), self.conf('port'))):
+ log.error('XMPP failed: Connection to server failed.')
+ return False
+
+ # Authenticate
+ if not client.auth(jid.getNode(), self.conf('password'), resource = jid.getResource()):
+ log.error('XMPP failed: Failed to authenticate.')
+ return False
+
+ # Send message
+ client.send(xmpp.protocol.Message(to = self.conf('to'), body = message, typ = 'chat'))
+
+ # Disconnect
+ # some older servers will not send the message if you disconnect immediately after sending
+ sleep(1)
+ client.disconnect()
+
+ log.info('XMPP notifications sent.')
+ return True
+
+ except:
+ log.error('XMPP failed: %s', traceback.format_exc())
+
+ return False
+
config = [{
'name': 'xmpp',
diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation.py
similarity index 54%
rename from couchpotato/core/plugins/automation/__init__.py
rename to couchpotato/core/plugins/automation.py
index 482a0090..39d7c9e7 100644
--- a/couchpotato/core/plugins/automation/__init__.py
+++ b/couchpotato/core/plugins/automation.py
@@ -1,8 +1,55 @@
-from .main import Automation
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+
+log = CPLog(__name__)
+
+autoload = 'Automation'
-def start():
- return Automation()
+class Automation(Plugin):
+
+ def __init__(self):
+
+ addEvent('app.load', self.setCrons)
+
+ if not Env.get('dev'):
+ addEvent('app.load', self.addMovies)
+
+ addEvent('setting.save.automation.hour.after', self.setCrons)
+
+ def setCrons(self):
+ fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
+
+ def addMovies(self):
+
+ movies = fireEvent('automation.get_movies', merge = True)
+ movie_ids = []
+
+ for imdb_id in movies:
+
+ if self.shuttingDown():
+ break
+
+ prop_name = 'automation.added.%s' % imdb_id
+ added = Env.prop(prop_name, default = False)
+ if not added:
+ added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_after = True, single = True)
+ if added_movie:
+ movie_ids.append(added_movie['_id'])
+ Env.prop(prop_name, True)
+
+ for movie_id in movie_ids:
+
+ if self.shuttingDown():
+ break
+
+ movie_dict = fireEvent('media.get', movie_id, single = True)
+ fireEvent('movie.searcher.single', movie_dict)
+
+ return True
+
config = [{
'name': 'automation',
diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py
deleted file mode 100644
index 2edcd3be..00000000
--- a/couchpotato/core/plugins/automation/main.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.environment import Env
-
-log = CPLog(__name__)
-
-
-class Automation(Plugin):
-
- def __init__(self):
-
- addEvent('app.load', self.setCrons)
-
- if not Env.get('dev'):
- addEvent('app.load', self.addMovies)
-
- addEvent('setting.save.automation.hour.after', self.setCrons)
-
- def setCrons(self):
- fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
-
- def addMovies(self):
-
- movies = fireEvent('automation.get_movies', merge = True)
- movie_ids = []
-
- for imdb_id in movies:
-
- if self.shuttingDown():
- break
-
- prop_name = 'automation.added.%s' % imdb_id
- added = Env.prop(prop_name, default = False)
- if not added:
- added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
- if added_movie:
- movie_ids.append(added_movie['id'])
- Env.prop(prop_name, True)
-
- for movie_id in movie_ids:
-
- if self.shuttingDown():
- break
-
- movie_dict = fireEvent('media.get', movie_id, single = True)
- fireEvent('movie.searcher.single', movie_dict)
-
- return True
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index d7487a10..bc66123f 100644
--- a/couchpotato/core/plugins/base.py
+++ b/couchpotato/core/plugins/base.py
@@ -1,14 +1,4 @@
-from couchpotato.core.event import fireEvent, addEvent
-from couchpotato.core.helpers.encoding import ss, toSafeString, \
- toUnicode, sp
-from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt
-from couchpotato.core.logger import CPLog
-from couchpotato.environment import Env
-import requests
-from requests.packages.urllib3 import Timeout
-from requests.packages.urllib3.exceptions import MaxRetryError
-from tornado import template
-from tornado.web import StaticFileHandler
+from urllib import quote
from urlparse import urlparse
import glob
import inspect
@@ -16,7 +6,19 @@ import os.path
import re
import time
import traceback
-import urllib2
+
+from couchpotato.core.event import fireEvent, addEvent
+from couchpotato.core.helpers.encoding import ss, toSafeString, \
+ toUnicode, sp
+from couchpotato.core.helpers.variable import getExt, md5, isLocalIP, scanForPassword, tryInt, getIdentifier
+from couchpotato.core.logger import CPLog
+from couchpotato.environment import Env
+import requests
+from requests.packages.urllib3 import Timeout
+from requests.packages.urllib3.exceptions import MaxRetryError
+from tornado import template
+from tornado.web import StaticFileHandler
+
log = CPLog(__name__)
@@ -24,6 +26,7 @@ log = CPLog(__name__)
class Plugin(object):
_class_name = None
+ _database = None
plugin_path = None
enabled_option = 'enabled'
@@ -37,10 +40,9 @@ class Plugin(object):
http_time_between_calls = 0
http_failed_request = {}
http_failed_disabled = {}
- http_opener = requests.Session()
- def __new__(typ, *args, **kwargs):
- new_plugin = super(Plugin, typ).__new__(typ)
+ def __new__(cls, *args, **kwargs):
+ new_plugin = super(Plugin, cls).__new__(cls)
new_plugin.registerPlugin()
return new_plugin
@@ -53,6 +55,17 @@ class Plugin(object):
if self.auto_register_static:
self.registerStatic(inspect.getfile(self.__class__))
+ # Setup database
+ if self._database:
+ addEvent('database.setup', self.databaseSetup)
+
+ def databaseSetup(self):
+
+ for index_name in self._database:
+ klass = self._database[index_name]
+
+ fireEvent('database.setup_index', index_name, klass)
+
def conf(self, attr, value = None, default = None, section = None):
class_name = self.getName().lower().split(':')[0].lower()
return Env.setting(attr, section = section if section else class_name, value = value, default = default)
@@ -98,7 +111,7 @@ class Plugin(object):
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def createFile(self, path, content, binary = False):
- path = ss(path)
+ path = sp(path)
self.makeDir(os.path.dirname(path))
@@ -116,7 +129,7 @@ class Plugin(object):
os.remove(path)
def makeDir(self, path):
- path = ss(path)
+ path = sp(path)
try:
if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder'))
@@ -126,9 +139,35 @@ class Plugin(object):
return False
+ def deleteEmptyFolder(self, folder, show_error = True, only_clean = None):
+ folder = sp(folder)
+
+ for item in os.listdir(folder):
+ full_folder = os.path.join(folder, item)
+
+ if not only_clean or (item in only_clean and os.path.isdir(full_folder)):
+
+ for root, dirs, files in os.walk(full_folder):
+
+ for dir_name in dirs:
+ full_path = os.path.join(root, dir_name)
+
+ if len(os.listdir(full_path)) == 0:
+ try:
+ os.rmdir(full_path)
+ except:
+ if show_error:
+ log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
+
+ try:
+ os.rmdir(folder)
+ except:
+ if show_error:
+ log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
+
# http request
def urlopen(self, url, timeout = 30, data = None, headers = None, files = None, show_error = True):
- url = urllib2.quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
+ url = quote(ss(url), safe = "%/:=&?~#+!$,;'@()*[]")
if not headers: headers = {}
if not data: data = {}
@@ -144,7 +183,7 @@ class Plugin(object):
headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
- r = self.http_opener
+ r = Env.get('http_opener')
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
@@ -166,11 +205,12 @@ class Plugin(object):
'data': data if len(data) > 0 else None,
'timeout': timeout,
'files': files,
+ 'verify': False, #verify_ssl, Disable for now as to many wrongly implemented certificates..
}
method = 'post' if len(data) > 0 or files else 'get'
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data'))
- response = r.request(method, url, verify = False, **kwargs)
+ response = r.request(method, url, **kwargs)
if response.status_code == requests.codes.ok:
data = response.content
@@ -223,7 +263,7 @@ class Plugin(object):
def afterCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__), False)
- def doShutdown(self):
+ def doShutdown(self, *args, **kwargs):
self.shuttingDown(True)
return True
@@ -291,19 +331,22 @@ class Plugin(object):
if name_password:
release_name, password = name_password
tag += '{{%s}}' % password
+ elif data.get('password'):
+ tag += '{{%s}}' % data.get('password')
- max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames
+ max_length = 127 - len(tag) # Some filesystems don't support 128+ long filenames
return '%s%s' % (toSafeString(toUnicode(release_name)[:max_length]), tag)
def createFileName(self, data, filedata, media):
- name = sp(os.path.join(self.createNzbName(data, media)))
+ name = self.createNzbName(data, media)
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('protocol'))
def cpTag(self, media):
if Env.setting('enabled', 'renamer'):
- return '.cp(' + media['library'].get('identifier') + ')' if media['library'].get('identifier') else ''
+ identifier = getIdentifier(media)
+ return '.cp(' + identifier + ')' if identifier else ''
return ''
@@ -311,6 +354,7 @@ class Plugin(object):
now = time.time()
file_too_new = False
+ file_time = []
for cur_file in files:
# File got removed while checking
diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser.py
similarity index 95%
rename from couchpotato/core/plugins/browser/main.py
rename to couchpotato/core/plugins/browser.py
index 956a7680..013a4823 100644
--- a/couchpotato/core/plugins/browser/main.py
+++ b/couchpotato/core/plugins/browser.py
@@ -1,11 +1,14 @@
-from couchpotato.api import addApiView
-from couchpotato.core.helpers.variable import getUserDir
-from couchpotato.core.plugins.base import Plugin
import ctypes
import os
import string
+
+from couchpotato.api import addApiView
+from couchpotato.core.helpers.encoding import sp
+from couchpotato.core.helpers.variable import getUserDir
+from couchpotato.core.plugins.base import Plugin
import six
+
if os.name == 'nt':
import imp
try:
@@ -15,7 +18,11 @@ if os.name == 'nt':
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
else:
- import win32file #@UnresolvedImport
+ # noinspection PyUnresolvedReferences
+ import win32file
+
+autoload = 'FileBrowser'
+
class FileBrowser(Plugin):
@@ -44,6 +51,7 @@ class FileBrowser(Plugin):
path = '/'
dirs = []
+ path = sp(path)
for f in os.listdir(path):
p = os.path.join(path, f)
if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
diff --git a/couchpotato/core/plugins/browser/__init__.py b/couchpotato/core/plugins/browser/__init__.py
deleted file mode 100644
index fae50657..00000000
--- a/couchpotato/core/plugins/browser/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .main import FileBrowser
-
-
-def start():
- return FileBrowser()
-
-config = []
diff --git a/couchpotato/core/plugins/category/__init__.py b/couchpotato/core/plugins/category/__init__.py
index dcdae90b..d147092f 100644
--- a/couchpotato/core/plugins/category/__init__.py
+++ b/couchpotato/core/plugins/category/__init__.py
@@ -1,7 +1,5 @@
from .main import CategoryPlugin
-def start():
+def autoload():
return CategoryPlugin()
-
-config = []
diff --git a/couchpotato/core/plugins/category/index.py b/couchpotato/core/plugins/category/index.py
new file mode 100644
index 00000000..6445de3c
--- /dev/null
+++ b/couchpotato/core/plugins/category/index.py
@@ -0,0 +1,31 @@
+from CodernityDB.tree_index import TreeBasedIndex
+
+
+class CategoryIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = 'i'
+ super(CategoryIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return key
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'category':
+ return data.get('order', -99), None
+
+
+class CategoryMediaIndex(TreeBasedIndex):
+ _version = 1
+
+ def __init__(self, *args, **kwargs):
+ kwargs['key_format'] = '32s'
+ super(CategoryMediaIndex, self).__init__(*args, **kwargs)
+
+ def make_key(self, key):
+ return str(key)
+
+ def make_key_value(self, data):
+ if data.get('_t') == 'media' and data.get('category_id'):
+ return str(data.get('category_id')), None
diff --git a/couchpotato/core/plugins/category/main.py b/couchpotato/core/plugins/category/main.py
index c7abaee4..a0852cc1 100644
--- a/couchpotato/core/plugins/category/main.py
+++ b/couchpotato/core/plugins/category/main.py
@@ -1,20 +1,25 @@
import traceback
-from couchpotato import get_session
+
+from couchpotato import get_db
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Media, Category
+from .index import CategoryIndex, CategoryMediaIndex
+
log = CPLog(__name__)
class CategoryPlugin(Plugin):
- def __init__(self):
- addEvent('category.all', self.all)
+ _database = {
+ 'category': CategoryIndex,
+ 'category_media': CategoryMediaIndex,
+ }
+ def __init__(self):
addApiView('category.save', self.save)
addApiView('category.save_order', self.saveOrder)
addApiView('category.delete', self.delete)
@@ -26,54 +31,53 @@ class CategoryPlugin(Plugin):
}"""}
})
+ addEvent('category.all', self.all)
+
def allView(self, **kwargs):
return {
'success': True,
- 'list': self.all()
+ 'categories': self.all()
}
def all(self):
- db = get_session()
- categories = db.query(Category).all()
+ db = get_db()
+ categories = db.all('category', with_doc = True)
- temp = []
- for category in categories:
- temp.append(category.to_dict())
-
- return temp
+ return [x['doc'] for x in categories]
def save(self, **kwargs):
try:
- db = get_session()
+ db = get_db()
- c = db.query(Category).filter_by(id = kwargs.get('id')).first()
- if not c:
- c = Category()
- db.add(c)
+ category = {
+ '_t': 'category',
+ 'order': kwargs.get('order', 999),
+ 'label': toUnicode(kwargs.get('label', '')),
+ 'ignored': toUnicode(kwargs.get('ignored', '')),
+ 'preferred': toUnicode(kwargs.get('preferred', '')),
+ 'required': toUnicode(kwargs.get('required', '')),
+ 'destination': toUnicode(kwargs.get('destination', '')),
+ }
- c.order = kwargs.get('order', c.order if c.order else 0)
- c.label = toUnicode(kwargs.get('label', ''))
- c.ignored = toUnicode(kwargs.get('ignored', ''))
- c.preferred = toUnicode(kwargs.get('preferred', ''))
- c.required = toUnicode(kwargs.get('required', ''))
- c.destination = toUnicode(kwargs.get('destination', ''))
+ try:
+ c = db.get('id', kwargs.get('id'))
+ category['order'] = c.get('order', category['order'])
+ c.update(category)
- db.commit()
-
- category_dict = c.to_dict()
+ db.update(c)
+ except:
+ c = db.insert(category)
+ c.update(category)
return {
'success': True,
- 'category': category_dict
+ 'category': c
}
except:
log.error('Failed: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
return {
'success': False,
@@ -83,25 +87,21 @@ class CategoryPlugin(Plugin):
def saveOrder(self, **kwargs):
try:
- db = get_session()
+ db = get_db()
order = 0
for category_id in kwargs.get('ids', []):
- c = db.query(Category).filter_by(id = category_id).first()
- c.order = order
+ c = db.get('id', category_id)
+ c['order'] = order
+ db.update(c)
order += 1
- db.commit()
-
return {
'success': True
}
except:
log.error('Failed: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
return {
'success': False
@@ -110,21 +110,20 @@ class CategoryPlugin(Plugin):
def delete(self, id = None, **kwargs):
try:
- db = get_session()
+ db = get_db()
success = False
message = ''
try:
- c = db.query(Category).filter_by(id = id).first()
+ c = db.get('id', id)
db.delete(c)
- db.commit()
# Force defaults on all empty category movies
self.removeFromMovie(id)
success = True
- except Exception as e:
- message = log.error('Failed deleting category: %s', e)
+ except:
+ message = log.error('Failed deleting category: %s', traceback.format_exc())
return {
'success': success,
@@ -132,9 +131,6 @@ class CategoryPlugin(Plugin):
}
except:
log.error('Failed: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
return {
'success': False
@@ -143,15 +139,12 @@ class CategoryPlugin(Plugin):
def removeFromMovie(self, category_id):
try:
- db = get_session()
- movies = db.query(Media).filter(Media.category_id == category_id).all()
+ db = get_db()
+ movies = [x['doc'] for x in db.get_many('category_media', category_id, with_doc = True)]
if len(movies) > 0:
for movie in movies:
- movie.category_id = None
- db.commit()
+ movie['category_id'] = None
+ db.update(movie)
except:
log.error('Failed: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
diff --git a/couchpotato/core/plugins/category/static/category.css b/couchpotato/core/plugins/category/static/category.css
index 0987c197..3218a79c 100644
--- a/couchpotato/core/plugins/category/static/category.css
+++ b/couchpotato/core/plugins/category/static/category.css
@@ -22,7 +22,7 @@
.category > .delete:hover {
opacity: 1;
}
-
+
.category .ctrlHolder:hover {
background: none;
}
@@ -69,7 +69,7 @@
}
#category_ordering li .handle {
- background: url('../../static/profile_plugin/handle.png') center;
+ background: url('../../images/handle.png') center;
width: 20px;
float: right;
}
@@ -79,4 +79,4 @@
float: right;
width: 250px;
margin: 0;
- }
\ No newline at end of file
+ }
diff --git a/couchpotato/core/plugins/category/static/category.js b/couchpotato/core/plugins/category/static/category.js
index 168b70de..6d160be1 100644
--- a/couchpotato/core/plugins/category/static/category.js
+++ b/couchpotato/core/plugins/category/static/category.js
@@ -3,13 +3,13 @@ var CategoryListBase = new Class({
initialize: function(){
var self = this;
- App.addEvent('load', self.addSettings.bind(self));
+ App.addEvent('loadSettings', self.addSettings.bind(self));
},
setup: function(categories){
var self = this;
- self.categories = []
+ self.categories = [];
Array.each(categories, self.createCategory.bind(self));
},
@@ -17,7 +17,7 @@ var CategoryListBase = new Class({
addSettings: function(){
var self = this;
- self.settings = App.getPage('Settings')
+ self.settings = App.getPage('Settings');
self.settings.addEvent('create', function(){
var tab = self.settings.createSubTab('category', {
'label': 'Categories',
@@ -31,7 +31,7 @@ var CategoryListBase = new Class({
self.createList();
self.createOrdering();
- })
+ });
// Add categories in renamer
self.settings.addEvent('create', function(){
@@ -86,7 +86,7 @@ var CategoryListBase = new Class({
getCategory: function(id){
return this.categories.filter(function(category){
- return category.data.id == id
+ return category.data._id == id
}).pick()
},
@@ -97,9 +97,9 @@ var CategoryListBase = new Class({
createCategory: function(data){
var self = this;
- var data = data || {'id': randomString()}
- var category = new Category(data)
- self.categories.include(category)
+ var data = data || {'id': randomString()};
+ var category = new Category(data);
+ self.categories.include(category);
return category;
},
@@ -108,7 +108,7 @@ var CategoryListBase = new Class({
var self = this;
var category_list;
- var group = self.settings.createGroup({
+ self.settings.createGroup({
'label': 'Category ordering'
}).adopt(
new Element('.ctrlHolder#category_ordering').adopt(
@@ -118,10 +118,10 @@ var CategoryListBase = new Class({
'html': 'Change the order the categories are in the dropdown list.
First one will be default.'
})
)
- ).inject(self.content)
+ ).inject(self.content);
Array.each(self.categories, function(category){
- new Element('li', {'data-id': category.data.id}).adopt(
+ new Element('li', {'data-id': category.data._id}).adopt(
new Element('span.category_label', {
'text': category.data.label
}),
@@ -145,7 +145,7 @@ var CategoryListBase = new Class({
var ids = [];
- self.category_sortable.list.getElements('li').each(function(el, nr){
+ self.category_sortable.list.getElements('li').each(function(el){
ids.include(el.get('data-id'));
});
@@ -157,7 +157,7 @@ var CategoryListBase = new Class({
}
-})
+});
window.CategoryList = new CategoryListBase();
@@ -235,8 +235,6 @@ var Category = new Class({
if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){
- var data = self.getData();
-
Api.request('category.save', {
'data': self.getData(),
'useSpinner': true,
@@ -257,21 +255,19 @@ var Category = new Class({
getData: function(){
var self = this;
- var data = {
- 'id' : self.data.id,
+ return {
+ 'id' : self.data._id,
'label' : self.el.getElement('.category_label input').get('value'),
'required' : self.el.getElement('.category_required input').get('value'),
'preferred' : self.el.getElement('.category_preferred input').get('value'),
'ignored' : self.el.getElement('.category_ignored input').get('value'),
'destination': self.data.destination
}
-
- return data
},
del: function(){
var self = this;
-
+
if(self.data.label == undefined){
self.el.destroy();
return;
@@ -286,7 +282,7 @@ var Category = new Class({
(e).preventDefault();
Api.request('category.delete', {
'data': {
- 'id': self.data.id
+ 'id': self.data._id
},
'useSpinner': true,
'spinnerOptions': {
@@ -329,4 +325,4 @@ var Category = new Class({
return this.el
}
-});
\ No newline at end of file
+});
diff --git a/couchpotato/core/plugins/custom/main.py b/couchpotato/core/plugins/custom.py
similarity index 96%
rename from couchpotato/core/plugins/custom/main.py
rename to couchpotato/core/plugins/custom.py
index a15c915c..20b4c3f7 100644
--- a/couchpotato/core/plugins/custom/main.py
+++ b/couchpotato/core/plugins/custom.py
@@ -1,11 +1,15 @@
+import os
+
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-import os
+
log = CPLog(__name__)
+autoload = 'Custom'
+
class Custom(Plugin):
diff --git a/couchpotato/core/plugins/custom/__init__.py b/couchpotato/core/plugins/custom/__init__.py
deleted file mode 100644
index 20a39351..00000000
--- a/couchpotato/core/plugins/custom/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .main import Custom
-
-
-def start():
- return Custom()
-
-config = []
diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py
new file mode 100644
index 00000000..776f24ec
--- /dev/null
+++ b/couchpotato/core/plugins/dashboard.py
@@ -0,0 +1,106 @@
+from datetime import date
+import random as rndm
+import time
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import fireEvent
+from couchpotato.core.helpers.variable import splitString, tryInt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+
+
+log = CPLog(__name__)
+
+autoload = 'Dashboard'
+
+
+class Dashboard(Plugin):
+
+ def __init__(self):
+ addApiView('dashboard.soon', self.getSoonView)
+
+ def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
+
+ db = get_db()
+ now = time.time()
+
+ # Get profiles first, determine pre or post theater
+ profiles = fireEvent('profile.all', single = True)
+ pre_releases = fireEvent('quality.pre_releases', single = True)
+
+ # See what the profile contain and cache it
+ profile_pre = {}
+ for profile in profiles:
+ contains = {}
+ for q_identifier in profile.get('qualities', []):
+ contains['theater' if q_identifier in pre_releases else 'dvd'] = True
+
+ profile_pre[profile.get('_id')] = contains
+
+ # Add limit
+ limit = 12
+ if limit_offset:
+ splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
+ limit = tryInt(splt[0])
+
+ # Get all active medias
+ active_ids = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)]
+
+ medias = []
+ now_year = date.today().year
+
+ if len(active_ids) > 0:
+
+ # Order by title or randomize
+ if not random:
+ orders_ids = db.all('media_title')
+ active_ids = [x['_id'] for x in orders_ids if x['_id'] in active_ids]
+ else:
+ rndm.shuffle(active_ids)
+
+ for media_id in active_ids:
+ media = db.get('id', media_id)
+
+ pp = profile_pre.get(media['profile_id'])
+ if not pp: continue
+
+ eta = media['info'].get('release_date', {}) or {}
+ coming_soon = False
+
+ # Theater quality
+ if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True):
+ coming_soon = True
+ elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True):
+ coming_soon = True
+
+ if coming_soon:
+
+ # Don't list older movies
+ if ((not late and (media['info']['year'] >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
+ (late and (media['info']['year'] < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))):
+
+ add = True
+
+ # Check if it doesn't have any releases
+ if late:
+ media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
+
+ for release in media.get('releases'):
+ if release.get('status') in ['snatched', 'available', 'seeding', 'downloaded']:
+ add = False
+ break
+
+ if add:
+ medias.append(media)
+
+ if len(medias) >= limit:
+ break
+
+ return {
+ 'success': True,
+ 'empty': len(medias) == 0,
+ 'movies': medias,
+ }
+
+ getLateView = getSoonView
diff --git a/couchpotato/core/plugins/dashboard/__init__.py b/couchpotato/core/plugins/dashboard/__init__.py
deleted file mode 100644
index c43a44eb..00000000
--- a/couchpotato/core/plugins/dashboard/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .main import Dashboard
-
-
-def start():
- return Dashboard()
-
-config = []
diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py
deleted file mode 100644
index 3367ffb7..00000000
--- a/couchpotato/core/plugins/dashboard/main.py
+++ /dev/null
@@ -1,130 +0,0 @@
-from datetime import date
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import fireEvent
-from couchpotato.core.helpers.variable import splitString, tryInt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.settings.model import Media, Library, LibraryTitle, \
- Release
-from sqlalchemy.orm import joinedload_all
-from sqlalchemy.sql.expression import asc, or_
-import random as rndm
-import time
-
-log = CPLog(__name__)
-
-
-class Dashboard(Plugin):
-
- def __init__(self):
- addApiView('dashboard.soon', self.getSoonView)
-
- def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
-
- db = get_session()
- now = time.time()
-
- # Get profiles first, determine pre or post theater
- profiles = fireEvent('profile.all', single = True)
- qualities = fireEvent('quality.all', single = True)
- pre_releases = fireEvent('quality.pre_releases', single = True)
-
- id_pre = {}
- for quality in qualities:
- id_pre[quality.get('id')] = quality.get('identifier') in pre_releases
-
- # See what the profile contain and cache it
- profile_pre = {}
- for profile in profiles:
- contains = {}
- for profile_type in profile.get('types', []):
- contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True
-
- profile_pre[profile.get('id')] = contains
-
- # Add limit
- limit = 12
- if limit_offset:
- splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
- limit = tryInt(splt[0])
-
- # Get all active movies
- active_status, ignored_status = fireEvent('status.get', ['active', 'ignored'], single = True)
- q = db.query(Media) \
- .join(Library) \
- .outerjoin(Media.releases) \
- .filter(Media.status_id == active_status.get('id')) \
- .with_entities(Media.id, Media.profile_id, Library.info, Library.year) \
- .group_by(Media.id) \
- .filter(or_(Release.id == None, Release.status_id == ignored_status.get('id')))
-
- if not random:
- q = q.join(LibraryTitle) \
- .filter(LibraryTitle.default == True) \
- .order_by(asc(LibraryTitle.simple_title))
-
- active = q.all()
- movies = []
- now_year = date.today().year
-
- if len(active) > 0:
-
- # Do the shuffle
- if random:
- rndm.shuffle(active)
-
- movie_ids = []
- for movie in active:
- movie_id, profile_id, info, year = movie
-
- pp = profile_pre.get(profile_id)
- if not pp: continue
-
- eta = info.get('release_date', {}) or {}
- coming_soon = False
-
- # Theater quality
- if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, year, single = True):
- coming_soon = True
- elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, year, single = True):
- coming_soon = True
-
- if coming_soon:
-
- # Don't list older movies
- if ((not late and (year >= now_year-1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
- (late and ((year < now_year-1) or ((eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))))):
- movie_ids.append(movie_id)
-
- if len(movie_ids) >= limit:
- break
-
- if len(movie_ids) > 0:
-
- # Get all movie information
- movies_raw = db.query(Media) \
- .options(joinedload_all('library.titles')) \
- .options(joinedload_all('library.files')) \
- .options(joinedload_all('files')) \
- .filter(Media.id.in_(movie_ids)) \
- .all()
-
- # Create dict by movie id
- movie_dict = {}
- for movie in movies_raw:
- movie_dict[movie.id] = movie
-
- for movie_id in movie_ids:
- movies.append(movie_dict[movie_id].to_dict({
- 'library': {'titles': {}, 'files': {}},
- 'files': {},
- }))
-
- return {
- 'success': True,
- 'empty': len(movies) == 0,
- 'movies': movies,
- }
-
- getLateView = getSoonView
diff --git a/couchpotato/core/plugins/file.py b/couchpotato/core/plugins/file.py
new file mode 100644
index 00000000..51adf8c9
--- /dev/null
+++ b/couchpotato/core/plugins/file.py
@@ -0,0 +1,78 @@
+import os.path
+import traceback
+
+from couchpotato import get_db
+from couchpotato.api import addApiView
+from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.core.helpers.encoding import toUnicode
+from couchpotato.core.helpers.variable import md5, getExt
+from couchpotato.core.logger import CPLog
+from couchpotato.core.plugins.base import Plugin
+from couchpotato.environment import Env
+from tornado.web import StaticFileHandler
+
+
+log = CPLog(__name__)
+
+autoload = 'FileManager'
+
+
+class FileManager(Plugin):
+
+ def __init__(self):
+ addEvent('file.download', self.download)
+
+ addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
+ 'desc': 'Return a file from the cp_data/cache directory',
+ 'params': {
+ 'filename': {'desc': 'path/filename of the wanted file'}
+ },
+ 'return': {'type': 'file'}
+ })
+
+ fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24)
+
+ def cleanup(self):
+
+ # Wait a bit after starting before cleanup
+ log.debug('Cleaning up unused files')
+
+ try:
+ db = get_db()
+ cache_dir = Env.get('cache_dir')
+ medias = db.all('media', with_doc = True)
+
+ files = []
+ for media in medias:
+ file_dict = media['doc'].get('files', {})
+ for x in file_dict.keys():
+ files.extend(file_dict[x])
+
+ for f in os.listdir(cache_dir):
+ if os.path.splitext(f)[1] in ['.png', '.jpg', '.jpeg']:
+ file_path = os.path.join(cache_dir, f)
+ if toUnicode(file_path) not in files:
+ os.remove(file_path)
+ except:
+ log.error('Failed removing unused file: %s', traceback.format_exc())
+
+ def showCacheFile(self, route, **kwargs):
+ Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
+
+ def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
+ if not urlopen_kwargs: urlopen_kwargs = {}
+
+ if not dest: # to Cache
+ dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
+
+ if not overwrite and os.path.isfile(dest):
+ return dest
+
+ try:
+ filedata = self.urlopen(url, **urlopen_kwargs)
+ except:
+ log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
+ return False
+
+ self.createFile(dest, filedata, binary = True)
+ return dest
diff --git a/couchpotato/core/plugins/file/__init__.py b/couchpotato/core/plugins/file/__init__.py
deleted file mode 100644
index 3dced3d0..00000000
--- a/couchpotato/core/plugins/file/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from .main import FileManager
-
-
-def start():
- return FileManager()
-
-config = []
diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py
deleted file mode 100644
index c52a9801..00000000
--- a/couchpotato/core/plugins/file/main.py
+++ /dev/null
@@ -1,175 +0,0 @@
-from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent
-from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import md5, getExt
-from couchpotato.core.logger import CPLog
-from couchpotato.core.plugins.base import Plugin
-from couchpotato.core.plugins.scanner.main import Scanner
-from couchpotato.core.settings.model import FileType, File
-from couchpotato.environment import Env
-from tornado.web import StaticFileHandler
-import os.path
-import time
-import traceback
-
-log = CPLog(__name__)
-
-
-class FileManager(Plugin):
-
- def __init__(self):
- addEvent('file.add', self.add)
- addEvent('file.download', self.download)
- addEvent('file.types', self.getTypes)
-
- addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
- 'desc': 'Return a file from the cp_data/cache directory',
- 'params': {
- 'filename': {'desc': 'path/filename of the wanted file'}
- },
- 'return': {'type': 'file'}
- })
-
- addApiView('file.types', self.getTypesView, docs = {
- 'desc': 'Return a list of all the file types and their ids.',
- 'return': {'type': 'object', 'example': """{
- 'types': [
- {
- "identifier": "poster_original",
- "type": "image",
- "id": 1,
- "name": "Poster_original"
- },
- {
- "identifier": "poster",
- "type": "image",
- "id": 2,
- "name": "Poster"
- },
- etc
- ]
-}"""}
- })
-
- addEvent('app.load', self.cleanup)
- addEvent('app.load', self.init)
-
- def init(self):
-
- for type_tuple in Scanner.file_types.values():
- self.getType(type_tuple)
-
- def cleanup(self):
-
- # Wait a bit after starting before cleanup
- time.sleep(3)
- log.debug('Cleaning up unused files')
-
- try:
- db = get_session()
- for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
- for filename in walk_files:
- if os.path.splitext(filename)[1] in ['.png', '.jpg', '.jpeg']:
- file_path = os.path.join(root, filename)
- f = db.query(File).filter(File.path == toUnicode(file_path)).first()
- if not f:
- os.remove(file_path)
- except:
- log.error('Failed removing unused file: %s', traceback.format_exc())
- db.rollback()
- finally:
- db.close()
-
- def showCacheFile(self, route, **kwargs):
- Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
-
- def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
- if not urlopen_kwargs: urlopen_kwargs = {}
-
- if not dest: # to Cache
- dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
-
- if not overwrite and os.path.isfile(dest):
- return dest
-
- try:
- filedata = self.urlopen(url, **urlopen_kwargs)
- except:
- log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
- return False
-
- self.createFile(dest, filedata, binary = True)
- return dest
-
- def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = None):
- if not properties: properties = {}
-
- try:
- db = get_session()
- type_id = self.getType(type_tuple).get('id')
-
- f = db.query(File).filter(File.path == toUnicode(path)).first()
- if not f:
- f = File()
- db.add(f)
-
- f.path = toUnicode(path)
- f.part = part
- f.available = available
- f.type_id = type_id
-
- db.commit()
-
- file_dict = f.to_dict()
-
- return file_dict
- except:
- log.error('Failed adding file: %s, %s', (path, traceback.format_exc()))
- db.rollback()
- finally:
- db.close()
-
- def getType(self, type_tuple):
-
- try:
- db = get_session()
- type_type, type_identifier = type_tuple
-
- ft = db.query(FileType).filter_by(identifier = type_identifier).first()
- if not ft:
- ft = FileType(
- type = toUnicode(type_type),
- identifier = type_identifier,
- name = toUnicode(type_identifier[0].capitalize() + type_identifier[1:])
- )
- db.add(ft)
- db.commit()
-
- type_dict = ft.to_dict()
-
- return type_dict
- except:
- log.error('Failed getting type: %s, %s', (type_tuple, traceback.format_exc()))
- db.rollback()
- finally:
- db.close()
-
-
- def getTypes(self):
-
- db = get_session()
-
- results = db.query(FileType).all()
-
- types = []
- for type_object in results:
- types.append(type_object.to_dict())
-
- return types
-
- def getTypesView(self, **kwargs):
-
- return {
- 'types': self.getTypes()
- }
diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js
deleted file mode 100644
index 6659c882..00000000
--- a/couchpotato/core/plugins/file/static/file.js
+++ /dev/null
@@ -1,86 +0,0 @@
-var File = new Class({
-
- initialize: function(type, file){
- var self = this;
-
- if(!file){
- self.empty = true;
- self.el = new Element('div.empty_file.'+type);
- return
- }
-
- self.data = file;
- self.type = File.Type.get(file.type_id);
-
- self['create'+(self.type.type).capitalize()]()
-
- },
-
- createImage: function(){
- var self = this;
-
- var file_name = self.data.path.replace(/^.*[\\\/]/, '');
-
- self.el = new Element('div', {
- 'class': 'type_image ' + self.type.identifier,
- 'styles': {
- 'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
- }
- }).adopt(
- new Element('img', {
- 'src': Api.createUrl('file.cache') + file_name
- })
- )
- },
-
- toElement: function(){
- return this.el;
- }
-
-});
-
-var FileSelect = new Class({
-
- multiple: function(type, files, single){
-
- var results = files.filter(function(file){
- return file.type_id == File.Type.get(type).id;
- });
-
- if(single)
- return new File(type, results.pop());
-
- return results;
-
- },
-
- single: function(type, files){
- return this.multiple(type, files, true);
- }
-
-});
-window.File.Select = new FileSelect();
-
-var FileTypeBase = new Class({
-
- setup: function(types){
- var self = this;
-
- self.typesById = {};
- self.typesByKey = {};
- Object.each(types, function(type){
- self.typesByKey[type.identifier] = type;
- self.typesById[type.id] = type;
- });
-
- },
-
- get: function(identifier){
- if(typeOf(identifier) == 'number')
- return this.typesById[identifier]
- else
- return this.typesByKey[identifier]
- }
-
-});
-window.File.Type = new FileTypeBase();
diff --git a/couchpotato/core/plugins/log/__init__.py b/couchpotato/core/plugins/log/__init__.py
index f5d9d105..3760b567 100644
--- a/couchpotato/core/plugins/log/__init__.py
+++ b/couchpotato/core/plugins/log/__init__.py
@@ -1,7 +1,5 @@
from .main import Logging
-def start():
+def autoload():
return Logging()
-
-config = []
diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py
index 2f471586..003529b1 100644
--- a/couchpotato/core/plugins/log/main.py
+++ b/couchpotato/core/plugins/log/main.py
@@ -1,11 +1,14 @@
+import os
+import re
+import traceback
+
from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
-import os
-import traceback
+
log = CPLog(__name__)
@@ -20,7 +23,11 @@ class Logging(Plugin):
},
'return': {'type': 'object', 'example': """{
'success': True,
- 'log': string, //Log file
+ 'log': [{
+ 'time': '03-12 09:12:59',
+ 'type': 'INFO',
+ 'message': 'Log message'
+ }, ..], //Log file
'total': int, //Total log files available
}"""}
})
@@ -32,7 +39,11 @@ class Logging(Plugin):
},
'return': {'type': 'object', 'example': """{
'success': True,
- 'log': string, //Log file
+ 'log': [{
+ 'time': '03-12 09:12:59',
+ 'type': 'INFO',
+ 'message': 'Log message'
+ }, ..]
}"""}
})
addApiView('logging.clear', self.clear, docs = {
@@ -65,20 +76,22 @@ class Logging(Plugin):
if x is nr:
current_path = path
- log = ''
+ log_content = ''
if current_path:
f = open(current_path, 'r')
- log = f.read()
+ log_content = f.read()
+ logs = self.toList(log_content)
return {
'success': True,
- 'log': toUnicode(log),
+ 'log': logs,
'total': total,
}
- def partial(self, type = 'all', lines = 30, **kwargs):
+ def partial(self, type = 'all', lines = 30, offset = 0, **kwargs):
total_lines = tryInt(lines)
+ offset = tryInt(offset)
log_lines = []
@@ -91,28 +104,57 @@ class Logging(Plugin):
break
f = open(path, 'r')
- reversed_lines = toUnicode(f.read()).split('[0m\n')
- reversed_lines.reverse()
+ log_content = toUnicode(f.read())
+ raw_lines = self.toList(log_content)
+ raw_lines.reverse()
brk = False
- for line in reversed_lines:
+ for line in raw_lines:
- if type == 'all' or '%s ' % type.upper() in line:
+ if type == 'all' or line.get('type') == type.upper():
log_lines.append(line)
- if len(log_lines) >= total_lines:
+ if len(log_lines) >= (total_lines + offset):
brk = True
break
if brk:
break
+ log_lines = log_lines[offset:]
log_lines.reverse()
+
return {
'success': True,
- 'log': '[0m\n'.join(log_lines),
+ 'log': log_lines,
}
+ def toList(self, log_content = ''):
+
+ logs_raw = toUnicode(log_content).split('[0m\n')
+
+ logs = []
+ for log_line in logs_raw:
+ split = splitString(log_line, '\x1b')
+ if split:
+ try:
+ date, time, log_type = splitString(split[0], ' ')
+ timestamp = '%s %s' % (date, time)
+ except:
+ timestamp = 'UNKNOWN'
+ log_type = 'UNKNOWN'
+
+ message = ''.join(split[1]) if len(split) > 1 else split[0]
+ message = re.sub('\[\d+m\[', '[', message)
+
+ logs.append({
+ 'time': timestamp,
+ 'type': log_type,
+ 'message': message
+ })
+
+ return logs
+
def clear(self, **kwargs):
for x in range(0, 50):
diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css
index 524b588d..c7aace61 100644
--- a/couchpotato/core/plugins/log/static/log.css
+++ b/couchpotato/core/plugins/log/static/log.css
@@ -9,16 +9,21 @@
bottom: 0;
left: 0;
background: #4E5969;
+ z-index: 100;
}
.page.log .nav li {
display: inline-block;
padding: 5px 10px;
margin: 0;
+ }
+
+ .page.log .nav li.select,
+ .page.log .nav li.clear {
cursor: pointer;
}
- .page.log .nav li:hover:not(.active) {
+ .page.log .nav li:hover:not(.active):not(.filter) {
background: rgba(255, 255, 255, 0.1);
}
@@ -27,17 +32,30 @@
cursor: default;
background: rgba(255,255,255,.1);
}
-
+
@media all and (max-width: 480px) {
.page.log .nav {
font-size: 14px;
}
-
+
.page.log .nav li {
padding: 5px;
}
}
+ .page.log .nav li.hint {
+ text-align: center;
+ width: 400px;
+ left: 50%;
+ margin-left: -200px;
+ font-style: italic;
+ font-size: 11px;
+ position: absolute;
+ right: 20px;
+ opacity: .5;
+ bottom: 5px;
+ }
+
.page.log .loading {
text-align: center;
font-size: 20px;
@@ -47,31 +65,135 @@
.page.log .container {
padding: 30px 0 60px;
overflow: hidden;
-}
-
-.page.log .container span {
- float: left;
- width: 86%;
line-height: 150%;
- padding: 3px 0;
- border-top: 1px solid rgba(255, 255, 255, 0.2);
font-size: 11px;
- font-family: Lucida Console, Monaco, Nimbus Mono L;
+ color: #FFF;
}
- .page.log .container .error {
- color: #FFA4A4;
- white-space: pre-wrap;
+ .page.log .container select {
+ vertical-align: top;
}
- .page.log .container .debug { color: lightgrey; }
.page.log .container .time {
clear: both;
- width: 14%;
color: lightgrey;
- padding: 3px 0;
font-size: 10px;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ position: relative;
+ overflow: hidden;
+ padding: 0 3px;
+ font-family: Lucida Console, Monaco, Nimbus Mono L, monospace, serif;
+ }
+ .page.log .container .time.highlight {
+ background: rgba(255, 255, 255, 0.1);
+ }
+ .page.log .container .time span {
+ padding: 5px 0 3px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .page.log[data-filter=INFO] .error,
+ .page.log[data-filter=INFO] .debug,
+ .page.log[data-filter=ERROR] .debug,
+ .page.log[data-filter=ERROR] .info,
+ .page.log[data-filter=DEBUG] .info,
+ .page.log[data-filter=DEBUG] .error {
+ display: none;
+ }
+
+ .page.log .container .type {
+ margin-left: 10px;
}
- .page.log .container .time:last-child { display: none; }
+ .page.log .container .message {
+ float: right;
+ width: 86%;
+ white-space: pre-wrap;
+ }
+ .page.log .container .error { color: #FFA4A4; }
+ .page.log .container .debug span { opacity: .6; }
+
+.do_report {
+ position: absolute;
+ padding: 10px;
+}
+
+.page.log .report {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,.7);
+ left: 0;
+ top: 0;
+ z-index: 99999;
+ font-size: 14px;
+}
+
+ .page.log .report .button {
+ display: inline-block;
+ margin: 10px 0;
+ padding: 10px;
+ }
+
+ .page.log .report .bug {
+ width: 800px;
+ height: 80%;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin: 0 0 0 -400px;
+ transform: translate(0, -50%);
+ }
+
+ .page.log .report .bug textarea {
+ display: block;
+ width: 100%;
+ background: #FFF;
+ padding: 20px;
+ overflow: auto;
+ color: #666;
+ height: 70%;
+ font-size: 12px;
+ }
+
+.page.log .container .time ::-webkit-selection {
+ background-color: #000;
+ color: #FFF;
+}
+
+.page.log .container .time ::-moz-selection {
+ background-color: #000;
+ color: #FFF;
+}
+
+.page.log .container .time ::-ms-selection {
+ background-color: #000;
+ color: #FFF;
+}
+
+.page.log .container .time.highlight ::selection {
+ background-color: transparent;
+ color: inherit;
+}
+
+.page.log .container .time.highlight ::-webkit-selection {
+ background-color: transparent;
+ color: inherit;
+}
+
+.page.log .container .time.highlight ::-moz-selection {
+ background-color: transparent;
+ color: inherit;
+}
+
+.page.log .container .time.highlight ::-ms-selection {
+ background-color: transparent;
+ color: inherit;
+}
+
+.page.log .container .time.highlight ::selection {
+ background-color: transparent;
+ color: inherit;
+}
diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js
index 159bfeaa..11acb5ca 100644
--- a/couchpotato/core/plugins/log/static/log.js
+++ b/couchpotato/core/plugins/log/static/log.js
@@ -2,81 +2,295 @@ Page.Log = new Class({
Extends: PageBase,
+ order: 60,
name: 'log',
title: 'Show recent logs.',
has_tab: false,
- indexAction: function(){
+ log_items: [],
+ report_text: '\
+### Steps to reproduce:\n\
+1. ..\n\
+2. ..\n\
+\n\
+### Information:\n\
+Movie(s) I have this with: ...\n\
+Quality of the movie being searched: ...\n\
+Providers I use: ...\n\
+Version of CouchPotato: {version}\n\
+Running on: ...\n\
+\n\
+### Logs:\n\
+```\n{issue}```',
+
+ indexAction: function () {
var self = this;
self.getLogs(0);
},
- getLogs: function(nr){
+ getLogs: function (nr) {
var self = this;
- if(self.log) self.log.destroy();
+ if (self.log) self.log.destroy();
self.log = new Element('div.container.loading', {
- 'text': 'loading...'
+ 'text': 'loading...',
+ 'events': {
+ 'mouseup:relay(.time)': function(e){
+ self.showSelectionButton.delay(100, self, e);
+ }
+ }
}).inject(self.el);
Api.request('logging.get', {
'data': {
'nr': nr
},
- 'onComplete': function(json){
- self.log.set('html', self.addColors(json.log));
+ 'onComplete': function (json) {
+ self.log.set('text', '');
+ self.log_items = self.createLogElements(json.log);
+ self.log.adopt(self.log_items);
self.log.removeClass('loading');
- new Fx.Scroll(window, {'duration': 0}).toBottom();
+ var nav = new Element('ul.nav', {
+ 'events': {
+ 'click:relay(li.select)': function (e, el) {
+ self.getLogs(parseInt(el.get('text')) - 1);
+ }
+ }
+ });
- var nav = new Element('ul.nav').inject(self.log, 'top');
- for (var i = 0; i <= json.total; i++) {
- new Element('li', {
- 'text': i+1,
- 'class': nr == i ? 'active': '',
+ // Type selection
+ new Element('li.filter').grab(
+ new Element('select', {
'events': {
- 'click': function(e){
- self.getLogs(e.target.get('text')-1);
+ 'change': function () {
+ var type_filter = this.getSelected()[0].get('value');
+ self.el.set('data-filter', type_filter);
+ self.scrollToBottom();
}
}
- }).inject(nav);
- };
+ }).adopt(
+ new Element('option', {'value': 'ALL', 'text': 'Show all logs'}),
+ new Element('option', {'value': 'INFO', 'text': 'Show only INFO'}),
+ new Element('option', {'value': 'DEBUG', 'text': 'Show only DEBUG'}),
+ new Element('option', {'value': 'ERROR', 'text': 'Show only ERROR'})
+ )
+ ).inject(nav);
- new Element('li', {
+ // Selections
+ for (var i = 0; i <= json.total; i++) {
+ new Element('li', {
+ 'text': i + 1,
+ 'class': 'select ' + (nr == i ? 'active' : '')
+ }).inject(nav);
+ }
+
+ // Clear button
+ new Element('li.clear', {
'text': 'clear',
'events': {
- 'click': function(){
+ 'click': function () {
Api.request('logging.clear', {
- 'onComplete': function(){
+ 'onComplete': function () {
self.getLogs(0);
}
});
}
}
- }).inject(nav)
+ }).inject(nav);
+
+ // Hint
+ new Element('li.hint', {
+ 'text': 'Select multiple lines & report an issue'
+ }).inject(nav);
+
+ // Add to page
+ nav.inject(self.log, 'top');
+
+ self.scrollToBottom();
}
});
},
- addColors: function(text){
- var self = this;
+ createLogElements: function (logs) {
- text = text
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/\u001b\[31m/gi, '')
- .replace(/\u001b\[36m/gi, '')
- .replace(/\u001b\[33m/gi, '')
- .replace(/\u001b\[0m\n/gi, '