Browse Source

Merge remote-tracking branch 'RuudBurger/develop' into tv

Conflicts:
	couchpotato/core/plugins/quality/main.py
pull/3730/merge
Dean Gardiner 11 years ago
parent
commit
f7ed5d4b2f
  1. 13
      CouchPotato.py
  2. 18
      README.md
  3. 7
      couchpotato/__init__.py
  4. 5
      couchpotato/api.py
  5. 8
      couchpotato/core/_base/_core.py
  6. 1
      couchpotato/core/_base/downloader/main.py
  7. 4
      couchpotato/core/_base/scheduler.py
  8. 35
      couchpotato/core/_base/updater/main.py
  9. 153
      couchpotato/core/database.py
  10. 1
      couchpotato/core/downloaders/rtorrent_.py
  11. 9
      couchpotato/core/downloaders/transmission.py
  12. 73
      couchpotato/core/helpers/variable.py
  13. 21
      couchpotato/core/media/_base/media/index.py
  14. 113
      couchpotato/core/media/_base/media/main.py
  15. 1
      couchpotato/core/media/_base/providers/nzb/binsearch.py
  16. 13
      couchpotato/core/media/_base/providers/nzb/newznab.py
  17. 1
      couchpotato/core/media/_base/providers/nzb/nzbclub.py
  18. 1
      couchpotato/core/media/_base/providers/nzb/nzbindex.py
  19. 1
      couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py
  20. 5
      couchpotato/core/media/_base/providers/torrent/awesomehd.py
  21. 3
      couchpotato/core/media/_base/providers/torrent/bithdtv.py
  22. 8
      couchpotato/core/media/_base/providers/torrent/bitsoup.py
  23. 4
      couchpotato/core/media/_base/providers/torrent/hdbits.py
  24. 1
      couchpotato/core/media/_base/providers/torrent/ilovetorrents.py
  25. 3
      couchpotato/core/media/_base/providers/torrent/iptorrents.py
  26. 3
      couchpotato/core/media/_base/providers/torrent/kickasstorrents.py
  27. 3
      couchpotato/core/media/_base/providers/torrent/passthepopcorn.py
  28. 136
      couchpotato/core/media/_base/providers/torrent/publichd.py
  29. 3
      couchpotato/core/media/_base/providers/torrent/sceneaccess.py
  30. 3
      couchpotato/core/media/_base/providers/torrent/thepiratebay.py
  31. 3
      couchpotato/core/media/_base/providers/torrent/torrentbytes.py
  32. 3
      couchpotato/core/media/_base/providers/torrent/torrentday.py
  33. 3
      couchpotato/core/media/_base/providers/torrent/torrentleech.py
  34. 1
      couchpotato/core/media/_base/providers/torrent/torrentpotato.py
  35. 10
      couchpotato/core/media/_base/providers/torrent/torrentshack.py
  36. 3
      couchpotato/core/media/_base/providers/torrent/torrentz.py
  37. 3
      couchpotato/core/media/_base/providers/torrent/yify.py
  38. 17
      couchpotato/core/media/movie/_base/main.py
  39. 2
      couchpotato/core/media/movie/_base/static/movie.actions.js
  40. 26
      couchpotato/core/media/movie/_base/static/movie.css
  41. 19
      couchpotato/core/media/movie/_base/static/movie.js
  42. 3
      couchpotato/core/media/movie/library.py
  43. 4
      couchpotato/core/media/movie/providers/automation/imdb.py
  44. 14
      couchpotato/core/media/movie/providers/automation/moviemeter.py
  45. 13
      couchpotato/core/media/movie/providers/info/fanarttv.py
  46. 4
      couchpotato/core/media/movie/providers/info/themoviedb.py
  47. 14
      couchpotato/core/media/movie/providers/torrent/publichd.py
  48. 7
      couchpotato/core/media/movie/providers/trailer/hdtrailers.py
  49. 31
      couchpotato/core/media/movie/searcher.py
  50. 1
      couchpotato/core/media/movie/suggestion/main.py
  51. 69
      couchpotato/core/notifications/boxcar.py
  52. 7
      couchpotato/core/notifications/core/static/notification.js
  53. 11
      couchpotato/core/notifications/pushbullet.py
  54. 17
      couchpotato/core/notifications/pushover.py
  55. 6
      couchpotato/core/notifications/xbmc.py
  56. 2
      couchpotato/core/plugins/base.py
  57. 2
      couchpotato/core/plugins/browser.py
  58. 34
      couchpotato/core/plugins/file.py
  59. 46
      couchpotato/core/plugins/manage.py
  60. 4
      couchpotato/core/plugins/profile/main.py
  61. 5
      couchpotato/core/plugins/profile/static/profile.css
  62. 27
      couchpotato/core/plugins/profile/static/profile.js
  63. 86
      couchpotato/core/plugins/quality/main.py
  64. 12
      couchpotato/core/plugins/quality/static/quality.js
  65. 57
      couchpotato/core/plugins/release/main.py
  66. 116
      couchpotato/core/plugins/renamer.py
  67. 17
      couchpotato/core/plugins/scanner.py
  68. 2
      couchpotato/core/plugins/trailer.py
  69. 10
      couchpotato/core/settings.py
  70. 8
      couchpotato/environment.py
  71. 36
      couchpotato/runner.py
  72. 15
      couchpotato/static/scripts/couchpotato.js
  73. 3
      couchpotato/static/scripts/page/home.js
  74. 25
      couchpotato/static/scripts/page/settings.js
  75. 34
      couchpotato/static/style/settings.css
  76. 6
      couchpotato/templates/index.html
  77. 28
      libs/unrar2/__init__.py
  78. 30
      libs/unrar2/unix.py
  79. 24
      libs/unrar2/windows.py

13
CouchPotato.py

@ -19,7 +19,12 @@ base_path = dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(base_path, 'libs')) sys.path.insert(0, os.path.join(base_path, 'libs'))
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.variable import getDataDir from couchpotato.core.helpers.variable import getDataDir, removePyc
# Remove pyc files before dynamic load (sees .pyc files regular .py modules)
removePyc(base_path)
class Loader(object): class Loader(object):
@ -67,10 +72,11 @@ class Loader(object):
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
addEvent('app.after_shutdown', self.afterShutdown) addEvent('app.do_shutdown', self.setRestart)
def afterShutdown(self, restart): def setRestart(self, restart):
self.do_restart = restart self.do_restart = restart
return True
def onExit(self, signal, frame): def onExit(self, signal, frame):
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
@ -98,7 +104,6 @@ class Loader(object):
# Release log files and shutdown logger # Release log files and shutdown logger
logging.shutdown() logging.shutdown()
time.sleep(3)
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:] args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
subprocess.Popen(args) subprocess.Popen(args)

18
README.md

@ -29,17 +29,21 @@ OS X:
* Then do `python CouchPotatoServer/CouchPotato.py` * Then do `python CouchPotatoServer/CouchPotato.py`
* Your browser should open up, but if it doesn't go to `http://localhost:5050/` * Your browser should open up, but if it doesn't go to `http://localhost:5050/`
Linux (Ubuntu / Debian): Linux:
* Install [GIT](http://git-scm.com/) with `apt-get install git-core` * (Ubuntu / Debian) Install [GIT](http://git-scm.com/) with `apt-get install git-core`
* (Fedora / CentOS) Install [GIT](http://git-scm.com/) with `yum install git`
* 'cd' to the folder of your choosing. * 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git` * Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then do `python CouchPotatoServer/CouchPotato.py` to start * Then do `python CouchPotatoServer/CouchPotato.py` to start
* To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato` * (Ubuntu / Debian) To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
* Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato` * (Ubuntu / Debian) Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
* Change the paths inside the default file `sudo nano /etc/default/couchpotato` * (Ubuntu / Debian) Change the paths inside the default file `sudo nano /etc/default/couchpotato`
* Make it executable `sudo chmod +x /etc/init.d/couchpotato` * (Ubuntu / Debian) Make it executable `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults `sudo update-rc.d couchpotato defaults` * (Ubuntu / Debian) Add it to defaults `sudo update-rc.d couchpotato defaults`
* (systemd) To run on boot copy the systemd config `sudo cp CouchPotatoServer/init/couchpotato.fedora.service /etc/systemd/system/couchpotato.service`
* (systemd) Update the systemd config file with your user and path to CouchPotato.py
* (systemd) Enable it at boot with `sudo systemctl enable couchpotato`
* Open your browser and go to `http://localhost:5050/` * Open your browser and go to `http://localhost:5050/`

7
couchpotato/__init__.py

@ -1,3 +1,7 @@
import os
import time
import traceback
from couchpotato.api import api_docs, api_docs_missing, api from couchpotato.api import api_docs, api_docs_missing, api
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import md5, tryInt from couchpotato.core.helpers.variable import md5, tryInt
@ -5,9 +9,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from tornado import template from tornado import template
from tornado.web import RequestHandler, authenticated from tornado.web import RequestHandler, authenticated
import os
import time
import traceback
log = CPLog(__name__) log = CPLog(__name__)

5
couchpotato/api.py

@ -89,8 +89,13 @@ class ApiHandler(RequestHandler):
route = route.strip('/') route = route.strip('/')
if not api.get(route): if not api.get(route):
self.write('API call doesn\'t seem to exist') self.write('API call doesn\'t seem to exist')
self.finish()
return return
# Create lock if it doesn't exist
if route in api_locks and not api_locks.get(route):
api_locks[route] = threading.Lock()
api_locks[route].acquire() api_locks[route].acquire()
try: try:

8
couchpotato/core/_base/_core.py

@ -118,7 +118,7 @@ class Core(Plugin):
self.shutdown_started = True self.shutdown_started = True
fireEvent('app.do_shutdown') fireEvent('app.do_shutdown', restart = restart)
log.debug('Every plugin got shutdown event') log.debug('Every plugin got shutdown event')
loop = True loop = True
@ -143,9 +143,11 @@ class Core(Plugin):
log.debug('Safe to shutdown/restart') log.debug('Safe to shutdown/restart')
loop = IOLoop.current()
try: try:
if not IOLoop.current()._closing: if not loop._closing:
IOLoop.current().stop() loop.stop()
except RuntimeError: except RuntimeError:
pass pass
except: except:

1
couchpotato/core/_base/downloader/main.py

@ -25,6 +25,7 @@ class DownloaderBase(Provider):
status_support = True status_support = True
torrent_sources = [ torrent_sources = [
'https://zoink.it/torrent/%s.torrent',
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'https://torcache.net/torrent/%s.torrent', 'https://torcache.net/torrent/%s.torrent',
] ]

4
couchpotato/core/_base/scheduler.py

@ -33,9 +33,9 @@ class Scheduler(Plugin):
except: except:
pass pass
def doShutdown(self): def doShutdown(self, *args, **kwargs):
self.stop() self.stop()
return super(Scheduler, self).doShutdown() return super(Scheduler, self).doShutdown(*args, **kwargs)
def stop(self): def stop(self):
if self.started: if self.started:

35
couchpotato/core/_base/updater/main.py

@ -11,6 +11,7 @@ from threading import RLock
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import removePyc
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@ -141,11 +142,11 @@ class Updater(Plugin):
'success': success 'success': success
} }
def doShutdown(self): def doShutdown(self, *args, **kwargs):
if not Env.get('dev'): if not Env.get('dev') and not Env.get('desktop'):
self.updater.deletePyc(show_logs = False) removePyc(Env.get('app_dir'), show_logs = False)
return super(Updater, self).doShutdown() return super(Updater, self).doShutdown(*args, **kwargs)
class BaseUpdater(Plugin): class BaseUpdater(Plugin):
@ -181,30 +182,6 @@ class BaseUpdater(Plugin):
def check(self): def check(self):
pass pass
def deletePyc(self, only_excess = True, show_logs = True):
for root, dirs, files in os.walk(Env.get('app_dir')):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
if show_logs: log.debug('Removing old PYC file: %s', full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
class GitUpdater(BaseUpdater): class GitUpdater(BaseUpdater):
@ -328,7 +305,7 @@ class SourceUpdater(BaseUpdater):
data_dir = Env.get('data_dir') data_dir = Env.get('data_dir')
# Get list of files we want to overwrite # Get list of files we want to overwrite
self.deletePyc() removePyc(app_dir)
existing_files = [] existing_files = []
for root, subfiles, filenames in os.walk(app_dir): for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames: for filename in filenames:

153
couchpotato/core/database.py

@ -3,10 +3,12 @@ import os
import time import time
import traceback import traceback
from CodernityDB.database import RecordNotFound
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
from couchpotato import CPLog from couchpotato import CPLog
from couchpotato.api import addApiView 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 toUnicode from couchpotato.core.helpers.encoding import toUnicode, sp
from couchpotato.core.helpers.variable import getImdb, tryInt from couchpotato.core.helpers.variable import getImdb, tryInt
@ -15,18 +17,22 @@ log = CPLog(__name__)
class Database(object): class Database(object):
indexes = [] indexes = None
db = None db = None
def __init__(self): def __init__(self):
self.indexes = {}
addApiView('database.list_documents', self.listDocuments) addApiView('database.list_documents', self.listDocuments)
addApiView('database.reindex', self.reindex) addApiView('database.reindex', self.reindex)
addApiView('database.compact', self.compact) addApiView('database.compact', self.compact)
addApiView('database.document.update', self.updateDocument) addApiView('database.document.update', self.updateDocument)
addApiView('database.document.delete', self.deleteDocument) addApiView('database.document.delete', self.deleteDocument)
addEvent('database.setup.after', self.startup_compact)
addEvent('database.setup_index', self.setupIndex) addEvent('database.setup_index', self.setupIndex)
addEvent('app.migrate', self.migrate) addEvent('app.migrate', self.migrate)
addEvent('app.after_shutdown', self.close) addEvent('app.after_shutdown', self.close)
@ -43,26 +49,45 @@ class Database(object):
def setupIndex(self, index_name, klass): def setupIndex(self, index_name, klass):
self.indexes.append(index_name) self.indexes[index_name] = klass
db = self.getDB() db = self.getDB()
# Category index # Category index
index_instance = klass(db.path, index_name) index_instance = klass(db.path, index_name)
try: try:
db.add_index(index_instance)
db.reindex_index(index_name) # Make sure store and bucket don't exist
except: exists = []
previous = db.indexes_names[index_name] for x in ['buck', 'stor']:
previous_version = previous._version full_path = os.path.join(db.path, '%s_%s' % (index_name, x))
current_version = klass._version if os.path.exists(full_path):
exists.append(full_path)
# Only edit index if versions are different
if previous_version < current_version: if index_name not in db.indexes_names:
log.debug('Index "%s" already exists, updating and reindexing', index_name)
db.destroy_index(previous) # Remove existing buckets if index isn't there
for x in exists:
os.unlink(x)
# Add index (will restore buckets)
db.add_index(index_instance) db.add_index(index_instance)
db.reindex_index(index_name) db.reindex_index(index_name)
else:
# Previous info
previous = db.indexes_names[index_name]
previous_version = previous._version
current_version = klass._version
# Only edit index if versions are different
if previous_version < current_version:
log.debug('Index "%s" already exists, updating and reindexing', index_name)
db.destroy_index(previous)
db.add_index(index_instance)
db.reindex_index(index_name)
except:
log.error('Failed adding index %s: %s', (index_name, traceback.format_exc()))
def deleteDocument(self, **kwargs): def deleteDocument(self, **kwargs):
@ -136,20 +161,108 @@ class Database(object):
'success': success 'success': success
} }
def compact(self, **kwargs): def compact(self, try_repair = True, **kwargs):
success = False
db = self.getDB()
# Removing left over compact files
db_path = sp(db.path)
for f in os.listdir(sp(db.path)):
for x in ['_compact_buck', '_compact_stor']:
if f[-len(x):] == x:
os.unlink(os.path.join(db_path, f))
success = True
try: try:
db = self.getDB() start = time.time()
size = float(db.get_db_details().get('size', 0))
log.debug('Compacting database, current size: %sMB', round(size/1048576, 2))
db.compact() db.compact()
new_size = float(db.get_db_details().get('size', 0))
log.debug('Done compacting database in %ss, new size: %sMB, saved: %sMB', (round(time.time()-start, 2), round(new_size/1048576, 2), round((size-new_size)/1048576, 2)))
success = True
except (IndexException, AttributeError):
if try_repair:
log.error('Something wrong with indexes, trying repair')
# Remove all indexes
old_indexes = self.indexes.keys()
for index_name in old_indexes:
try:
db.destroy_index(index_name)
except IndexNotFoundException:
pass
except:
log.error('Failed removing old index %s', index_name)
# Add them again
for index_name in self.indexes:
klass = self.indexes[index_name]
# Category index
index_instance = klass(db.path, index_name)
try:
db.add_index(index_instance)
db.reindex_index(index_name)
except IndexConflict:
pass
except:
log.error('Failed adding index %s', index_name)
raise
self.compact(try_repair = False)
else:
log.error('Failed compact: %s', traceback.format_exc())
except: except:
log.error('Failed compact: %s', traceback.format_exc()) log.error('Failed compact: %s', traceback.format_exc())
success = False
return { return {
'success': success 'success': success
} }
# Compact on start
def startup_compact(self):
from couchpotato import Env
db = self.getDB()
# Try fix for migration failures on desktop
if Env.get('desktop'):
try:
list(db.all('profile', with_doc = True))
except RecordNotFound:
failed_location = '%s_failed' % db.path
old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db.old')
if not os.path.isdir(failed_location) and os.path.isfile(old_db):
log.error('Corrupt database, trying migrate again')
db.close()
# Rename database folder
os.rename(db.path, '%s_failed' % db.path)
# Rename .old database to try another migrate
os.rename(old_db, old_db[:-4])
fireEventAsync('app.restart')
else:
log.error('Migration failed and couldn\'t recover database. Please report on GitHub, with this message.')
db.reindex()
return
# Check size and compact if needed
size = db.get_db_details().get('size')
prop_name = 'last_db_compact'
last_check = int(Env.prop(prop_name, default = 0))
if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days
self.compact()
Env.prop(prop_name, value = int(time.time()))
def migrate(self): def migrate(self):
from couchpotato import Env from couchpotato import Env
@ -219,6 +332,8 @@ class Database(object):
log.info('Getting data took %s', time.time() - migrate_start) log.info('Getting data took %s', time.time() - migrate_start)
db = self.getDB() db = self.getDB()
if not db.opened:
return
# Use properties # Use properties
properties = migrate_data['properties'] properties = migrate_data['properties']

