Compare commits

..

117 Commits

Author SHA1 Message Date
Ruud
d6522d8f38 One up installer 2012-10-27 18:49:44 +02:00
Ruud
78eab890e7 Merge branch 'refs/heads/develop' into desktop 2012-10-27 18:25:36 +02:00
Ruud
35c0356734 Let esky manage it's own restart 2012-10-27 18:25:27 +02:00
Ruud
1a56191f83 Don't unzip 2012-10-27 18:22:50 +02:00
Ruud
41c0f34d95 Properly restart 2012-10-27 18:22:40 +02:00
Ruud
37bf205d7a Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-10-27 11:56:57 +02:00
Ruud
b66af0b6c6 One up 2012-10-27 11:52:59 +02:00
Ruud
d1e798323c SAB error doesn't show anything. Add 1 more line 2012-10-26 23:50:59 +02:00
Ruud
7420785eaf Show branch on about page 2012-10-26 22:14:48 +02:00
Ruud
a86522a810 Don't download next when the release isn't found in downloaded. fix #924 2012-10-26 22:02:34 +02:00
Ruud
d636314971 Contributing.md 2012-10-26 14:53:35 +02:00
Ruud
e918e6b12f Check watchlist adds in automation plugin, not the providers. fix #838 2012-10-23 00:20:04 +02:00
Ruud
b71f003ad8 Use secure connections when available. 2012-10-22 22:22:52 +02:00
Ruud
a432ad4f5a Use new kat.ph url. fix #959
Also use https while we're at it.
2012-10-22 21:42:42 +02:00
Ruud
ac04121dd3 Just use q as name in imdbapi search. 2012-10-21 20:49:54 +02:00
Ruud
61a3a0386e Search after added all movies. fix #702 2012-10-21 20:02:24 +02:00
Ruud
7b1f17c062 Limit threads per event 2012-10-21 20:02:06 +02:00
Ruud
6f7b565103 Use multiple tag when renaming extras. fix #652 2012-10-21 18:07:14 +02:00
Ruud
4fb7467e97 Send 2 tweets when message is above 140 chars. fix #441 2012-10-21 16:14:11 +02:00
Ruud
bcdc633a5e Use urlopen for Prowl notifier. fix #932 2012-10-21 15:26:29 +02:00
Ruud
19f74e398f Commit before adding more info 2012-10-21 14:38:58 +02:00
Ruud
09f723bda5 Use jsonrpc for xbmc request. fix #927 #945 2012-10-21 13:37:45 +02:00
Ruud
fbeadb8d9e Don't show "add your first movie" when searching. fix #937 2012-10-21 12:11:37 +02:00
Ruud
5bda44d419 Add api for file types 2012-10-20 11:30:10 +02:00
Ruud
84eccbf9cf Chmod metadata. fix #928 2012-10-16 22:18:37 +02:00
Ruud
9ebc4dbf38 Reworked some download code. #924 2012-10-15 23:50:05 +02:00
Ruud
907f821e50 Some more logging for SABNZBd 2012-10-14 20:42:31 +02:00
Ruud
9dc1843f25 Also use data param on notifier 2012-10-14 20:42:12 +02:00
Ruud
ad0a1b1efe Pass all the renamed files when adding new release 2012-10-14 20:26:10 +02:00
Ruud
8bfad087e1 Add podnapisi to subtitle providers 2012-10-14 20:17:13 +02:00
Ruud
67c87444de Subliminal update 2012-10-14 17:58:09 +02:00
Ruud
4dfd8b4cd5 Don't show trailer 404 errors 2012-10-14 17:32:45 +02:00
Ruud
3ffc6e122e Double genres because of trailing spaces 2012-10-14 15:28:26 +02:00
Ruud
f204309ed7 Remove unused file before counting them 2012-10-14 14:37:02 +02:00
Ruud
d3ebe531d5 Don't use 2 events after rename 2012-10-14 14:13:10 +02:00
Ruud
6106fd4e82 Only scroll back to top when added 2012-10-13 21:19:56 +02:00
Ruud
981ba61458 Progress bars for manage updates 2012-10-13 21:17:42 +02:00
Ruud
a5534c4bd2 Create file types on load 2012-10-13 21:16:05 +02:00
Ruud
2cd887b70a Empty list styling 2012-10-13 12:00:47 +02:00
Ruud
19ddd03204 Improved movie year matching 2012-10-13 11:23:13 +02:00
Ruud
d0d9ac07a6 CSS gradient "fix" 2012-10-13 10:52:35 +02:00
Ruud
0773d6e6ad Merge branch 'develop' of https://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2012-10-12 23:35:15 +02:00
Ruud
1011e2e9b8 Show "how to" when wanted list is empty 2012-10-12 23:34:39 +02:00
Ruud
84f5dcc134 Wizard fix 2012-10-12 23:34:13 +02:00
clinton-hall
fef3eb1b84 Added replacement of uid and key for nzbsrus
Prevent people posting log files with their account information
2012-10-12 13:29:41 -07:00
Ruud
ef6d0e04c0 Use event value 2012-10-12 21:04:49 +02:00
Ruud
269e98b049 Removed all commented out db.close 2012-10-12 19:09:57 +02:00
Ruud
378d1ccd1c Check if db exists before loading plugins 2012-10-12 19:03:44 +02:00
Ruud
f3e3632dd3 Use less db.commits when adding quality and profiles. 2012-10-12 18:52:44 +02:00
Ruud
27635caa1d Simpler options for events 2012-10-12 18:50:57 +02:00
Ruud
4836a9ffdc Merge branch 'fix_scoring' of https://github.com/dersphere/CouchPotatoServer into dersphere-fix_scoring 2012-10-12 13:21:07 +02:00
Ruud
8874bd4e2b Wrongly assuming quality when no quality in the name. fix #901 2012-10-12 13:15:40 +02:00
Tristan Fischer
799b665f15 fix audio scoring
The elements of name_scores are compared to a lower-cased version of the release name so they need to be also lower cased.
2012-10-08 23:30:10 +02:00
Ruud
7f90135947 checksnatched debug code leftover. fix #892 2012-10-02 22:16:21 +02:00
Ruud
50a2bca459 Don't open releases when all are ignored 2012-09-30 17:34:09 +02:00
Ruud
a3b3b9c218 Don't use cached in_wanted when re-adding movie 2012-09-30 17:33:51 +02:00
Ruud
d38bd03422 Use finish by default when adding new type 2012-09-30 17:15:47 +02:00
Ruud
9184a97fcd Don't loop over releases when download doesn't support check snatched. fix #887 2012-09-30 15:56:51 +02:00
Ruud
ce0bf7b51a Standardized checking snatched 2012-09-28 13:10:58 +02:00
Ruud
151b100573 Merge branch 'develop' of https://github.com/clinton-hall/CouchPotatoServer into clinton-hall-develop 2012-09-28 08:35:32 +02:00
Ruud
0e23413069 Mark movie snatched on manual download 2012-09-27 08:39:07 +02:00
Ruud
2ac2b0ff06 Remove debug sleep 2012-09-26 21:12:14 +02:00
Ruud
86bf08cbd4 Add documentation to progress api 2012-09-26 21:01:48 +02:00
Ruud
ed0e54d64d Add force full search options to wanted list 2012-09-26 20:59:03 +02:00
Ruud
3da0b1a804 Let user know to report weird errors 2012-09-26 20:12:03 +02:00
Ruud
39c2567d5a Also listen to search ended per movie. 2012-09-26 20:11:19 +02:00
Ruud
95c5d16991 Don't check retention when it's 0 2012-09-26 19:35:59 +02:00
Ruud
08b450fc0a Remove trailer feature not implemented yet. 2012-09-26 17:10:34 +02:00
clinton-hall
dc63796e48 Added nzbname
previously defined in Downloader. I forgot to bring this across.
2012-09-26 17:12:19 +09:30
clinton-hall
c6cba2f6e5 Only request SABnzbd status once
Performs the checking of queue and history here. Requests delete of failed downlaods via download.remove event.
2012-09-26 16:59:36 +09:30
clinton-hall
ef945597d2 Removed checking of status results from here
getDownloadStatus is only called once from renamer and all results are passed back. Def remove is added so that renamer can request a failed downlaod to be deleted from SABnzbd if enabled.
2012-09-26 16:51:51 +09:30
clinton-hall
ba36c738c7 Added def remove
Allows renamer to request deletion of failed downloads
2012-09-26 16:46:10 +09:30
Ruud
3a3a4fb1f3 Cleanup javascript events on movie delete 2012-09-25 23:11:27 +02:00
Ruud
3fa352e7c8 Firefox fix for directory input 2012-09-25 19:02:28 +02:00
Ruud
08ef153bbf remove dropdown arrow 2012-09-25 16:03:31 +02:00
Ruud
7e3a6eeb83 Make sure top quality is always checked. 2012-09-25 15:47:50 +02:00
Ruud
24ad975917 Movie providers in their own subgroup 2012-09-25 15:09:35 +02:00
Ruud
952f29918e Hide git command when not using git
Re-order automation
2012-09-25 14:30:35 +02:00
Ruud
08ae51dbe6 Improved folder select 2012-09-25 14:22:48 +02:00
Ruud
d40ad1ddf2 Improved wizard 2012-09-25 12:43:22 +02:00
Ruud
0132012276 Hide ip change 2012-09-25 11:10:49 +02:00
Ruud
0f1e8eeff9 Make sure to always search for old movies 2012-09-25 10:43:46 +02:00
Ruud
8eee2af49b Renamer fileinput. fix #861 2012-09-24 22:23:15 +02:00
Ruud
3d26a53fbd Don't capitalize labels in settings 2012-09-24 21:59:36 +02:00
Ruud
d4600635e1 Use sudo in readme. Thanks jbillo 2012-09-24 17:35:13 +02:00
Ruud
6fc9d383de Add some error handling to sabnzbd statuscheck 2012-09-22 09:20:37 +02:00
Ruud
5776b2caad Convert torrent hash to uppercase 2012-09-22 09:05:44 +02:00
Ruud
f82e2a3e6e Limit sabnzbd history check. 2012-09-21 12:38:38 +02:00
Ruud
a5fa503970 Copy paste error. 2012-09-21 12:33:22 +02:00
Ruud
6f7d2caa9b Properly update release dates. 2012-09-21 12:16:36 +02:00
Ruud
c0012c9243 Properly use newer_than. fix #850 2012-09-20 11:25:38 +02:00
Ruud
5cc7250528 Image optimize 2012-09-20 09:16:26 +02:00
Ruud
aa1fa3eb9a Add description 2012-09-19 15:42:33 +02:00
Ruud
f474962225 Just check for synoindex file on test. 2012-09-19 15:31:27 +02:00
Ruud
0e2f8a612c Extract zip after build, for testing 2012-09-19 15:29:07 +02:00
Ruud
ad7de32e70 Send time with API requests 2012-09-19 13:23:40 +02:00
Ruud
d295b881af Use new CP url for api 2012-09-16 21:58:58 +02:00
Ruud
20f81d06c0 Send headers with CPAPI 2012-09-16 20:44:10 +02:00
Ruud
8fb24bb101 Couldn't test Synoindex. fix #814 2012-09-16 20:20:47 +02:00
Ruud
ce3efd3a3c Cleanup cache on startup 2012-09-16 20:02:15 +02:00
Ruud
ef7fc62c66 Download fanart en bigger poster when needed 2012-09-16 19:25:53 +02:00
Ruud
4be8d02cbb Added CP search & info provider 2012-09-16 17:47:14 +02:00
Ruud
7ce8a4fc45 Show proper date in update info 2012-09-16 13:21:01 +02:00
Ruud
465e7b2abc Merge branch 'refs/heads/develop' into desktop 2012-09-16 12:36:17 +02:00
Ruud
102a0177de Only use desktop updater callback on done and error 2012-09-16 12:34:24 +02:00
Ruud
578fb45785 Installer 1 up 2012-09-16 11:35:56 +02:00
Ruud
96995bbbe5 Merge branch 'refs/heads/develop' into desktop
Conflicts:
	version.py