1
couchpotato/core/downloaders/rtorrent_.py

@ -5,7 +5,6 @@ from urlparse import urlparse
import os import os
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.helpers.variable import cleanHost, splitString

9
couchpotato/core/downloaders/transmission.py

@ -23,17 +23,14 @@ class Transmission(DownloaderBase):
log = CPLog(__name__) log = CPLog(__name__)
trpc = None trpc = None
def connect(self, reconnect = False): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if not self.trpc or reconnect: self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
return self.trpc
def download(self, data = None, media = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not media: media = {} if not media: media = {}
@ -88,7 +85,7 @@ class Transmission(DownloaderBase):
return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def test(self): def test(self):
if self.connect(True) and self.trpc.get_session(): if self.connect() and self.trpc.get_session():
return True return True
return False return False

73
couchpotato/core/helpers/variable.py

@ -1,4 +1,5 @@
import collections import collections
import ctypes
import hashlib import hashlib
import os import os
import platform import platform
@ -6,8 +7,9 @@ import random
import re import re
import string import string
import sys import sys
import traceback
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import six import six
from six.moves import map, zip, filter from six.moves import map, zip, filter
@ -290,9 +292,14 @@ def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()]) return all([k in b and b[k] == v for k, v in a.items()])
# Returns True if sub_folder is the same as or inside base_folder
def isSubFolder(sub_folder, base_folder): def isSubFolder(sub_folder, base_folder):
# Returns True if sub_folder is the same as or inside base_folder if base_folder and sub_folder:
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep) base = sp(os.path.realpath(base_folder)) + os.path.sep
subfolder = sp(os.path.realpath(sub_folder)) + os.path.sep
return os.path.commonprefix([subfolder, base]) == base
return False
# From SABNZBD # From SABNZBD
@ -313,3 +320,63 @@ under_pat = re.compile(r'_([a-z])')
def underscoreToCamel(name): def underscoreToCamel(name):
return under_pat.sub(lambda x: x.group(1).upper(), name) return under_pat.sub(lambda x: x.group(1).upper(), name)
def removePyc(folder, only_excess = True, show_logs = True):
folder = sp(folder)
for root, dirs, files in os.walk(folder):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file)
if show_logs: log.debug('Removing old PYC file: %s', full_path)
try:
os.remove(full_path)
except:
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs:
full_path = os.path.join(root, dir_name)
if len(os.listdir(full_path)) == 0:
try:
os.rmdir(full_path)
except:
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
def getFreeSpace(directories):
single = not isinstance(directories, (tuple, list))
if single:
directories = [directories]
free_space = {}
for folder in directories:
size = None
if os.path.isdir(folder):
if os.name == 'nt':
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
ctypes.c_ulonglong()
if sys.version_info >= (3,) or isinstance(folder, unicode):
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
else:
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
if ret == 0:
raise ctypes.WinError()
return [total.value, free.value]
else:
s = os.statvfs(folder)
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
if single: return size
free_space[folder] = size
return free_space

21
couchpotato/core/media/_base/media/index.py

@ -176,3 +176,24 @@ class MediaChildrenIndex(TreeBasedIndex):
if data.get('_t') == 'media' and data.get('parent_id'): if data.get('_t') == 'media' and data.get('parent_id'):
return data.get('parent_id'), None return data.get('parent_id'), None
class MediaTagIndex(MultiTreeBasedIndex):
_version = 2
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
def __init__(self, *args, **kwargs):
kwargs['key_format'] = '32s'
super(MediaTagIndex, self).__init__(*args, **kwargs)
def make_key_value(self, data):
if data.get('_t') == 'media' and data.get('tags') and len(data.get('tags', [])) > 0:
tags = set()
for tag in data.get('tags', []):
tags.add(self.make_key(tag))
return list(tags), None
def make_key(self, key):
return md5(key).hexdigest()

113
couchpotato/core/media/_base/media/main.py

@ -1,7 +1,10 @@
from datetime import timedelta
from operator import itemgetter
import time
import traceback import traceback
from string import ascii_lowercase from string import ascii_lowercase
from CodernityDB.database import RecordNotFound from CodernityDB.database import RecordNotFound, RecordDeleted
from couchpotato import tryInt, get_db from couchpotato import tryInt, get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
@ -9,7 +12,7 @@ from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString, getImdb, getTitle from couchpotato.core.helpers.variable import splitString, getImdb, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
log = CPLog(__name__) log = CPLog(__name__)
@ -21,6 +24,7 @@ class MediaPlugin(MediaBase):
'media': MediaIndex, 'media': MediaIndex,
'media_search_title': TitleSearchIndex, 'media_search_title': TitleSearchIndex,
'media_status': MediaStatusIndex, 'media_status': MediaStatusIndex,
'media_tag': MediaTagIndex,
'media_by_type': MediaTypeIndex, 'media_by_type': MediaTypeIndex,
'media_title': TitleIndex, 'media_title': TitleIndex,
'media_startswith': StartsWithIndex, 'media_startswith': StartsWithIndex,
@ -81,6 +85,8 @@ class MediaPlugin(MediaBase):
addEvent('media.list', self.list) addEvent('media.list', self.list)
addEvent('media.delete', self.delete) addEvent('media.delete', self.delete)
addEvent('media.restatus', self.restatus) addEvent('media.restatus', self.restatus)
addEvent('media.tag', self.tag)
addEvent('media.untag', self.unTag)
def refresh(self, id = '', **kwargs): def refresh(self, id = '', **kwargs):
handlers = [] handlers = []
@ -140,7 +146,7 @@ class MediaPlugin(MediaBase):
return media return media
except RecordNotFound: except (RecordNotFound, RecordDeleted):
log.error('Media with id "%s" not found', media_id) log.error('Media with id "%s" not found', media_id)
except: except:
raise raise
@ -161,8 +167,15 @@ class MediaPlugin(MediaBase):
status = list(status if isinstance(status, (list, tuple)) else [status]) status = list(status if isinstance(status, (list, tuple)) else [status])
for s in status: for s in status:
for ms in db.get_many('media_status', s, with_doc = with_doc): for ms in db.get_many('media_status', s):
yield ms['doc'] if with_doc else ms if with_doc:
try:
doc = db.get('id', ms['_id'])
yield doc
except RecordNotFound:
log.debug('Record not found, skipping: %s', ms['_id'])
else:
yield ms
def withIdentifiers(self, identifiers, with_doc = False): def withIdentifiers(self, identifiers, with_doc = False):
@ -177,7 +190,7 @@ class MediaPlugin(MediaBase):
log.debug('No media found with identifiers: %s', identifiers) log.debug('No media found with identifiers: %s', identifiers)
def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, starts_with = None, search = None): def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None):
db = get_db() db = get_db()
@ -188,6 +201,8 @@ class MediaPlugin(MediaBase):
release_status = [release_status] release_status = [release_status]
if types and not isinstance(types, (list, tuple)): if types and not isinstance(types, (list, tuple)):
types = [types] types = [types]
if with_tags and not isinstance(with_tags, (list, tuple)):
with_tags = [with_tags]
# query media ids # query media ids
if types: if types:
@ -214,11 +229,17 @@ class MediaPlugin(MediaBase):
# Add search filters # Add search filters
if starts_with: if starts_with:
filter_by['starts_with'] = set()
starts_with = toUnicode(starts_with.lower())[0] starts_with = toUnicode(starts_with.lower())[0]
starts_with = starts_with if starts_with in ascii_lowercase else '#' starts_with = starts_with if starts_with in ascii_lowercase else '#'
filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)] filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)]
# Add tag filter
if with_tags:
filter_by['with_tags'] = set()
for tag in with_tags:
for x in db.get_many('media_tag', tag):
filter_by['with_tags'].add(x['_id'])
# Filter with search query # Filter with search query
if search: if search:
filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)] filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)]
@ -271,6 +292,7 @@ class MediaPlugin(MediaBase):
release_status = splitString(kwargs.get('release_status')), release_status = splitString(kwargs.get('release_status')),
status_or = kwargs.get('status_or') is not None, status_or = kwargs.get('status_or') is not None,
limit_offset = kwargs.get('limit_offset'), limit_offset = kwargs.get('limit_offset'),
with_tags = splitString(kwargs.get('with_tags')),
starts_with = kwargs.get('starts_with'), starts_with = kwargs.get('starts_with'),
search = kwargs.get('search') search = kwargs.get('search')
) )
@ -389,16 +411,18 @@ class MediaPlugin(MediaBase):
total_deleted += 1 total_deleted += 1
new_media_status = 'done' new_media_status = 'done'
elif delete_from == 'manage': elif delete_from == 'manage':
if release.get('status') == 'done': if release.get('status') == 'done' or media.get('status') == 'done':
db.delete(release) db.delete(release)
total_deleted += 1 total_deleted += 1
if (total_releases == total_deleted and media['status'] != 'active') or (delete_from == 'wanted' and media['status'] == 'active') or (not new_media_status and delete_from == 'late'): if (total_releases == total_deleted and media['status'] != 'active') or (total_releases == 0 and not new_media_status) or (not new_media_status and delete_from == 'late'):
db.delete(media) db.delete(media)
deleted = True deleted = True
elif new_media_status: elif new_media_status:
media['status'] = new_media_status media['status'] = new_media_status
db.update(media) db.update(media)
fireEvent('media.untag', media['_id'], 'recent', single = True)
else: else:
fireEvent('media.restatus', media.get('_id'), single = True) fireEvent('media.restatus', media.get('_id'), single = True)
@ -438,24 +462,75 @@ class MediaPlugin(MediaBase):
if not m['profile_id']: if not m['profile_id']:
m['status'] = 'done' m['status'] = 'done'
else: else:
move_to_wanted = True m['status'] = 'active'
profile = db.get('id', m['profile_id']) try:
media_releases = fireEvent('release.for_media', m['_id'], single = True) profile = db.get('id', m['profile_id'])
media_releases = fireEvent('release.for_media', m['_id'], single = True)
done_releases = [release for release in media_releases if release.get('status') == 'done']
for q_identifier in profile['qualities']: if done_releases:
index = profile['qualities'].index(q_identifier) # Only look at latest added release
release = sorted(done_releases, key = itemgetter('last_edit'), reverse = True)[0]
for release in media_releases: # Check if we are finished with the media
if q_identifier == release['quality'] and (release.get('status') == 'done' and profile['finish'][index]): if fireEvent('quality.isfinish', {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, timedelta(seconds = time.time() - release['last_edit']).days, single = True):
move_to_wanted = False m['status'] = 'done'
elif previous_status == 'done':
m['status'] = 'done'
m['status'] = 'active' if move_to_wanted else 'done' except RecordNotFound:
log.debug('Failed restatus, keeping previous: %s', traceback.format_exc())
m['status'] = previous_status
# Only update when status has changed # Only update when status has changed
if previous_status != m['status']: if previous_status != m['status']:
db.update(m) db.update(m)
return True # Tag media as recent
self.tag(media_id, 'recent', update_edited = True)
return m['status']
except: except:
log.error('Failed restatus: %s', traceback.format_exc()) log.error('Failed restatus: %s', traceback.format_exc())
def tag(self, media_id, tag, update_edited = False):
try:
db = get_db()
m = db.get('id', media_id)
if update_edited:
m['last_edit'] = int(time.time())
tags = m.get('tags') or []
if tag not in tags:
tags.append(tag)
m['tags'] = tags
db.update(m)
return True
except:
log.error('Failed tagging: %s', traceback.format_exc())
return False
def unTag(self, media_id, tag):
try:
db = get_db()
m = db.get('id', media_id)
tags = m.get('tags') or []
if tag in tags:
new_tags = list(set(tags))
new_tags.remove(tag)
m['tags'] = new_tags
db.update(m)
return True
except:
log.error('Failed untagging: %s', traceback.format_exc())
return False

1
couchpotato/core/media/_base/providers/nzb/binsearch.py

@ -100,6 +100,7 @@ config = [{
'name': 'binsearch', 'name': 'binsearch',
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>', 'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAATklEQVQY02NwQAMMWAXOnz+PKvD//3/CAvM//z+fgiwAAs+RBab4PP//vwbFjPlAffgEChzOo2r5fBuIfRAC5w8D+QUofkkp8MHjOWQAAM3Sbogztg2wAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

13
couchpotato/core/media/_base/providers/nzb/newznab.py

@ -220,8 +220,9 @@ config = [{
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \ 'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \ <a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \ <a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>', <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@ -230,30 +231,30 @@ config = [{
}, },
{ {
'name': 'use', 'name': 'use',
'default': '0,0,0,0,0,0' 'default': '0,0,0,0,0'
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws', 'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://api.nzbgeek.info,https://www.nzbfinder.ws',
'description': 'The hostname of your newznab provider', 'description': 'The hostname of your newznab provider',
}, },
{ {
'name': 'extra_score', 'name': 'extra_score',
'advanced': True, 'advanced': True,
'label': 'Extra Score', 'label': 'Extra Score',
'default': '0,0,0,0,0,0', 'default': '0,0,0,0,0',
'description': 'Starting score for each release found via this provider.', 'description': 'Starting score for each release found via this provider.',
}, },
{ {
'name': 'custom_tag', 'name': 'custom_tag',
'advanced': True, 'advanced': True,
'label': 'Custom tag', 'label': 'Custom tag',
'default': ',,,,,', 'default': ',,,,',
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org', 'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
}, },
{ {
'name': 'api_key', 'name': 'api_key',
'default': ',,,,,', 'default': ',,,,',
'label': 'Api Key', 'label': 'Api Key',
'description': 'Can be found on your profile page', 'description': 'Can be found on your profile page',
'type': 'combined', 'type': 'combined',

1
couchpotato/core/media/_base/providers/nzb/nzbclub.py

@ -80,6 +80,7 @@ config = [{
'name': 'NZBClub', 'name': 'NZBClub',
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>', 'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACEUlEQVQ4y3VSMWgUQRR9/8/s7OzeJSdnTsVGghLEYBNQjBpQiRBFhIB2EcHG1kbs0murhZAmVocExEZQ0c7CxkLINYcJJpoYj9wZcnu72fF21uJSXMzuhyne58/j/fcf4b+KokgBIOSU53lxP5b9oNVqDT36dH+5UjoiKvIwPFEEgWBshGZ3E7/NOupL9fMjx0e+ZhKsrq+c/FPZKJi0w4FsQXMBDEJsd7BNW9h2tuyP9vfTALIJkMIu1hYRtINM+dpzcWc0sbkreK4fUEogyraAmKGF3+7vcT/wtR9QwkCabSAzQQuvk0uglAo5YaQ5DASGYjfMXcHVOqKu6NmR7iehlKAdHWUqWPv1c3i+9uwVdRlEBGaGEAJCCrDo9ShhvF6qPq8tL57bp+DbRn2sHtUuCY9YphLMu5921VhrwYJ5tbt0tt6sjQP4vEfB2Ikz7/ytwbeR6ljHkXCUA6UcOLtPOg4MYhtH8ZcLw5er+xQMDAwEURRNl96X596Y6oxFwsw9fmtTOAr2Ik19nL365FZpsLSdnQPPM8aYewc+lDcX4rkHqbQMAGTJXulOLzycmr1bKBTi3DOGYagajcahiaOT89fbM0/dxEsUu3aidfPljWO3HzebzYNBELi5Z5RSJlrrHd/3w8lT114MrVTWOn875fHRiYVisRhorWMpZXdvNnLKGCOstb0AMlulVJI19w/+nceU4D0aCwAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

1
couchpotato/core/media/_base/providers/nzb/nzbindex.py

@ -105,6 +105,7 @@ config = [{
'name': 'nzbindex', 'name': 'nzbindex',
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>', 'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAo0lEQVR42t2SQQ2AMBAEcUCwUAv94QMLfHliAQtYqIVawEItYAG6yZFMLkUANNlk79Kbbtp2P1j9uKxVV9VWFeStl+Wh3fWK9hNwEoADZkJtMD49AqS5AUjWGx6A+m+ARICGrM5W+wSTB0gETKzdHZwCEZAJ8PGZQN4AiQAmkR9s06EBAugJiBoAAPFfAQcBgZcIHzwA6TYP4JsXeSg3P9L31w3eksbH3zMb/wAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

1
couchpotato/core/media/_base/providers/nzb/omgwtfnzbs.py

@ -74,6 +74,7 @@ config = [{
'name': 'OMGWTFNZBs', 'name': 'OMGWTFNZBs',
'description': 'See <a href="http://omgwtfnzbs.org/">OMGWTFNZBs</a>', 'description': 'See <a href="http://omgwtfnzbs.org/">OMGWTFNZBs</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAIAAADAAbR1AAADbElEQVR4AZ2UW0ybZRiAy/OvdHaLYvB0YTRIFi7GkM44zRLmIfNixkWdiRMyYoxRE8/TC7MYvXCGEBmr3mxLwVMwY0wYA7e6Wso4lB6h/U9taSlMGIfBXLYlJMyo0S///2dJI5lxN8/F2/f9nu9737e/jYmXr6KTbN9BGG9HE/NotQ76UWziNzrXFiETk/5ARUNH+7+0kW7fSgTl0VKGOLZzidOkmuuIo7q2oTArNLPIzhdIkqXkerFOm2CaD/5bcKrjIL2c3fkhPxOq93Kcb91v46fV9TQKF4TgV/TbUsQtzfCaK6jMOd5DJrguSIIhexmqqVxN0FXbRR8/ND/LYTTj6J7nl2gnL47OkDW4KJhnQHCa6JpKVNJGA3OC58nwBJoZ//ebbIyKpBxjrr0o1q1FMRkrKXZnHWF85VvxMrJxibwhGyd0f5bLnKzqJs1k0Sfo+EU8hdAUvkbcwKEgs2D0OiV4jmmD1zb+Tp6er0JMMvDxPo5xev9zTBF683NS+N56n1YiB95B5crr93KRuKhKI0tb0Kw2mgLLqTjLEWO8424i9IvURaYeOckwf3+/yCC9e3bQQ/MuD+Monk0k+XFXMUfx7z5EEP+XlXi5tLlMxH8zLppw7idJrugcus30kC86gc7UrQqjLIukM8zWHOACeU+TiMxXN6ExVOkgz4lvPEzice1GIVhxhG4CrZvpl6TH55giKWqXGLy9hZh5aUtgDSew/msSyCKpl+DDNfxJc8NBIsxUxUnz14O/oONu+IIIvso9TLBQ1SY5rUhuSzUhAqJ2mRXBLDOCeUtgUZXsaObT8BffhUJPqWgiV+3zKKzYH0ClvTRLhD77HIqVkyh5jThnivehoG+qJctIRSPn6bxvO4FCgTl9c1DmbpjLajbQFE8aW5SU3rg+zOPGUjTUF9NFpLEbH2c/KmGYlY69/GQJVtGMSUcEp9eCbB1nctbxHTLRdTUkGDf+B02uGWRG3OvpJ/zSMwzif+oxVBID3cQKBavLCiPmB2PM2UuSCUPgrX4VDb97AwEG67bh4+KTOlncvu3M31BwA5rLHbCfEjwkNDky9e/SSbSxnD46Pg0RJtpXRvhmBSZHpRjWtKwFybjuQeXaKxto4WjLZZZvVmC17pZLJFkwxm5++PS2Mrwc7nyIMYZe/IzoP5d6QgEybqTXAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

5
couchpotato/core/media/_base/providers/torrent/awesomehd.py

@ -61,7 +61,7 @@ class Base(TorrentProvider):
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)), 'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')), 'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
'detail_url': self.urls['detail'] % torrent_id, 'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(entry.find('size').get_text()), 'size': tryInt(entry.find('size').get_text()) / 1048576,
'seeders': tryInt(entry.find('seeders').get_text()), 'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()), 'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore 'score': torrentscore
@ -78,8 +78,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'Awesome-HD', 'name': 'Awesome-HD',
'description': 'See <a href="https://awesome-hd.net">AHD</a>', 'description': '<a href="https://awesome-hd.net">AHD</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/bithdtv.py

@ -93,8 +93,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'BiT-HDTV', 'name': 'BiT-HDTV',
'description': 'See <a href="http://bit-hdtv.com">BiT-HDTV</a>', 'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

8
couchpotato/core/media/_base/providers/torrent/bitsoup.py

@ -1,6 +1,6 @@
import traceback import traceback
from bs4 import BeautifulSoup from bs4 import BeautifulSoup, SoupStrainer
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
@ -20,6 +20,7 @@ class Base(TorrentProvider):
} }
http_time_between_calls = 1 # Seconds http_time_between_calls = 1 # Seconds
only_tables_tags = SoupStrainer('table')
def _searchOnTitle(self, title, movie, quality, results): def _searchOnTitle(self, title, movie, quality, results):
@ -27,7 +28,7 @@ class Base(TorrentProvider):
data = self.getHTMLData(url) data = self.getHTMLData(url)
if data: if data:
html = BeautifulSoup(data) html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
try: try:
result_table = html.find('table', attrs = {'class': 'koptekst'}) result_table = html.find('table', attrs = {'class': 'koptekst'})
@ -87,8 +88,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'Bitsoup', 'name': 'Bitsoup',
'description': 'See <a href="https://bitsoup.me">Bitsoup</a>', 'description': '<a href="https://bitsoup.me">Bitsoup</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

4
couchpotato/core/media/_base/providers/torrent/hdbits.py

@ -71,7 +71,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'HDBits', 'name': 'HDBits',
'description': 'See <a href="http://hdbits.org">HDBits</a>', 'wizard': True,
'description': '<a href="http://hdbits.org">HDBits</a>',
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

1
couchpotato/core/media/_base/providers/torrent/ilovetorrents.py

@ -146,6 +146,7 @@ config = [{
'name': 'ILoveTorrents', 'name': 'ILoveTorrents',
'description': 'Where the Love of Torrents is Born', 'description': 'Where the Love of Torrents is Born',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPUlEQVR4AYWM0U9SbxjH3+v266I/oNvWZTfd2J1d0ZqbZEFwWrUImOKs4YwtumFKZvvlJJADR2TCQQlMPKg5NmpREgaekAPnBATKgmK1LqQlx6awHnZWF1Tr2Xfvvs+7z+dB0mlO7StpAh+M4S/2jbo3w8+xvJvlnSneEt+10zwer5ujNUOoChjALWFw5XOwdCAk/P57cGvPl+Oht0W7VJHN5NC1uW1BON4hGjXbwpVWMZhsy9v7sEIXAsDNYBXgdkEoIKyWD2CF8ut/aOXTZc/fBSgLWw1BgA4BDHOV0GkT90cBQpXahU5TFomsb38XhJC5/Tbh1P8c6rJlBeGfAeyMhUFwNVcs9lxV9Ot0dwmyd+mrNvRtbJ2fSPC6Z3Vsvub2z3sDFACAAYzk0+kUyxEkyfN7PopqNBro55A+P6yPKIrL5zF1HwjdeBJJCObIsZO79bo3sHhWhglo5WMV3mazuVPb4fLvSL8/FAkB1hK6rXQPwYhMyROK8VK5LAiH/jsMt0HQjxiN4/ePdoilllcqDyt3Mkg8mRBNbIhMb8RERkowQA/p76g0/UDDdCoNmDminM0qSK5vlpE5kugCHhNPxntwWmJPYTMZtYcFR6ABHQsVRlYLukVORaaULvqKI46keFSCv77kSPS6kxrPptLNDHgz16fWBtyxe6v5h08LUy+KI8ushqTPWWIX8Sg6b45IrGtyW6zXFb/hpQf9m3oqfWuB0fpSw0uZ4WB69En69uOk2rmO2V52PXj+A/mI4ESKpb2HAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/iptorrents.py

@ -120,8 +120,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'IPTorrents', 'name': 'IPTorrents',
'description': 'See <a href="http://www.iptorrents.com">IPTorrents</a>', 'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/kickasstorrents.py

@ -132,8 +132,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'KickAssTorrents', 'name': 'KickAssTorrents',
'description': 'See <a href="https://kat.ph/">KickAssTorrents</a>', 'description': '<a href="https://kat.ph/">KickAssTorrents</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/passthepopcorn.py

@ -187,8 +187,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'PassThePopcorn', 'name': 'PassThePopcorn',
'description': 'See <a href="https://passthepopcorn.me">PassThePopcorn.me</a>', 'description': '<a href="https://passthepopcorn.me">PassThePopcorn.me</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAARklEQVQoz2NgIAP8BwMiGWRpIN1JNWn/t6T9f532+W8GkNt7vzz9UkfarZVpb68BuWlbnqW1nU7L2DMx7eCoBlpqGOppCQB83zIgIg+wWQAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

136
couchpotato/core/media/_base/providers/torrent/publichd.py

@ -1,136 +0,0 @@
from urlparse import parse_qs
import re
import traceback
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
import six
log = CPLog(__name__)
class Base(TorrentMagnetProvider):
urls = {
'test': 'https://publichd.se',
'detail': 'https://publichd.se/index.php?page=torrent-details&id=%s',
'search': 'https://publichd.se/index.php',
}
http_time_between_calls = 0
def search(self, movie, quality):
if not quality.get('hd', False):
return []
return super(Base, self).search(movie, quality)
def _search(self, media, quality, results):
query = self.buildUrl(media)
params = tryUrlencode({
'page': 'torrents',
'search': query,
'active': 1,
})
data = self.getHTMLData('%s?%s' % (self.urls['search'], params))
if data:
try:
soup = BeautifulSoup(data)
results_table = soup.find('table', attrs = {'id': 'bgtorrlist2'})
entries = results_table.find_all('tr')
for result in entries[2:len(entries) - 1]:
info_url = result.find(href = re.compile('torrent-details'))
download = result.find(href = re.compile('magnet:'))
if info_url and download:
url = parse_qs(info_url['href'])
results.append({
'id': url['id'][0],
'name': six.text_type(info_url.string),
'url': download['href'],
'detail_url': self.urls['detail'] % url['id'][0],
'size': self.parseSize(result.find_all('td')[7].string),
'seeders': tryInt(result.find_all('td')[4].string),
'leechers': tryInt(result.find_all('td')[5].string),
'get_more_info': self.getMoreInfo
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def getMoreInfo(self, item):
cache_key = 'publichd.%s' % item['id']
description = self.getCache(cache_key)
if not description:
try:
full_description = self.urlopen(item['detail_url'])
html = BeautifulSoup(full_description)
nfo_pre = html.find('div', attrs = {'id': 'torrmain'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
except:
log.error('Failed getting more info for %s', item['name'])
description = ''
self.setCache(cache_key, description, timeout = 25920000)
item['description'] = description
return item
config = [{
'name': 'publichd',
'groups': [
{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'PublicHD',
'description': 'Public Torrent site with only HD content. See <a href="https://publichd.se/">PublicHD</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': True,
},
{
'name': 'seed_ratio',
'label': 'Seed ratio',
'type': 'float',
'default': 1,
'description': 'Will not be (re)moved until this seed ratio is met.',
},
{
'name': 'seed_time',
'label': 'Seed time',
'type': 'int',
'default': 40,
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]

3
couchpotato/core/media/_base/providers/torrent/sceneaccess.py

@ -89,8 +89,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'SceneAccess', 'name': 'SceneAccess',
'description': 'See <a href="https://sceneaccess.eu/">SceneAccess</a>', 'description': '<a href="https://sceneaccess.eu/">SceneAccess</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/thepiratebay.py

@ -129,8 +129,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'ThePirateBay', 'name': 'ThePirateBay',
'description': 'The world\'s largest bittorrent tracker. See <a href="http://fucktimkuik.org/">ThePirateBay</a>', 'description': 'The world\'s largest bittorrent tracker. <a href="http://fucktimkuik.org/">ThePirateBay</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAA3UlEQVQY02P4DwT/YADIZvj//7qnozMYODmtAAusZoCDELDAegYGViZhAWZmRoYoqIDupfhNN1M3dTBEggXWMZg9jZRXV77YxhAOFpjDwMAPMoCXmcHsF1SAQZ6bQY2VgUEbKHClcAYzg3mINEO8jSCD478/DPsZmvqWblu1bOmStes3Pp0ezVDF4Gif0Hfx9///74/ObRZ2YNiZ47C8XIRBxFJR0jbSSUud4f9zAQWn8NTuziAt2zy5xIMM/z8LFX0E+fD/x0MRDCeA1v7Z++Y/FDzyvAtyBxIA+h8A8ZKLeT+lJroAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/torrentbytes.py

@ -90,8 +90,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentBytes', 'name': 'TorrentBytes',
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>', 'description': '<a href="http://torrentbytes.net">TorrentBytes</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/torrentday.py

@ -68,8 +68,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentDay', 'name': 'TorrentDay',
'description': 'See <a href="http://www.td.af/">TorrentDay</a>', 'description': '<a href="http://www.td.af/">TorrentDay</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/torrentleech.py

@ -80,8 +80,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentLeech', 'name': 'TorrentLeech',
'description': 'See <a href="http://torrentleech.org">TorrentLeech</a>', 'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

1
couchpotato/core/media/_base/providers/torrent/torrentpotato.py

@ -134,6 +134,7 @@ config = [{
'order': 10, 'order': 10,
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.', 'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABSElEQVR4AZ2Nz0oCURTGv8t1YMpqUxt9ARFxoQ/gQtppgvUKcu/sxB5iBJkogspaBC6iVUplEC6kv+oiiKDNhAtt16roP0HQgdsMLgaxfvy4nHP4Pi48qE2g4v91JOqT1CH/UnA7w7icUlLawyEdj+ZI/7h6YluWbRiddHonHh9M70aj7VTKzuXuikUMci/EO/ACnAI15599oAk8AR/AgxBQNCzreD7bmpl+FOIVuAHqQDUcJo+AK+CZFKLt95/MpSmMt0TiW9POxse6UvYZ6zB2wFgjFiNpOGesR0rZ0PVPXf8KhUCl22CwClz4eN8weoZBb9c0bdPsOWvHx/cYu9Y0CoNoZTJrwAbn5DrnZc6XOV+igVbnsgo0IxEomlJuA1vUIYGyq3PZBChwmExCUSmVZgMBDIUCK4UCFIv5vHIhm/XUDeAf/ADbcpd5+aXSWQAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

10
couchpotato/core/media/_base/providers/torrent/torrentshack.py

@ -48,9 +48,9 @@ class Base(TorrentProvider):
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}), 'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'], 'detail_url': self.urls['download'] % link['href'],
'size': self.parseSize(result.find_all('td')[4].string), 'size': self.parseSize(result.find_all('td')[5].string),
'seeders': tryInt(result.find_all('td')[6].string), 'seeders': tryInt(result.find_all('td')[7].string),
'leechers': tryInt(result.find_all('td')[7].string), 'leechers': tryInt(result.find_all('td')[8].string),
}) })
except: except:
@ -80,7 +80,9 @@ config = [{
'tab': 'searcher', 'tab': 'searcher',
'list': 'torrent_providers', 'list': 'torrent_providers',
'name': 'TorrentShack', 'name': 'TorrentShack',
'description': 'See <a href="https://www.torrentshack.net/">TorrentShack</a>', 'description': '<a href="https://www.torrentshack.net/">TorrentShack</a>',
'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

3
couchpotato/core/media/_base/providers/torrent/torrentz.py

@ -80,11 +80,12 @@ config = [{
'name': 'Torrentz', 'name': 'Torrentz',
'description': 'Torrentz is a free, fast and powerful meta-search engine. <a href="https://torrentz.eu/">Torrentz</a>', 'description': 'Torrentz is a free, fast and powerful meta-search engine. <a href="https://torrentz.eu/">Torrentz</a>',
'wizard': True, 'wizard': True,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQklEQVQ4y2NgAALjtJn/ycEMlGiGG0IVAxiwAKzOxaKGARcgxgC8YNSAwWoAzuRMjgsIugqfAUR5CZcBRIcHsWEAADSA96Ig020yAAAAAElFTkSuQmCC',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
'type': 'enabler', 'type': 'enabler',
'default': False 'default': True
}, },
{ {
'name': 'verified_only', 'name': 'verified_only',

3
couchpotato/core/media/_base/providers/torrent/yify.py

@ -49,7 +49,7 @@ class Base(TorrentMagnetProvider):
if result['Quality'] and result['Quality'] not in result['MovieTitle']: if result['Quality'] and result['Quality'] not in result['MovieTitle']:
title = result['MovieTitle'] + ' BrRip ' + result['Quality'] title = result['MovieTitle'] + ' BrRip ' + result['Quality']
else: else:
title = result['MovieTitle'] + ' BrRip' title = result['MovieTitle'] + ' BrRip'
results.append({ results.append({
@ -79,6 +79,7 @@ config = [{
'name': 'Yify', 'name': 'Yify',
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.', 'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
'wizard': False, 'wizard': False,
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACL0lEQVR4AS1SPW/UQBAd23fxne/Ld2dvzvHuzPocEBAKokCBqGiQ6IgACYmvUKRBFEQgKKGg4BAlUoggggYUEQpSHOI7CIEoQs/fYcbLaU/efTvvvZlnA1qydoxU5kcxX0CkgmQZtPy0hCUjvK+WgEByOZ5dns1O5bzna8fRVkgsxH8B0YouIvBhdD5T11NiVOoKrsttyUcpRW0InUrFnwe9HzuP2uaQZYhF2LQ76TTXw2RVMTK8mYYbjfh+zNquMVCrqn93aArLSixPxnafdGDLaz1tjY5rmNa8z5BczEQOxQfCl1GyoqoWxYRN1bkh7ELw3q/vhP6HIL4TG9KumpjgvwuyM7OsjSj98E/vszMfZ7xvPtMaWxGO5crwIumKCR5HxDtJ0AWKGG204RfUd/3smJYqwem/Q7BTS1ZGfM4LNpVwuKAz6cMeROst0S2EwNE7GjTehO2H3dxqIpdkydat15G3F8SXBi4GlpBNlSz012L/k2+W0CLLk/jbcf13rf41yJeMQ8QWUZiHCfCA9ad+81nEKPtoS9mJOf9v0NmMJHgUT6xayheK9EIK7JJeU/AF4scDF7Y5SPlJrRcxJ+um4ibNEdObxLiIwJim+eT2AL5D9CIcnZ5zvSJi9eIlNHVVtZ831dk5svPgvjPWTq+ktWkd/kD0qtm71x+sDQe3kt6DXnM7Ct+GajmTxKlkAokWljyAKSm5oWa2w+BH4P2UuVub7eTyiGOQYapY/wEztHduSDYz5gAAAABJRU5ErkJggg==',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',

17
couchpotato/core/media/movie/_base/main.py

@ -2,6 +2,7 @@ import os
import traceback import traceback
import time import time
from CodernityDB.database import RecordNotFound
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
@ -90,7 +91,7 @@ class MovieBase(MovieTypeBase):
# Default profile and category # Default profile and category
default_profile = {} default_profile = {}
if not params.get('profile_id'): if (not params.get('profile_id') and status != 'done') or params.get('ignore_previous', False):
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id') cat_id = params.get('category_id')
@ -117,8 +118,17 @@ class MovieBase(MovieTypeBase):
media['info'] = info media['info'] = info
new = False new = False
previous_profile = None
try: try:
m = db.get('media', 'imdb-%s' % params.get('identifier'), with_doc = True)['doc'] m = db.get('media', 'imdb-%s' % params.get('identifier'), with_doc = True)['doc']
try:
db.get('id', m.get('profile_id'))
previous_profile = m.get('profile_id')
except RecordNotFound:
pass
except:
log.error('Failed getting previous profile: %s', traceback.format_exc())
except: except:
new = True new = True
m = db.insert(media) m = db.insert(media)
@ -146,9 +156,10 @@ class MovieBase(MovieTypeBase):
else: else:
fireEvent('release.delete', release['_id'], single = True) fireEvent('release.delete', release['_id'], single = True)
m['profile_id'] = params.get('profile_id', default_profile.get('id')) m['profile_id'] = (params.get('profile_id') or default_profile.get('_id')) if not previous_profile else previous_profile
m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None) m['category_id'] = cat_id if cat_id is not None and len(cat_id) > 0 else (m.get('category_id') or None)
m['last_edit'] = int(time.time()) m['last_edit'] = int(time.time())
m['tags'] = []
do_search = True do_search = True
db.update(m) db.update(m)
@ -225,7 +236,7 @@ class MovieBase(MovieTypeBase):
db.update(m) db.update(m)
fireEvent('media.restatus', m['_id']) fireEvent('media.restatus', m['_id'], single = True)
m = db.get('id', media_id) m = db.get('id', media_id)

2
couchpotato/core/media/movie/_base/static/movie.actions.js

@ -302,7 +302,7 @@ MA.Release = new Class({
self.movie.data.releases.each(function(release){ self.movie.data.releases.each(function(release){
if(has_available && has_snatched) return; if(has_available && has_snatched) return;
if(['snatched', 'downloaded', 'seeding'].contains(release.status)) if(['snatched', 'downloaded', 'seeding', 'done'].contains(release.status))
has_snatched = true; has_snatched = true;
if(['available'].contains(release.status)) if(['available'].contains(release.status))

26
couchpotato/core/media/movie/_base/static/movie.css

@ -365,6 +365,32 @@
display: none; display: none;
} }
.movies .data .eta {
display: none;
}
.movies.details_list .data .eta {
position: absolute;
bottom: 0;
right: 0;
display: block;
min-height: 20px;
text-align: right;
font-style: italic;
opacity: .8;
font-size: 11px;
}
.movies.details_list .movie:hover .data .eta {
display: none;
}
.movies.thumbs_list .data .eta {
display: block;
position: absolute;
bottom: 40px;
}
.movies .data .quality { .movies .data .quality {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;

19
couchpotato/core/media/movie/_base/static/movie.js

@ -136,6 +136,21 @@ var Movie = new Class({
self.el.addClass('status_'+self.get('status')); self.el.addClass('status_'+self.get('status'));
var eta = null,
eta_date = null,
now = Math.round(+new Date()/1000);
if(self.data.info.release_date)
[self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){
if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now)))
eta = timestamp;
});
if(eta){
eta_date = new Date(eta * 1000);
eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear();
}
self.el.adopt( self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', { self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': { 'events': {
@ -161,6 +176,10 @@ var Movie = new Class({
self.description = new Element('div.description.tiny_scroll', { self.description = new Element('div.description.tiny_scroll', {
'text': self.data.info.plot 'text': self.data.info.plot
}), }),
self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', {
'text': eta_date,
'title': 'ETA'
}) : null,
self.quality = new Element('div.quality', { self.quality = new Element('div.quality', {
'events': { 'events': {
'click': function(e){ 'click': function(e){

3
couchpotato/core/media/movie/library.py

@ -1,4 +1,5 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.library.base import LibraryBase from couchpotato.core.media._base.library.base import LibraryBase
@ -17,7 +18,9 @@ class MovieLibraryPlugin(LibraryBase):
if media.get('type') != 'movie': if media.get('type') != 'movie':
return return
default_title = getTitle(media)
titles = media['info'].get('titles', []) titles = media['info'].get('titles', [])
titles.insert(0, default_title)
# Add year identifier to titles # Add year identifier to titles
if include_year: if include_year:

4
couchpotato/core/media/movie/providers/automation/imdb.py

@ -263,7 +263,7 @@ config = [{
'name': 'automation_charts_rentals', 'name': 'automation_charts_rentals',
'type': 'bool', 'type': 'bool',
'label': 'DVD Rentals', 'label': 'DVD Rentals',
'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals/">rentals</a> chart', 'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals">rentals</a> chart',
'default': True, 'default': True,
}, },
{ {
@ -312,7 +312,7 @@ config = [{
'name': 'chart_display_rentals', 'name': 'chart_display_rentals',
'type': 'bool', 'type': 'bool',
'label': 'DVD Rentals', 'label': 'DVD Rentals',
'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals/">rentals</a> chart', 'description': 'Top DVD <a href="http://www.imdb.com/boxoffice/rentals">rentals</a> chart',
'default': True, 'default': True,
}, },
{ {

14
couchpotato/core/media/movie/providers/automation/moviemeter.py

@ -21,11 +21,15 @@ class Moviemeter(Automation, RSS):
for movie in rss_movies: for movie in rss_movies:
name_year = fireEvent('scanner.name_year', self.getTextElement(movie, 'title'), single = True) title = self.getTextElement(movie, 'title')
imdb = self.search(name_year.get('name'), name_year.get('year')) name_year = fireEvent('scanner.name_year', title, single = True)
if name_year.get('name') and name_year.get('year'):
if imdb and self.isMinimalMovie(imdb): imdb = self.search(name_year.get('name'), name_year.get('year'))
movies.append(imdb['imdb'])
if imdb and self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
else:
log.error('Failed getting name and year from: %s', title)
return movies return movies

13
couchpotato/core/media/movie/providers/info/fanarttv.py

@ -14,7 +14,7 @@ autoload = 'FanartTV'
class FanartTV(MovieProvider): class FanartTV(MovieProvider):
urls = { urls = {
'api': 'http://api.fanart.tv/webservice/movie/b28b14e9be662e027cfbc7c3dd600405/%s/JSON/all/1/2' 'api': 'http://webservice.fanart.tv/v3/movies/%s?api_key=b28b14e9be662e027cfbc7c3dd600405'
} }
MAX_EXTRAFANART = 20 MAX_EXTRAFANART = 20
@ -36,9 +36,8 @@ class FanartTV(MovieProvider):
fanart_data = self.getJsonData(url) fanart_data = self.getJsonData(url)
if fanart_data: if fanart_data:
name, resource = fanart_data.items()[0] log.debug('Found images for %s', fanart_data.get('name'))
log.debug('Found images for %s', name) images = self._parseMovie(fanart_data)
images = self._parseMovie(resource)
except: except:
log.error('Failed getting extra art for %s: %s', log.error('Failed getting extra art for %s: %s',
@ -95,7 +94,7 @@ class FanartTV(MovieProvider):
for image in images: for image in images:
if tryInt(image.get('likes')) > highscore: if tryInt(image.get('likes')) > highscore:
highscore = tryInt(image.get('likes')) highscore = tryInt(image.get('likes'))
image_url = image.get('url') image_url = image.get('url') or image.get('href')
return image_url return image_url
@ -118,7 +117,9 @@ class FanartTV(MovieProvider):
if tryInt(image.get('likes')) > highscore: if tryInt(image.get('likes')) > highscore:
highscore = tryInt(image.get('likes')) highscore = tryInt(image.get('likes'))
best = image best = image
image_urls.append(best.get('url')) url = best.get('url') or best.get('href')
if url:
image_urls.append(url)
pool.remove(best) pool.remove(best)
return image_urls return image_urls

4
couchpotato/core/media/movie/providers/info/themoviedb.py

@ -153,8 +153,10 @@ class TheMovieDb(MovieProvider):
movie_data = dict((k, v) for k, v in movie_data.items() if v) movie_data = dict((k, v) for k, v in movie_data.items() if v)
# Add alternative names # Add alternative names
if movie_data['original_title'] and movie_data['original_title'] not in movie_data['titles']:
movie_data['titles'].append(movie_data['original_title'])
if extended: if extended:
movie_data['titles'].append(movie.originaltitle)
for alt in movie.alternate_titles: for alt in movie.alternate_titles:
alt_name = alt.title alt_name = alt.title
if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None: if alt_name and alt_name not in movie_data['titles'] and alt_name.lower() != 'none' and alt_name is not None:

14
couchpotato/core/media/movie/providers/torrent/publichd.py

@ -1,14 +0,0 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.event import fireEvent
from couchpotato.core.media._base.providers.torrent.publichd import Base
from couchpotato.core.media.movie.providers.base import MovieProvider
log = CPLog(__name__)
autoload = 'PublicHD'
class PublicHD(MovieProvider, Base):
def buildUrl(self, media):
return fireEvent('library.query', media, single = True).replace(':', '')

7
couchpotato/core/media/movie/providers/trailer/hdtrailers.py

@ -21,6 +21,7 @@ class HDTrailers(TrailerProvider):
'backup': 'http://www.hd-trailers.net/blog/', 'backup': 'http://www.hd-trailers.net/blog/',
} }
providers = ['apple.ico', 'yahoo.ico', 'moviefone.ico', 'myspace.ico', 'favicon.ico'] providers = ['apple.ico', 'yahoo.ico', 'moviefone.ico', 'myspace.ico', 'favicon.ico']
only_tables_tags = SoupStrainer('table')
def search(self, group): def search(self, group):
@ -67,8 +68,7 @@ class HDTrailers(TrailerProvider):
return results return results
try: try:
tables = SoupStrainer('div') html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
html = BeautifulSoup(data, parse_only = tables)
result_table = html.find_all('h2', text = re.compile(movie_name)) result_table = html.find_all('h2', text = re.compile(movie_name))
for h2 in result_table: for h2 in result_table:
@ -90,8 +90,7 @@ class HDTrailers(TrailerProvider):
results = {'480p':[], '720p':[], '1080p':[]} results = {'480p':[], '720p':[], '1080p':[]}
try: try:
tables = SoupStrainer('table') html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
html = BeautifulSoup(data, parse_only = tables)
result_table = html.find('table', attrs = {'class':'bottomTable'}) result_table = html.find('table', attrs = {'class':'bottomTable'})
for tr in result_table.find_all('tr'): for tr in result_table.find_all('tr'):

31
couchpotato/core/media/movie/searcher.py

@ -120,8 +120,19 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if not movie['profile_id'] or (movie['status'] == 'done' and not manual): if not movie['profile_id'] or (movie['status'] == 'done' and not manual):
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
fireEvent('media.restatus', movie['_id'], single = True)
return return
default_title = getTitle(movie)
if not default_title:
log.error('No proper info found for movie, removing it from library to stop it from causing more issues.')
fireEvent('media.delete', movie['_id'], single = True)
return
# Update media status and check if it is still not done (due to the stop searching after feature
if fireEvent('media.restatus', movie['_id'], single = True) == 'done':
log.debug('No better quality found, marking movie %s as done.', default_title)
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True) release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True)
@ -131,12 +142,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
outside_eta_results = 0 outside_eta_results = 0
alway_search = self.conf('always_search') alway_search = self.conf('always_search')
ignore_eta = manual ignore_eta = manual
total_result_count = 0
default_title = getTitle(movie)
if not default_title:
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
fireEvent('media.delete', movie['_id'], single = True)
return
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title)
@ -153,8 +159,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
profile = db.get('id', movie['profile_id']) profile = db.get('id', movie['profile_id'])
ret = False ret = False
index = 0 for index, q_identifier in enumerate(profile.get('qualities', [])):
for q_identifier in profile.get('qualities'):
quality_custom = { quality_custom = {
'index': index, 'index': index,
'quality': q_identifier, 'quality': q_identifier,
@ -163,8 +168,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'3d': profile['3d'][index] if profile.get('3d') else False '3d': profile['3d'][index] if profile.get('3d') else False
} }
index += 1
could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']) could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year'])
if not alway_search and could_not_be_released: if not alway_search and could_not_be_released:
too_early_to_search.append(q_identifier) too_early_to_search.append(q_identifier)
@ -188,7 +191,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
if has_better_quality > 0: if has_better_quality > 0:
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title)) log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title))
fireEvent('media.restatus', movie['_id']) fireEvent('media.restatus', movie['_id'], single = True)
break break
quality = fireEvent('quality.single', identifier = q_identifier, single = True) quality = fireEvent('quality.single', identifier = q_identifier, single = True)
@ -199,6 +202,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or []
results_count = len(results) results_count = len(results)
total_result_count += results_count
if results_count == 0: if results_count == 0:
log.debug('Nothing found for %s in %s', (default_title, quality['label'])) log.debug('Nothing found for %s in %s', (default_title, quality['label']))
@ -218,7 +222,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title))
# Try find a valid result and download it # Try find a valid result and download it
if (force_download or not could_not_be_released) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): if (force_download or not could_not_be_released or alway_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True):
ret = True ret = True
# Remove releases that aren't found anymore # Remove releases that aren't found anymore
@ -235,6 +239,9 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if self.shuttingDown() or ret: if self.shuttingDown() or ret:
break break
if total_result_count > 0:
fireEvent('media.tag', movie['_id'], 'recent', update_edited = True, single = True)
if len(too_early_to_search) > 0: if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))

1
couchpotato/core/media/movie/suggestion/main.py

@ -1,4 +1,3 @@
from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier from couchpotato.core.helpers.variable import splitString, removeDuplicate, getIdentifier

69
couchpotato/core/notifications/boxcar.py

@ -1,69 +0,0 @@
import time
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
autoload = 'Boxcar'
class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try:
message = message.strip()
data = {
'email': self.conf('email'),
'notification[from_screen_name]': self.default_title,
'notification[message]': toUnicode(message),
'notification[from_remote_service_id]': int(time.time()),
}
self.urlopen(self.url, data = data)
except:
log.error('Check your email and added services on boxcar.io')
return False
log.info('Boxcar notification successful.')
return True
def isEnabled(self):
return super(Boxcar, self).isEnabled() and self.conf('email')
config = [{
'name': 'boxcar',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'boxcar',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'email',
'description': 'Your Boxcar registration emailaddress.'
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]

7
couchpotato/core/notifications/core/static/notification.js

@ -122,9 +122,12 @@ var NotificationBase = new Class({
startPoll: function(){ startPoll: function(){
var self = this; var self = this;
if(self.stopped || (self.request && self.request.isRunning())) if(self.stopped)
return; return;
if(self.request && self.request.isRunning())
self.request.cancel();
self.request = Api.request('nonblock/notification.listener', { self.request = Api.request('nonblock/notification.listener', {
'onSuccess': function(json){ 'onSuccess': function(json){
self.processData(json, false) self.processData(json, false)
@ -149,7 +152,7 @@ var NotificationBase = new Class({
var self = this; var self = this;
// Process data // Process data
if(json){ if(json && json.result){
Array.each(json.result, function(result){ Array.each(json.result, function(result){
App.trigger(result._t || result.type, [result]); App.trigger(result._t || result.type, [result]);
if(result.message && result.read === undefined && !init) if(result.message && result.read === undefined && !init)

11
couchpotato/core/notifications/pushbullet.py

@ -14,7 +14,7 @@ autoload = 'Pushbullet'
class Pushbullet(Notification): class Pushbullet(Notification):
url = 'https://api.pushbullet.com/api/%s' url = 'https://api.pushbullet.com/v2/%s'
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
@ -25,11 +25,7 @@ class Pushbullet(Notification):
# Get all the device IDs linked to this user # Get all the device IDs linked to this user
if not len(devices): if not len(devices):
response = self.request('devices') devices = [None]
if not response:
return False
devices += [device.get('id') for device in response['devices']]
successful = 0 successful = 0
for device in devices: for device in devices:
@ -88,7 +84,8 @@ config = [{
}, },
{ {
'name': 'api_key', 'name': 'api_key',
'label': 'User API Key' 'label': 'Access Token',
'description': 'Can be found on <a href="https://www.pushbullet.com/account" target="_blank">Account Settings</a>',
}, },
{ {
'name': 'devices', 'name': 'devices',

17
couchpotato/core/notifications/pushover.py

@ -1,7 +1,7 @@
from httplib import HTTPSConnection from httplib import HTTPSConnection
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import getTitle from couchpotato.core.helpers.variable import getTitle, getIdentifier
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@ -13,7 +13,6 @@ autoload = 'Pushover'
class Pushover(Notification): class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
@ -22,15 +21,15 @@ class Pushover(Notification):
api_data = { api_data = {
'user': self.conf('user_key'), 'user': self.conf('user_key'),
'token': self.app_token, 'token': self.conf('api_token'),
'message': toUnicode(message), 'message': toUnicode(message),
'priority': self.conf('priority'), 'priority': self.conf('priority'),
'sound': self.conf('sound'), 'sound': self.conf('sound'),
} }
if data and data.get('identifier'): if data and getIdentifier(data):
api_data.update({ api_data.update({
'url': toUnicode('http://www.imdb.com/title/%s/' % data['identifier']), 'url': toUnicode('http://www.imdb.com/title/%s/' % getIdentifier(data)),
'url_title': toUnicode('%s on IMDb' % getTitle(data)), 'url_title': toUnicode('%s on IMDb' % getTitle(data)),
}) })
@ -49,7 +48,7 @@ class Pushover(Notification):
log.error('Pushover auth failed: %s', response.reason) log.error('Pushover auth failed: %s', response.reason)
return False return False
else: else:
log.error('Pushover notification failed.') log.error('Pushover notification failed: %s', request_status)
return False return False
@ -71,6 +70,12 @@ config = [{
'description': 'Register on pushover.net to get one.' 'description': 'Register on pushover.net to get one.'
}, },
{ {
'name': 'api_token',
'description': '<a href="https://pushover.net/apps/clone/couchpotato" target="_blank">Register on pushover.net</a> to get one.',
'advanced': True,
'default': 'YkxHMYDZp285L265L3IwH3LmzkTaCy',
},
{
'name': 'priority', 'name': 'priority',
'default': 0, 'default': 0,
'type': 'dropdown', 'type': 'dropdown',

6
couchpotato/core/notifications/xbmc.py

@ -8,7 +8,7 @@ from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import requests import requests
from requests.packages.urllib3.exceptions import MaxRetryError from requests.packages.urllib3.exceptions import MaxRetryError, ConnectionError
log = CPLog(__name__) log = CPLog(__name__)
@ -172,7 +172,7 @@ class XBMC(Notification):
# manually fake expected response array # manually fake expected response array
return [{'result': 'Error'}] return [{'result': 'Error'}]
except (MaxRetryError, requests.exceptions.Timeout): except (MaxRetryError, requests.exceptions.Timeout, ConnectionError):
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}] return [{'result': 'Error'}]
except: except:
@ -208,7 +208,7 @@ class XBMC(Notification):
log.debug('Returned from request %s: %s', (host, response)) log.debug('Returned from request %s: %s', (host, response))
return response return response
except (MaxRetryError, requests.exceptions.Timeout): except (MaxRetryError, requests.exceptions.Timeout, ConnectionError):
log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off') log.info2('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [] return []
except: except:

2
couchpotato/core/plugins/base.py

@ -263,7 +263,7 @@ class Plugin(object):
def afterCall(self, handler): def afterCall(self, handler):
self.isRunning('%s.%s' % (self.getName(), handler.__name__), False) self.isRunning('%s.%s' % (self.getName(), handler.__name__), False)
def doShutdown(self): def doShutdown(self, *args, **kwargs):
self.shuttingDown(True) self.shuttingDown(True)
return True return True

2
couchpotato/core/plugins/browser.py

@ -3,6 +3,7 @@ import os
import string import string
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import six import six
@ -50,6 +51,7 @@ class FileBrowser(Plugin):
path = '/' path = '/'
dirs = [] dirs = []
path = sp(path)
for f in os.listdir(path): for f in os.listdir(path):
p = os.path.join(path, f) p = os.path.join(path, f)
if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)): if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):

34
couchpotato/core/plugins/file.py

@ -5,7 +5,7 @@ from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt, isSubFolder
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@ -32,6 +32,8 @@ class FileManager(Plugin):
fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24) fireEvent('schedule.interval', 'file.cleanup', self.cleanup, hours = 24)
addEvent('app.test', self.doSubfolderTest)
def cleanup(self): def cleanup(self):
# Wait a bit after starting before cleanup # Wait a bit after starting before cleanup
@ -76,3 +78,33 @@ class FileManager(Plugin):
self.createFile(dest, filedata, binary = True) self.createFile(dest, filedata, binary = True)
return dest return dest
def doSubfolderTest(self):
tests = {
('/test/subfolder', '/test/sub'): False,
('/test/sub/folder', '/test/sub'): True,
('/test/sub/folder', '/test/sub2'): False,
('/sub/fold', '/test/sub/fold'): False,
('/sub/fold', '/test/sub/folder'): False,
('/opt/couchpotato', '/var/opt/couchpotato'): False,
('/var/opt', '/var/opt/couchpotato'): False,
('/CapItaLs/Are/OK', '/CapItaLs/Are/OK'): True,
('/CapItaLs/Are/OK', '/CapItaLs/Are/OK2'): False,
('/capitals/are/not/OK', '/capitals/are/NOT'): False,
('\\\\Mounted\\Volume\\Test', '\\\\Mounted\\Volume'): True,
('C:\\\\test\\path', 'C:\\\\test2'): False
}
failed = 0
for x in tests:
if isSubFolder(x[0], x[1]) is not tests[x]:
log.error('Failed subfolder test %s %s', x)
failed += 1
if failed > 0:
log.error('Subfolder test failed %s tests', failed)
else:
log.info('Subfolder test succeeded')
return failed == 0

46
couchpotato/core/plugins/manage.py

@ -1,13 +1,12 @@
import ctypes
import os import os
import sys
import time import time
import traceback import traceback
from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier from couchpotato.core.helpers.variable import splitString, getTitle, tryInt, getIdentifier, getFreeSpace
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@ -136,6 +135,7 @@ class Manage(Plugin):
# Get movies with done status # Get movies with done status
total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True) total_movies, done_movies = fireEvent('media.list', types = 'movie', status = 'done', release_status = 'done', status_or = True, single = True)
deleted_releases = []
for done_movie in done_movies: for done_movie in done_movies:
if getIdentifier(done_movie) not in added_identifiers: if getIdentifier(done_movie) not in added_identifiers:
fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all') fireEvent('media.delete', media_id = done_movie['_id'], delete_from = 'all')
@ -165,12 +165,10 @@ class Manage(Plugin):
already_used = used_files.get(release_file) already_used = used_files.get(release_file)
if already_used: if already_used:
# delete current one release_id = release['_id'] if already_used.get('last_edit', 0) < release.get('last_edit', 0) else already_used['_id']
if already_used.get('last_edit', 0) < release.get('last_edit', 0): if release_id not in deleted_releases:
fireEvent('release.delete', release['_id'], single = True) fireEvent('release.delete', release_id, single = True)
# delete previous one deleted_releases.append(release_id)
else:
fireEvent('release.delete', already_used['_id'], single = True)
break break
else: else:
used_files[release_file] = release used_files[release_file] = release
@ -180,6 +178,10 @@ class Manage(Plugin):
if self.shuttingDown(): if self.shuttingDown():
break break
if not self.shuttingDown():
db = get_db()
db.reindex()
Env.prop(last_update_key, time.time()) Env.prop(last_update_key, time.time())
except: except:
log.error('Failed updating library: %s', (traceback.format_exc())) log.error('Failed updating library: %s', (traceback.format_exc()))
@ -269,31 +271,7 @@ class Manage(Plugin):
fireEvent('release.add', group = group) fireEvent('release.add', group = group)
def getDiskSpace(self): def getDiskSpace(self):
return getFreeSpace(self.directories())
free_space = {}
for folder in self.directories():
size = None
if os.path.isdir(folder):
if os.name == 'nt':
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
ctypes.c_ulonglong()
if sys.version_info >= (3,) or isinstance(folder, unicode):
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
else:
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
if ret == 0:
raise ctypes.WinError()
used = total.value - free.value
return [total.value, used, free.value]
else:
s = os.statvfs(folder)
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
free_space[folder] = size
return free_space
config = [{ config = [{

4
couchpotato/core/plugins/profile/main.py

@ -88,6 +88,7 @@ class ProfilePlugin(Plugin):
'core': kwargs.get('core', False), 'core': kwargs.get('core', False),
'qualities': [], 'qualities': [],
'wait_for': [], 'wait_for': [],
'stop_after': [],
'finish': [], 'finish': [],
'3d': [] '3d': []
} }
@ -97,6 +98,7 @@ class ProfilePlugin(Plugin):
for type in kwargs.get('types', []): for type in kwargs.get('types', []):
profile['qualities'].append(type.get('quality')) profile['qualities'].append(type.get('quality'))
profile['wait_for'].append(tryInt(kwargs.get('wait_for', 0))) profile['wait_for'].append(tryInt(kwargs.get('wait_for', 0)))
profile['stop_after'].append(tryInt(kwargs.get('stop_after', 0)))
profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True) profile['finish'].append((tryInt(type.get('finish')) == 1) if order > 0 else True)
profile['3d'].append(tryInt(type.get('3d'))) profile['3d'].append(tryInt(type.get('3d')))
order += 1 order += 1
@ -217,6 +219,7 @@ class ProfilePlugin(Plugin):
'qualities': profile.get('qualities'), 'qualities': profile.get('qualities'),
'finish': [], 'finish': [],
'wait_for': [], 'wait_for': [],
'stop_after': [],
'3d': [] '3d': []
} }
@ -224,6 +227,7 @@ class ProfilePlugin(Plugin):
for q in profile.get('qualities'): for q in profile.get('qualities'):
pro['finish'].append(True) pro['finish'].append(True)
pro['wait_for'].append(0) pro['wait_for'].append(0)
pro['stop_after'].append(0)
pro['3d'].append(threed.pop() if threed else False) pro['3d'].append(threed.pop() if threed else False)
db.insert(pro) db.insert(pro)

5
couchpotato/core/plugins/profile/static/profile.css

@ -43,9 +43,8 @@
} }
.profile .wait_for { .profile .wait_for {
position: absolute; padding-top: 0;
right: 60px; padding-bottom: 20px;
top: 0;
} }
.profile .wait_for input { .profile .wait_for input {

27
couchpotato/core/plugins/profile/static/profile.js

@ -37,20 +37,28 @@ var Profile = new Class({
'placeholder': 'Profile name' 'placeholder': 'Profile name'
}) })
), ),
new Element('div.wait_for.ctrlHolder').adopt(
new Element('span', {'text':'Wait'}),
new Element('input.inlay.xsmall', {
'type':'text',
'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
}),
new Element('span', {'text':'day(s) for a better quality.'})
),
new Element('div.qualities.ctrlHolder').adopt( new Element('div.qualities.ctrlHolder').adopt(
new Element('label', {'text': 'Search for'}), new Element('label', {'text': 'Search for'}),
self.type_container = new Element('ol.types'), self.type_container = new Element('ol.types'),
new Element('div.formHint', { new Element('div.formHint', {
'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality." 'html': "Search these qualities (2 minimum), from top to bottom. Use the checkbox, to stop searching after it found this quality."
}) })
),
new Element('div.wait_for.ctrlHolder').adopt(
// "Wait the entered number of days for a checked quality, before downloading a lower quality release."
new Element('span', {'text':'Wait'}),
new Element('input.inlay.wait_for_input.xsmall', {
'type':'text',
'value': data.wait_for && data.wait_for.length > 0 ? data.wait_for[0] : 0
}),
new Element('span', {'text':'day(s) for a better quality '}),
new Element('span.advanced', {'text':'and keep searching'}),
// "After a checked quality is found and downloaded, continue searching for even better quality releases for the entered number of days."
new Element('input.inlay.xsmall.stop_after_input.advanced', {
'type':'text',
'value': data.stop_after && data.stop_after.length > 0 ? data.stop_after[0] : 0
}),
new Element('span.advanced', {'text':'day(s) for a better (checked) quality.'})
) )
); );
@ -116,7 +124,8 @@ var Profile = new Class({
var data = { var data = {
'id' : self.data._id, 'id' : self.data._id,
'label' : self.el.getElement('.quality_label input').get('value'), 'label' : self.el.getElement('.quality_label input').get('value'),
'wait_for' : self.el.getElement('.wait_for input').get('value'), 'wait_for' : self.el.getElement('.wait_for_input').get('value'),
'stop_after' : self.el.getElement('.stop_after_input').get('value'),
'types': [] 'types': []
}; };

86
couchpotato/core/plugins/quality/main.py

@ -1,12 +1,12 @@
import traceback import traceback
import re import re
from CodernityDB.database import RecordNotFound
from CodernityDB.database import RecordNotFound
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.quality.index import QualityIndex from couchpotato.core.plugins.quality.index import QualityIndex
@ -22,17 +22,17 @@ class QualityPlugin(Plugin):
} }
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']},
{'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts', 'x264', 'h264']}, {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264']},
{'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']},
{'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':[], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip')], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd'], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':[], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p'], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}, {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p'], 'ext':[]},
# TODO come back to this later, think this could be handled better, this is starting to get out of hand.... # TODO come back to this later, think this could be handled better, this is starting to get out of hand....
# BluRay # BluRay
@ -113,15 +113,14 @@ class QualityPlugin(Plugin):
db = get_db() db = get_db()
qualities = db.all('quality', with_doc = True)
temp = [] temp = []
for quality in qualities: for quality in self.qualities:
quality = quality['doc'] quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc']
q = mergeDicts(self.getQuality(quality.get('identifier')), quality) q = mergeDicts(quality, quality_doc)
temp.append(q) temp.append(q)
self.cached_qualities = temp if len(temp) == len(self.qualities):
self.cached_qualities = temp
return temp return temp
@ -227,10 +226,15 @@ class QualityPlugin(Plugin):
for cur_file in files: for cur_file in files:
words = re.split('\W+', cur_file.lower()) words = re.split('\W+', cur_file.lower())
name_year = fireEvent('scanner.name_year', cur_file, file_name = cur_file, single = True)
threed_words = words
if name_year and name_year.get('name'):
split_name = splitString(name_year.get('name'), ' ')
threed_words = [x for x in words if x not in split_name]
for quality in qualities: for quality in qualities:
contains_score = self.containsTagScore(quality, words, cur_file) contains_score = self.containsTagScore(quality, words, cur_file)
threedscore = self.contains3D(quality, words, cur_file) if quality.get('allow_3d') else (0, None) threedscore = self.contains3D(quality, threed_words, cur_file) if quality.get('allow_3d') else (0, None)
self.calcScore(score, quality, contains_score, threedscore) self.calcScore(score, quality, contains_score, threedscore)
@ -275,6 +279,9 @@ class QualityPlugin(Plugin):
cur_file = ss(cur_file) cur_file = ss(cur_file)
score = 0 score = 0
extension = words[-1]
words = words[:-1]
points = { points = {
'identifier': 10, 'identifier': 10,
'label': 10, 'label': 10,
@ -294,7 +301,7 @@ class QualityPlugin(Plugin):
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) score += points.get(tag_type)
if isinstance(alt, (str, unicode)) and ss(alt.lower()) in cur_file.lower(): if isinstance(alt, (str, unicode)) and ss(alt.lower()) in words:
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file)) log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
score += points.get(tag_type) / 2 score += points.get(tag_type) / 2
@ -304,8 +311,8 @@ class QualityPlugin(Plugin):
# Check extention # Check extention
for ext in quality.get('ext', []): for ext in quality.get('ext', []):
if ext == words[-1]: if ext == extension:
log.debug('Found %s extension in %s', (ext, cur_file)) log.debug('Found %s with .%s extension in %s', (quality['identifier'], ext, cur_file))
score += points['ext'] score += points['ext']
return score return score
@ -390,26 +397,31 @@ class QualityPlugin(Plugin):
if score.get(q.get('identifier')): if score.get(q.get('identifier')):
score[q.get('identifier')]['score'] -= 1 score[q.get('identifier')]['score'] -= 1
def isFinish(self, quality, profile): def isFinish(self, quality, profile, release_age = 0):
if not isinstance(profile, dict) or not profile.get('qualities'): if not isinstance(profile, dict) or not profile.get('qualities'):
return False # No profile so anything (scanned) is good enough
return True
try: try:
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0]
return profile['finish'][quality_order]
if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])):
return True
return False
except: except:
return False return False
def isHigher(self, quality, compare_with, profile = None): def isHigher(self, quality, compare_with, profile = None):
if not isinstance(profile, dict) or not profile.get('qualities'): if not isinstance(profile, dict) or not profile.get('qualities'):
profile = {'qualities': self.order} profile = fireEvent('profile.default', single = True)
# Try to find quality in profile, if not found: a quality we do not want is lower than anything else # Try to find quality in profile, if not found: a quality we do not want is lower than anything else
try: try:
quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0]
except: except:
log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \ log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \
[identifier + ('3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])]))
return 'lower' return 'lower'
# Try to find compare quality in profile, if not found: anything is higher than a not wanted quality # Try to find compare quality in profile, if not found: anything is higher than a not wanted quality
@ -451,7 +463,18 @@ class QualityPlugin(Plugin):
'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'}, 'Movie Monuments 2013 BrRip 720p': {'size': 1300, 'quality': 'brrip'},
'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True}, 'The.Movie.2014.3D.1080p.BluRay.AVC.DTS-HD.MA.5.1-GroupName': {'size': 30000, 'quality': 'bd50', 'is_3d': True},
'/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': False}, '/home/namehou/Movie Monuments (2013)/Movie Monuments.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': False},
'/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True} '/home/namehou/Movie Monuments (2013)/Movie Monuments Full-OU.mkv': {'size': 4500, 'quality': '1080p', 'is_3d': True},
'/volume1/Public/3D/Moviename/Moviename (2009).3D.SBS.ts': {'size': 7500, 'quality': '1080p', 'is_3d': True},
'/volume1/Public/Moviename/Moviename (2009).ts': {'size': 5500, 'quality': '1080p'},
'/movies/BluRay HDDVD H.264 MKV 720p EngSub/QuiQui le fou (criterion collection #123, 1915)/QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p'},
'C:\\movies\QuiQui le fou (collection #123, 1915)\QuiQui le fou (1915) half-sbs 720p x264 BluRay.mkv': {'size': 5500, 'quality': '720p', 'is_3d': True},
'Moviename 2014 720p HDCAM XviD DualAudio': {'size': 4000, 'quality': 'cam'},
'Moviename (2014) - 720p CAM x264': {'size': 2250, 'quality': 'cam'},
'Movie Name (2014).mp4': {'size': 750, 'quality': 'brrip'},
'Moviename.2014.720p.R6.WEB-DL.x264.AC3-xyz': {'size': 750, 'quality': 'r5'},
'Movie name 2014 New Source 720p HDCAM x264 AC3 xyz': {'size': 750, 'quality': 'cam'},
'Movie.Name.2014.720p.HD.TS.AC3.x264': {'size': 750, 'quality': 'ts'}
} }
correct = 0 correct = 0
@ -459,7 +482,10 @@ class QualityPlugin(Plugin):
test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {} test_quality = self.guess(files = [name], extra = tests[name].get('extra', None), size = tests[name].get('size', None)) or {}
success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False) success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False)
if not success: if not success:
log.error('%s failed check, thinks it\'s %s', (name, test_quality.get('identifier'))) log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name,
test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''),
tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '')
))
correct += success correct += success

12
couchpotato/core/plugins/quality/static/quality.js

@ -8,6 +8,7 @@ var QualityBase = new Class({
self.qualities = data.qualities; self.qualities = data.qualities;
self.profiles_list = null;
self.profiles = []; self.profiles = [];
Array.each(data.profiles, self.createProfilesClass.bind(self)); Array.each(data.profiles, self.createProfilesClass.bind(self));
@ -35,7 +36,7 @@ var QualityBase = new Class({
}).pick(); }).pick();
} }
catch(e){} catch(e){}
return {} return {}
}, },
@ -106,14 +107,13 @@ var QualityBase = new Class({
createProfileOrdering: function(){ createProfileOrdering: function(){
var self = this; var self = this;
var profile_list;
self.settings.createGroup({ self.settings.createGroup({
'label': 'Profile Defaults', 'label': 'Profile Defaults',
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)' 'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
}).adopt( }).adopt(
new Element('.ctrlHolder#profile_ordering').adopt( new Element('.ctrlHolder#profile_ordering').adopt(
new Element('label[text=Order]'), new Element('label[text=Order]'),
profile_list = new Element('ul'), self.profiles_list = new Element('ul'),
new Element('p.formHint', { new Element('p.formHint', {
'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.<br />First one will be default.' 'html': 'Change the order the profiles are in the dropdown list. Uncheck to hide it completely.<br />First one will be default.'
}) })
@ -133,7 +133,7 @@ var QualityBase = new Class({
'text': profile.data.label 'text': profile.data.label
}), }),
new Element('span.handle') new Element('span.handle')
).inject(profile_list); ).inject(self.profiles_list);
new Form.Check(check); new Form.Check(check);
@ -141,7 +141,7 @@ var QualityBase = new Class({
// Sortable // Sortable
var sorted_changed = false; var sorted_changed = false;
self.profile_sortable = new Sortables(profile_list, { self.profile_sortable = new Sortables(self.profiles_list, {
'revert': true, 'revert': true,
'handle': '.handle', 'handle': '.handle',
'opacity': 0.5, 'opacity': 0.5,
@ -163,7 +163,7 @@ var QualityBase = new Class({
ids = [], ids = [],
hidden = []; hidden = [];
self.profile_sortable.list.getElements('li').each(function(el, nr){ self.profiles_list.getElements('li').each(function(el, nr){
ids.include(el.get('data-id')); ids.include(el.get('data-id'));
hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked'); hidden[nr] = +!el.getElement('input[type=checkbox]').get('checked');
}); });

57
couchpotato/core/plugins/release/main.py

@ -3,7 +3,7 @@ import os
import time import time
import traceback import traceback
from CodernityDB.database import RecordDeleted from CodernityDB.database import RecordDeleted, RecordNotFound
from couchpotato import md5, get_db from couchpotato import md5, get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
@ -79,6 +79,13 @@ class Release(Plugin):
try: try:
db.get('id', release.get('key')) db.get('id', release.get('key'))
media_exist.append(release.get('key')) media_exist.append(release.get('key'))
try:
if release['doc'].get('status') == 'ignore':
release['doc']['status'] = 'ignored'
db.update(release['doc'])
except:
log.error('Failed fixing mis-status tag: %s', traceback.format_exc())
except RecordDeleted: except RecordDeleted:
db.delete(release['doc']) db.delete(release['doc'])
log.debug('Deleted orphaned release: %s', release['doc']) log.debug('Deleted orphaned release: %s', release['doc'])
@ -100,9 +107,11 @@ class Release(Plugin):
if rel['status'] in ['available']: if rel['status'] in ['available']:
self.delete(rel['_id']) self.delete(rel['_id'])
# Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the move # Set all snatched and downloaded releases to ignored to make sure they are ignored when re-adding the media
elif rel['status'] in ['snatched', 'downloaded']: elif rel['status'] in ['snatched', 'downloaded']:
self.updateStatus(rel['_id'], status = 'ignore') self.updateStatus(rel['_id'], status = 'ignored')
fireEvent('media.untag', media.get('_id'), 'recent', single = True)
def add(self, group, update_info = True, update_id = None): def add(self, group, update_info = True, update_id = None):
@ -149,7 +158,7 @@ class Release(Plugin):
r = db.get('release_identifier', release_identifier, with_doc = True)['doc'] r = db.get('release_identifier', release_identifier, with_doc = True)['doc']
r['media_id'] = media['_id'] r['media_id'] = media['_id']
except: except:
log.error('Failed updating release by identifier: %s', traceback.format_exc()) log.debug('Failed updating release by identifier "%s". Inserting new.', release_identifier)
r = db.insert(release) r = db.insert(release)
# Update with ref and _id # Update with ref and _id
@ -162,7 +171,7 @@ class Release(Plugin):
release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v) release['files'] = dict((k, [toUnicode(x) for x in v]) for k, v in group['files'].items() if v)
db.update(release) db.update(release)
fireEvent('media.restatus', media['_id']) fireEvent('media.restatus', media['_id'], single = True)
return True return True
except: except:
@ -184,7 +193,7 @@ class Release(Plugin):
db.delete(rel) db.delete(rel)
return True return True
except RecordDeleted: except RecordDeleted:
log.error('Already deleted: %s', release_id) log.debug('Already deleted: %s', release_id)
return True return True
except: except:
log.error('Failed: %s', traceback.format_exc()) log.error('Failed: %s', traceback.format_exc())
@ -318,7 +327,7 @@ class Release(Plugin):
log_movie = '%s (%s) in %s' % (getTitle(media), media['info'].get('year'), rls['quality']) log_movie = '%s (%s) in %s' % (getTitle(media), media['info'].get('year'), rls['quality'])
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message) log.info(snatch_message)
fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls) fireEvent('%s.snatched' % data['type'], message = snatch_message, data = media)
# Mark release as snatched # Mark release as snatched
if renamer_enabled: if renamer_enabled:
@ -329,22 +338,14 @@ class Release(Plugin):
if media['status'] == 'active': if media['status'] == 'active':
profile = db.get('id', media['profile_id']) profile = db.get('id', media['profile_id'])
finished = False if fireEvent('quality.isfinish', {'identifier': rls['quality'], 'is_3d': rls.get('is_3d', False)}, profile, single = True):
if rls['quality'] in profile['qualities']:
nr = profile['qualities'].index(rls['quality'])
finished = profile['finish'][nr]
if finished:
log.info('Renamer disabled, marking media as finished: %s', log_movie) log.info('Renamer disabled, marking media as finished: %s', log_movie)
# Mark release done # Mark release done
self.updateStatus(rls['_id'], status = 'done') self.updateStatus(rls['_id'], status = 'done')
# Mark media done # Mark media done
mdia = db.get('id', media['_id']) fireEvent('media.restatus', media['_id'], single = True)
mdia['status'] = 'done'
mdia['last_edit'] = int(time.time())
db.update(mdia)
return True return True
@ -371,7 +372,11 @@ class Release(Plugin):
continue continue
if rel['score'] <= 0: if rel['score'] <= 0:
log.info('Ignored, score to low: %s', rel['name']) log.info('Ignored, score "%s" to low: %s', (rel['score'], rel['name']))
continue
if rel['size'] <= 50:
log.info('Ignored, size "%sMB" to low: %s', (rel['size'], rel['name']))
continue continue
rel['wait_for'] = False rel['wait_for'] = False
@ -469,7 +474,7 @@ class Release(Plugin):
rel = db.get('id', release_id) rel = db.get('id', release_id)
if rel and rel.get('status') != status: if rel and rel.get('status') != status:
release_name = rel['info'].get('name') release_name = None
if rel.get('files'): if rel.get('files'):
for file_type in rel.get('files', {}): for file_type in rel.get('files', {}):
if file_type == 'movie': if file_type == 'movie':
@ -477,6 +482,9 @@ class Release(Plugin):
release_name = os.path.basename(release_file) release_name = os.path.basename(release_file)
break break
if not release_name and rel.get('info'):
release_name = rel['info'].get('name')
#update status in Db #update status in Db
log.debug('Marking release %s as %s', (release_name, status)) log.debug('Marking release %s as %s', (release_name, status))
rel['status'] = status rel['status'] = status
@ -500,8 +508,15 @@ class Release(Plugin):
status = list(status if isinstance(status, (list, tuple)) else [status]) status = list(status if isinstance(status, (list, tuple)) else [status])
for s in status: for s in status:
for ms in db.get_many('release_status', s, with_doc = with_doc): for ms in db.get_many('release_status', s):
yield ms['doc'] if with_doc else ms if with_doc:
try:
doc = db.get('id', ms['_id'])
yield doc
except RecordNotFound:
log.debug('Record not found, skipping: %s', ms['_id'])
else:
yield ms
def forMedia(self, media_id): def forMedia(self, media_id):

116
couchpotato/core/plugins/renamer.py

@ -123,11 +123,6 @@ class Renamer(Plugin):
no_process = [to_folder] no_process = [to_folder]
cat_list = fireEvent('category.all', single = True) or [] cat_list = fireEvent('category.all', single = True) or []
no_process.extend([item['destination'] for item in cat_list]) no_process.extend([item['destination'] for item in cat_list])
try:
if Env.setting('library', section = 'manage').strip():
no_process.extend([sp(manage_folder) for manage_folder in splitString(Env.setting('library', section = 'manage'), '::')])
except:
pass
# Check to see if the no_process folders are inside the "from" folder. # Check to see if the no_process folders are inside the "from" folder.
if not os.path.isdir(base_folder) or not os.path.isdir(to_folder): if not os.path.isdir(base_folder) or not os.path.isdir(to_folder):
@ -136,7 +131,7 @@ class Renamer(Plugin):
else: else:
for item in no_process: for item in no_process:
if isSubFolder(item, base_folder): if isSubFolder(item, base_folder):
log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder.') log.error('To protect your data, the media libraries can\'t be inside of or the same as the "from" folder. "%s" in "%s"', (item, base_folder))
return return
# Check to see if the no_process folders are inside the provided media_folder # Check to see if the no_process folders are inside the provided media_folder
@ -168,7 +163,7 @@ class Renamer(Plugin):
if media_folder: if media_folder:
for item in no_process: for item in no_process:
if isSubFolder(item, media_folder): if isSubFolder(item, media_folder):
log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder.') log.error('To protect your data, the media libraries can\'t be inside of or the same as the provided media folder. "%s" in "%s"', (item, media_folder))
return return
# Make sure a checkSnatched marked all downloads/seeds as such # Make sure a checkSnatched marked all downloads/seeds as such
@ -202,14 +197,18 @@ class Renamer(Plugin):
db = get_db() db = get_db()
# Extend the download info with info stored in the downloaded release # Extend the download info with info stored in the downloaded release
keep_original = self.moveTypeIsLinked()
is_torrent = False
if release_download: if release_download:
release_download = self.extendReleaseDownload(release_download) release_download = self.extendReleaseDownload(release_download)
is_torrent = self.downloadIsTorrent(release_download)
keep_original = True if is_torrent and self.conf('file_action') not in ['move'] else keep_original
# Unpack any archives # Unpack any archives
extr_files = None extr_files = None
if self.conf('unrar'): if self.conf('unrar'):
folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files, folder, media_folder, files, extr_files = self.extractFiles(folder = folder, media_folder = media_folder, files = files,
cleanup = self.conf('cleanup') and not self.downloadIsTorrent(release_download)) cleanup = self.conf('cleanup') and not keep_original)
groups = fireEvent('scanner.scan', folder = folder if folder else base_folder, groups = fireEvent('scanner.scan', folder = folder if folder else base_folder,
files = files, release_download = release_download, return_ignored = False, single = True) or [] files = files, release_download = release_download, return_ignored = False, single = True) or []
@ -326,7 +325,7 @@ class Renamer(Plugin):
if file_type is 'nfo' and not self.conf('rename_nfo'): if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled', file_type) log.debug('Skipping, renaming of %s disabled', file_type)
for current_file in group['files'][file_type]: for current_file in group['files'][file_type]:
if self.conf('cleanup') and (not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)): if self.conf('cleanup') and (not keep_original or self.fileIsAdded(current_file, group)):
remove_files.append(current_file) remove_files.append(current_file)
continue continue
@ -446,19 +445,22 @@ class Renamer(Plugin):
# Before renaming, remove the lower quality files # Before renaming, remove the lower quality files
remove_leftovers = True remove_leftovers = True
# Mark movie "done" once it's found the quality with the finish check # Get media quality profile
profile = None profile = None
try: if media.get('profile_id'):
if media.get('status') == 'active' and media.get('profile_id'): try:
profile = db.get('id', media['profile_id']) profile = db.get('id', media['profile_id'])
if fireEvent('quality.isfinish', group['meta_data']['quality'], profile, single = True): except:
mdia = db.get('id', media['_id']) # Set profile to None as it does not exist anymore
mdia['status'] = 'done' mdia = db.get('id', media['_id'])
mdia['last_edit'] = int(time.time()) mdia['profile_id'] = None
db.update(mdia) db.update(mdia)
log.error('Error getting quality profile for %s: %s', (media_title, traceback.format_exc()))
else:
log.debug('Media has no quality profile: %s', media_title)
except: # Mark media for dashboard
log.error('Failed marking movie finished: %s', (traceback.format_exc())) mark_as_recent = False
# Go over current movie releases # Go over current movie releases
for release in fireEvent('release.for_media', media['_id'], single = True): for release in fireEvent('release.for_media', media['_id'], single = True):
@ -468,7 +470,7 @@ class Renamer(Plugin):
# This is where CP removes older, lesser quality releases or releases that are not wanted anymore # This is where CP removes older, lesser quality releases or releases that are not wanted anymore
is_higher = fireEvent('quality.ishigher', \ is_higher = fireEvent('quality.ishigher', \
group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, profile, single = True) group['meta_data']['quality'], {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, single = True)
if is_higher == 'higher': if is_higher == 'higher':
log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality'))) log.info('Removing lesser or not wanted quality %s for %s.', (media_title, release.get('quality')))
@ -493,7 +495,7 @@ class Renamer(Plugin):
self.tagRelease(group = group, tag = 'exists') self.tagRelease(group = group, tag = 'exists')
# Notify on rename fail # Notify on rename fail
download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('identifier')) download_message = 'Renaming of %s (%s) cancelled, exists in %s already.' % (media_title, group['meta_data']['quality']['label'], release.get('quality'))
fireEvent('movie.renaming.canceled', message = download_message, data = group) fireEvent('movie.renaming.canceled', message = download_message, data = group)
remove_leftovers = False remove_leftovers = False
@ -506,14 +508,21 @@ class Renamer(Plugin):
# Set the release to downloaded # Set the release to downloaded
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
group['release_download'] = release_download group['release_download'] = release_download
mark_as_recent = True
elif release_download['status'] == 'seeding': elif release_download['status'] == 'seeding':
# Set the release to seeding # Set the release to seeding
fireEvent('release.update_status', release['_id'], status = 'seeding', single = True) fireEvent('release.update_status', release['_id'], status = 'seeding', single = True)
mark_as_recent = True
elif release.get('identifier') == group['meta_data']['quality']['identifier']: elif release.get('quality') == group['meta_data']['quality']['identifier']:
# Set the release to downloaded # Set the release to downloaded
fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True) fireEvent('release.update_status', release['_id'], status = 'downloaded', single = True)
group['release_download'] = release_download group['release_download'] = release_download
mark_as_recent = True
# Mark media for dashboard
if mark_as_recent:
fireEvent('media.tag', group['media'].get('_id'), 'recent', update_edited = True, single = True)
# Remove leftover files # Remove leftover files
if not remove_leftovers: # Don't remove anything if not remove_leftovers: # Don't remove anything
@ -522,7 +531,7 @@ class Renamer(Plugin):
log.debug('Removing leftover files') log.debug('Removing leftover files')
for current_file in group['files']['leftover']: for current_file in group['files']['leftover']:
if self.conf('cleanup') and not self.conf('move_leftover') and \ if self.conf('cleanup') and not self.conf('move_leftover') and \
(not self.downloadIsTorrent(release_download) or self.fileIsAdded(current_file, group)): (not keep_original or self.fileIsAdded(current_file, group)):
remove_files.append(current_file) remove_files.append(current_file)
# Remove files # Remove files
@ -569,7 +578,7 @@ class Renamer(Plugin):
self.makeDir(os.path.dirname(dst)) self.makeDir(os.path.dirname(dst))
try: try:
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(release_download) or self.fileIsAdded(src, group)) self.moveFile(src, dst, use_default = not is_torrent or self.fileIsAdded(src, group))
group['renamed_files'].append(dst) group['renamed_files'].append(dst)
except: except:
log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) log.error('Failed renaming the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
@ -585,7 +594,7 @@ class Renamer(Plugin):
self.untagRelease(group = group, tag = 'failed_rename') self.untagRelease(group = group, tag = 'failed_rename')
# Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent # Tag folder if it is in the 'from' folder and it will not be removed because it is a torrent
if self.movieInFromFolder(media_folder) and self.downloadIsTorrent(release_download): if self.movieInFromFolder(media_folder) and keep_original:
self.tagRelease(group = group, tag = 'renamed_already') self.tagRelease(group = group, tag = 'renamed_already')
# Remove matching releases # Remove matching releases
@ -596,7 +605,7 @@ class Renamer(Plugin):
except: except:
log.error('Failed removing %s: %s', (release, traceback.format_exc())) log.error('Failed removing %s: %s', (release, traceback.format_exc()))
if group['dirname'] and group['parentdir'] and not self.downloadIsTorrent(release_download): if group['dirname'] and group['parentdir'] and not keep_original:
if media_folder: if media_folder:
# Delete the movie folder # Delete the movie folder
group_folder = media_folder group_folder = media_folder
@ -758,10 +767,15 @@ Remove it if you want it to be renamed (again, or at least let it try again)
return False return False
def moveFile(self, old, dest, forcemove = False): def moveFile(self, old, dest, use_default = False):
dest = sp(dest) dest = sp(dest)
try: try:
if forcemove or self.conf('file_action') not in ['copy', 'link']:
move_type = self.conf('file_action')
if use_default:
move_type = self.conf('default_file_action')
if move_type not in ['copy', 'link']:
try: try:
shutil.move(old, dest) shutil.move(old, dest)
except: except:
@ -770,16 +784,16 @@ Remove it if you want it to be renamed (again, or at least let it try again)
os.unlink(old) os.unlink(old)
else: else:
raise raise
elif self.conf('file_action') == 'copy': elif move_type == 'copy':
shutil.copy(old, dest) shutil.copy(old, dest)
elif self.conf('file_action') == 'link': else:
# First try to hardlink # First try to hardlink
try: try:
log.debug('Hardlinking file "%s" to "%s"...', (old, dest)) log.debug('Hardlinking file "%s" to "%s"...', (old, dest))
link(old, dest) link(old, dest)
except: except:
# Try to simlink next # Try to simlink next
log.debug('Couldn\'t hardlink file "%s" to "%s". Simlinking instead. Error: %s.', (old, dest, traceback.format_exc())) log.debug('Couldn\'t hardlink file "%s" to "%s". Symlinking instead. Error: %s.', (old, dest, traceback.format_exc()))
shutil.copy(old, dest) shutil.copy(old, dest)
try: try:
symlink(dest, old + '.link') symlink(dest, old + '.link')
@ -1079,6 +1093,9 @@ Remove it if you want it to be renamed (again, or at least let it try again)
return False return False
return src in group['before_rename'] return src in group['before_rename']
def moveTypeIsLinked(self):
return self.conf('default_file_action') in ['copy', 'link']
def statusInfoComplete(self, release_download): def statusInfoComplete(self, release_download):
return release_download.get('id') and release_download.get('downloader') and release_download.get('folder') return release_download.get('id') and release_download.get('downloader') and release_download.get('folder')
@ -1130,14 +1147,20 @@ Remove it if you want it to be renamed (again, or at least let it try again)
log.info('Archive %s found. Extracting...', os.path.basename(archive['file'])) log.info('Archive %s found. Extracting...', os.path.basename(archive['file']))
try: try:
rar_handle = RarFile(archive['file']) rar_handle = RarFile(archive['file'], custom_path = self.conf('unrar_path'))
extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder)) extr_path = os.path.join(from_folder, os.path.relpath(os.path.dirname(archive['file']), folder))
self.makeDir(extr_path) self.makeDir(extr_path)
for packedinfo in rar_handle.infolist(): for packedinfo in rar_handle.infolist():
if not packedinfo.isdir and not os.path.isfile(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))): extr_file_path = sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))
if not packedinfo.isdir and not os.path.isfile(extr_file_path):
log.debug('Extracting %s...', packedinfo.filename) log.debug('Extracting %s...', packedinfo.filename)
rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False) rar_handle.extract(condition = [packedinfo.index], path = extr_path, withSubpath = False, overwrite = False)
extr_files.append(sp(os.path.join(extr_path, os.path.basename(packedinfo.filename)))) if self.conf('unrar_modify_date'):
try:
os.utime(extr_file_path, (os.path.getatime(archive['file']), os.path.getmtime(archive['file'])))
except:
log.error('Rar modify date enabled, but failed: %s', traceback.format_exc())
extr_files.append(extr_file_path)
del rar_handle del rar_handle
except Exception as e: except Exception as e:
log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc())) log.error('Failed to extract %s: %s %s', (archive['file'], e, traceback.format_exc()))
@ -1273,6 +1296,18 @@ config = [{
'default': False, 'default': False,
}, },
{ {
'advanced': True,
'name': 'unrar_path',
'description': 'Custom path to unrar bin',
},
{
'advanced': True,
'name': 'unrar_modify_date',
'type': 'bool',
'description': ('Set modify date of unrar-ed files to the rar-file\'s date.', 'This will allow XBMC to recognize extracted files as recently added even if the movie was released some time ago.'),
'default': False,
},
{
'name': 'cleanup', 'name': 'cleanup',
'type': 'bool', 'type': 'bool',
'description': 'Cleanup leftover files after successful rename.', 'description': 'Cleanup leftover files after successful rename.',
@ -1323,13 +1358,22 @@ config = [{
'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'), 'description': ('Replace all the spaces with a character.', 'Example: ".", "-" (without quotes). Leave empty to use spaces.'),
}, },
{ {
'name': 'default_file_action',
'label': 'Default File Action',
'default': 'move',
'type': 'dropdown',
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.',
'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.'),
'advanced': True,
},
{
'name': 'file_action', 'name': 'file_action',
'label': 'Torrent File Action', 'label': 'Torrent File Action',
'default': 'link', 'default': 'link',
'type': 'dropdown', 'type': 'dropdown',
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')], 'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
'description': ('<strong>Link</strong>, <strong>Copy</strong> or <strong>Move</strong> after download completed.', 'description': 'See above. It is prefered to use link when downloading torrents as it will save you space, while still beeing able to seed.',
'Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy. It is perfered to use link when downloading torrents as it will save you space, while still beeing able to seed.'),
'advanced': True, 'advanced': True,
}, },
{ {

17
couchpotato/core/plugins/scanner.py

@ -105,7 +105,7 @@ class Scanner(Plugin):
'HDTV': ['hdtv'] 'HDTV': ['hdtv']
} }
clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|ou|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \ clean = '([ _\,\.\(\)\[\]\-]|^)(3d|hsbs|sbs|half.sbs|full.sbs|ou|half.ou|full.ou|extended|extended.cut|directors.cut|french|fr|swedisch|sw|danish|dutch|nl|swesub|subs|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip' \
'|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)' '|hdtvrip|webdl|web.dl|webrip|web.rip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|hc|\[.*\])(?=[ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [ multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
@ -553,7 +553,7 @@ class Scanner(Plugin):
scan_result = [] scan_result = []
for p in paths: for p in paths:
if not group['is_dvd']: if not group['is_dvd']:
video = Video.from_path(toUnicode(p)) video = Video.from_path(sp(p))
video_result = [(video, video.scan())] video_result = [(video, video.scan())]
scan_result.extend(video_result) scan_result.extend(video_result)
@ -634,7 +634,14 @@ class Scanner(Plugin):
name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None) name_year = self.getReleaseNameYear(identifier, file_name = filename if not group['is_dvd'] else None)
if name_year.get('name') and name_year.get('year'): if name_year.get('name') and name_year.get('year'):
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1) search_q = '%(name)s %(year)s' % name_year
movie = fireEvent('movie.search', q = search_q, merge = True, limit = 1)
# Try with other
if len(movie) == 0 and name_year.get('other') and name_year['other'].get('name') and name_year['other'].get('year'):
search_q2 = '%(name)s %(year)s' % name_year.get('other')
if search_q2 != search_q:
movie = fireEvent('movie.search', q = search_q2, merge = True, limit = 1)
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0].get('imdb') imdb_id = movie[0].get('imdb')
@ -903,6 +910,7 @@ class Scanner(Plugin):
log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc())) log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
# Backup to simple # Backup to simple
release_name = os.path.basename(release_name.replace('\\', '/'))
cleaned = ' '.join(re.split('\W+', simplifyString(release_name))) cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
cleaned = re.sub(self.clean, ' ', cleaned) cleaned = re.sub(self.clean, ' ', cleaned)
@ -937,8 +945,11 @@ class Scanner(Plugin):
pass pass
if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')): if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
cp_guess['other'] = guess
return cp_guess return cp_guess
elif guess == {}: elif guess == {}:
cp_guess['other'] = guess
return cp_guess return cp_guess
guess['other'] = cp_guess
return guess return guess