2012-09-16 10:45:19 +02:00
Ruud
43e1adbff9 One up 2012-09-16 10:43:43 +02:00
Ruud
6e8b828cd4 Use proper object get 2012-09-16 10:11:24 +02:00
Ruud
5851f226b6 Add more disks to Windows root directory. 2012-09-16 10:11:04 +02:00
Ruud
4cfdafebbc Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:15:47 +02:00
Ruud
006163bd2b Don't show error when thetvdb failes 2012-09-14 13:15:18 +02:00
Ruud
b97acb8ef5 Merge branch 'refs/heads/develop' into desktop 2012-09-14 13:08:19 +02:00
Ruud
de6d686fe5 Add API version to CP calls 2012-09-14 12:44:26 +02:00
Ruud
45bb88460c Remove wrongly added movies. 2012-09-14 12:42:52 +02:00
Ruud
d2901bc68a Use thetvdb to check if the movie is a TV Show. 2012-09-11 21:32:36 +02:00
Ruud
2c0af15325 Torcache doesn't give back proper 404 2012-09-11 20:48:27 +02:00
277 changed files with 24160 additions and 1809 deletions

View File

@@ -1,3 +1,4 @@
from esky.util import appdir_from_executable #@UnresolvedImport
from threading import Thread
from wx.lib.softwareupdate import SoftwareUpdate
import os
@@ -5,8 +6,6 @@ import sys
import time
import webbrowser
import wx
import subprocess
# Include proper dirs
if hasattr(sys, 'frozen'):
@@ -214,5 +213,18 @@ if __name__ == '__main__':
time.sleep(1)
if app.restart:
args = [sys.executable] + [os.path.join(base_path, 'Desktop.py')] + sys.argv[1:]
subprocess.Popen(args)
def appexe_from_executable(exepath):
appdir = appdir_from_executable(exepath)
exename = os.path.basename(exepath)
if sys.platform == "darwin":
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
return os.path.join(appdir, "Contents", "MacOS", exename)
return os.path.join(appdir, exename)
exe = appexe_from_executable(sys.executable)
os.chdir(os.path.dirname(exe))
os.execv(exe, [exe] + sys.argv[1:])

View File