2
couchpotato/core/plugins/trailer.py

@ -32,7 +32,7 @@ class Trailer(Plugin):
destination = os.path.join(group['destination_dir'], filename) destination = os.path.join(group['destination_dir'], filename)
if not os.path.isfile(destination): if not os.path.isfile(destination):
trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one if trailer_file and os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
os.unlink(trailer_file) os.unlink(trailer_file)
continue continue
else: else:

10
couchpotato/core/settings.py

@ -71,15 +71,7 @@ class Settings(object):
self.connectEvents() self.connectEvents()
def databaseSetup(self): def databaseSetup(self):
from couchpotato import get_db fireEvent('database.setup_index', 'property', PropertyIndex)
db = get_db()
try:
db.add_index(PropertyIndex(db.path, 'property'))
except:
self.log.debug('Index for properties already exists')
db.edit_index(PropertyIndex(db.path, 'property'))
def parser(self): def parser(self):
return self.p return self.p

8
couchpotato/environment.py

@ -2,6 +2,7 @@ import os
from couchpotato.core.database import Database from couchpotato.core.database import Database
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.loader import Loader from couchpotato.core.loader import Loader
from couchpotato.core.settings import Settings from couchpotato.core.settings import Settings
@ -38,8 +39,11 @@ class Env(object):
return Env._debug return Env._debug
@staticmethod @staticmethod
def get(attr): def get(attr, unicode = False):
return getattr(Env, '_' + attr) if unicode:
return toUnicode(getattr(Env, '_' + attr))
else:
return getattr(Env, '_' + attr)
@staticmethod @staticmethod
def all(): def all():

36
couchpotato/runner.py

@ -17,7 +17,7 @@ from couchpotato import KeyHandler, LoginHandler, LogoutHandler
from couchpotato.api import NonBlockHandler, ApiHandler from couchpotato.api import NonBlockHandler, ApiHandler
from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.event import fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import getDataDir, tryInt from couchpotato.core.helpers.variable import getDataDir, tryInt, getFreeSpace
import requests import requests
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.web import Application, StaticFileHandler, RedirectHandler from tornado.web import Application, StaticFileHandler, RedirectHandler
@ -87,6 +87,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Do db stuff # Do db stuff
db_path = sp(os.path.join(data_dir, 'database')) db_path = sp(os.path.join(data_dir, 'database'))
old_db_path = os.path.join(data_dir, 'couchpotato.db')
# Remove database folder if both exists
if os.path.isdir(db_path) and os.path.isfile(old_db_path):
db = SuperThreadSafeDatabase(db_path)
db.open()
db.destroy()
# Check if database exists # Check if database exists
db = SuperThreadSafeDatabase(db_path) db = SuperThreadSafeDatabase(db_path)
@ -195,6 +202,15 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
log = CPLog(__name__) log = CPLog(__name__)
log.debug('Started with options %s', options) log.debug('Started with options %s', options)
# Check available space
try:
total_space, available_space = getFreeSpace(data_dir)
if available_space < 100:
log.error('Shutting down as CP needs some space to work. You\'ll get corrupted data otherwise. Only %sMB left', available_space)
return
except:
log.error('Failed getting diskspace: %s', traceback.format_exc())
def customwarn(message, category, filename, lineno, file = None, line = None): def customwarn(message, category, filename, lineno, file = None, line = None):
log.warning('%s %s %s line:%s', (category, message, filename, lineno)) log.warning('%s %s %s line:%s', (category, message, filename, lineno))
warnings.showwarning = customwarn warnings.showwarning = customwarn
@ -277,22 +293,23 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
loop = IOLoop.current() loop = IOLoop.current()
# Reload hook # Reload hook
def test(): def reload_hook():
fireEvent('app.shutdown') fireEvent('app.shutdown')
add_reload_hook(test) add_reload_hook(reload_hook)
# Some logging and fire load event # Some logging and fire load event
try: log.info('Starting server on port %(port)s', config) try: log.info('Starting server on port %(port)s', config)
except: pass except: pass
fireEventAsync('app.load') fireEventAsync('app.load')
ssl_options = None
if config['ssl_cert'] and config['ssl_key']: if config['ssl_cert'] and config['ssl_key']:
server = HTTPServer(application, no_keep_alive = True, ssl_options = { ssl_options = {
'certfile': config['ssl_cert'], 'certfile': config['ssl_cert'],
'keyfile': config['ssl_key'], 'keyfile': config['ssl_key'],
}) }
else:
server = HTTPServer(application, no_keep_alive = True) server = HTTPServer(application, no_keep_alive = True, ssl_options = ssl_options)
try_restart = True try_restart = True
restart_tries = 5 restart_tries = 5
@ -301,6 +318,9 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
try: try:
server.listen(config['port'], config['host']) server.listen(config['port'], config['host'])
loop.start() loop.start()
server.close_all_connections()
server.stop()
loop.close(all_fds = True)
except Exception as e: except Exception as e:
log.error('Failed starting: %s', traceback.format_exc()) log.error('Failed starting: %s', traceback.format_exc())
try: try:
@ -314,6 +334,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
continue continue
else: else:
return return
except ValueError:
return
except: except:
pass pass