@@ -33,7 +33,7 @@ Linux (ubuntu / debian):
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
* Make it executable. `chmod +x /etc/init.d/couchpotato`
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults`

14
contributing.md Normal file
View File

@@ -0,0 +1,14 @@
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
##Just make sure you think of the following things:
* Search through the existing (and closed) issues first. See if you can get your answer there.
* Double check the result manually, because it could be an external issue.
* Post logs! Without seeing what is going on, I can't reproduce the error.
* What are you settings for the specific problem
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
* Give me a short step by step of how to reproduce
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**

View File

@@ -27,6 +27,7 @@ config = [{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'hidden': True,
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},

View File

@@ -1,4 +1,6 @@
from .main import Updater
from couchpotato.environment import Env
import os
def start():
return Updater()
@@ -33,6 +35,7 @@ config = [{
{
'name': 'git_command',
'default': 'git',
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
'advanced': True
},
],

View File

@@ -8,9 +8,11 @@ from couchpotato.environment import Env
from datetime import datetime
from dateutil.parser import parse
from git.repository import LocalRepository
import atexit
import json
import os
import shutil
import sys
import tarfile
import time
import traceback
@@ -106,6 +108,10 @@ class Updater(Plugin):
if success:
fireEventAsync('app.restart')
# Assume the updater handles things
if not success:
success = True
return jsonified({
'success': success
})
@@ -383,11 +389,6 @@ class SourceUpdater(BaseUpdater):
class DesktopUpdater(BaseUpdater):
version = None
update_failed = False
update_version = None
last_check = 0
def __init__(self):
self.desktop = Env.get('desktop')
@@ -396,11 +397,12 @@ class DesktopUpdater(BaseUpdater):
def do_restart(e):
if e['status'] == 'done':
fireEventAsync('app.restart')
else:
elif e['status'] == 'error':
log.error('Failed updating desktop: %s', e['exception'])
self.update_failed = True
self.desktop._esky.auto_update(callback = do_restart)
return
except:
self.update_failed = True
@@ -411,7 +413,7 @@ class DesktopUpdater(BaseUpdater):
'last_check': self.last_check,
'update_version': self.update_version,
'version': self.getVersion(),
'branch': 'desktop_build',
'branch': self.branch,
}
def check(self):

View File

@@ -1,10 +1,7 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
import random
import re
@@ -23,29 +20,17 @@ class Downloader(Plugin):
def __init__(self):
addEvent('download', self.download)
addEvent('download.status', self.getDownloadStatus)
addEvent('download.status', self.getAllDownloadStatus)
addEvent('download.remove_failed', self.removeFailed)
def download(self, data = {}, movie = {}, manual = False, filedata = None):
pass
def getDownloadStatus(self, data = {}, movie = {}):
def getAllDownloadStatus(self):
return False
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def removeFailed(self, name = {}, nzo_id = {}):
return False
def isCorrectType(self, item_type):
is_correct = item_type in self.type
@@ -56,7 +41,7 @@ class Downloader(Plugin):
return is_correct
def magnetToTorrent(self, magnet_link):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
# Convert base 32 to hex
if len(torrent_hash) == 32:
@@ -67,7 +52,10 @@ class Downloader(Plugin):
for source in sources:
try:
filedata = self.urlopen(source % torrent_hash, show_error = False)
filedata = self.urlopen(source % torrent_hash, headers = {'Referer': ''}, show_error = False)
if 'torcache' in filedata and 'file not found' in filedata.lower():
continue
return filedata
except:
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))

View File

@@ -11,7 +11,6 @@ config = [{
'name': 'nzbget',
'label': 'NZBGet',
'description': 'Send NZBs to your NZBGet installation.',
'wizard': True,
'options': [
{
'name': 'enabled',

View File

@@ -12,7 +12,6 @@ config = [{
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Download the .strm file to a specific folder.',
'wizard': True,
'options': [
{
'name': 'enabled',

View File

@@ -1,9 +1,10 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog
import traceback
from urllib2 import URLError
import json
import traceback
log = CPLog(__name__)
@@ -36,126 +37,124 @@ class Sabnzbd(Downloader):
else:
params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
try:
if params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (nzb_filename, filedata)}, multipart = True, show_error = False)
else:
sab = self.urlopen(url, timeout = 60, show_error = False)
except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False
except:
log.error('Failed sending release: %s', traceback.format_exc())
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.error('SABnzbd didn\'t return anything.')
return False
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info("NZB sent to SAB successfully.")
log.debug('Result text from SAB: %s', result[:40])
if result[:2] == 'ok':
log.info('NZB sent to SAB successfully.')
return True
elif result == "Missing authentication":
log.error("Incorrect username/password.")
return False
else:
log.error("Unknown error: " + result[:40])
log.error(result[:40])
return False
def getDownloadStatus(self, data = {}, movie = {}):
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
return
def getAllDownloadStatus(self):
if self.isDisabled(manual = False):
return False
nzbname = self.createNzbName(data, movie)
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
log.debug('Checking SABnzbd download status.')
# Go through Queue
params = {
'apikey': self.conf('api_key'),
'mode': 'queue',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
queue = self.call({
'mode': 'queue',
})
except:
log.error('Failed checking status: %s', traceback.format_exc())
log.error('Failed getting queue: %s', traceback.format_exc(1))
return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing json status: %s', traceback.format_exc())
return False
for slot in history['queue']['slots']:
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
if slot['filename'] == nzbname:
return slot['status'].lower()
# Go through history items
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'output': 'json'
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
history = self.call({
'mode': 'history',
'limit': 15,
})
except:
log.error('Failed getting history json: %s', traceback.format_exc(1))
return False
statuses = []
# Get busy releases
for item in queue.get('slots', []):
statuses.append({
'id': item['nzo_id'],
'name': item['filename'],
'status': 'busy',
'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1,
})
# Get old releases
for item in history.get('slots', []):
status = 'busy'
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
status = 'failed'
elif item['status'] == 'Completed':
status = 'completed'
statuses.append({
'id': item['nzo_id'],
'name': item['name'],
'status': status,
'original_status': item['status'],
'timeleft': 0,
})
return statuses
def removeFailed(self, item):
if not self.conf('delete_failed', default = True):
return False
log.info('%s failed downloading, deleting...', item['name'])
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
self.call({
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': item['id']
}, use_json = False)
except:
log.error('Failed getting history: %s', traceback.format_exc())
return
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
try:
history = json.loads(sab)
except:
log.debug("Result text from SAB: " + sab[:40])
log.error('Failed parsing history json: %s', traceback.format_exc())
return
return True
for slot in history['history']['slots']:
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
if slot['name'] == nzbname:
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
def call(self, params, use_json = True):
# Delete failed download
if self.conf('delete_failed', default = True):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
'apikey': self.conf('api_key'),
'output': 'json'
}))
log.info('%s failed downloading, deleting...', slot['name'])
params = {
'apikey': self.conf('api_key'),
'mode': 'history',
'name': 'delete',
'del_files': '1',
'value': slot['nzo_id']
}
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
data = self.urlopen(url, timeout = 60, show_error = False)
if use_json:
d = json.loads(data)
if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {}
try:
sab = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error('Failed deleting: %s', traceback.format_exc())
return False
return d[params['mode']]
else:
return data
result = sab.strip()
if not result:
log.error("SABnzbd didn't return anything.")
log.debug("Result text from SAB: " + result[:40])
if result == "ok":
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
elif result == "Missing authentication":
log.error("Incorrect username/password or API?.")
else:
log.error("Unknown error: " + result[:40])
return 'failed'
else:
return slot['status'].lower()
return 'not_found'

View File

@@ -19,7 +19,7 @@ def addEvent(name, handler, priority = 100):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs):
@@ -46,49 +46,30 @@ def fireEvent(name, *args, **kwargs):
#log.debug('Firing event %s', name)
try:
# Fire after event
is_after_event = False
try:
del kwargs['is_after_event']
is_after_event = True
except: pass
options = {
'is_after_event': False, # Fire after event
'on_complete': False, # onComplete event
'single': False, # Return single handler
'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish
}
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
# Merge items
in_order = False
try:
del kwargs['in_order']
in_order = True
except: pass
# Do options
for x in options:
try:
val = kwargs[x]
del kwargs[x]
options[x] = val
except: pass
e = events[name]
if not in_order: e.lock.acquire()
if not options['in_order']: e.lock.acquire()
e.asynchronous = False
e.in_order = in_order
e.in_order = options['in_order']
result = e(*args, **kwargs)
if not in_order: e.lock.release()
if not options['in_order']: e.lock.release()
if single and not merge:
if options['single'] and not options['merge']:
results = None
# Loop over results, stop when first not None result is found.
@@ -112,7 +93,7 @@ def fireEvent(name, *args, **kwargs):
errorHandler(r[1])
# Merge
if merge and len(results) > 0:
if options['merge'] and len(results) > 0:
# Dict
if type(results[0]) == dict:
merged = {}
@@ -133,11 +114,11 @@ def fireEvent(name, *args, **kwargs):
log.debug('Return modified results for %s', name)
results = modified_results
if not is_after_event:
if not options['is_after_event']:
fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
if options['on_complete']:
options['on_complete']()
return results
except KeyError, e:

View File

@@ -118,8 +118,16 @@ def getTitle(library_dict):
try:
return library_dict['titles'][0]['title']
except:
log.error('Could not get title for %s', library_dict['identifier'])
return None
try:
for title in library_dict.titles:
if title.default:
return title.title
except:
log.error('Could not get title for %s', library_dict.identifier)
return None
log.error('Could not get title for %s', library_dict['identifier'])
return None
except:
log.error('Could not get title for library item: %s', library_dict)
return None
@@ -127,3 +135,5 @@ def getTitle(library_dict):
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','):
return [x.strip() for x in str.split(split_on)]

View File

@@ -5,7 +5,7 @@ import traceback
class CPLog(object):
context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
def __init__(self, context = ''):
if context.endswith('.main'):

View File

@@ -14,7 +14,7 @@ class Notification(Plugin):
test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = [
'movie.downloaded', 'movie.snatched',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
dont_listen_to = []
@@ -30,10 +30,10 @@ class Notification(Plugin):
addEvent(listener, self.createNotifyHandler(listener))
def createNotifyHandler(self, listener):
def notify(message, data):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return
return self.notify(message = message, data = data, listener = listener)
return self.notify(message = message, data = data if data else group, listener = listener)
return notify

View File

@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt
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
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
listeners = []
listen_to = [
'movie.downloaded', 'movie.snatched',
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
ids = None
if getParam('ids'):
ids = [x.strip() for x in getParam('ids').split(',')]
ids = splitString(getParam('ids'))
db = get_session()
@@ -79,7 +79,6 @@ class CoreNotifier(Notification):
q.update({Notif.read: True})
db.commit()
#db.close()
return jsonified({
'success': True
@@ -93,7 +92,7 @@ class CoreNotifier(Notification):
q = db.query(Notif)
if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')]
splt = splitString(limit_offset)
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
@@ -107,7 +106,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification'
notifications.append(ndict)
#db.close()
return jsonified({
'success': True,
'empty': len(notifications) == 0,
@@ -133,7 +131,6 @@ class CoreNotifier(Notification):
self.frontend(type = listener, data = data)
#db.close()
return True
def frontend(self, type = 'notification', data = {}, message = None):

View File

@@ -69,7 +69,7 @@ class NMJ(Notification):
'mount': mount,
})
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
host = self.conf('host')
@@ -114,8 +114,8 @@ class NMJ(Notification):
def failed(self):
return jsonified({'success': False})
def test(self):
return jsonified({'success': self.addToLibrary()})

View File

@@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import pynma
@@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
if self.isDisabled(): return
nma = pynma.PyNMA()
keys = [x.strip() for x in self.conf('api_key').split(',')]
keys = splitString(self.conf('api_key'))
nma.addkey(keys)
nma.developerkey(self.conf('dev_key'))

View File

@@ -1,3 +1,4 @@
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP
@@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
keys = [x.strip() for x in self.conf('api_key').split(',')]
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)

View File

@@ -17,7 +17,7 @@ class Plex(Notification):
super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
log.info('Sending notification to Plex')

View File

@@ -1,39 +1,35 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
import traceback
log = CPLog(__name__)
class Prowl(Notification):
urls = {
'api': 'https://api.prowlapp.com/publicapi/add'
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection('api.prowlapp.com')
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'
}
http_handler.request('POST',
'/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
log.info('Prowl notifications sent.')
return True
elif request_status == 401:
log.error('Prowl auth failed: %s', response.reason)
return False
else:
log.error('Prowl notification failed.')
return False
except:
log.error('Prowl failed: %s', traceback.format_exc())
return False

View File

@@ -1,6 +1,8 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import os
import subprocess
log = CPLog(__name__)
@@ -8,14 +10,17 @@ log = CPLog(__name__)
class Synoindex(Notification):
index_path = '/usr/syno/bin/synoindex'
def __init__(self):
super(Synoindex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ', command)
command = [self.index_path, '-A', group.get('destination_dir')]
log.info('Executing synoindex command: %s ', command)
try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate()
@@ -26,3 +31,6 @@ class Synoindex(Notification):
return False
return True
def test(self):
return jsonified({'success': os.path.isfile(self.index_path)})

View File

@@ -38,22 +38,33 @@ class Twitter(Notification):
direct_message = self.conf('direct_message')
direct_message_users = self.conf('screen_name')
mention = self.conf('mention')
mention_tag = None
if mention:
if direct_message:
direct_message_users = '%s %s' % (direct_message_users, mention)
direct_message_users = direct_message_users.replace('@',' ')
direct_message_users = direct_message_users.replace(',',' ')
direct_message_users = direct_message_users.replace('@', ' ')
direct_message_users = direct_message_users.replace(',', ' ')
else:
message = '%s @%s' % (message, mention.lstrip('@'))
mention_tag = '@%s' % mention.lstrip('@')
message = '%s %s' % (message, mention_tag)
try:
if direct_message:
for user in direct_message_users.split():
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
else:
api.PostUpdate('[%s] %s' % (self.default_title, message))
update_message = '[%s] %s' % (self.default_title, message)
if len(update_message) > 140:
if mention_tag:
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
else:
api.PostUpdate(update_message[:135] + ' 1/2')
api.PostUpdate(update_message[135:] + ' 2/2')
else:
api.PostUpdate(update_message)
except Exception, e:
log.error('Error sending tweet: %s', e)
return False

58
couchpotato/core/notifications/xbmc/main.py Normal file → Executable file
View File

@@ -1,6 +1,7 @@
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64
log = CPLog(__name__)
@@ -8,36 +9,51 @@ log = CPLog(__name__)
class XBMC(Notification):
listen_to = ['movie.downloaded']
listen_to = ['renamer.after']
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = [x.strip() for x in self.conf('host').split(",")]
hosts = splitString(self.conf('host'))
successful = 0
for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
successful += 1
response = self.request(host, [
('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
('VideoLibrary.Scan', {}),
])
return successful == len(hosts)*2
for result in response:
if result['result'] == "OK":
successful += 1
def send(self, command, host):
return successful == len(hosts) * 2
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
def request(self, host, requests):
server = 'http://%s/jsonrpc' % host
data = []
for req in requests:
method, kwargs = req
data.append({
'method': method,
'params': kwargs,
'jsonrpc': '2.0',
'id': method,
})
data = json.dumps(data)
headers = {
'Content-Type': 'application/json',
}
headers = {}
if self.conf('password'):
headers = {
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
}
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
headers['Authorization'] = 'Basic %s' % base64string
try:
self.urlopen(url, headers = headers, show_error = False)
except:
log.error("Couldn't sent command to XBMC")
return False
log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response))
return response
log.info('XBMC notification to %s successful.', host)
return True

View File

@@ -5,13 +5,12 @@ def start():
config = [{
'name': 'automation',
'order': 30,
'order': 101,
'groups': [
{
'tab': 'automation',
'name': 'automation',
'label': 'Automation',
'description': 'Minimal movie requirements',
'label': 'Minimal movie requirements',
'options': [
{
'name': 'year',

View File

@@ -18,9 +18,17 @@ class Automation(Plugin):
def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True)
movie_ids = []
for imdb_id in movies:
prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False)
if not added:
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
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:
movie_dict = fireEvent('movie.get', movie_id, single = True)
fireEvent('searcher.single', movie_dict)

View File

@@ -1,7 +1,8 @@
from StringIO import StringIO
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss, \
toSafeString
from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
@@ -123,7 +124,7 @@ class Plugin(object):
try:
if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers)
cookies = cookielib.CookieJar()
@@ -238,13 +239,30 @@ class Plugin(object):
self.setCache(cache_key, data, timeout = cache_timeout)
return data
except:
pass
if not kwargs.get('show_error'):
raise
def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout)
return value
def createNzbName(self, data, movie):
tag = self.cpTag(movie)
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type'))
def cpTag(self, movie):
if Env.setting('enabled', 'renamer'):
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
return ''
def isDisabled(self):
return not self.isEnabled()

View File

@@ -27,6 +27,8 @@ class FileBrowser(Plugin):
},
'return': {'type': 'object', 'example': """{
'is_root': bool, //is top most folder
'parent': string, //parent folder of requested path
'home': string, //user home folder
'empty': bool, //directory is empty
'dirs': array, //directory names
}"""}
@@ -55,7 +57,7 @@ class FileBrowser(Plugin):
driveletters = []
for drive in string.ascii_uppercase:
if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE]:
if win32file.GetDriveType(drive + ":") in [win32file.DRIVE_FIXED, win32file.DRIVE_REMOTE, win32file.DRIVE_RAMDISK, win32file.DRIVE_REMOVABLE]:
driveletters.append(drive + ":\\")
return driveletters
@@ -64,14 +66,35 @@ class FileBrowser(Plugin):
path = getParam('path', '/')
# Set proper home dir for some systems
try:
import pwd
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
except:
pass
home = os.path.expanduser('~')
if not path:
path = home
try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except:
dirs = []
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
parent = '/'
elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep
return jsonified({
'is_root': path == '/' or not path,
'is_root': path == '/',
'empty': len(dirs) == 0,
'parent': parent,
'home': home + os.path.sep,
'platform': os.name,
'dirs': dirs,
})

View File

@@ -2,12 +2,15 @@ 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.request import jsonified
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
import os.path
import time
import traceback
log = CPLog(__name__)
@@ -28,6 +31,52 @@ class FileManager(Plugin):
'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:
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())
def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir')
@@ -89,7 +138,6 @@ class FileManager(Plugin):
db.commit()
type_dict = ft.to_dict()
#db.close()
return type_dict
def getTypes(self):
@@ -102,5 +150,10 @@ class FileManager(Plugin):
for type_object in results:
types.append(type_object.to_dict())
#db.close()
return types
def getTypesView(self):
return jsonified({
'types': self.getTypes()
})

View File

@@ -1,6 +1,7 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File
@@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict)
#db.close()
return library_dict
def update(self, identifier, default_title = '', force = False):
@@ -111,18 +111,20 @@ class LibraryPlugin(Plugin):
# Files
images = info.get('images', [])
for type in images:
for image in images[type]:
if not isinstance(image, str):
for image_type in ['poster']:
for image in images.get(image_type, []):
if not isinstance(image, (str, unicode)):
continue
file_path = fireEvent('file.download', url = image, single = True)
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
break
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
@@ -136,26 +138,23 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info:
library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_dates')
library_dict = self.update(identifier, force = True)
dates = library_dict.get('info', {}).get('release_date')
else:
dates = library.info.get('release_date')
if dates and dates.get('expires', 0) < time.time():
if dates and dates.get('expires', 0) < time.time() or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info['release_date'] = dates
library.info = library.info
library.info = mergeDicts(library.info, {'release_date': dates })
db.commit()
dates = library.info.get('release_date', {})
#db.close()
return dates
def simplifyTitle(self, title):
title = toUnicode(title)
nr_prefix = '' if title[0] in ascii_letters else '#'
title = simplifyString(title)

View File

@@ -2,22 +2,32 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import getTitle, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
import time
import traceback
log = CPLog(__name__)
class Manage(Plugin):
in_progress = False
def __init__(self):
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
addEvent('manage.update', self.updateLibrary)
# Add files after renaming
def after_rename(message = None, group = {}):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename, priority = 110)
addApiView('manage.update', self.updateLibraryView, docs = {
'desc': 'Update the library by scanning for new movies',
'params': {
@@ -25,11 +35,23 @@ class Manage(Plugin):
}
})
addApiView('manage.progress', self.getProgress, docs = {
'desc': 'Get the progress of current manage update',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
if not Env.get('dev'):
def updateLibrary():
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def updateLibraryView(self):
full = getParam('full', default = 1)
@@ -43,49 +65,127 @@ class Manage(Plugin):
def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0))
if self.isDisabled() or (last_update > time.time() - 20):
if self.in_progress:
log.info('Already updating library: %s', self.in_progress)
return
elif self.isDisabled() or (last_update > time.time() - 20):
return
directories = self.directories()
added_identifiers = []
self.in_progress = {}
fireEvent('notify.frontend', type = 'manage.updating', data = True)
for directory in directories:
try:
if not os.path.isdir(directory):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', directory)
continue
directories = self.directories()
added_identifiers = []
log.info('Updating manage library: %s', directory)
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers:
added_identifiers.extend(identifiers)
# Add some progress
self.in_progress = {}
for directory in directories:
self.in_progress[os.path.normpath(directory)] = {
'total': None,
'to_go': None,
}
# Break if CP wants to shut down
if self.shuttingDown():
for directory in directories:
folder = os.path.normpath(directory)
if not os.path.isdir(folder):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s', folder)
continue
log.info('Updating manage library: %s', folder)
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
onFound = self.createAddToLibrary(folder, added_identifiers)
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
# Break if CP wants to shut down
if self.shuttingDown():
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
Env.prop('manage.last_update', time.time())
except:
log.error('Failed updating library: %s', (traceback.format_exc()))
while True and not self.shuttingDown():
delete_me = {}
for folder in self.in_progress:
if self.in_progress[folder]['to_go'] <= 0:
delete_me[folder] = True
for delete in delete_me:
del self.in_progress[delete]
if len(self.in_progress) == 0:
break
# If cleanup option is enabled, remove offline files from database
if self.conf('cleanup') and full and not self.shuttingDown():
time.sleep(1)
# Get movies with done status
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
fireEvent('notify.frontend', type = 'manage.updating', data = False)
self.in_progress = False
for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
else:
for release in done_movie.get('releases', []):
for release_file in release.get('files', []):
# Remove release not available anymore
if not os.path.isfile(ss(release_file['path'])):
fireEvent('release.clean', release['id'])
break
def createAddToLibrary(self, folder, added_identifiers = []):
def addToLibrary(group, total_found, to_go):
if self.in_progress[folder]['total'] is None:
self.in_progress[folder] = {
'total': total_found,
'to_go': total_found,
}
Env.prop('manage.last_update', time.time())
if group['library']:
identifier = group['library'].get('identifier')
added_identifiers.append(identifier)
# Add it to release and update the info
fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
return addToLibrary
def createAfterUpdate(self, folder, identifier):
# Notify frontend
def afterUpdate():
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
total = self.in_progress[folder]['total']
movie_dict = fireEvent('movie.get', identifier, single = True)
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
return afterUpdate
def directories(self):
try:
return [x.strip() for x in self.conf('library', default = '').split('::')]
return splitString(self.conf('library', default = ''), '::')
except:
return []
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)

View File

@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.helpers.variable import getImdb, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
@@ -107,13 +107,18 @@ class MoviePlugin(Plugin):
def get(self, movie_id):
db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first()
imdb_id = getImdb(str(movie_id))
if(imdb_id):
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
else:
m = db.query(Movie).filter_by(id = movie_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
#db.close()
return results
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
@@ -161,7 +166,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('files'))
if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')]
splt = splitString(limit_offset)
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset)
@@ -239,7 +244,7 @@ class MoviePlugin(Plugin):
db = get_session()
for id in getParam('id').split(','):
for id in splitString(getParam('id')):
movie = db.query(Movie).filter_by(id = id).first()
if movie:
@@ -278,13 +283,27 @@ class MoviePlugin(Plugin):
'movies': movies,
})
def add(self, params = {}, force_readd = True, search_after = True):
def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
if not params.get('identifier'):
log.error('Can\'t add movie without imdb identifier.')
msg = 'Can\'t add movie without imdb identifier.'
log.error(msg)
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
return False
else:
try:
url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
if tvdb and 'series' in tvdb.lower():
msg = 'Can\'t add movie, seems to be a TV show.'
log.error(msg)
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
return False
except:
pass
library = fireEvent('library.add', single = True, attrs = params, update_after = False)
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status
status_active = fireEvent('status.add', 'active', single = True)
@@ -367,7 +386,7 @@ class MoviePlugin(Plugin):
available_status = fireEvent('status.get', 'available', single = True)
ids = [x.strip() for x in params.get('id').split(',')]
ids = splitString(params.get('id'))
for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first()
@@ -403,7 +422,7 @@ class MoviePlugin(Plugin):
params = getParams()
ids = [x.strip() for x in params.get('id').split(',')]
ids = splitString(params.get('id'))
for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
@@ -417,9 +436,11 @@ class MoviePlugin(Plugin):
movie = db.query(Movie).filter_by(id = movie_id).first()
if movie:
deleted = False
if delete_from == 'all':
db.delete(movie)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
@@ -442,6 +463,7 @@ class MoviePlugin(Plugin):
if total_releases == total_deleted:
db.delete(movie)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
@@ -450,6 +472,9 @@ class MoviePlugin(Plugin):
else:
fireEvent('movie.restatus', movie.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
#db.close()
return True

View File

@@ -33,16 +33,34 @@ var MovieList = new Class({
);
self.getMovies();
if(options.add_new)
App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
},
movieDeleted: function(notification){
var self = this;
if(self.movies_added[notification.data.id]){
self.movies.each(function(movie){
if(movie.get('id') == notification.data.id){
movie.destroy();
delete self.movies_added[notification.data.id]
}
})
}
self.checkIfEmpty();
},
movieAdded: function(notification){
var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id])
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0);
self.createMovie(notification.data, 'top');
self.checkIfEmpty();
}
},
create: function(){
@@ -86,18 +104,19 @@ var MovieList = new Class({
Object.each(movies, function(movie){
self.createMovie(movie);
});
self.total_movies = total;
self.setCounter(total);
},
setCounter: function(count){
var self = this;
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
},
createMovie: function(movie, inject_at){
@@ -309,6 +328,8 @@ var MovieList = new Class({
erase_movies.each(function(movie){
self.movies.erase(movie);
movie.destroy()
});
self.calculateSelected();
@@ -458,6 +479,8 @@ var MovieList = new Class({
self.addMovies(json.movies, json.total);
self.load_more.set('text', 'load more movies');
if(self.scrollspy) self.scrollspy.start();
self.checkIfEmpty()
}
});
},
@@ -475,6 +498,28 @@ var MovieList = new Class({
},
checkIfEmpty: function(){
var self = this;
var is_empty = self.movies.length == 0 && self.total_movies == 0;
if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element);
if(self.navigation)
self.navigation.hide();
self.empty_element = self.options.on_empty_element;
}
else if(self.empty_element){
self.empty_element.destroy();
if(self.navigation)
self.navigation.show();
}
},
toElement: function(){
return this.el;
}

View File

@@ -24,7 +24,7 @@
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03);
}
.movies .movie_container {
overflow: hidden;
}
@@ -313,7 +313,7 @@
padding-bottom: 4px;
height: auto;
}
.movies .movie .trailer_container {
width: 100%;
background: #000;
@@ -324,7 +324,7 @@
.movies .movie .trailer_container.hide {
height: 0 !important;
}
.movies .movie .hide_trailer {
position: absolute;
top: 0;
@@ -340,12 +340,12 @@
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .movie .try_container {
padding: 5px 10px;
text-align: center;
}
.movies .movie .try_container a {
margin: 0 5px;
padding: 2px 5px;
@@ -354,15 +354,15 @@
.movies .movie .releases .next_release {
border-left: 6px solid #2aa300;
}
.movies .movie .releases .next_release > :first-child {
margin-left: -6px;
}
.movies .movie .releases .last_release {
border-left: 6px solid #ffa200;
}
.movies .movie .releases .last_release > :first-child {
margin-left: -6px;
}
@@ -394,8 +394,8 @@
border-radius: 0;
}
.movies .alph_nav ul.numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.actions {
list-style: none;
padding: 0 0 1px;
@@ -518,7 +518,7 @@
padding: 3px 7px;
}
.movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .refresh,
.movies .alph_nav .mass_edit_form .delete {
float: left;
padding: 8px 0 0 8px;
@@ -534,5 +534,67 @@
}
.movies .alph_nav .more_menu > a {
background-position: center -157px;
background-position: center -158px;
}
.movies .empty_wanted {
background-image: url('../images/emptylist.png');
height: 750px;
width: 800px;
padding-top: 260px;
margin-top: -50px;
}
.movies .empty_manage {
text-align: center;
font-size: 25px;
line-height: 150%;
}
.movies .empty_manage .after_manage {
margin-top: 30px;
font-size: 16px;
}
.movies .progress {
border-radius: 2px;
padding: 10px;
margin: 5px 0;
text-align: left;
}
.movies .progress > div {
padding: 5px 10px;
font-size: 12px;
line-height: 12px;
text-align: left;
display: inline-block;
width: 49%;
background: rgba(255, 255, 255, 0.05);
margin: 2px 0.5%;
border-radius: 3px;
}
.movies .progress > div .folder {
display: inline-block;
padding: 5px 20px 5px 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 85%;
direction: rtl;
vertical-align: middle;
}
.movies .progress > div .percentage {
font-weight: bold;
display: inline-block;
text-transform: uppercase;
text-shadow: none;
font-weight: normal;
font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2);
width: 15%;
text-align: right;
vertical-align: middle;
}

View File

@@ -16,14 +16,41 @@ var Movie = new Class({
self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self));
self.addEvents();
},
addEvents: function(){
var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+data.id, function(notification){
App.addEvent(listener+'.'+self.data.id, function(notification){
if(notification.data)
self.busy(true)
});
})
App.addEvent('searcher.ended.'+self.data.id, function(notification){
if(notification.data)
self.busy(false)
});
},
destroy: function(){
var self = this;
self.el.destroy();
delete self.list.movies_added[self.get('id')];
self.list.movies.erase(self)
self.list.checkIfEmpty();
// Remove events
App.removeEvents('movie.update.'+self.data.id);
['movie.busy', 'searcher.started'].each(function(listener){
App.removeEvents(listener+'.'+self.data.id);
})
},
busy: function(set_busy){
@@ -359,7 +386,7 @@ var ReleaseAction = new Class({
var status = Status.get(release.status_id);
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
@@ -397,19 +424,11 @@ var ReleaseAction = new Class({
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id) || {},
info = release.info;
if( status.identifier == 'ignored' || status.identifier == 'failed'){
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
release.status = status;
// Create release
new Element('div', {
'class': 'item '+status.identifier +
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
'class': 'item '+status.identifier,
'id': 'release_'+release.id
}).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
@@ -442,11 +461,27 @@ var ReleaseAction = new Class({
}
})
).inject(self.release_container)
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
self.last_release = release;
}
else if(!self.next_release && status.identifier == 'available'){
self.next_release = release;
}
});
if(self.last_release){
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
}
if(self.next_release){
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
}
self.trynext_container.adopt(
new Element('span.or', {
'text': 'Download'
'text': 'This movie is snatched, if anything went wrong, download'
}),
self.last_release ? new Element('a.button.orange', {
'text': 'the same release again',
@@ -455,7 +490,7 @@ var ReleaseAction = new Class({
}
}) : null,
self.next_release && self.last_release ? new Element('span.or', {
'text': 'or'
'text': ','
}) : null,
self.next_release ? [new Element('a.button.green', {
'text': self.last_release ? 'another release' : 'the best release',

View File

@@ -324,7 +324,7 @@ Block.Search.Item = new Class({
var self = this;
if(!self.options.hasClass('set')){
if(self.info.in_library){
var in_library = [];
self.info.in_library.releases.each(function(release){
@@ -339,7 +339,7 @@ Block.Search.Item = new Class({
'height': null,
'width': null
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', {
'text': 'Already in library: ' + in_library.join(', ')

View File

@@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
for profile in profiles:
temp.append(profile.to_dict(self.to_dict))
#db.close()
return temp
def save(self):
@@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict)
#db.close()
return jsonified({
'success': True,
'profile': profile_dict
@@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
db = get_session()
default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict)
#db.close()
return default_dict
@@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
order += 1
db.commit()
#db.close()
return jsonified({
'success': True
@@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
except Exception, e:
message = log.error('Failed deleting Profile: %s', e)
#db.close()
return jsonified({
'success': success,
'message': message
@@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
)
p.types.append(profile_type)
db.commit()
quality_order += 1
order += 1
#db.close()
db.commit()
return True

View File

@@ -86,7 +86,10 @@ var Profile = new Class({
},
'onComplete': function(json){
if(json.success){
self.data = json.profile
self.data = json.profile;
self.type_container.getElement('li:first-child input[type=checkbox]')
.set('checked', true)
.getParent().addClass('checked');
}
}
});
@@ -239,9 +242,17 @@ Profile.Type = new Class({
),
new Element('span.finish').adopt(
self.finish = new Element('input.inlay.finish[type=checkbox]', {
'checked': data.finish,
'checked': data.finish !== undefined ? data.finish : 1,
'events': {
'change': self.fireEvent.bind(self, 'change')
'change': function(e){
if(self.el == self.el.getParent().getElement(':first-child')){
self.finish_class.check();
alert('Top quality always finishes the search')
return;
}
self.fireEvent('change');
}
}
})
),
@@ -255,7 +266,7 @@ Profile.Type = new Class({
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
new Form.Check(self.finish);
self.finish_class = new Form.Check(self.finish);
},

View File

@@ -9,6 +9,7 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Quality, Profile, ProfileType
import os.path
import re
import time
log = CPLog(__name__)
@@ -68,7 +69,6 @@ class QualityPlugin(Plugin):
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
temp.append(q)
#db.close()
return temp
def single(self, identifier = ''):
@@ -80,7 +80,6 @@ class QualityPlugin(Plugin):
if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
#db.close()
return quality_dict
def getQuality(self, identifier):
@@ -100,7 +99,6 @@ class QualityPlugin(Plugin):
setattr(quality, params.get('value_type'), params.get('value'))
db.commit()
#db.close()
return jsonified({
'success': True
})
@@ -113,46 +111,48 @@ class QualityPlugin(Plugin):
for q in self.qualities:
# Create quality
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality:
if not qual:
log.info('Creating quality: %s', q.get('label'))
quality = Quality()
db.add(quality)
qual = Quality()
qual.order = order
qual.identifier = q.get('identifier')
qual.label = toUnicode(q.get('label'))
qual.size_min, qual.size_max = q.get('size')
quality.order = order
quality.identifier = q.get('identifier')
quality.label = toUnicode(q.get('label'))
quality.size_min, quality.size_max = q.get('size')
db.add(qual)
# Create single quality profile
profile = db.query(Profile).filter(
prof = db.query(Profile).filter(
Profile.core == True
).filter(
Profile.types.any(quality = quality)
Profile.types.any(quality = qual)
).all()
if not profile:
if not prof:
log.info('Creating profile: %s', q.get('label'))
profile = Profile(
prof = Profile(
core = True,
label = toUnicode(quality.label),
label = toUnicode(qual.label),
order = order
)
db.add(profile)
db.add(prof)
profile_type = ProfileType(
quality = quality,
profile = profile,
quality = qual,
profile = prof,
finish = True,
order = 0
)
profile.types.append(profile_type)
prof.types.append(profile_type)
order += 1
db.commit()
#db.close()
db.commit()
time.sleep(0.3) # Wait a moment
return True
def guess(self, files, extra = {}):

View File

@@ -88,8 +88,6 @@ class Release(Plugin):
fireEvent('movie.restatus', movie.id)
#db.close()
return True
@@ -108,7 +106,6 @@ class Release(Plugin):
release_id = getParam('id')
#db.close()
return jsonified({
'success': self.delete(release_id)
})
@@ -152,7 +149,6 @@ class Release(Plugin):
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit()
#db.close()
return jsonified({
'success': True
})
@@ -161,6 +157,7 @@ class Release(Plugin):
db = get_session()
id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
rel = db.query(Relea).filter_by(id = id).first()
if rel:
@@ -181,14 +178,16 @@ class Release(Plugin):
'files': {}
}), manual = True, single = True)
#db.close()
if success:
rel.status_id = status_snatched.get('id')
db.commit()
return jsonified({
'success': success
})
else:
log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({
'success': False
})

View File

@@ -3,7 +3,8 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release
@@ -20,6 +21,7 @@ log = CPLog(__name__)
class Renamer(Plugin):
renaming_started = False
checking_snatched = False
def __init__(self):
@@ -33,6 +35,7 @@ class Renamer(Plugin):
addEvent('app.load', self.scan)
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = 2)
def scanView(self):
@@ -48,7 +51,7 @@ class Renamer(Plugin):
return
if self.renaming_started is True:
log.info('Renamer is disabled to avoid infinite looping of the same error.')
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
return
# Check to see if the "to" folder is inside the "from" folder.
@@ -127,6 +130,8 @@ class Renamer(Plugin):
'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'],
'cd': '',
'cd_nr': '',
}
for file_type in group['files']:
@@ -144,7 +149,7 @@ class Renamer(Plugin):
continue
# Move other files
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
cd = 1 if multiple else 0
for current_file in sorted(list(group['files'][file_type])):
@@ -157,23 +162,19 @@ class Renamer(Plugin):
replacements['ext'] = getExt(current_file)
# cd #
replacements['cd'] = ' cd%d' % cd if cd else ''
replacements['cd_nr'] = cd
replacements['cd'] = ' cd%d' % cd if multiple else ''
replacements['cd_nr'] = cd if multiple else ''
# Naming
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
# Group filename without cd extension
replacements['cd'] = ''
replacements['cd_nr'] = ''
# Meta naming
if file_type is 'trailer':
final_file_name = self.doReplace(trailer_name, replacements)
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
elif file_type is 'nfo':
final_file_name = self.doReplace(nfo_name, replacements)
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
# Seperator replace
if separator:
@@ -204,10 +205,16 @@ class Renamer(Plugin):
# Check for extra subtitle files
if file_type is 'subtitle':
# rename subtitles with or without language
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
remove_multiple = False
if len(group['files']['movie']) == 1:
remove_multiple = True
sub_langs = group['subtitle_language'].get(current_file, [])
# rename subtitles with or without language
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_extras = self.getRenameExtras(
extra_type = 'subtitle_extra',
replacements = replacements,
@@ -215,20 +222,19 @@ class Renamer(Plugin):
file_name = file_name,
destination = destination,
group = group,
current_file = current_file
current_file = current_file,
remove_multiple = remove_multiple,
)
# Don't add language if multiple languages in 1 file
if len(sub_langs) > 1:
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
elif len(sub_langs) == 1:
# Don't add language if multiple languages in 1 subtitle file
if len(sub_langs) == 1:
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
rename_files = mergeDicts(rename_files, rename_extras)
# Filename without cd etc
if file_type is 'movie':
elif file_type is 'movie':
rename_extras = self.getRenameExtras(
extra_type = 'movie_extra',
replacements = replacements,
@@ -240,7 +246,7 @@ class Renamer(Plugin):
)
rename_files = mergeDicts(rename_files, rename_extras)
group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)]
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
group['destination_dir'] = os.path.join(destination, final_folder_name)
if multiple:
@@ -375,22 +381,19 @@ class Renamer(Plugin):
except:
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Search for trailers etc
fireEventAsync('renamer.after', group)
# Notify on download
# Notify on download, search for trailers etc
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group)
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
# Break if CP wants to shut down
if self.shuttingDown():
break
#db.close()
self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
replacements = replacements.copy()
rename_files = {}
def test(s):
@@ -399,8 +402,8 @@ class Renamer(Plugin):
for extra in set(filter(test, group['files'][extra_type])):
replacements['ext'] = getExt(extra)
final_folder_name = self.doReplace(folder_name, replacements)
final_file_name = self.doReplace(file_name, replacements)
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
return rename_files
@@ -455,11 +458,16 @@ class Renamer(Plugin):
return True
def doReplace(self, string, replacements):
def doReplace(self, string, replacements, remove_multiple = False):
'''
replace confignames with the real thing
'''
replacements = replacements.copy()
if remove_multiple:
replacements['cd'] = ''
replacements['cd_nr'] = ''
replaced = toUnicode(string)
for x, r in replacements.iteritems():
if r is not None:
@@ -495,6 +503,11 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
@@ -504,57 +517,69 @@ class Renamer(Plugin):
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
if rels:
log.debug('Checking status snatched releases...')
scan_required = False
for rel in rels:
if rels:
self.checking_snatched = True
log.debug('Checking status snatched releases...')
# Get current selected title
default_title = ''
for title in rel.movie.library.titles:
if title.default: default_title = title.title
# Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
db.commit()
continue
item = {}
for info in rel.info:
item[info.identifier] = info.value
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
# check status
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
if not downloadstatus:
statuses = fireEvent('download.status', merge = True)
if not statuses:
log.debug('Download status functionality is not implemented for active downloaders.')
scan_required = True
else:
log.debug('Download status: %s' , downloadstatus)
try:
for rel in rels:
rel_dict = rel.to_dict({'info': {}})
if downloadstatus == 'failed':
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
db.commit()
# Get current selected title
default_title = getTitle(rel.movie.library)
log.info('Download of %s failed.', item['name'])
# Check if movie has already completed and is manage tab (legacy db correction)
if rel.movie.status_id == done_status.get('id'):
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
rel.status_id = ignored_status.get('id')
db.commit()
continue
elif downloadstatus == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
elif downloadstatus == 'not_found':
log.info('%s not found in downloaders', item['name'])
rel.status_id = ignored_status.get('id')
db.commit()
# check status
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
found = False
for item in statuses:
if item['name'] == nzbname or getImdb(item['name']) == movie_dict['library']['identifier']:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
if item['status'] == 'busy':
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id')
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
found = True
break
if not found:
log.info('%s not found in downloaders', nzbname)
except:
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
if scan_required:
fireEvent('renamer.scan')
self.checking_snatched = False
return True

View File

@@ -34,6 +34,7 @@ class Scanner(Plugin):
'subtitle_extra': ['idx'],
'trailer': ['mov', 'mp4', 'flv']
}
file_types = {
'subtitle': ('subtitle', 'subtitle'),
'subtitle_extra': ('subtitle', 'subtitle_extra'),
@@ -42,6 +43,8 @@ class Scanner(Plugin):
'movie': ('video', 'movie'),
'movie_extra': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'),
'poster': ('image', 'poster'),
'thumbnail': ('image', 'thumbnail'),
'leftover': ('leftover', 'leftover'),
}
@@ -80,55 +83,10 @@ class Scanner(Plugin):
addEvent('scanner.remove_cptag', self.removeCPTag)
addEvent('scanner.scan', self.scan)
addEvent('scanner.files', self.scanFilesToLibrary)
addEvent('scanner.folder', self.scanFolderToLibrary)
addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber)
def after_rename(group):
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('renamer.after', after_rename)
def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
if group['library']:
fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
folder = os.path.normpath(folder)
if not os.path.isdir(folder):
return
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = []
while True and not self.shuttingDown():
try:
identifier, group = groups.popitem()
except:
break
# Save to DB
if group['library']:
# Add release
fireEvent('release.add', group = group)
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
if library_item:
added_identifier.append(library_item['identifier'])
return added_identifier
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
folder = ss(os.path.normpath(folder))
@@ -141,7 +99,8 @@ class Scanner(Plugin):
leftovers = []
# Scan all files of the folder if no files are set
if len(files) == 0:
if not files:
check_file_date = True
try:
files = []
for root, dirs, walk_files in os.walk(folder):
@@ -150,6 +109,7 @@ class Scanner(Plugin):
except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
check_file_date = False
files = [ss(x) for x in files]
db = get_session()
@@ -279,8 +239,8 @@ class Scanner(Plugin):
del path_identifiers[identifier]
del delete_identifiers
# Determine file types
processed_movies = {}
# Make sure we remove older / still extracting files
valid_files = {}
while True and not self.shuttingDown():
try:
identifier, group = movie_files.popitem()
@@ -302,7 +262,7 @@ class Scanner(Plugin):
if file_too_new:
break
if file_too_new:
if check_file_date and file_too_new:
try:
time_string = time.ctime(file_time[0])
except:
@@ -320,17 +280,33 @@ class Scanner(Plugin):
# Only process movies newer than x
if newer_than and newer_than > 0:
has_new_files = False
for cur_file in group['unsorted_files']:
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
if file_time[0] > time.time() or file_time[1] > time.time():
if file_time[0] > newer_than or file_time[1] > newer_than:
has_new_files = True
break
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
if not has_new_files:
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
# Delete the unsorted list
del group['unsorted_files']
# Delete the unsorted list
del group['unsorted_files']
continue
continue
valid_files[identifier] = group
del movie_files
# Determine file types
processed_movies = {}
total_found = len(valid_files)
while True and not self.shuttingDown():
try:
identifier, group = valid_files.popitem()
except:
break
# Group extra (and easy) files first
# images = self.getImages(group['unsorted_files'])
@@ -392,9 +368,12 @@ class Scanner(Plugin):
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group
# Notify parent & progress on something found
if on_found:
on_found(group, total_found, total_found - len(processed_movies))
if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
@@ -539,7 +518,6 @@ class Scanner(Plugin):
break
except:
pass
#db.close()
# Search based on OpenSubtitleHash
if not imdb_id and not group['is_dvd']:

View File

@@ -10,7 +10,7 @@ name_scores = [
# Video
'x264:1', 'h264:1',
# Audio
'DTS:4', 'AC3:2',
'dts:4', 'ac3:2',
# Quality
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
# Language / Subs

View File

@@ -1,6 +1,6 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle
@@ -36,9 +36,38 @@ class Searcher(Plugin):
},
})
addApiView('searcher.full_search', self.allMoviesView, docs = {
'desc': 'Starts a full search for all wanted movies',
})
addApiView('searcher.progress', self.getProgress, docs = {
'desc': 'Get the progress of current full search',
'return': {'type': 'object', 'example': """{
'progress': False || object, total & to_go,
}"""},
})
# Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self):
in_progress = self.in_progress
if not in_progress:
fireEventAsync('searcher.all')
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({
'success': not in_progress
})
def getProgress(self):
return jsonified({
'progress': self.in_progress
})
def allMovies(self):
@@ -54,6 +83,11 @@ class Searcher(Plugin):
Movie.status.has(identifier = 'active')
).all()
self.in_progress = {
'total': len(movies),
'to_go': len(movies),
}
for movie in movies:
movie_dict = movie.to_dict({
'profile': {'types': {'quality': {}}},
@@ -65,15 +99,17 @@ class Searcher(Plugin):
try:
self.single(movie_dict)
except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1
# Break if CP wants to shut down
if self.shuttingDown():
break
#db.close()
self.in_progress = False
def single(self, movie):
@@ -93,6 +129,8 @@ class Searcher(Plugin):
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('movie.delete', movie['id'], single = True)
return
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
@@ -142,10 +180,10 @@ class Searcher(Plugin):
status_id = available_status.get('id')
)
db.add(rls)
db.commit()
else:
[db.delete(info) for info in rls.info]
db.commit()
[db.delete(old_info) for old_info in rls.info]
db.commit()
for info in nzb:
try:
@@ -157,10 +195,11 @@ class Searcher(Plugin):
value = toUnicode(nzb[info])
)
rls.info.append(rls_info)
db.commit()
except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
db.commit()
nzb['status_id'] = rls.status_id
@@ -190,7 +229,6 @@ class Searcher(Plugin):
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return ret
def download(self, data, movie, manual = False):
@@ -208,40 +246,43 @@ class Searcher(Plugin):
if successful:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
rls.status_id = snatched_status.get('id')
db.commit()
try:
# Mark release as snatched
db = get_session()
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
if rls:
rls.status_id = snatched_status.get('id')
db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# If renamer isn't used, mark movie done
if not Env.setting('enabled', 'renamer'):
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
try:
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
# Mark release done
rls.status_id = done_status.get('id')
db.commit()
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
db.commit()
except:
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
# Mark movie done
mvie = db.query(Movie).filter_by(id = movie['id']).first()
mvie.status_id = done_status.get('id')
db.commit()
except Exception, e:
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
except:
log.error('Failed marking movie finished: %s', traceback.format_exc())
#db.close()
return True
log.info('Tried to download, but none of the downloaders are enabled')
@@ -252,7 +293,7 @@ class Searcher(Plugin):
imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
if nzb.get('seeds') is None and 0 < retention < nzb.get('age', 0):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False
@@ -352,9 +393,11 @@ class Searcher(Plugin):
year_name = fireEvent('scanner.name_year', name, single = True)
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr
return 'dvdr' == preferred_quality['identifier']
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size))
found['dvdr'] = True
else: # Assume dvdrip
return 'dvdrip' == preferred_quality['identifier']
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
found['dvdrip'] = True
# Allow other qualities
for allowed in preferred_quality.get('allow'):
@@ -371,12 +414,17 @@ class Searcher(Plugin):
return False
def correctYear(self, haystack, year, range):
def correctYear(self, haystack, year, year_range):
for string in haystack:
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
year_name = fireEvent('scanner.name_year', string, single = True)
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
return True
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
return False
def correctName(self, check_name, movie_name):
@@ -408,6 +456,11 @@ class Searcher(Plugin):
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
return True
else:
# For movies before 1972
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True
if wanted_quality in pre_releases:
# Prerelease 1 week before theaters
if dates.get('theater') - 604800 < now:

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle',
'label': 'Download subtitles after rename',
'label': 'Download subtitles',
'description': 'after rename',
'options': [
{
'name': 'enabled',

View File

@@ -1,6 +1,7 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType
@@ -13,7 +14,7 @@ log = CPLog(__name__)
class Subtitle(Plugin):
services = ['opensubtitles', 'thesubdb', 'subswiki']
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
def __init__(self):
addEvent('renamer.before', self.searchSingle)
@@ -40,8 +41,6 @@ class Subtitle(Plugin):
# get subtitles for those files
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
#db.close()
def searchSingle(self, group):
if self.isDisabled(): return
@@ -69,4 +68,4 @@ class Subtitle(Plugin):
return False
def getLanguages(self):
return [x.strip() for x in self.conf('languages').split(',')]
return splitString(self.conf('languages'))

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'renamer',
'subtab': 'trailer',
'name': 'trailer',
'label': 'Download trailer after rename',
'label': 'Download trailer',
'description': 'after rename',
'options': [
{
'name': 'enabled',
@@ -24,12 +24,6 @@ config = [{
'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
},
{
'name': 'automatic',
'default': False,
'type': 'bool',
'description': 'Automaticly search & download for movies in library',
},
],
},
],

View File

@@ -12,7 +12,7 @@ class Trailer(Plugin):
def __init__(self):
addEvent('renamer.after', self.searchSingle)
def searchSingle(self, group):
def searchSingle(self, message = None, group = {}):
if self.isDisabled() or len(group['files']['trailer']) > 0: return
@@ -28,6 +28,8 @@ class Trailer(Plugin):
else:
log.debug('Trailer already exists: %s', destination)
group['renamed_files'].append(destination)
# Download first and break
break

View File

@@ -14,12 +14,13 @@
.page.wizard .tab_wrapper {
background: #5c697b;
padding: 18px 0;
font-size: 23px;
padding: 10px 0;
font-size: 18px;
position: fixed;
top: 0;
margin: 0;
width: 100%;
min-width: 960px;
left: 0;
z-index: 2;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
@@ -36,7 +37,7 @@
display: inline-block;
}
.page.wizard .tabs li a {
padding: 20px 30px;
padding: 20px 10px;
}
.page.wizard .tab_wrapper .pointer {
@@ -45,7 +46,7 @@
border-top: 10px solid #5c697b;
display: block;
position: absolute;
top: 60px;
top: 44px;
}
.page.wizard .tab_content {
@@ -58,11 +59,25 @@
.page.wizard .wgroup_finish {
height: 300px;
}
.page.wizard .wgroup_finish h1 {
text-align: center;
}
.page.wizard .wgroup_finish .wizard_support,
.page.wizard .wgroup_finish .description {
font-size: 25px;
line-height: 120%;
margin: 20px 0;
text-align: center;
}
.page.wizard .button.green {
padding: 20px;
font-size: 25px;
margin: 10px 30px;
margin: 10px 30px 80px;
display: block;
text-align: center;
}
}
.page.wizard .tab_nzb_providers {
margin: 20px 0 0 0;
}

View File

@@ -1,214 +1,266 @@
Page.Wizard = new Class({
Extends: Page.Settings,
name: 'wizard',
has_tab: false,
wizard_only: true,
headers: {
'welcome': {
'title': 'Welcome to the new CouchPotato',
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
'content': new Element('div', {
'styles': {
'margin': '0 0 0 30px'
}
}).adopt(
new Element('div', {
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
}),
self.import_iframe = new Element('iframe', {
'styles': {
'height': 40,
'width': 300,
'border': 0,
'overflow': 'hidden'
}
})
),
'event': function(){
self.import_iframe.set('src', Api.createUrl('v1.import'))
}
},
'general': {
'title': 'General',
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
},
'downloaders': {
'title': 'What download apps are you using?',
'description': 'If you don\'t have any of these listed, you have to use Blackhole. Or drop me a line, maybe I\'ll support your download app.'
},
'providers': {
'title': 'Are you registered at any of these sites?',
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more.'
},
'renamer': {
'title': 'Move & rename the movies after downloading?',
'description': ''
},
'finish': {
'title': 'Finish Up',
'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!',
'content': new Element('div').adopt(
new Element('a.button.green', {
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
'events': {
'click': function(e){
(e).preventDefault();
Api.request('settings.save', {
'data': {
'section': 'core',
'name': 'show_wizard',
'value': 0
},
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(){
window.location = App.createUrl();
}
});
}
}
})
)
}
},
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'finish'],
open: function(action, params){
var self = this;
if(!self.initialized){
App.fireEvent('unload');
App.getBlock('header').hide();
self.parent(action, params);
self.addEvent('create', function(){
self.order();
});
self.initialized = true;
self.scroll = new Fx.Scroll(document.body, {
'transition': 'quint:in:out'
});
}
else
(function(){
var sc = self.el.getElement('.wgroup_'+action);
self.scroll.start(0, sc.getCoordinates().top-80);
}).delay(1)
},
order: function(){
var self = this;
var form = self.el.getElement('.uniForm');
var tabs = self.el.getElement('.tabs');
self.groups.each(function(group){
if(self.headers[group]){
group_container = new Element('.wgroup_'+group, {
'styles': {
'opacity': 0.2
},
'tween': {
'duration': 350
}
});
group_container.adopt(
new Element('h1', {
'text': self.headers[group].title
}),
self.headers[group].description ? new Element('span.description', {
'html': self.headers[group].description
}) : null,
self.headers[group].content ? self.headers[group].content : null
).inject(form);
}
var tab_navigation = tabs.getElement('.t_'+group);
if(tab_navigation && group_container){
tab_navigation.inject(tabs); // Tab navigation
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
if(self.headers[group]){
var a = tab_navigation.getElement('a');
a.set('text', (self.headers[group].label || group).capitalize());
var url_split = a.get('href').split('wizard')[1].split('/');
if(url_split.length > 3)
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
}
}
else {
new Element('li.t_'+group).adopt(
new Element('a', {
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
).inject(tabs);
}
if(self.headers[group] && self.headers[group].event)
self.headers[group].event.call()
});
// Remove toggle
self.el.getElement('.advanced_toggle').destroy();
// Hide retention
self.el.getElement('.tab_searcher').hide();
self.el.getElement('.t_searcher').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
self.groups.each(function(group, nr){
var g = self.el.getElement('.wgroup_'+group);
if(!g || !g.isVisible()) return;
var t = self.el.getElement('.t_'+group);
if(!t) return;
var func = function(){
var ct = t.getCoordinates();
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
g.tween('opacity', 1);
}
if(nr == 0)
func();
var ss = new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2);
return top > minimum ? minimum : top
},
max: function(){
var c = g.getCoordinates();
return c.top+(c.height/2)
},
onEnter: func,
onLeave: function(){
g.tween('opacity', 0.2)
}
});
});
}
Page.Wizard = new Class({
Extends: Page.Settings,
name: 'wizard',
has_tab: false,
wizard_only: true,
headers: {
'welcome': {
'title': 'Welcome to the new CouchPotato',
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
'content': new Element('div', {
'styles': {
'margin': '0 0 0 30px'
}
}).adopt(
new Element('div', {
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
}),
self.import_iframe = new Element('iframe', {
'styles': {
'height': 40,
'width': 300,
'border': 0,
'overflow': 'hidden'
}
})
),
'event': function(){
self.import_iframe.set('src', Api.createUrl('v1.import'))
}
},
'general': {
'title': 'General',
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
},
'downloaders': {
'title': 'What download apps are you using?',
'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use Blackhole.'
},
'providers': {
'title': 'Are you registered at any of these sites?',
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.',
'include': ['nzb_providers', 'torrent_providers']
},
'renamer': {
'title': 'Move & rename the movies after downloading?',
'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
},
'automation': {
'title': 'Easily add movies to your wanted list!',
'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' +
'<br />Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
'content': function(){
return App.createUserscriptButtons().setStyles({
'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
})
}
},
'finish': {
'title': 'Finishing Up',
'description': 'Are you done? Did you fill in everything as much as possible?' +
'<br />Be sure to check the settings to see what more CP can do!<br /><br />' +
'<div class="wizard_support">After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. <br />Or by getting a subscription at <a href="https://usenetserver.com/partners/?a_aid=couchpotato&a_bid=3f357c6f">Usenet Server</a> or <a href="http://www.newshosting.com/partners/?a_aid=couchpotato&a_bid=a0b022df">Newshosting</a>.</div>',
'content': new Element('div').adopt(
new Element('a.button.green', {
'styles': {
'margin-top': 20
},
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
'events': {
'click': function(e){
(e).preventDefault();
Api.request('settings.save', {
'data': {
'section': 'core',
'name': 'show_wizard',
'value': 0
},
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(){
window.location = App.createUrl();
}
});
}
}
})
)
}
},
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'automation', 'finish'],
open: function(action, params){
var self = this;
if(!self.initialized){
App.fireEvent('unload');
App.getBlock('header').hide();
self.parent(action, params);
self.addEvent('create', function(){
self.order();
});
self.initialized = true;
self.scroll = new Fx.Scroll(document.body, {
'transition': 'quint:in:out'
});
}
else
(function(){
var sc = self.el.getElement('.wgroup_'+action);
self.scroll.start(0, sc.getCoordinates().top-80);
}).delay(1)
},
order: function(){
var self = this;
var form = self.el.getElement('.uniForm');
var tabs = self.el.getElement('.tabs');
self.groups.each(function(group, nr){
if(self.headers[group]){
group_container = new Element('.wgroup_'+group, {
'styles': {
'opacity': 0.2
},
'tween': {
'duration': 350
}
});
if(self.headers[group].include){
self.headers[group].include.each(function(inc){
group_container.addClass('wgroup_'+inc);
})
}
var content = self.headers[group].content
group_container.adopt(
new Element('h1', {
'text': self.headers[group].title
}),
self.headers[group].description ? new Element('span.description', {
'html': self.headers[group].description
}) : null,
content ? (typeOf(content) == 'function' ? content() : content) : null
).inject(form);
}
var tab_navigation = tabs.getElement('.t_'+group);
if(!tab_navigation && self.headers[group] && self.headers[group].include){
tab_navigation = []
self.headers[group].include.each(function(inc){
tab_navigation.include(tabs.getElement('.t_'+inc));
})
}
if(tab_navigation && group_container){
tabs.adopt(tab_navigation); // Tab navigation
if(self.headers[group] && self.headers[group].include){
self.headers[group].include.each(function(inc){
self.el.getElement('.tab_'+inc).inject(group_container);
})
new Element('li.t_'+group).adopt(
new Element('a', {
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
).inject(tabs);
}
else
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
if(tab_navigation.getElement && self.headers[group]){
var a = tab_navigation.getElement('a');
a.set('text', (self.headers[group].label || group).capitalize());
var url_split = a.get('href').split('wizard')[1].split('/');
if(url_split.length > 3)
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
}
}
else {
new Element('li.t_'+group).adopt(
new Element('a', {
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
).inject(tabs);
}
if(self.headers[group] && self.headers[group].event)
self.headers[group].event.call()
});
// Remove toggle
self.el.getElement('.advanced_toggle').destroy();
// Hide retention
self.el.getElement('.tab_searcher').hide();
self.el.getElement('.t_searcher').hide();
self.el.getElement('.t_nzb_providers').hide();
self.el.getElement('.t_torrent_providers').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
self.groups.each(function(group, nr){
var g = self.el.getElement('.wgroup_'+group);
if(!g || !g.isVisible()) return;
var t = self.el.getElement('.t_'+group);
if(!t) return;
var func = function(){
var ct = t.getCoordinates();
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
g.tween('opacity', 1);
}
if(nr == 0)
func();
var ss = new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2);
return top > minimum ? minimum : top
},
max: function(){
var c = g.getCoordinates();
return c.top+(c.height/2)
},
onEnter: func,
onLeave: function(){
g.tween('opacity', 0.2)
}
});
});
}
});

View File

@@ -28,9 +28,18 @@ class Automation(Plugin):
return self.getIMDBids()
def search(self, name, year = None, imdb_only = False):
prop_name = 'automation.cached.%s.%s' % (name, year)
cached_imdb = Env.prop(prop_name, default = False)
if cached_imdb and imdb_only:
return cached_imdb
result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True)
if len(result) > 0:
if imdb_only and result[0].get('imdb'):
Env.prop(prop_name, result[0].get('imdb'))
return result[0].get('imdb') if imdb_only else result[0]
else:
return None

View File

@@ -2,7 +2,6 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)

View File

@@ -2,9 +2,6 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
import time
import traceback
import xml.etree.ElementTree as XMLTree
@@ -34,10 +31,6 @@ class IMDB(Automation, RSS):
log.error('This isn\'t the correct url.: %s', rss_url)
continue
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
last_movie_added = 0
try:
cache_key = 'imdb.rss.%s' % md5(rss_url)
@@ -46,20 +39,10 @@ class IMDB(Automation, RSS):
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
imdb = getImdb(self.getTextElement(movie, "link"))
if created > last_movie_added:
last_movie_added = created
if not imdb or created <= last_update:
continue
movies.append(imdb)
except:
log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, last_movie_added)
return movies

View File

@@ -3,10 +3,7 @@ from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
from xml.etree.ElementTree import ParseError
import time
import traceback
import xml.etree.ElementTree as XMLTree
@@ -33,10 +30,6 @@ class MoviesIO(Automation, RSS):
if not enablers[index]:
continue
prop_name = 'automation.moviesio.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0))
last_movie_added = 0
try:
cache_key = 'imdb.rss.%s' % md5(rss_url)
@@ -45,12 +38,6 @@ class MoviesIO(Automation, RSS):
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
if created > last_movie_added:
last_movie_added = created
if created <= last_update:
continue
nameyear = fireEvent('scanner.name_year', self.getTextElement(movie, "title"), single = True)
imdb = self.search(nameyear.get('name'), nameyear.get('year'), imdb_only = True)
@@ -64,6 +51,4 @@ class MoviesIO(Automation, RSS):
except:
log.error('Failed loading Movies.io watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, last_movie_added)
return movies

View File

@@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
import os
import shutil
import traceback
@@ -16,23 +17,23 @@ class MetaDataBase(Plugin):
def __init__(self):
addEvent('renamer.after', self.create)
def create(self, release):
def create(self, message = None, group = {}):
if self.isDisabled(): return
log.info('Creating %s metadata.', self.getName())
# Update library to get latest info
try:
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True)
release['library'] = mergeDicts(release['library'], updated_library)
updated_library = fireEvent('library.update', group['library']['identifier'], force = True, single = True)
group['library'] = mergeDicts(group['library'], updated_library)
except:
log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
root_name = self.getRootName(release)
root_name = self.getRootName(group)
meta_name = os.path.basename(root_name)
root = os.path.dirname(root_name)
movie_info = release['library'].get('info')
movie_info = group['library'].get('info')
for file_type in ['nfo', 'thumbnail', 'fanart']:
try:
@@ -42,13 +43,19 @@ class MetaDataBase(Plugin):
if name and self.conf('meta_' + file_type):
# Get file content
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release)
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = group)
if content:
log.debug('Creating %s file: %s', (file_type, name))
if os.path.isfile(content):
shutil.copy2(content, name)
else:
self.createFile(name, content)
group['renamed_files'].append(name)
try:
os.chmod(name, Env.getPermission('file'))
except:
log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc()))
except:
log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
@@ -74,9 +81,18 @@ class MetaDataBase(Plugin):
if file_type.get('identifier') == wanted_file_type:
break
# See if it is in current files
for cur_file in data['library'].get('files', []):
if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
return cur_file.get('path')
# Download using existing info
try:
images = data['library']['info']['images'][wanted_file_type]
file_path = fireEvent('file.download', url = images[0], single = True)
return file_path
except:
pass
def getFanart(self, movie_info = {}, data = {}):
return self.getThumbnail(movie_info = movie_info, data = data, wanted_file_type = 'backdrop_original')

View File

@@ -70,7 +70,6 @@ class MovieResultModifier(Plugin):
except:
log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
#db.close()
return temp
def checkLibrary(self, result):

View File

@@ -1,36 +1,71 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie
from flask.helpers import json
import time
import traceback
log = CPLog(__name__)
class CouchPotatoApi(MovieProvider):
api_url = 'http://couchpota.to/api/%s/'
urls = {
'search': 'https://couchpota.to/api/search/%s/',
'info': 'https://couchpota.to/api/info/%s/',
'eta': 'https://couchpota.to/api/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/%s/%s/',
}
http_time_between_calls = 0
api_version = 1
def __init__(self):
#addApiView('movie.suggest', self.suggestView)
addEvent('movie.info', self.getInfo)
addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate)
def search(self, q, limit = 12):
cache_key = 'cpapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode(q), timeout = 3, headers = self.getRequestHeaders())
if cached:
try:
movies = json.loads(cached)
return movies
except:
log.error('Failed parsing search results: %s', traceback.format_exc())
return []
def getInfo(self, identifier = None):
return {
'release_date': self.getReleaseDate(identifier)
}
if not identifier:
return
cache_key = 'cpapi.cache.info.%s' % identifier
cached = self.getCache(cache_key, self.urls['info'] % identifier, timeout = 3, headers = self.getRequestHeaders())
if cached:
try:
movie = json.loads(cached)
return movie
except:
log.error('Failed parsing info results: %s', traceback.format_exc())
return {}
def getReleaseDate(self, identifier = None):
if identifier is None: return {}
try:
headers = {'X-CP-Version': fireEvent('app.version', single = True)}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
data = self.urlopen(self.urls['eta'] % identifier, headers = self.getRequestHeaders())
dates = json.loads(data)
log.debug('Found ETA for %s: %s', (identifier, dates))
return dates
@@ -41,7 +76,7 @@ class CouchPotatoApi(MovieProvider):
def suggest(self, movies = [], ignore = []):
try:
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
data = self.urlopen(self.urls['suggest'] % (','.join(movies), ','.join(ignore)))
suggestions = json.loads(data)
log.info('Found Suggestions for %s', (suggestions))
except Exception, e:
@@ -59,7 +94,6 @@ class CouchPotatoApi(MovieProvider):
db = get_session()
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
movies = [x.library.identifier for x in active_movies]
#db.close()
suggestions = self.suggest(movies, ignore)
@@ -68,3 +102,10 @@ class CouchPotatoApi(MovieProvider):
'count': len(suggestions),
'suggestions': suggestions
})
def getRequestHeaders(self):
return {
'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': self.api_version,
'X-CP-Time': time.time(),
}

View File

@@ -1,6 +1,6 @@
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, tryFloat
from couchpotato.core.helpers.variable import tryInt, tryFloat, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
import json
@@ -13,8 +13,8 @@ log = CPLog(__name__)
class IMDBAPI(MovieProvider):
urls = {
'search': 'http://www.imdbapi.com/?tomatoes=true&%s',
'info': 'http://www.imdbapi.com/?tomatoes=true&i=%s',
'search': 'http://www.imdbapi.com/?%s',
'info': 'http://www.imdbapi.com/?i=%s',
}
http_time_between_calls = 0
@@ -27,8 +27,10 @@ class IMDBAPI(MovieProvider):
name_year = fireEvent('scanner.name_year', q, single = True)
if not q or not name_year or (name_year and not name_year.get('name')):
return []
if not name_year or (name_year and not name_year.get('name')):
name_year = {
'name': q
}
cache_key = 'imdbapi.cache.%s' % q
cached = self.getCache(cache_key, self.urls['search'] % tryUrlencode({'t': name_year.get('name'), 'y': name_year.get('year', '')}), timeout = 3)
@@ -90,17 +92,17 @@ class IMDBAPI(MovieProvider):
},
'rating': {
'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))),
'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', '').replace(',', ''))),
},
'imdb': str(movie.get('imdbID', '')),
'runtime': self.runtimeToMinutes(movie.get('Runtime', '')),
'released': movie.get('Released', ''),
'year': year if isinstance(year, (int)) else None,
'plot': movie.get('Plot', ''),
'genres': movie.get('Genre', '').split(','),
'directors': movie.get('Director', '').split(','),
'writers': movie.get('Writer', '').split(','),
'actors': movie.get('Actors', '').split(','),
'genres': splitString(movie.get('Genre', '')),
'directors': splitString(movie.get('Director', '')),
'writers': splitString(movie.get('Writer', '')),
'actors': splitString(movie.get('Actors', '')),
}
except:
log.error('Failed parsing IMDB API json: %s', traceback.format_exc())

View File

@@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider):
def __init__(self):
addEvent('movie.by_hash', self.byHash)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 2)
addEvent('movie.info', self.getInfo, priority = 2)
addEvent('movie.info_by_tmdb', self.getInfoByTMDBId)
# Use base wrapper

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'Mysterbin',
'description': 'Free provider, less accurate. See <a href="http://www.mysterbin.com/">Mysterbin</a>',
'description': 'Free provider, less accurate. See <a href="https://www.mysterbin.com/">Mysterbin</a>',
'options': [
{
'name': 'enabled',

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'newzbin',
'description': 'See <a href="https://www.newzbin2.es/">Newzbin</a>',
'wizard': True,

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'newznab',
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a> and <a href="http://nzbs.org" target="_blank">nzbs.org</a>',
'order': 10,
'description': 'Enable multiple NewzNab providers such as <a href="https://nzb.su" target="_blank">NZB.su</a> and <a href="https://nzbs.org" target="_blank">nzbs.org</a>',
'wizard': True,
'options': [
{

View File

@@ -1,7 +1,7 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env
@@ -161,9 +161,9 @@ class Newznab(NZBProvider, RSS):
def getHosts(self):
uses = [x.strip() for x in str(self.conf('use')).split(',')]
hosts = [x.strip() for x in self.conf('host').split(',')]
api_keys = [x.strip() for x in self.conf('api_key').split(',')]
uses = splitString(str(self.conf('use')))
hosts = splitString(self.conf('host'))
api_keys = splitString(self.conf('api_key'))
list = []
for nr in range(len(hosts)):

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'NZBClub',
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
'options': [

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'nzbindex',
'description': 'Free provider, less accurate. See <a href="http://www.nzbindex.nl/">NZBIndex</a>',
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
'options': [
{
'name': 'enabled',

View File

@@ -19,8 +19,8 @@ log = CPLog(__name__)
class NzbIndex(NZBProvider, RSS):
urls = {
'download': 'http://www.nzbindex.nl/download/',
'api': 'http://www.nzbindex.nl/rss/',
'download': 'https://www.nzbindex.com/download/',
'api': 'https://www.nzbindex.com/rss/',
}
http_time_between_calls = 1 # Seconds

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'nzbmatrix',
'label': 'NZBMatrix',
'description': 'See <a href="https://nzbmatrix.com/">NZBMatrix</a>',

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'nzb_providers',
'name': 'nzbsrus',
'label': 'Nzbsrus',
'description': 'See <a href="https://www.nzbsrus.com/">NZBsRus</a>',

View File

@@ -8,9 +8,10 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'torrent_providers',
'name': 'KickAssTorrents',
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>',
'wizard': True,
'options': [
{
'name': 'enabled',

View File

@@ -1,6 +1,7 @@
from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import re
@@ -12,9 +13,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentProvider):
urls = {
'test': 'http://kat.ph/',
'detail': 'http://kat.ph/%s',
'search': 'http://kat.ph/i%s/',
'test': 'https://kat.ph/',
'detail': 'https://kat.ph/%s',
'search': 'https://kat.ph/%s-i%s/',
}
cat_ids = [
@@ -35,8 +36,10 @@ class KickAssTorrents(TorrentProvider):
if self.isDisabled():
return results
title = simplifyString(getTitle(movie['library'])).replace(' ', '-')
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['identifier'].replace('tt', '')))
data = self.getCache(cache_key, self.urls['search'] % (title, movie['library']['identifier'].replace('tt', '')))
if data:
cat_ids = self.getCatId(quality['identifier'])

View File

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

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'torrent_providers',
'name': 'PublicHD',
'description': 'Public Torrent site with only HD content. See <a href="http://publichd.eu/">PublicHD</a>',
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.eu/">PublicHD</a>',
'options': [
{
'name': 'enabled',

View File

@@ -14,9 +14,9 @@ log = CPLog(__name__)
class PublicHD(TorrentProvider):
urls = {
'test': 'http://publichd.eu',
'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s',
'search': 'http://publichd.eu/index.php',
'test': 'https://publichd.eu',
'detail': 'https://publichd.eu/index.php?page=torrent-details&id=%s',
'search': 'https://publichd.eu/index.php',
}
http_time_between_calls = 0
@@ -24,7 +24,7 @@ class PublicHD(TorrentProvider):
results = []
if self.isDisabled() or quality['hd'] != True:
if self.isDisabled() or not quality.get('hd', False):
return results
params = tryUrlencode({

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'torrent_providers',
'name': 'SceneAccess',
'description': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>',
'options': [

View File

@@ -8,9 +8,9 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'torrent_providers',
'name': 'SceneHD',
'description': 'See <a href="http://scenehd.org">SceneHD</a>',
'description': 'See <a href="https://scenehd.org">SceneHD</a>',
'options': [
{
'name': 'enabled',

View File

@@ -12,11 +12,11 @@ log = CPLog(__name__)
class SceneHD(TorrentProvider):
urls = {
'test': 'http://scenehd.org/',
'login' : 'http://scenehd.org/takelogin.php',
'detail': 'http://scenehd.org/details.php?id=%s',
'search': 'http://scenehd.org/browse.php?ajax',
'download': 'http://scenehd.org/download.php?id=%s',
'test': 'https://scenehd.org/',
'login' : 'https://scenehd.org/takelogin.php',
'detail': 'https://scenehd.org/details.php?id=%s',
'search': 'https://scenehd.org/browse.php?ajax',
'download': 'https://scenehd.org/download.php?id=%s',
}
http_time_between_calls = 1 #seconds

View File

@@ -5,23 +5,26 @@ def start():
config = [{
'name': 'thepiratebay',
'groups': [{
'tab': 'searcher',
'subtab': 'providers',
'name': 'ThePirateBay',
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
}
],
}]
'groups': [
{
'tab': 'searcher',
'subtab': 'torrent_providers',
'name': 'ThePirateBay',
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False
},
{
'name': 'domain',
'advanced': True,
'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
}
],
}
]
}]

View File

@@ -8,7 +8,7 @@ config = [{
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'subtab': 'torrent_providers',
'name': 'TorrentLeech',
'description': 'See <a href="http://torrentleech.org">TorrentLeech</a>',
'options': [

View File

@@ -4,6 +4,7 @@ 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
log = CPLog(__name__)
@@ -22,7 +23,12 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library'])
url = self.urls['api'] % self.movieUrlName(movie_name)
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url)
try:
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url, show_error = False)
except HTTPError:
log.debug('No page found for: %s', movie_name)
data = None
result_data = {'480p':[], '720p':[], '1080p':[]}
if not data:
@@ -47,7 +53,14 @@ class HDTrailers(TrailerProvider):
movie_name = getTitle(group['library'])
url = "%s?%s" % (self.urls['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)
try:
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url, show_error = False)
except HTTPError:
log.debug('No alternative page found for: %s', movie_name)
data = None
if not data:
return results
try:
tables = SoupStrainer('div')

View File

@@ -204,7 +204,6 @@ class Settings(object):
except:
pass
#db.close()
return prop
def setProperty(self, identifier, value = ''):
@@ -221,4 +220,3 @@ class Settings(object):
p.value = toUnicode(value)
db.commit()
#db.close()

View File

@@ -170,18 +170,17 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
log.warning('%s %s %s line:%s', (category, message, filename, lineno))
warnings.showwarning = customwarn
# Check if database exists
db = Env.get('db_path')
db_exists = os.path.isfile(db_path)
# Load configs & plugins
loader = Env.get('loader')
loader.preload(root = base_path)
loader.run()
# Load migrations
initialize = True
db = Env.get('db_path')
if os.path.isfile(db_path):
initialize = False
if db_exists:
from migrate.versioning.api import version_control, db_version, version, upgrade
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
@@ -201,7 +200,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from couchpotato.core.settings.model import setup
setup()
if initialize:
# Fill database with needed stuff
if not db_exists:
fireEvent('app.initialize', in_order = True)
# Create app

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 B

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 435 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 151 B

Some files were not shown because too many files have changed in this diff Show More