15
couchpotato/static/scripts/couchpotato.js

@ -272,11 +272,18 @@
(function(){ (function(){
Api.request('app.available', { var onFailure = function(){
'onFailure': function(){ self.checkAvailable.delay(1000, self, [delay, onAvailable]);
self.checkAvailable.delay(1000, self, [delay, onAvailable]); self.fireEvent('unload');
self.fireEvent('unload'); }
var request = Api.request('app.available', {
'timeout': 2000,
'onTimeout': function(){
request.cancel();
onFailure();
}, },
'onFailure': onFailure,
'onSuccess': function(){ 'onSuccess': function(){
if(onAvailable) if(onAvailable)
onAvailable(); onAvailable();

3
couchpotato/static/scripts/page/home.js

@ -54,7 +54,8 @@ Page.Home = new Class({
}) })
), ),
'filter': { 'filter': {
'release_status': 'snatched,seeding,missing,available,downloaded' 'release_status': 'snatched,missing,available,downloaded,done,seeding',
'with_tags': 'recent'
}, },
'limit': null, 'limit': null,
'onLoaded': function(){ 'onLoaded': function(){

25
couchpotato/static/scripts/page/settings.js

@ -120,7 +120,13 @@ Page.Settings = new Class({
var self = this; var self = this;
self.tabs_container = new Element('ul.tabs'); self.tabs_container = new Element('ul.tabs');
self.containers = new Element('form.uniForm.containers').adopt( self.containers = new Element('form.uniForm.containers', {
'events': {
'click:relay(.enabler.disabled h2)': function(e, el){
el.getPrevious().getElements('.check').fireEvent('click');
}
}
}).adopt(
new Element('label.advanced_toggle').adopt( new Element('label.advanced_toggle').adopt(
new Element('span', { new Element('span', {
'text': 'Show advanced settings' 'text': 'Show advanced settings'
@ -285,14 +291,23 @@ Page.Settings = new Class({
}) })
} }
var icon;
if(group.icon){
icon = new Element('span.icon').grab(new Element('img', {
'src': 'data:image/png;base64,' + group.icon
}));
}
var label = new Element('span.group_label', {
'text': group.label || (group.name).capitalize()
})
return new Element('fieldset', { return new Element('fieldset', {
'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '') 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '')
}).grab( }).grab(
new Element('h2', { new Element('h2').adopt(icon, label, hint)
'text': group.label || (group.name).capitalize() );
}).grab(hint)
);
}, },
createList: function(content_container){ createList: function(content_container){

34
couchpotato/static/style/settings.css

@ -75,6 +75,8 @@
color: #edc07f; color: #edc07f;
} }
.page.show_advanced .advanced { display: block; } .page.show_advanced .advanced { display: block; }
.page.show_advanced span.advanced,
.page.show_advanced input.advanced { display: inline; }
.page.settings .tab_content { .page.settings .tab_content {
display: none; display: none;
@ -92,6 +94,22 @@
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
box-shadow: 0 1px 0 rgba(255,255,255, 0.15); box-shadow: 0 1px 0 rgba(255,255,255, 0.15);
} }
.page fieldset h2 .icon {
vertical-align: bottom;
position: absolute;
left: -25px;
top: 3px;
background: #FFF;
border-radius: 2.5px;
line-height: 0;
overflow: hidden;
}
.page fieldset.enabler:hover h2 .icon {
display: none;
}
.page fieldset h2 .hint { .page fieldset h2 .hint {
font-size: 12px; font-size: 12px;
margin-left: 10px; margin-left: 10px;
@ -160,7 +178,7 @@
padding: 6px 0 0; padding: 6px 0 0;
} }
.page .xsmall { width: 20px !important; text-align: center; } .page .xsmall { width: 25px !important; text-align: center; }
.page .enabler { .page .enabler {
display: block; display: block;
@ -200,17 +218,23 @@
.page .option_list .enabler.disabled { .page .option_list .enabler.disabled {
display: inline-block; display: inline-block;
margin: 3px 3px 3px 20px; padding: 4px 0 5px;
padding: 4px 0; width: 24%;
width: 173px;
vertical-align: top; vertical-align: top;
} }
.page .option_list .enabler:not(.disabled) .icon {
display: none;
}
.page .option_list .enabler.disabled h2 { .page .option_list .enabler.disabled h2 {
cursor: pointer;
border: none; border: none;
box-shadow: none; box-shadow: none;
padding: 0 10px 0 25px; padding: 0 10px 0 0;
font-size: 16px; font-size: 16px;
position: relative;
left: 25px;
margin-right: 25px;
} }
.page .option_list .enabler:not(.disabled) h2 { .page .option_list .enabler:not(.disabled) h2 {

6
couchpotato/templates/index.html

@ -73,10 +73,10 @@
App.setup({ App.setup({
'base_url': {{ json_encode(Env.get('web_base')) }}, 'base_url': {{ json_encode(Env.get('web_base')) }},
'args': {{ json_encode(Env.get('args')) }}, 'args': {{ json_encode(Env.get('args', unicode = True)) }},
'options': {{ json_encode(('%s' % Env.get('options'))) }}, 'options': {{ json_encode(('%s' % Env.get('options'))) }},
'app_dir': {{ json_encode(Env.get('app_dir')) }}, 'app_dir': {{ json_encode(Env.get('app_dir', unicode = True)) }},
'data_dir': {{ json_encode(Env.get('data_dir')) }}, 'data_dir': {{ json_encode(Env.get('data_dir', unicode = True)) }},
'pid': {{ json_encode(Env.getPid()) }}, 'pid': {{ json_encode(Env.getPid()) }},
'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }} 'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }}
}); });

28
libs/unrar2/__init__.py

@ -21,7 +21,7 @@
# SOFTWARE. # SOFTWARE.
""" """
pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll.
It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple, It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple,
stable and foolproof. stable and foolproof.
@ -45,8 +45,8 @@ if in_windows:
from windows import RarFileImplementation from windows import RarFileImplementation
else: else:
from unix import RarFileImplementation from unix import RarFileImplementation
import fnmatch, time, weakref import fnmatch, time, weakref
class RarInfo(object): class RarInfo(object):
@ -62,7 +62,7 @@ class RarInfo(object):
isdir - True if the file is a directory isdir - True if the file is a directory
size - size in bytes of the uncompressed file size - size in bytes of the uncompressed file
comment - comment associated with the file comment - comment associated with the file
Note - this is not currently intended to be a Python file-like object. Note - this is not currently intended to be a Python file-like object.
""" """
@ -74,7 +74,7 @@ class RarInfo(object):
self.size = data['size'] self.size = data['size']
self.datetime = data['datetime'] self.datetime = data['datetime']
self.comment = data['comment'] self.comment = data['comment']
def __str__(self): def __str__(self):
@ -86,7 +86,7 @@ class RarInfo(object):
class RarFile(RarFileImplementation): class RarFile(RarFileImplementation):
def __init__(self, archiveName, password=None): def __init__(self, archiveName, password=None, custom_path = None):
"""Instantiate the archive. """Instantiate the archive.
archiveName is the name of the RAR file. archiveName is the name of the RAR file.
@ -99,7 +99,7 @@ class RarFile(RarFileImplementation):
This is a test. This is a test.
""" """
self.archiveName = archiveName self.archiveName = archiveName
RarFileImplementation.init(self, password) RarFileImplementation.init(self, password, custom_path)
def __del__(self): def __del__(self):
self.destruct() self.destruct()
@ -130,31 +130,31 @@ class RarFile(RarFileImplementation):
"""Read specific files from archive into memory. """Read specific files from archive into memory.
If "condition" is a list of numbers, then return files which have those positions in infolist. If "condition" is a list of numbers, then return files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract. If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns boolean True (extract) or False (skip). and returns boolean True (extract) or False (skip).
If "condition" is omitted, all files are returned. If "condition" is omitted, all files are returned.
Returns list of tuples (RarInfo info, str contents) Returns list of tuples (RarInfo info, str contents)
""" """
checker = condition2checker(condition) checker = condition2checker(condition)
return RarFileImplementation.read_files(self, checker) return RarFileImplementation.read_files(self, checker)
def extract(self, condition='*', path='.', withSubpath=True, overwrite=True): def extract(self, condition='*', path='.', withSubpath=True, overwrite=True):
"""Extract specific files from archive to disk. """Extract specific files from archive to disk.
If "condition" is a list of numbers, then extract files which have those positions in infolist. If "condition" is a list of numbers, then extract files which have those positions in infolist.
If "condition" is a string, then it is treated as a wildcard for names of files to extract. If "condition" is a string, then it is treated as a wildcard for names of files to extract.
If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object
and returns either boolean True (extract) or boolean False (skip). and returns either boolean True (extract) or boolean False (skip).
DEPRECATED: If "condition" callback returns string (only supported for Windows) - DEPRECATED: If "condition" callback returns string (only supported for Windows) -
that string will be used as a new name to save the file under. that string will be used as a new name to save the file under.
If "condition" is omitted, all files are extracted. If "condition" is omitted, all files are extracted.
"path" is a directory to extract to "path" is a directory to extract to
"withSubpath" flag denotes whether files are extracted with their full path in the archive. "withSubpath" flag denotes whether files are extracted with their full path in the archive.
"overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true. "overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true.
Returns list of RarInfos for extracted files.""" Returns list of RarInfos for extracted files."""
checker = condition2checker(condition) checker = condition2checker(condition)
return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite) return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite)

30
libs/unrar2/unix.py

@ -21,25 +21,37 @@
# SOFTWARE. # SOFTWARE.
# Unix version uses unrar command line executable # Unix version uses unrar command line executable
import platform
import stat
import subprocess import subprocess
import gc import gc
import os
import os, os.path import os.path
import time, re import time
import re
from rar_exceptions import * from rar_exceptions import *
class UnpackerNotInstalled(Exception): pass class UnpackerNotInstalled(Exception): pass
rar_executable_cached = None rar_executable_cached = None
rar_executable_version = None rar_executable_version = None
def call_unrar(params): osx_unrar = os.path.join(os.path.dirname(__file__), 'unrar')
if os.path.isfile(osx_unrar) and 'darwin' in platform.platform().lower():
try:
st = os.stat(osx_unrar)
os.chmod(osx_unrar, st.st_mode | stat.S_IEXEC)
except:
pass
def call_unrar(params, custom_path = None):
"Calls rar/unrar command line executable, returns stdout pipe" "Calls rar/unrar command line executable, returns stdout pipe"
global rar_executable_cached global rar_executable_cached
if rar_executable_cached is None: if rar_executable_cached is None:
for command in ('unrar', 'rar', os.path.join(os.path.dirname(__file__), 'unrar')): for command in (custom_path, 'unrar', 'rar', osx_unrar):
if not command: continue
try: try:
subprocess.Popen([command], stdout = subprocess.PIPE) subprocess.Popen([command], stdout = subprocess.PIPE)
rar_executable_cached = command rar_executable_cached = command
@ -59,10 +71,10 @@ def call_unrar(params):
class RarFileImplementation(object): class RarFileImplementation(object):
def init(self, password = None): def init(self, password = None, custom_path = None):
global rar_executable_version global rar_executable_version
self.password = password self.password = password
self.custom_path = custom_path
stdoutdata, stderrdata = self.call('v', []).communicate() stdoutdata, stderrdata = self.call('v', []).communicate()
@ -118,7 +130,7 @@ class RarFileImplementation(object):
def call(self, cmd, options = [], files = []): def call(self, cmd, options = [], files = []):
options2 = options + ['p' + self.escaped_password()] options2 = options + ['p' + self.escaped_password()]
soptions = ['-' + x for x in options2] soptions = ['-' + x for x in options2]
return call_unrar([cmd] + soptions + ['--', self.archiveName] + files) return call_unrar([cmd] + soptions + ['--', self.archiveName] + files, self.custom_path)
def infoiter(self): def infoiter(self):

24
libs/unrar2/windows.py

@ -174,7 +174,7 @@ class PassiveReader:
def __init__(self, usercallback = None): def __init__(self, usercallback = None):
self.buf = [] self.buf = []
self.ucb = usercallback self.ucb = usercallback
def _callback(self, msg, UserData, P1, P2): def _callback(self, msg, UserData, P1, P2):
if msg == UCM_PROCESSDATA: if msg == UCM_PROCESSDATA:
data = (ctypes.c_char*P2).from_address(P1).raw data = (ctypes.c_char*P2).from_address(P1).raw
@ -183,7 +183,7 @@ class PassiveReader:
else: else:
self.buf.append(data) self.buf.append(data)
return 1 return 1
def get_result(self): def get_result(self):
return ''.join(self.buf) return ''.join(self.buf)
@ -197,10 +197,10 @@ class RarInfoIterator(object):
raise IncorrectRARPassword raise IncorrectRARPassword
self.arc.lockStatus = "locked" self.arc.lockStatus = "locked"
self.arc.needskip = False self.arc.needskip = False
def __iter__(self): def __iter__(self):
return self return self
def next(self): def next(self):
if self.index>0: if self.index>0:
if self.arc.needskip: if self.arc.needskip:
@ -208,9 +208,9 @@ class RarInfoIterator(object):
self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData))
if self.res: if self.res:
raise StopIteration raise StopIteration
self.arc.needskip = True self.arc.needskip = True
data = {} data = {}
data['index'] = self.index data['index'] = self.index
data['filename'] = self.headerData.FileName data['filename'] = self.headerData.FileName
@ -224,7 +224,7 @@ class RarInfoIterator(object):
self.index += 1 self.index += 1
return data return data
def __del__(self): def __del__(self):
self.arc.lockStatus = "finished" self.arc.lockStatus = "finished"
@ -237,7 +237,7 @@ def generate_password_provider(password):
class RarFileImplementation(object): class RarFileImplementation(object):
def init(self, password=None): def init(self, password=None, custom_path = None):
self.password = password self.password = password
archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT)
self._handle = RAROpenArchiveEx(ctypes.byref(archiveData)) self._handle = RAROpenArchiveEx(ctypes.byref(archiveData))
@ -254,9 +254,9 @@ class RarFileImplementation(object):
if password: if password:
RARSetPassword(self._handle, password) RARSetPassword(self._handle, password)
self.lockStatus = "ready" self.lockStatus = "ready"
def destruct(self): def destruct(self):
@ -287,7 +287,7 @@ class RarFileImplementation(object):
self.needskip = False self.needskip = False
res.append((info, reader.get_result())) res.append((info, reader.get_result()))
return res return res
def extract(self, checker, path, withSubpath, overwrite): def extract(self, checker, path, withSubpath, overwrite):
res = [] res = []
@ -300,7 +300,7 @@ class RarFileImplementation(object):
fn = os.path.split(fn)[-1] fn = os.path.split(fn)[-1]
target = os.path.join(path, fn) target = os.path.join(path, fn)
else: else:
raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows" raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows"
target = checkres target = checkres
if overwrite or (not os.path.exists(target)): if overwrite or (not os.path.exists(target)):
tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target) tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target)

Loading…
Cancel
Save